Files
2026-10-21 10:14:49 +07:00

339 lines
17 KiB
PHP

<?php
use App\Http\Controllers\Web\Dashboard\SummaryController;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
beforeEach(function () {
Schema::create('master_komoditas', function (Blueprint $table) {
$table->id();
$table->string('nama');
$table->softDeletes();
});
foreach ([
'master_kabupaten_kota' => 'provinsi_id',
'master_kecamatan' => 'kabupaten_kota_id',
'master_desa_kelurahan' => 'kecamatan_id',
] as $name => $parent) {
Schema::create($name, function (Blueprint $table) use ($parent) {
$table->id();
$table->integer($parent);
});
}
Schema::create('petani', function (Blueprint $table) {
$table->id();
$table->integer('komoditas_id');
$table->integer('desa_kelurahan_id');
$table->softDeletes();
});
Schema::create('harga_jual_panen', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->string('bentuk_produk');
$table->integer('harga_panen_terakhir')->nullable();
$table->integer('harga_tahun_lalu')->nullable();
$table->integer('harga_dua_tahun_lalu')->nullable();
});
Schema::create('petani_skor', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->float('skor_kesiapan_adopsi');
$table->softDeletes();
});
Schema::create('akses_pasar', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->text('tujuan_penjualan')->nullable();
$table->text('sifat_hubungan_bisnis')->nullable();
$table->string('frekuensi_penjualan')->nullable();
$table->integer('skor_pengalaman')->nullable();
$table->integer('skor_kesadaran_premium')->nullable();
$table->softDeletes();
});
DB::table('master_komoditas')->insert([
['id' => 1, 'nama' => 'Kakao'], ['id' => 2, 'nama' => 'Vanili'],
['id' => 3, 'nama' => 'Kopi'], ['id' => 4, 'nama' => 'Kemiri'],
]);
DB::table('master_kabupaten_kota')->insert(['id' => 1, 'provinsi_id' => 1]);
DB::table('master_kecamatan')->insert(['id' => 1, 'kabupaten_kota_id' => 1]);
DB::table('master_desa_kelurahan')->insert([
['id' => 1, 'kecamatan_id' => 1], ['id' => 2, 'kecamatan_id' => 1],
]);
DB::table('petani')->insert([
['id' => 1, 'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 2, 'komoditas_id' => 1, 'desa_kelurahan_id' => 2, 'deleted_at' => null],
['id' => 3, 'komoditas_id' => 2, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 4, 'komoditas_id' => 3, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 5, 'komoditas_id' => 4, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 6, 'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'deleted_at' => '2026-01-01'],
]);
foreach ([
[1, 'Biji kering/Biji Kupas', 10000], [2, 'Biji kering/Biji Kupas', 20000],
[1, 'Biji kering/Biji Kupas', 0], [1, 'Biji kering/Biji Kupas', null],
[1, 'Biji kering/Biji Kupas', -1000], [1, 'Biji basah', 90000],
[1, 'Buah segar hasil petik', 5000],
[3, 'Buah segar hasil petik', 30000], [3, 'Biji kering/Biji Kupas', 500000],
[3, 'Terfermentasi (Rp/kg)', 900000], [4, 'Buah segar hasil petik', 12000],
[5, 'Buah segar hasil petik', 8000], [5, 'Biji kering/Biji Kupas', 40000],
[6, 'Biji kering/Biji Kupas', 1000000],
] as [$farmer, $product, $price]) {
DB::table('harga_jual_panen')->insert([
'petani_id' => $farmer, 'bentuk_produk' => $product, 'harga_panen_terakhir' => $price,
]);
}
DB::table('petani_skor')->insert([
['petani_id' => 1, 'skor_kesiapan_adopsi' => 4.2, 'deleted_at' => null],
['petani_id' => 2, 'skor_kesiapan_adopsi' => 2.5, 'deleted_at' => null],
['petani_id' => 3, 'skor_kesiapan_adopsi' => 4.5, 'deleted_at' => '2026-01-01'],
]);
$controller = app(SummaryController::class);
$method = new ReflectionMethod($controller, 'marketDashboardData');
$this->marketCards = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['priceCards'];
$this->marketCharts = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['marketCharts'];
$this->priceTrend = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['priceTrend'];
$this->premiumAwareness = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['premiumAwareness'];
});
test('market shows all four prices using the appropriate product and positive active farmer prices', function () {
$cards = ($this->marketCards)();
expect(array_column($cards, 'label'))->toBe([
'Kakao - Harga Biji Kering', 'Vanili - Harga Buah Segar',
'Kopi - Harga Ceri Merah', 'Kemiri - Harga Gelondong',
]);
expect(array_column($cards, 'value'))->toBe(['Rp 15.000', 'Rp 30.000', 'Rp 12.000', 'Rp 8.000']);
expect(array_column($cards, 'records'))->toBe([2, 1, 1, 1]);
});
test('market prices respect each region level and commodity selection', function () {
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
expect(($this->marketCards)([$region => 1])[0]['value'])
->toBe($region === 'desaKelurahan_id' ? 'Rp 10.000' : 'Rp 15.000');
expect(array_column(($this->marketCards)([$region => 99]), 'records'))->toBe([0, 0, 0, 0]);
}
$cards = ($this->marketCards)(['komoditas_id' => 2]);
expect(array_column($cards, 'records'))->toBe([0, 1, 0, 0]);
expect(array_column($cards, 'value'))->toBe(['Rp 0', 'Rp 30.000', 'Rp 0', 'Rp 0']);
});
test('market prices respect segment selection and ignore deleted scores', function () {
$cards = ($this->marketCards)(['segmen' => 'Penggerak']);
expect(array_column($cards, 'records'))->toBe([1, 0, 0, 0]);
expect($cards[0]['value'])->toBe('Rp 10.000');
expect(($this->marketCards)(['segmen' => 'Pemerhati'])[0]['value'])->toBe('Rp 20.000');
});
test('market keeps four empty cards when no prices or active commodities are available', function () {
DB::table('master_komoditas')->update(['deleted_at' => '2026-01-01']);
$cards = ($this->marketCards)();
expect(array_column($cards, 'records'))->toBe([0, 0, 0, 0]);
expect(array_column($cards, 'value'))->toBe(['Rp 0', 'Rp 0', 'Rp 0', 'Rp 0']);
});
test('market charts count multiple channels once per respondent and use answered question denominators', function () {
DB::table('akses_pasar')->insert([
[
'petani_id' => 1,
'tujuan_penjualan' => json_encode(['Koperasi', 'BUMDes', 'Koperasi', 'Lainnya']),
'sifat_hubungan_bisnis' => 'Bebas, bisa pindah pembeli kapan saja',
'frekuensi_penjualan' => 'Langsung setelah panen',
'skor_pengalaman' => 5,
],
[
'petani_id' => 2,
'tujuan_penjualan' => json_encode(['Koperasi']),
'sifat_hubungan_bisnis' => 'Titip jual,dibayar setelah produk terjual',
'frekuensi_penjualan' => null,
'skor_pengalaman' => 1,
],
[
'petani_id' => 3,
'tujuan_penjualan' => '[]',
'sifat_hubungan_bisnis' => '',
'frekuensi_penjualan' => 'Saat butuh uang mendesak',
'skor_pengalaman' => 3,
],
]);
$charts = ($this->marketCharts)();
expect($charts['saluranPenjualan']['series'])->toBe([0, 2, 1, 0, 1]);
expect($charts['saluranPenjualan']['respondents'])->toBe(2);
expect($charts['saluranPenjualan']['percentages'])->toBe([0.0, 100.0, 50.0, 0.0, 50.0]);
expect($charts['hubunganPembeli']['series'])->toBe([1, 0, 0, 1, 0, 0]);
expect($charts['hubunganPembeli']['respondents'])->toBe(2);
expect($charts['waktuPenjualan']['series'])->toBe([1, 0, 1]);
expect($charts['waktuPenjualan']['respondents'])->toBe(2);
expect($charts['aksesSaluranLain']['series'])->toBe([1, 0, 1, 0, 1]);
expect($charts['aksesSaluranLain']['percentages'])->toBe([33.3, 0.0, 33.3, 0.0, 33.3]);
});
test('market charts respect all dashboard filters and exclude deleted farmers and responses', function () {
foreach ([1, 2, 3, 6] as $farmer) {
DB::table('akses_pasar')->insert([
'petani_id' => $farmer,
'tujuan_penjualan' => json_encode(['Koperasi']),
'sifat_hubungan_bisnis' => 'Langganan tetap, tapi tidak terikat',
'frekuensi_penjualan' => 'Ditumpuk dulu beberapa waktu',
'skor_pengalaman' => 4,
]);
}
DB::table('akses_pasar')->insert([
'petani_id' => 4, 'tujuan_penjualan' => json_encode(['BUMDes']), 'deleted_at' => '2026-01-01',
]);
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
foreach (($this->marketCharts)([$region => 1]) as $chart) {
expect($chart['respondents'])->toBe($region === 'desaKelurahan_id' ? 2 : 3);
}
foreach (($this->marketCharts)([$region => 99]) as $chart) {
expect($chart['respondents'])->toBe(0);
}
}
foreach ([['komoditas_id' => 1], ['komoditas_id' => 2], ['segmen' => 'Penggerak'], ['segmen' => 'Pemerhati']] as $filters) {
foreach (($this->marketCharts)($filters) as $chart) {
expect($chart['respondents'])->toBe($filters === ['komoditas_id' => 1] ? 2 : 1);
}
}
foreach (($this->marketCharts)(['komoditas_id' => 2, 'segmen' => 'Penggerak']) as $chart) {
expect($chart['respondents'])->toBe(0);
}
});
test('market charts keep ordered zero categories when answers are missing or invalid', function () {
DB::table('akses_pasar')->insert([
'petani_id' => 1, 'tujuan_penjualan' => '[null, {}, "Unknown"]',
'sifat_hubungan_bisnis' => 'Unknown', 'frekuensi_penjualan' => '', 'skor_pengalaman' => 9,
]);
$charts = ($this->marketCharts)();
expect(array_map(fn ($chart) => count($chart['series']), $charts))->toBe([
'saluranPenjualan' => 5, 'hubunganPembeli' => 6, 'waktuPenjualan' => 3, 'aksesSaluranLain' => 5,
]);
foreach ($charts as $chart) {
expect($chart['respondents'])->toBe(0);
expect(array_sum($chart['series']))->toBe(0);
expect(array_sum($chart['percentages']))->toBe(0);
}
});
test('price trend averages each product and commodity chronologically without treating missing prices as zero', function () {
DB::table('harga_jual_panen')->delete();
foreach ([
[1, 'Buah segar hasil petik', 10000, 8000, 6000],
[2, 'Buah segar hasil petik', 20000, 12000, null],
[3, 'Buah segar hasil petik', 30000, 20000, 10000],
[1, 'Biji basah', 0, 7000, 4000],
[1, 'Biji kering/Biji Kupas', -1000, null, 0],
[1, 'Terfermentasi (Rp/kg)', null, 20000, 10000],
[6, 'Buah segar hasil petik', 1000000, 1000000, 1000000],
] as [$farmer, $product, $latest, $lastYear, $twoYearsAgo]) {
DB::table('harga_jual_panen')->insert([
'petani_id' => $farmer, 'bentuk_produk' => $product,
'harga_panen_terakhir' => $latest, 'harga_tahun_lalu' => $lastYear, 'harga_dua_tahun_lalu' => $twoYearsAgo,
]);
}
$trend = ($this->priceTrend)();
$commodities = array_column($trend['commodities'], null, 'id');
expect($trend['periods'])->toBe(['Dua Tahun Lalu', 'Tahun Lalu', 'Panen Terakhir']);
expect(array_column($commodities[1]['series'], 'name'))->toBe([
'Buah segar hasil petik', 'Biji basah', 'Biji kering/Biji Kupas', 'Terfermentasi',
]);
expect(array_column($commodities[1]['series'], 'data'))->toBe([
[6000.0, 10000.0, 15000.0], [4000.0, 7000.0, null], [null, null, null], [10000.0, 20000.0, null],
]);
expect($commodities[2]['series'][0]['data'])->toBe([10000.0, 20000.0, 30000.0]);
});
test('price trend respects dashboard region commodity and segment filters', function () {
// Historical prices must follow the same filters as the latest harvest.
DB::table('harga_jual_panen')->where('harga_panen_terakhir', '>', 0)
->update(['harga_tahun_lalu' => DB::raw('harga_panen_terakhir / 2')]);
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
$commodities = array_column(($this->priceTrend)([$region => 1])['commodities'], null, 'id');
$price = $region === 'desaKelurahan_id' ? 10000.0 : 15000.0;
expect($commodities[1]['series'][2]['data'])->toBe([null, $price / 2, $price]);
foreach (($this->priceTrend)([$region => 99])['commodities'] as $commodity) {
expect(array_column($commodity['series'], 'data'))->toBe(array_fill(0, 4, [null, null, null]));
}
}
$trend = ($this->priceTrend)(['komoditas_id' => 2]);
expect(array_column($trend['commodities'], 'id'))->toBe([2]);
expect($trend['commodities'][0]['series'][0]['data'])->toBe([null, 15000.0, 30000.0]);
foreach (['Penggerak' => 10000.0, 'Pemerhati' => 20000.0] as $segment => $price) {
$commodities = array_column(($this->priceTrend)(['segmen' => $segment])['commodities'], null, 'id');
expect($commodities[1]['series'][2]['data'])->toBe([null, $price / 2, $price]);
expect($commodities[2]['series'][0]['data'])->toBe([null, null, null]);
}
});
test('price trend retains missing points and handles empty or deleted commodity selections', function () {
DB::table('harga_jual_panen')->delete();
$trend = ($this->priceTrend)();
expect($trend['commodities'])->toHaveCount(4);
foreach ($trend['commodities'] as $commodity) {
expect(array_column($commodity['series'], 'data'))->toBe(array_fill(0, 4, [null, null, null]));
}
expect(($this->priceTrend)(['komoditas_id' => 99])['commodities'])->toBe([]);
DB::table('master_komoditas')->where('id', 1)->update(['deleted_at' => '2026-01-01']);
expect(($this->priceTrend)(['komoditas_id' => 1])['commodities'])->toBe([]);
});
test('premium awareness counts each active farmer once and preserves the five ordered levels', function () {
DB::table('akses_pasar')->insert([
'petani_id' => 4, 'skor_kesadaran_premium' => 4, 'deleted_at' => '2026-01-01',
]);
foreach ([1 => 1, 2 => 3, 3 => 3, 4 => 5, 5 => null, 6 => 5] as $farmer => $score) {
DB::table('akses_pasar')->insert(['petani_id' => $farmer, 'skor_kesadaran_premium' => $score]);
}
DB::table('akses_pasar')->insert(['petani_id' => 1, 'skor_kesadaran_premium' => 2]);
$chart = ($this->premiumAwareness)();
expect($chart['series'])->toBe([1, 0, 2, 0, 1]);
expect($chart['percentages'])->toBe([25.0, 0.0, 50.0, 0.0, 25.0]);
expect($chart['respondents'])->toBe(4);
expect($chart['fullLabels'][0])->toBe('Belum pernah mendengar sama sekali');
expect($chart['fullLabels'][4])->toBe('Sudah mengakses dan mendapat harga premium secara konsisten');
});
test('premium awareness respects all dashboard region commodity and segment filters', function () {
foreach ([1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 5] as $farmer => $score) {
DB::table('akses_pasar')->insert(['petani_id' => $farmer, 'skor_kesadaran_premium' => $score]);
}
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
$chart = ($this->premiumAwareness)([$region => 1]);
expect($chart['respondents'])->toBe($region === 'desaKelurahan_id' ? 4 : 5);
expect(($this->premiumAwareness)([$region => 99])['respondents'])->toBe(0);
}
expect(($this->premiumAwareness)(['komoditas_id' => 1])['series'])->toBe([1, 1, 0, 0, 0]);
expect(($this->premiumAwareness)(['komoditas_id' => 2])['series'])->toBe([0, 0, 1, 0, 0]);
expect(($this->premiumAwareness)(['segmen' => 'Penggerak'])['series'])->toBe([1, 0, 0, 0, 0]);
expect(($this->premiumAwareness)(['segmen' => 'Pemerhati'])['series'])->toBe([0, 1, 0, 0, 0]);
expect(($this->premiumAwareness)(['komoditas_id' => 2, 'segmen' => 'Penggerak'])['respondents'])->toBe(0);
});
test('premium awareness retains empty categories and excludes missing or invalid scores', function () {
$emptyChart = ($this->premiumAwareness)();
foreach ([1 => 0, 2 => 6, 3 => null, 4 => -1, 5 => null] as $farmer => $score) {
DB::table('akses_pasar')->insert(['petani_id' => $farmer, 'skor_kesadaran_premium' => $score]);
}
$chart = ($this->premiumAwareness)();
expect($chart)->toBe($emptyChart);
expect($chart['series'])->toBe([0, 0, 0, 0, 0]);
expect($chart['respondents'])->toBe(0);
expect($chart['labels'])->toHaveCount(5);
expect(array_sum($chart['percentages']))->toBe(0);
});