false]);
$this->withoutMiddleware(CatatKunjungan::class);
(require database_path('migrations/0001_01_01_000000_create_users_table.php'))->up();
(require database_path('migrations/2026_02_12_110653_create_wilayah_table.php'))->up();
(require database_path('migrations/2026_08_05_170222_create_pengurus_koperasi_table.php'))->up();
(require database_path('migrations/2026_11_06_190000_add_wilayah_and_enumerator_to_pengurus_koperasi_table.php'))->up();
Event::forget('composing: *');
$user = new User(['name' => 'Admin Test', 'email' => 'admin@example.test', 'role' => 'admin']);
$user->id = 7;
$user->uid = 'test-admin';
$user->username = 'admin-test';
$user->password = 'test-password';
$user->saveQuietly();
$this->be($user);
$this->input = [
'nama_lengkap' => 'Budi Santoso',
'posisi_dalam_koperasi' => 'Anggota koperasi',
'nama_koperasi' => 'Koperasi Makmur',
'jenis_koperasi' => 'Koperasi Produksi',
'skala_operasi' => 'Level Desa',
'tahun_bergabung' => 2020,
'tahun_berdiri' => 2015,
'jumlah_anggota_awal' => 0,
'jumlah_anggota_dewasa' => 20,
'jumlah_anggota_anak' => 0,
'jumlah_pengurus' => 3,
'shu_2025' => 1200000,
];
$this->dom = function (string $html): DOMXPath {
$dom = new DOMDocument;
$previous = libxml_use_internal_errors(true);
$dom->loadHTML($html);
libxml_clear_errors();
libxml_use_internal_errors($previous);
return new DOMXPath($dom);
};
$this->location = function (string $suffix = '1'): array {
$province = Provinsi::create(['kode' => $suffix, 'nama' => 'Provinsi '.$suffix, 'iso' => 'ID-'.$suffix]);
$district = KabupatenKota::create(['kode' => $suffix, 'nama' => 'Kabupaten '.$suffix, 'provinsi_id' => $province->id]);
$subdistrict = Kecamatan::create(['kode' => $suffix, 'nama' => 'Kecamatan '.$suffix, 'kabupaten_kota_id' => $district->id]);
$village = DesaKelurahan::create(['kode' => $suffix, 'nama' => 'Desa '.$suffix, 'kecamatan_id' => $subdistrict->id]);
return ['provinsi_id' => $province->id, 'kabupaten_kota_id' => $district->id, 'kecamatan_id' => $subdistrict->id, 'desa_kelurahan_id' => $village->id];
};
});
test('koperasi creation renders the profile form without a redundant title and locks later tabs', function () {
$response = $this->get(route('koperasi.pengurus.create'))->assertOk()
->assertViewIs('koperasi.profil.create')->assertDontSee('Tambah Profil Koperasi')
->assertSee('Data Responden')->assertSee('Identitas Koperasi')->assertSee('assets/js/koperasi.js');
$dom = ($this->dom)($response->getContent());
expect($dom->query('//a[@data-koperasi-locked-tab and @aria-disabled="true" and @data-bs-original-title="Simpan Profil terlebih dahulu"]')->length)->toBe(3)
->and($dom->query('//form[@data-koperasi-profile]//select')->length)->toBe(8)
->and($dom->query('//a[contains(@class,"nav-link") and contains(@class,"active") and contains(.,"Profil")]')->length)->toBe(1)
->and($dom->query('//section[@id="koperasi-profile-summary"]')->length)->toBe(1)
->and(trim($dom->query('//*[@id="koperasi-profile-summary"]//*[@data-summary-field="anggota"]')->item(0)->textContent))->toBe('Belum terisi')
->and(trim($dom->query('//*[@id="koperasi-profile-summary"]//*[@data-summary-field="diperbarui"]')->item(0)->textContent))->toBe('Belum tersimpan');
});
test('koperasi list presents cooperative identity services and institutional documents', function () {
PengurusKoperasi::create([
...$this->input, 'nama_koperasi' => 'Koperasi Makmur', 'jenis_koperasi' => 'Koperasi Simpan Pinjam',
'alamat_koperasi' => 'Desa Maju', 'ad_art' => 'Ada, lengkap',
'layanan_koperasi' => ['Simpan pinjam', 'Pengadaan input produksi', 'Pemasaran hasil', 'Lainnya'], 'layanan_lainnya' => 'Sewa alat',
]);
$latest = PengurusKoperasi::create([
...$this->input, 'nama_koperasi' => 'Koperasi Sejahtera', 'nama_lengkap' => 'Siti Responden',
'ad_art' => 'Tidak tahu',
]);
$response = $this->get(route('koperasi.pengurus.index'))->assertOk()->assertSee('Data Koperasi')
->assertDontSee('Data Pengurus Koperasi')->assertViewHas('totalKoperasi', 2)
->assertViewHas('totalJenisKoperasi', 2)->assertViewHas('totalDokumenLengkap', 1);
$dom = ($this->dom)($response->getContent());
$table = '//table[contains(@class,"koperasi-list-table")]';
$headings = array_map(fn ($node) => trim($node->textContent), iterator_to_array($dom->query($table.'/thead/tr/th')));
expect($headings)->toBe(['No', 'Koperasi', 'Jenis Koperasi', 'Layanan Utama', 'Dokumen Kelembagaan', 'Nama Responden', 'Nama Enumerator', 'Aksi'])
->and($dom->query($table.'/tbody/tr[1]/td[2]/a[@href="'.route('koperasi.pengurus.edit', $latest).'"]')->item(0)->textContent)->toBe('Koperasi Sejahtera')
->and(trim($dom->query($table.'/tbody/tr[1]/td[5]')->item(0)->textContent))->toBe('Tidak tahu')
->and(trim($dom->query($table.'/tbody/tr[2]/td[5]')->item(0)->textContent))->toBe('Ada dan lengkap')
->and($dom->query($table.'//div[contains(text(),"Tahun Berdiri")]')->length)->toBe(0)
->and($dom->query($table.'/tbody/tr[2]/td[4]//div[@title="Simpan pinjam, Pengadaan input produksi, Pemasaran hasil, Lainnya: Sewa alat"]')->length)->toBe(1)
->and($dom->query($table.'/tbody/tr[2]/td[4]//span[text()="+2 lainnya"]')->length)->toBe(1)
->and($dom->query('//form[@data-koperasi-delete and @data-nama="Koperasi Sejahtera"]//input[@name="_method" and @value="DELETE"]')->length)->toBe(1)
->and($dom->query('//form[@data-koperasi-delete]//input[@name="_token"]')->length)->toBe(2);
$latest->update(['ad_art' => null]);
$dom = ($this->dom)($this->get(route('koperasi.pengurus.index'))->assertOk()->getContent());
expect(trim($dom->query($table.'/tbody/tr[1]/td[5]')->item(0)->textContent))->toBe('Belum terisi');
});
test('koperasi list searches organization identity location type and respondent without case sensitivity', function (string $search) {
$match = PengurusKoperasi::create([
...$this->input, 'nama_koperasi' => 'Koperasi Cahaya', 'alamat_koperasi' => 'Desa Bajawa',
'jenis_koperasi' => 'Koperasi Pemasaran', 'nama_lengkap' => 'Siti Responden',
]);
PengurusKoperasi::create([...$this->input, 'nama_koperasi' => 'Koperasi Lain', 'nama_lengkap' => 'Budi']);
$response = $this->get(route('koperasi.pengurus.index', ['search' => $search]))->assertOk()
->assertViewHas('totalKoperasi', 2)->assertViewHas('totalJenisKoperasi', 2);
expect($response->viewData('koperasi')->total())->toBe(1)
->and($response->viewData('koperasi')->first()->id)->toBe($match->id);
})->with(['cahaya', 'BAJAWA', 'pemasaran', 'siti responden']);
test('koperasi list pagination preserves searches and numbers rows across pages', function () {
$location = ($this->location)();
for ($i = 1; $i <= 12; $i++) {
PengurusKoperasi::create([
...$this->input, 'nama_koperasi' => 'Koperasi Paginasi '.$i,
'layanan_koperasi' => ['Pengadaan input produksi'], 'ad_art' => 'Tidak tahu', 'alamat_koperasi' => 'Desa Maju, Kecamatan Sejahtera',
'desa_kelurahan_id' => $location['desa_kelurahan_id'],
]);
}
$filters = [
'search' => 'Paginasi', 'jenis_koperasi' => 'Koperasi Produksi', 'layanan' => 'Pengadaan input produksi',
'ad_art' => 'Tidak tahu', 'sort_by' => 'nama_asc', ...array_diff_key($location, ['provinsi_id' => true]),
];
$url = route('koperasi.pengurus.index', [...$filters, 'per_page' => 10, 'page' => 2]);
$response = $this->get($url)->assertOk();
expect($response->viewData('koperasi')->count())->toBe(2)->and($response->viewData('koperasi')->total())->toBe(12);
$dom = ($this->dom)($response->getContent());
expect(trim($dom->query('//table/tbody/tr[1]/td[1]')->item(0)->textContent))->toBe('11')
->and($dom->query('//form[@id="koperasi-filter-form"]//input[@name="search" and @value="Paginasi"]')->length)->toBe(1)
->and($dom->query('//a[contains(@href,"search=Paginasi") and contains(@href,"per_page=10") and contains(@href,"page=1")]')->length)->toBeGreaterThan(0);
foreach ($filters as $name => $value) {
expect($dom->query('//form[@id="koperasi-pagination-form"]//input[@type="hidden" and @name="'.$name.'" and @value="'.$value.'"]')->length)->toBe(1);
}
$response = $this->get(route('koperasi.pengurus.index', ['per_page' => 25]))->assertOk();
expect($response->viewData('koperasi')->perPage())->toBe(25)->and($response->viewData('koperasi')->count())->toBe(12);
});
test('koperasi list validates filter values', function (array $input, string $field) {
$this->getJson(route('koperasi.pengurus.index', $input))->assertUnprocessable()->assertJsonValidationErrors($field);
})->with([
'unexpected page size' => [['per_page' => 100], 'per_page'],
'non numeric page size' => [['per_page' => 'semua'], 'per_page'],
'array search' => [['search' => ['invalid']], 'search'],
'array district' => [['kabupaten_kota_id' => ['invalid']], 'kabupaten_kota_id'],
'unknown village' => [['desa_kelurahan_id' => 999], 'desa_kelurahan_id'],
'overlong search' => [['search' => str_repeat('a', 256)], 'search'],
'array cooperative type' => [['jenis_koperasi' => ['invalid']], 'jenis_koperasi'],
'unknown service' => [['layanan' => 'invalid'], 'layanan'],
'unknown institutional document status' => [['ad_art' => 'invalid'], 'ad_art'],
'unknown sort order' => [['sort_by' => 'nama_koperasi desc; DROP TABLE pengurus_koperasi'], 'sort_by'],
]);
test('koperasi data filters combine with searches and support saved custom cooperative types', function () {
$base = [
...$this->input, 'jenis_koperasi' => 'Koperasi Jasa', 'layanan_koperasi' => ['Pengadaan input produksi'],
'ad_art' => 'Ada, lengkap', 'nama_koperasi' => 'Koperasi Bersama',
];
$match = PengurusKoperasi::create($base);
PengurusKoperasi::create([...$base, 'jenis_koperasi' => 'Koperasi Simpan Pinjam']);
PengurusKoperasi::create([...$base, 'layanan_koperasi' => ['Pemasaran hasil']]);
PengurusKoperasi::create([...$base, 'ad_art' => 'Tidak ada']);
PengurusKoperasi::create([...$base, 'nama_koperasi' => 'Koperasi Lain']);
$filters = ['jenis_koperasi' => 'Koperasi Jasa', 'layanan' => 'Pengadaan input produksi', 'ad_art' => 'Ada, lengkap', 'search' => 'Bersama'];
$response = $this->get(route('koperasi.pengurus.index', $filters))->assertOk()
->assertViewHas('totalKoperasi', 5)->assertViewHas('totalJenisKoperasi', 2)->assertViewHas('totalDokumenLengkap', 4);
expect($response->viewData('koperasi')->modelKeys())->toBe([$match->id])
->and($response->viewData('jenisKoperasiOptions')->all())->toContain('Koperasi Jasa');
$dom = ($this->dom)($response->getContent());
foreach (array_diff_key($filters, ['search' => true]) as $name => $value) {
expect($dom->query('//form[@id="koperasi-filter-form"]//select[@name="'.$name.'"]/option[@value="'.$value.'" and @selected]')->length)->toBe(1);
}
$otherTypes = $this->get(route('koperasi.pengurus.index', [...$filters, 'jenis_koperasi' => 'Lainnya']))->assertOk();
expect($otherTypes->viewData('koperasi')->modelKeys())->toBe([$match->id]);
});
test('koperasi document summary counts only complete documents while filters leave totals unchanged', function () {
foreach (['Ada, lengkap', 'Ada tapi tidak lengkap', 'Tidak ada', 'Tidak tahu'] as $document) {
PengurusKoperasi::create([...$this->input, 'ad_art' => $document]);
}
$response = $this->get(route('koperasi.pengurus.index', ['ad_art' => 'Tidak ada']))->assertOk()
->assertViewHas('totalKoperasi', 4)->assertViewHas('totalDokumenLengkap', 1);
expect($response->viewData('koperasi')->total())->toBe(1);
});
test('koperasi sorts names and complete member counts while unknown totals stay last', function (string $sort, array $expected) {
foreach ([['beta', 20, 0], ['Alpha', 0, 0], ['Gamma', 5, 0], ['Delta', null, 100]] as [$name, $adults, $children]) {
PengurusKoperasi::create([
...$this->input, 'nama_koperasi' => $name, 'jumlah_anggota_dewasa' => $adults, 'jumlah_anggota_anak' => $children,
]);
}
$response = $this->get(route('koperasi.pengurus.index', ['sort_by' => $sort]))->assertOk();
expect($response->viewData('koperasi')->pluck('nama_koperasi')->all())->toBe($expected);
})->with([
'name ascending' => ['nama_asc', ['Alpha', 'beta', 'Delta', 'Gamma']],
'name descending' => ['nama_desc', ['Gamma', 'Delta', 'beta', 'Alpha']],
'fewest members' => ['anggota_asc', ['Alpha', 'Gamma', 'beta', 'Delta']],
'most members' => ['anggota_desc', ['beta', 'Gamma', 'Alpha', 'Delta']],
'latest entries' => ['terbaru', ['Delta', 'Gamma', 'Alpha', 'beta']],
]);
test('koperasi list displays an empty state spanning the cooperative columns', function () {
$response = $this->get(route('koperasi.pengurus.index'))->assertOk()->assertViewHas('totalKoperasi', 0)
->assertViewHas('totalJenisKoperasi', 0)->assertViewHas('totalDokumenLengkap', 0);
$dom = ($this->dom)($response->getContent());
expect($dom->query('//table/tbody/tr/td[@colspan="8" and normalize-space(.)="Tidak ada data ditemukan."]')->length)->toBe(1);
expect($dom->query('//select[@name="jenis_koperasi"]/option')->length)->toBe(6);
});
test('koperasi regional dropdowns use saved location relationships rather than guesses from addresses', function (array $keys) {
$location = ($this->location)('1');
$other = ($this->location)('2');
$match = PengurusKoperasi::create([
...$this->input, 'nama_koperasi' => 'Koperasi Cahaya', 'jenis_koperasi' => 'Koperasi Pemasaran',
'desa_kelurahan_id' => $location['desa_kelurahan_id'], 'alamat_koperasi' => 'Alamat bebas', 'ad_art' => 'Ada, lengkap',
]);
PengurusKoperasi::create([
...$this->input, 'nama_koperasi' => 'Koperasi Cahaya', 'jenis_koperasi' => 'Koperasi Pemasaran',
'desa_kelurahan_id' => $other['desa_kelurahan_id'], 'alamat_koperasi' => 'Desa 1, Kecamatan 1, Kabupaten 1', 'ad_art' => 'Ada, lengkap',
]);
$unassigned = PengurusKoperasi::create([
...$this->input, 'nama_koperasi' => 'Koperasi Cahaya', 'jenis_koperasi' => 'Koperasi Pemasaran',
'alamat_koperasi' => 'Desa 1, Kecamatan 1, Kabupaten 1', 'ad_art' => 'Ada, lengkap',
]);
$filters = array_intersect_key($location, array_flip($keys));
$response = $this->get(route('koperasi.pengurus.index', [
...$filters, 'jenis_koperasi' => 'Koperasi Pemasaran', 'ad_art' => 'Ada, lengkap', 'search' => 'Cahaya',
]))->assertOk()->assertViewHas('totalKoperasi', 3);
expect($response->viewData('koperasi')->modelKeys())->toBe(count($keys) === 1 ? [$unassigned->id, $match->id] : [$match->id]);
$dom = ($this->dom)($response->getContent());
foreach ($filters as $field => $value) {
$control = $field === 'kabupaten_kota_id'
? '//input[@type="hidden" and @name="'.$field.'" and @value="'.$value.'"]'
: '//select[@name="'.$field.'"]/option[@value="'.$value.'" and @selected]';
expect($dom->query('//form[@id="koperasi-filter-form"]'.$control)->length)->toBe(1)
->and($dom->query('//form[@id="koperasi-pagination-form"]//input[@type="hidden" and @name="'.$field.'" and @value="'.$value.'"]')->length)->toBe(1);
}
})->with([
'district' => [['kabupaten_kota_id']],
'subdistrict' => [['kabupaten_kota_id', 'kecamatan_id']],
'village' => [['kabupaten_kota_id', 'kecamatan_id', 'desa_kelurahan_id']],
]);
test('koperasi captures the creator on create and retains that enumerator when another user updates location', function () {
$location = ($this->location)('1');
$other = ($this->location)('2');
$this->post(route('koperasi.pengurus.store'), [...$this->input, ...$location, 'created_by' => 999])
->assertSessionHasNoErrors()->assertRedirect();
$profile = PengurusKoperasi::firstOrFail();
expect($profile->created_by)->toBe(7)->and($profile->desa_kelurahan_id)->toBe($location['desa_kelurahan_id']);
$editor = User::create(['uid' => 'editor', 'username' => 'editor', 'name' => 'Editor Lain', 'email' => 'editor@example.test', 'password' => 'test', 'role' => 'admin']);
$this->be($editor);
$this->put(route('koperasi.pengurus.update', $profile), [...$this->input, ...$other, 'created_by' => $editor->id])
->assertSessionHasNoErrors();
expect($profile->fresh()->created_by)->toBe(7)->and($profile->fresh()->desa_kelurahan_id)->toBe($other['desa_kelurahan_id']);
$dom = ($this->dom)($this->get(route('koperasi.pengurus.index'))->assertOk()->getContent());
expect(trim($dom->query('//table/tbody/tr[1]/td[7]')->item(0)->textContent))->toBe('Admin Test');
$dom = ($this->dom)($this->get(route('koperasi.pengurus.edit', $profile))->assertOk()->getContent());
foreach ($other as $field => $value) {
expect($dom->query('//form[@data-koperasi-profile]//select[@name="'.$field.'"]/option[@value="'.$value.'" and @selected]')->length)->toBe(1);
}
$this->put(route('koperasi.pengurus.update', $profile), [...$this->input, 'provinsi_id' => ''])->assertSessionHasNoErrors();
expect($profile->fresh()->desa_kelurahan_id)->toBeNull()->and($profile->fresh()->created_by)->toBe(7);
});
test('koperasi defaults to Ngada and retains the selected district like the other profile filters', function () {
$location = ($this->location)('1');
$other = ($this->location)('2');
KabupatenKota::findOrFail($location['kabupaten_kota_id'])->update(['nama' => 'Kabupaten Ngada']);
$match = PengurusKoperasi::create([...$this->input, 'desa_kelurahan_id' => $location['desa_kelurahan_id']]);
$outside = PengurusKoperasi::create([...$this->input, 'desa_kelurahan_id' => $other['desa_kelurahan_id']]);
$legacy = PengurusKoperasi::create([...$this->input, 'nama_koperasi' => 'Koperasi Lama Tanpa Desa']);
$response = $this->get(route('koperasi.pengurus.index'))->assertOk();
expect($response->viewData('koperasi')->modelKeys())->toBe([$legacy->id, $match->id]);
$dom = ($this->dom)($response->getContent());
expect($dom->query('//form[@id="koperasi-filter-form"]//input[@type="hidden" and @name="kabupaten_kota_id" and @value="'.$location['kabupaten_kota_id'].'"]')->length)->toBe(1)
->and(trim($dom->query('//*[@role="text" and @aria-label="Kabupaten/Kota"]')->item(0)->textContent))->toBe('Ngada');
$response = $this->get(route('koperasi.pengurus.index', ['kabupaten_kota_id' => $other['kabupaten_kota_id']]))->assertOk();
expect($response->viewData('koperasi')->modelKeys())->toBe([$legacy->id, $outside->id]);
});
test('koperasi unassigned regions stay discoverable while data filters and selected subdistricts remain strict', function () {
$location = ($this->location)('1');
KabupatenKota::findOrFail($location['kabupaten_kota_id'])->update(['nama' => 'Ngada']);
for ($i = 1; $i <= 12; $i++) {
PengurusKoperasi::create([...$this->input, 'nama_koperasi' => 'Koperasi Lama '.$i]);
}
$mapped = PengurusKoperasi::create([...$this->input, 'nama_koperasi' => 'Koperasi Terpetakan', 'desa_kelurahan_id' => $location['desa_kelurahan_id']]);
$response = $this->get(route('koperasi.pengurus.index', ['kabupaten_kota_id' => $location['kabupaten_kota_id'], 'page' => 2]))->assertOk();
expect($response->viewData('koperasi')->total())->toBe(13)->and($response->viewData('koperasi')->count())->toBe(3);
$response = $this->get(route('koperasi.pengurus.index', ['jenis_koperasi' => 'Koperasi Simpan Pinjam']))->assertOk();
expect($response->viewData('koperasi')->total())->toBe(0);
$response = $this->get(route('koperasi.pengurus.index', ['search' => 'Tidak cocok']))->assertOk();
expect($response->viewData('koperasi')->total())->toBe(0);
$response = $this->get(route('koperasi.pengurus.index', ['kabupaten_kota_id' => $location['kabupaten_kota_id'], 'kecamatan_id' => $location['kecamatan_id']]))->assertOk();
expect($response->viewData('koperasi')->modelKeys())->toBe([$mapped->id]);
});
test('koperasi rejects incomplete or mismatched regional hierarchy', function (string $scenario, string $field) {
$location = ($this->location)('1');
$other = ($this->location)('2');
$input = match ($scenario) {
'incomplete' => ['provinsi_id' => $location['provinsi_id']],
'province' => [...$location, 'provinsi_id' => $other['provinsi_id']],
'district' => [...$location, 'kecamatan_id' => $other['kecamatan_id']],
'village' => [...$location, 'desa_kelurahan_id' => $other['desa_kelurahan_id']],
};
$this->post(route('koperasi.pengurus.store'), [...$this->input, ...$input])->assertSessionHasErrors($field);
if (! in_array($scenario, ['incomplete', 'province'], true)) {
$this->getJson(route('koperasi.pengurus.index', $input))->assertUnprocessable()->assertJsonValidationErrors($field);
}
expect(PengurusKoperasi::count())->toBe(0);
})->with([
'incomplete profile location' => ['incomplete', 'desa_kelurahan_id'],
'wrong province for district' => ['province', 'kabupaten_kota_id'],
'wrong district for subdistrict' => ['district', 'kecamatan_id'],
'wrong subdistrict for village' => ['village', 'desa_kelurahan_id'],
]);
test('koperasi shows missing enumerators honestly and follows the same role visibility as other profiles', function () {
PengurusKoperasi::create($this->input);
$dom = ($this->dom)($this->get(route('koperasi.pengurus.index'))->assertOk()->getContent());
expect(trim($dom->query('//table/tbody/tr[1]/td[7]')->item(0)->textContent))->toBe('Belum terdata');
auth()->user()->role = 'enumerator';
$dom = ($this->dom)($this->get(route('koperasi.pengurus.index'))->assertOk()->getContent());
expect($dom->query('//table/thead/tr/th[text()="Nama Enumerator"]')->length)->toBe(0)
->and($dom->query('//table/tbody/tr[1]/td')->length)->toBe(7);
});
test('koperasi location dropdowns use existing master endpoints outside the active program scope', function () {
$location = ($this->location)('1');
app()->instance('SystemProgram', (object) ['id' => 999, 'nama' => 'Program Test']);
foreach ([
['kabupatenkota.byprovinsi', $location['provinsi_id'], $location['kabupaten_kota_id']],
['kecamatan.bykabupatenkota', $location['kabupaten_kota_id'], $location['kecamatan_id']],
['desakelurahan.bykecamatan', $location['kecamatan_id'], $location['desa_kelurahan_id']],
] as [$route, $parent, $expected]) {
$this->getJson(route($route, $parent).'?all_wilayah=1')->assertOk()->assertJsonFragment(['id' => $expected]);
}
$this->get(route('koperasi.pengurus.create'))->assertOk();
});
test('koperasi keeps existing pages and saves working before the metadata migration is applied', function () {
(require database_path('migrations/2026_11_06_190000_add_wilayah_and_enumerator_to_pengurus_koperasi_table.php'))->down();
$this->post(route('koperasi.pengurus.store'), $this->input)->assertSessionHasNoErrors();
$profile = PengurusKoperasi::firstOrFail();
$dom = ($this->dom)($this->get(route('koperasi.pengurus.index'))->assertOk()->getContent());
expect($dom->query('//select[@data-wilayah-kecamatan and @disabled]')->length)->toBe(1);
$dom = ($this->dom)($this->get(route('koperasi.pengurus.edit', $profile))->assertOk()->getContent());
expect($dom->query('//*[@data-koperasi-wilayah]')->length)->toBe(0);
$this->put(route('koperasi.pengurus.update', $profile), [...$this->input, 'nama_lengkap' => 'Perubahan tetap tersimpan'])
->assertSessionHasNoErrors();
expect($profile->fresh()->nama_lengkap)->toBe('Perubahan tetap tersimpan');
});
test('koperasi metadata migration preserves existing data and recovers only proven creation actors', function () {
$migration = require database_path('migrations/2026_11_06_190000_add_wilayah_and_enumerator_to_pengurus_koperasi_table.php');
$migration->down();
$original = PengurusKoperasi::create($this->input);
$editedOnly = PengurusKoperasi::create([...$this->input, 'nama_koperasi' => 'Hanya Log Perubahan']);
$unknownActor = PengurusKoperasi::create([...$this->input, 'nama_koperasi' => 'Penginput Tidak Diketahui']);
Schema::create('activity_log', function (Blueprint $table) {
$table->id();
$table->string('subject_type');
$table->unsignedBigInteger('subject_id');
$table->string('causer_type');
$table->unsignedBigInteger('causer_id');
$table->string('event');
});
foreach ([[$original->id, 'created', 7], [$original->id, 'updated', 999], [$editedOnly->id, 'updated', 7], [$unknownActor->id, 'created', 999]] as [$subject, $event, $actor]) {
DB::table('activity_log')->insert([
'subject_type' => PengurusKoperasi::class, 'subject_id' => $subject,
'causer_type' => User::class, 'causer_id' => $actor, 'event' => $event,
]);
}
$migration->up();
expect($original->fresh()->created_by)->toBe(7)->and($original->fresh()->desa_kelurahan_id)->toBeNull()
->and($editedOnly->fresh()->created_by)->toBeNull()->and($unknownActor->fresh()->created_by)->toBeNull()
->and(PengurusKoperasi::count())->toBe(3);
$migration->down();
expect(PengurusKoperasi::count())->toBe(3)->and($original->fresh()->nama_koperasi)->toBe('Koperasi Makmur');
});
test('koperasi summary shows organization activities and respondent contacts on every tab', function () {
$profile = PengurusKoperasi::create([
...$this->input, 'nama_koperasi' => 'Koperasi Makmur Bersama', 'jenis_koperasi' => 'Koperasi Jasa',
'alamat_koperasi' => 'Desa Maju, Kecamatan Sejahtera', 'no_hp' => '081234567890',
'jumlah_anggota_dewasa' => 1200, 'jumlah_anggota_anak' => 30, 'jumlah_pengurus' => 7,
'fokus_komoditas' => 'Ya', 'detail_fokus_komoditas' => "Kopi, Kakao; Vanili\nKemiri, Kopi",
'layanan_koperasi' => ['Simpan pinjam', 'Pengadaan input produksi', 'Pemasaran hasil', 'Lainnya'],
'layanan_lainnya' => 'Sewa alat', 'shu_2025' => 25000000, 'shu_2024' => 10000000,
]);
$routes = ['koperasi.pengurus.edit', ...array_map(fn ($tab) => $tab->routePrefix().'.index', TabKoperasi::cases())];
foreach ($routes as $route) {
$response = $this->get(route($route, $profile))->assertOk();
$dom = ($this->dom)($response->getContent());
$card = '//*[@id="koperasi-profile-summary"]';
foreach ([
'judul' => 'Koperasi Makmur Bersama', 'jenis' => 'Koperasi Jasa',
'alamat' => 'Desa Maju, Kecamatan Sejahtera', 'tahun_berdiri' => '2015',
'anggota' => '1.230', 'pengurus' => '7',
'responden' => 'Budi Santoso', 'jabatan' => 'Anggota koperasi', 'kontak' => '081234567890',
] as $field => $value) {
expect(trim($dom->query($card.'//*[@data-summary-field="'.$field.'"]')->item(0)->textContent))->toBe($value);
}
expect($dom->query($card.'//*[@data-summary-field="diperbarui"]//time[@datetime]')->length)->toBe(1)
->and($dom->query($card.'//*[@data-summary-field="shu" or @data-summary-field="shu_label"]')->length)->toBe(0)
->and($dom->query($card.'//*[@data-summary-field="komoditas" or @data-summary-field="skala"]')->length)->toBe(0)
->and($dom->query($card.'//button[@data-bs-target="#koperasi-summary-extra-layanan" and normalize-space(.)="+2 lainnya"]')->length)->toBe(1)
->and($dom->query($card.'//*[@id="koperasi-summary-extra-layanan"]//span[text()="Lainnya: Sewa alat"]')->length)->toBe(1);
}
});
test('koperasi summary distinguishes missing counts from zero', function (array $input, array $expected) {
$profile = PengurusKoperasi::create([...$this->input, ...$input]);
$response = $this->get(route('koperasi.pengurus.edit', $profile))->assertOk();
$dom = ($this->dom)($response->getContent());
foreach ($expected as $field => $value) {
expect(trim($dom->query('//*[@id="koperasi-profile-summary"]//*[@data-summary-field="'.$field.'"]')->item(0)->textContent))->toBe($value);
}
})->with([
'zero members remain visible' => [
['jumlah_anggota_dewasa' => 0, 'jumlah_anggota_anak' => 0, 'jumlah_pengurus' => 0],
['anggota' => '0', 'pengurus' => '0'],
],
'incomplete counts' => [
['jumlah_anggota_dewasa' => 20, 'jumlah_anggota_anak' => null, 'jumlah_pengurus' => null],
['anggota' => 'Belum terisi', 'pengurus' => 'Belum terisi'],
],
'no counts' => [
['jumlah_anggota_dewasa' => null, 'jumlah_anggota_anak' => null, 'jumlah_pengurus' => null],
['anggota' => 'Belum terisi', 'pengurus' => 'Belum terisi'],
],
]);
test('koperasi summary escapes custom labels', function () {
$profile = PengurusKoperasi::create([
...$this->input, 'nama_koperasi' => '',
'fokus_komoditas' => 'Tidak', 'detail_fokus_komoditas' => 'Kopi, Kakao',
'layanan_koperasi' => ['Lainnya'], 'layanan_lainnya' => '
',
]);
$response = $this->get(route('koperasi.pengurus.edit', $profile))->assertOk();
$dom = ($this->dom)($response->getContent());
$card = '//*[@id="koperasi-profile-summary"]';
expect($dom->query($card.'//script | '.$card.'//img')->length)->toBe(0)
->and($dom->query($card.'//*[@data-summary-field="komoditas"]//span[contains(@class,"badge")]')->length)->toBe(0)
->and($dom->query($card.'//button[@data-bs-toggle="collapse"]')->length)->toBe(0);
});
test('koperasi profile saves custom types and updates the bound record without changing survey data', function () {
$this->post(route('koperasi.pengurus.store'), [
...$this->input, 'jenis_koperasi' => 'Lainnya', 'jenis_koperasi_lainnya' => 'Koperasi Jasa',
])->assertRedirect()->assertSessionHas('success', 'Profil koperasi berhasil ditambahkan.');
$profile = PengurusKoperasi::firstOrFail();
expect($profile->jenis_koperasi)->toBe('Koperasi Jasa')
->and($profile->posisi_dalam_koperasi)->toBe(PosisiKoperasi::ANGGOTA)
->and($profile->skala_operasi)->toBe(SkalaOperasiKoperasi::LEVEL_DESA)
->and($profile->jumlah_anggota_awal)->toBe(0)
->and($profile->profilLengkap())->toBeTrue();
$profile->update(['ad_art' => 'Ada, lengkap']);
$this->put(route('koperasi.pengurus.update', $profile->id), [
...$this->input, 'nama_lengkap' => 'Budi Baru', 'sejarah_lembaga' => 'Riwayat koperasi', 'ad_art' => 'Di luar tab profil',
])->assertRedirect(route('koperasi.pengurus.edit', $profile->id))->assertSessionHas('success', 'Profil koperasi berhasil diperbarui.');
expect($profile->fresh()->nama_lengkap)->toBe('Budi Baru')
->and($profile->fresh()->jenis_koperasi)->toBe('Koperasi Produksi')
->and($profile->fresh()->sejarah_lembaga)->toBe('Riwayat koperasi')
->and($profile->fresh()->ad_art->value)->toBe('Ada, lengkap')
->and(PengurusKoperasi::count())->toBe(1);
$this->get(route('koperasi.pengurus.edit', 999))->assertNotFound();
$this->put(route('koperasi.pengurus.update', 999), $this->input)->assertNotFound();
$this->get(route('koperasi.pengurus.index'))->assertOk()->assertSee('Data Koperasi')->assertSee('Koperasi Makmur')->assertSee('Koperasi Produksi');
});
test('koperasi profile rejects invalid selections with Indonesian validation messages', function () {
$response = $this->from(route('koperasi.pengurus.create'))->post(route('koperasi.pengurus.store'), [
...$this->input, 'nama_lengkap' => '', 'posisi_dalam_koperasi' => 'Pilihan salah',
'jenis_koperasi' => 'Pilihan salah', 'skala_operasi' => 'Pilihan salah', 'shu_2025' => 'abc',
]);
$response->assertRedirect(route('koperasi.pengurus.create'))->assertSessionHasErrors([
'nama_lengkap' => 'Nama responden wajib diisi.',
'posisi_dalam_koperasi' => 'Pilihan Posisi dalam koperasi tidak valid.',
'jenis_koperasi' => 'Pilihan Jenis koperasi tidak valid.',
'skala_operasi' => 'Pilihan Skala operasi koperasi tidak valid.',
'shu_2025' => 'SHU 2025 harus berupa angka.',
]);
expect(PengurusKoperasi::count())->toBe(0);
});
test('koperasi editing restores custom type and gives old form input precedence after validation fails', function () {
$profile = PengurusKoperasi::create([...$this->input, 'jenis_koperasi' => 'Koperasi Jasa']);
$editUrl = route('koperasi.pengurus.edit', $profile->id);
$dom = ($this->dom)($this->get($editUrl)->assertOk()->getContent());
expect($dom->query('//input[@name="jenis_koperasi_lainnya" and @value="Koperasi Jasa" and not(@disabled)]')->length)->toBe(1)
->and($dom->query('//select[@name="jenis_koperasi"]/option[@value="Lainnya" and @selected]')->length)->toBe(1)
->and($dom->query('//*[@aria-disabled="true"]')->length)->toBe(0);
$this->from($editUrl)->put(route('koperasi.pengurus.update', $profile->id), [
...$this->input, 'nama_lengkap' => 'Nama Koreksi', 'jenis_koperasi' => 'Koperasi Desa', 'shu_2025' => 'abc',
])->assertRedirect($editUrl)->assertSessionHasErrors('shu_2025');
$dom = ($this->dom)($this->get($editUrl)->assertOk()->getContent());
expect($dom->query('//input[@name="nama_lengkap" and @value="Nama Koreksi"]')->length)->toBe(1)
->and($dom->query('//select[@name="jenis_koperasi"]/option[@value="Koperasi Desa" and @selected]')->length)->toBe(1)
->and($dom->query('//input[@name="jenis_koperasi_lainnya" and @disabled]')->length)->toBe(1)
->and($profile->fresh()->jenis_koperasi)->toBe('Koperasi Jasa');
});
test('koperasi later tabs enforce profile completion for direct reads and writes', function () {
$profile = PengurusKoperasi::create([...$this->input, 'nama_lengkap' => '']);
foreach (TabKoperasi::cases() as $tab) {
$this->get(route('koperasi.pengurus.survey', [$profile->id, 'step' => $tab->value]))->assertForbidden();
$this->post(route('koperasi.pengurus.survey.store', [$profile->id, 'step' => $tab->value]), [])->assertForbidden();
$this->get(route($tab->routePrefix().'.index', $profile))->assertForbidden();
$this->post(route($tab->routePrefix().'.store', $profile), [])->assertForbidden();
}
$profile->update(['nama_lengkap' => 'Budi Santoso']);
foreach (TabKoperasi::cases() as $tab) {
$url = route($tab->routePrefix().'.index', $profile);
$this->get(route('koperasi.pengurus.survey', [$profile->id, 'step' => $tab->value]))->assertRedirect($url);
$this->get($url)->assertOk();
}
});
test('koperasi save stays on each active tab and history and commodity focus belong to profile', function () {
$response = $this->post(route('koperasi.pengurus.store'), [
...$this->input, 'sejarah_lembaga' => 'Didirikan untuk pemasaran bersama',
'fokus_komoditas' => 'Ya', 'detail_fokus_komoditas' => 'Kopi, Vanili',
]);
$profile = PengurusKoperasi::firstOrFail();
$response->assertRedirect(route('koperasi.pengurus.edit', $profile));
expect($profile->fokus_komoditas)->toBe(FokusKomoditasKoperasi::YA);
$this->get(route('koperasi.pengurus.edit', $profile))->assertOk()
->assertSee('Sejarah dan Tujuan Pendirian')->assertSee('Fokus Komoditas Pertanian')->assertSee('Kopi, Vanili');
foreach (TabKoperasi::cases() as $tab) {
$url = route($tab->routePrefix().'.index', $profile);
$this->post(route($tab->routePrefix().'.store', $profile), [
'nama_lengkap' => 'Tidak boleh ditimpa', 'sejarah_lembaga' => 'Tidak boleh dipindah lagi',
'fokus_komoditas' => 'Tidak', 'detail_fokus_komoditas' => 'Tidak boleh ditimpa',
])->assertRedirect($url)->assertSessionHas('success');
$dom = ($this->dom)($this->get($url)->assertOk()->getContent());
$actionCount = $tab === TabKoperasi::SOSIAL_EKONOMI ? 1 : 2;
expect($dom->query('//a[@href="'.route('koperasi.pengurus.index').'" and normalize-space(.)="Kembali"]')->length)->toBe($actionCount)
->and($dom->query('//button[@type="submit" and normalize-space(.)="Simpan"]')->length)->toBe($actionCount)
->and($dom->query('//*[@name="sejarah_lembaga" or @name="fokus_komoditas" or @name="detail_fokus_komoditas"]')->length)->toBe(0)
->and($dom->query('//a[contains(@class,"nav-link") and contains(@class,"active") and @href="'.$url.'"]')->length)->toBe(1);
}
expect($profile->fresh()->nama_lengkap)->toBe('Budi Santoso')
->and($profile->fresh()->sejarah_lembaga)->toBe('Didirikan untuk pemasaran bersama')
->and($profile->fresh()->fokus_komoditas)->toBe(FokusKomoditasKoperasi::YA)
->and($profile->fresh()->detail_fokus_komoditas)->toBe('Kopi, Vanili');
});
test('all koperasi rupiah inputs accept thousands separators and render formatted persisted values', function () {
$this->post(route('koperasi.pengurus.store'), [
...$this->input, 'shu_2025' => '1.234.567', 'shu_2024' => '0', 'shu_2023' => '-1.250.000',
])->assertSessionHasNoErrors();
$profile = PengurusKoperasi::firstOrFail();
$profileDom = ($this->dom)($this->get(route('koperasi.pengurus.edit', $profile))->assertOk()->getContent());
expect($profileDom->query('//input[@data-koperasi-rupiah and @name="shu_2025" and @value="1.234.567" and @type="text"]')->length)->toBe(1)
->and((int) $profile->shu_2025)->toBe(1234567)
->and((int) $profile->shu_2024)->toBe(0)
->and((int) $profile->shu_2023)->toBe(-1250000);
$prices = [];
foreach (['segar', 'basah', 'kering', 'fermentasi'] as $bentuk) {
foreach (['sekarang', '1_tahun', '2_tahun'] as $periode) {
$prices['harga_'.$bentuk.'_'.$periode] = '1.234.567';
}
}
$this->post(route(TabKoperasi::RANTAI_NILAI->routePrefix().'.store', $profile), [
...$prices, 'volume_beli_sekarang' => 1000,
'bentuk_pembelian' => array_map(fn ($case) => $case->value, BentukProdukKoperasi::cases()),
])->assertRedirect(route(TabKoperasi::RANTAI_NILAI->routePrefix().'.index', $profile))->assertSessionHasNoErrors();
foreach ($prices as $field => $formatted) {
expect((int) $profile->fresh()->{$field})->toBe((int) str_replace('.', '', $formatted));
}
$dom = ($this->dom)($this->get(route(TabKoperasi::RANTAI_NILAI->routePrefix().'.index', $profile))->assertOk()->getContent());
expect($dom->query('//input[@data-koperasi-rupiah]')->length)->toBe(12)
->and($dom->query('//input[@name="harga_segar_sekarang" and @value="1.234.567"]')->length)->toBe(1)
->and($dom->query('//input[@name="volume_beli_sekarang" and not(@data-koperasi-rupiah)]')->length)->toBe(1)
->and($dom->query('//input[@name="harga_premium_persen"]')->length)->toBe(0);
$this->post(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.store', $profile), [
'praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Ukuran biji', 'harga_premium_rupiah' => '10.000',
])->assertSessionHasNoErrors();
expect((int) $profile->fresh()->harga_premium_rupiah)->toBe(10000);
$opDom = ($this->dom)($this->get(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.index', $profile))->assertOk()->getContent());
expect($opDom->query('//input[@name="harga_premium_rupiah" and @data-koperasi-rupiah and @value="10.000"]')->length)->toBe(1)
->and($opDom->query('//input[@name="harga_premium_persen" and not(@data-koperasi-rupiah)]')->length)->toBe(1);
$this->post(route(TabKoperasi::SOSIAL_EKONOMI->routePrefix().'.store', $profile), ['simpanan_terbesar' => '2.500.000'])->assertSessionHasNoErrors();
expect((int) $profile->fresh()->simpanan_terbesar)->toBe(2500000);
$this->get(route(TabKoperasi::SOSIAL_EKONOMI->routePrefix().'.index', $profile))->assertOk()->assertSee('2.500.000');
});
test('koperasi tab validation rejects bad rupiah and clears unchecked saved selections', function () {
$profile = PengurusKoperasi::create([...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam']);
$route = TabKoperasi::SOSIAL_EKONOMI->routePrefix();
$this->post(route($route.'.store', $profile), ['alasan_pinjaman' => ['Produksi'], 'simpanan_terbesar' => '100.000'])->assertSessionHasNoErrors();
$this->from(route($route.'.index', $profile))->post(route($route.'.store', $profile), [
'simpanan_terbesar' => '1.23.000', 'alasan_pinjaman' => ['Pendidikan'],
])->assertSessionHasErrors('simpanan_terbesar');
expect((int) $profile->fresh()->simpanan_terbesar)->toBe(100000);
$dom = ($this->dom)($this->get(route($route.'.index', $profile))->assertOk()->getContent());
expect($dom->query('//input[@name="alasan_pinjaman[]" and @value="Pendidikan" and @checked]')->length)->toBe(1)
->and($dom->query('//input[@name="alasan_pinjaman[]" and @value="Produksi" and @checked]')->length)->toBe(0);
$this->post(route($route.'.store', $profile), [])->assertRedirect(route($route.'.index', $profile));
expect($profile->fresh()->alasan_pinjaman)->toHaveCount(0);
$this->post(route('koperasi.pengurus.survey.store', [$profile->id, 'step' => 3]), ['simpanan_terbesar' => '1.000.000'])
->assertRedirect(route($route.'.index', $profile))->assertSessionHasNoErrors();
expect((int) $profile->fresh()->simpanan_terbesar)->toBe(1000000);
});
test('koperasi profile hides and clears commodity details when its focus is disabled', function () {
$profile = PengurusKoperasi::create([
...$this->input, 'fokus_komoditas' => 'Ya', 'detail_fokus_komoditas' => 'Kopi',
]);
$this->put(route('koperasi.pengurus.update', $profile), [
...$this->input, 'fokus_komoditas' => 'Tidak', 'detail_fokus_komoditas' => 'Data lama',
])->assertRedirect(route('koperasi.pengurus.edit', $profile))->assertSessionHasNoErrors();
expect($profile->fresh()->detail_fokus_komoditas)->toBeNull();
$dom = ($this->dom)($this->get(route('koperasi.pengurus.edit', $profile))->assertOk()->getContent());
expect($dom->query('//input[@name="detail_fokus_komoditas" and @disabled]')->length)->toBe(1);
});
test('social covers savings loan needs and payment ties with the farmer group social icon', function () {
$profile = PengurusKoperasi::create([
...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam', 'ad_art' => 'Tidak ada', 'harga_segar_sekarang' => 12000,
]);
$prefix = TabKoperasi::SOSIAL_EKONOMI->routePrefix();
$input = [
'simpanan_terbesar' => '2.500.000', 'momen_simpanan_terbesar' => 'Juni, saat panen kopi',
'alasan_pinjaman' => ['Produksi', 'Pendidikan', 'Kesehatan', 'Acara keluarga', 'Renovasi', 'Sekunder', 'Lainnya'],
'alasan_pinjaman_lainnya' => 'Modal dagang', 'pembayaran_selain_uang' => 'Ada',
'bentuk_pembayaran_barang' => ['Hasil bumi', 'Ganti jasa', 'Lainnya'], 'bentuk_pembayaran_lainnya' => 'Barang dagangan',
'perhitungan_harga_pasar' => 'Tidak', 'alasan_bukan_harga_pasar' => 'Harga disepakati pada awal pinjaman',
];
$this->post(route($prefix.'.store', $profile), [...$input, 'ad_art' => 'Ada, lengkap', 'harga_segar_sekarang' => 1])
->assertSessionHasNoErrors()->assertRedirect(route($prefix.'.index', $profile));
$profile->refresh();
foreach ($input as $field => $expected) {
$actual = $profile->{$field};
if ($actual instanceof Collection) {
$actual = $actual->map(fn ($case) => $case->value)->all();
} elseif ($actual instanceof BackedEnum) {
$actual = $actual->value;
}
if ($field === 'simpanan_terbesar') {
$actual = (int) $actual;
$expected = 2500000;
}
expect($actual)->toBe($expected);
}
expect($profile->ad_art->value)->toBe('Tidak ada')->and((int) $profile->harga_segar_sekarang)->toBe(12000)
->and(json_decode($profile->getRawOriginal('alasan_pinjaman'), true))->toBe($input['alasan_pinjaman'])
->and(json_decode($profile->getRawOriginal('bentuk_pembayaran_barang'), true))->toBe($input['bentuk_pembayaran_barang']);
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
expect($dom->query('//a[@href="'.route($prefix.'.index', $profile).'"]//i[contains(@class,"tabler-heart-handshake")]')->length)->toBe(1)
->and($dom->query('//form[@data-koperasi-sosial-ekonomi]//input[@type="checkbox" and @checked and not(@disabled)]')->length)->toBe(10)
->and($dom->query('//input[@name="simpanan_terbesar" and @data-koperasi-rupiah and @value="2.500.000"]')->length)->toBe(1)
->and($dom->query('//select[@name="perhitungan_harga_pasar"]//option[@value="Tidak" and @selected]')->length)->toBe(1)
->and($dom->query('//textarea[@name="alasan_bukan_harga_pasar" and @required and not(@disabled)]')->length)->toBe(1);
foreach ($dom->query('//form[@data-koperasi-sosial-ekonomi]//label | //form[@data-koperasi-sosial-ekonomi]//legend') as $label) {
expect($label->textContent)->not->toContain('?');
}
foreach ($dom->query('//form[@data-koperasi-sosial-ekonomi]//h5') as $heading) {
expect(trim($heading->textContent))->not->toMatch('/^(K\d|\d+[.])/');
}
});
test('social follow ups and choices validate only applicable questions', function (array $input, string $field) {
$profile = PengurusKoperasi::create([...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam']);
$this->post(route(TabKoperasi::SOSIAL_EKONOMI->routePrefix().'.store', $profile), $input)->assertSessionHasErrors($field);
expect($profile->fresh()->pembayaran_selain_uang)->toBeNull()->and($profile->fresh()->simpanan_terbesar)->toBeNull();
})->with([
'other loan need requires detail' => [['alasan_pinjaman' => ['Lainnya']], 'alasan_pinjaman_lainnya'],
'non cash payments require forms' => [['pembayaran_selain_uang' => 'Ada'], 'bentuk_pembayaran_barang'],
'other payment form requires detail' => [['pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Lainnya']], 'bentuk_pembayaran_lainnya'],
'harvest payments require price basis' => [['pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Hasil bumi']], 'perhitungan_harga_pasar'],
'not using market price requires reason' => [['pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Hasil bumi'], 'perhitungan_harga_pasar' => 'Tidak'], 'alasan_bukan_harga_pasar'],
'invalid loan need' => [['alasan_pinjaman' => ['Pilihan salah']], 'alasan_pinjaman.0'],
'invalid payment mechanism' => [['pembayaran_selain_uang' => 'Pilihan salah'], 'pembayaran_selain_uang'],
'invalid payment form' => [['pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Pilihan salah']], 'bentuk_pembayaran_barang.0'],
'invalid price basis' => [['pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Hasil bumi'], 'perhitungan_harga_pasar' => 'Pilihan salah'], 'perhitungan_harga_pasar'],
'negative savings' => [['simpanan_terbesar' => -1000], 'simpanan_terbesar'],
]);
test('social loan questions use saved cooperative type rather than services or a forged request type', function (string $type, array $services, bool $isCreditCooperative) {
$profile = PengurusKoperasi::create([
...$this->input, 'jenis_koperasi' => $type, 'layanan_koperasi' => $services,
'alasan_pinjaman' => ['Lainnya'], 'alasan_pinjaman_lainnya' => 'Kebutuhan lama',
'pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Hasil bumi', 'Lainnya'],
'bentuk_pembayaran_lainnya' => 'Barang lama', 'perhitungan_harga_pasar' => 'Tidak', 'alasan_bukan_harga_pasar' => 'Alasan lama',
]);
$prefix = TabKoperasi::SOSIAL_EKONOMI->routePrefix();
$input = $isCreditCooperative ? [
'jenis_koperasi' => 'Koperasi Produksi', 'alasan_pinjaman' => ['Pendidikan'], 'pembayaran_selain_uang' => 'Tidak ada',
] : [
'jenis_koperasi' => 'Koperasi Simpan Pinjam', 'alasan_pinjaman' => 'Pilihan salah', 'alasan_pinjaman_lainnya' => ['data tersembunyi'],
'pembayaran_selain_uang' => 'Pilihan salah', 'bentuk_pembayaran_barang' => ['Pilihan salah'],
'bentuk_pembayaran_lainnya' => ['data tersembunyi'], 'perhitungan_harga_pasar' => 'Pilihan salah', 'alasan_bukan_harga_pasar' => ['data tersembunyi'],
];
$this->post(route($prefix.'.store', $profile), [...$input, 'simpanan_terbesar' => '125.000', 'momen_simpanan_terbesar' => 'Panen'])
->assertSessionHasNoErrors()->assertRedirect(route($prefix.'.index', $profile));
$profile->refresh();
expect($profile->jenis_koperasi)->toBe($type)->and((int) $profile->simpanan_terbesar)->toBe(125000)
->and($profile->momen_simpanan_terbesar)->toBe('Panen')
->and($profile->alasan_pinjaman->map(fn ($case) => $case->value)->all())->toBe($isCreditCooperative ? ['Pendidikan'] : [])
->and($profile->pembayaran_selain_uang?->value)->toBe($isCreditCooperative ? 'Tidak ada' : null)
->and($profile->alasan_pinjaman_lainnya)->toBeNull()->and($profile->bentuk_pembayaran_barang)->toHaveCount(0)
->and($profile->bentuk_pembayaran_lainnya)->toBeNull()->and($profile->perhitungan_harga_pasar)->toBeNull()
->and($profile->alasan_bukan_harga_pasar)->toBeNull();
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
expect($dom->query('//input[@name="alasan_pinjaman[]" and not(@disabled)]')->length)->toBe($isCreditCooperative ? 7 : 0)
->and($dom->query('//select[@name="pembayaran_selain_uang" and not(@disabled)]')->length)->toBe($isCreditCooperative ? 1 : 0)
->and($dom->query('//h5[text()="Ikatan Keuangan"]')->length)->toBe($isCreditCooperative ? 1 : 0);
})->with([
'production cooperative with credit service' => ['Koperasi Produksi', ['Simpan pinjam'], false],
'credit cooperative without selected services' => ['Koperasi Simpan Pinjam', [], true],
]);
test('social stops payment follow ups when non cash payments are unavailable', function () {
$profile = PengurusKoperasi::create([
...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam',
'pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Hasil bumi', 'Lainnya'],
'bentuk_pembayaran_lainnya' => 'Barang', 'perhitungan_harga_pasar' => 'Tidak', 'alasan_bukan_harga_pasar' => 'Kesepakatan',
]);
$prefix = TabKoperasi::SOSIAL_EKONOMI->routePrefix();
$this->post(route($prefix.'.store', $profile), [
'alasan_pinjaman' => ['Kesehatan'], 'pembayaran_selain_uang' => 'Tidak ada',
'bentuk_pembayaran_barang' => 'data tersembunyi', 'bentuk_pembayaran_lainnya' => ['data tersembunyi'],
'perhitungan_harga_pasar' => ['data tersembunyi'], 'alasan_bukan_harga_pasar' => ['data tersembunyi'],
])->assertSessionHasNoErrors();
$profile->refresh();
expect($profile->pembayaran_selain_uang->value)->toBe('Tidak ada')->and($profile->bentuk_pembayaran_barang)->toHaveCount(0)
->and($profile->bentuk_pembayaran_lainnya)->toBeNull()->and($profile->perhitungan_harga_pasar)->toBeNull()
->and($profile->alasan_bukan_harga_pasar)->toBeNull()->and($profile->alasan_pinjaman->first()->value)->toBe('Kesehatan');
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
expect($dom->query('//input[@name="bentuk_pembayaran_barang[]" and @disabled]')->length)->toBe(3)
->and($dom->query('//select[@name="perhitungan_harga_pasar" and @disabled]')->length)->toBe(1)
->and($dom->query('//textarea[@name="alasan_bukan_harga_pasar" and @disabled]')->length)->toBe(1);
});
test('social skips harvest prices for services and clears old price reasons when market prices apply', function (array $input, ?string $expectedPrice) {
$profile = PengurusKoperasi::create([
...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam',
'bentuk_pembayaran_lainnya' => 'Barang lama', 'alasan_bukan_harga_pasar' => 'Kesepakatan lama',
]);
$prefix = TabKoperasi::SOSIAL_EKONOMI->routePrefix();
$this->post(route($prefix.'.store', $profile), [
'pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_lainnya' => ['data tersembunyi'],
'alasan_bukan_harga_pasar' => ['data tersembunyi'], ...$input,
])->assertSessionHasNoErrors();
$profile->refresh();
expect($profile->perhitungan_harga_pasar?->value)->toBe($expectedPrice)
->and($profile->bentuk_pembayaran_lainnya)->toBeNull()->and($profile->alasan_bukan_harga_pasar)->toBeNull();
})->with([
'services only' => [['bentuk_pembayaran_barang' => ['Ganti jasa'], 'perhitungan_harga_pasar' => ['data tersembunyi']], null],
'harvest valued at market prices' => [['bentuk_pembayaran_barang' => ['Hasil bumi'], 'perhitungan_harga_pasar' => 'Ya'], 'Ya'],
]);
test('social restores active draft payment answers and Indonesian validation errors', function () {
$profile = PengurusKoperasi::create([
...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam', 'pembayaran_selain_uang' => 'Tidak ada',
]);
$prefix = TabKoperasi::SOSIAL_EKONOMI->routePrefix();
$this->from(route($prefix.'.index', $profile))->post(route($prefix.'.store', $profile), [
'pembayaran_selain_uang' => 'Ada', 'bentuk_pembayaran_barang' => ['Hasil bumi'], 'perhitungan_harga_pasar' => 'Tidak',
])->assertRedirect(route($prefix.'.index', $profile))->assertSessionHasErrors('alasan_bukan_harga_pasar');
$response = $this->get(route($prefix.'.index', $profile))->assertOk()->assertSee('Alasan Tidak Menggunakan Harga Pasar wajib diisi.');
$dom = ($this->dom)($response->getContent());
expect($dom->query('//select[@name="pembayaran_selain_uang"]/option[@value="Ada" and @selected]')->length)->toBe(1)
->and($dom->query('//input[@name="bentuk_pembayaran_barang[]" and @value="Hasil bumi" and @checked and not(@disabled)]')->length)->toBe(1)
->and($dom->query('//select[@name="perhitungan_harga_pasar"]/option[@value="Tidak" and @selected]')->length)->toBe(1)
->and($dom->query('//textarea[@name="alasan_bukan_harga_pasar" and @required and not(@disabled) and contains(@class,"is-invalid")]')->length)->toBe(1)
->and($profile->fresh()->pembayaran_selain_uang->value)->toBe('Tidak ada');
});
test('koperasi operational choices keep their stored values and render concise field labels', function () {
$profile = PengurusKoperasi::create([...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam']);
$prefix = TabKoperasi::KELEMBAGAAN->routePrefix();
$input = [
'ad_art' => 'Ada, lengkap', 'tahun_ad_art' => 2024,
'layanan_koperasi' => ['Simpan pinjam', 'Lainnya'], 'layanan_lainnya' => 'Pendampingan usaha',
'pelatihan_produksi' => 'Ya', 'pelatihan_pascapanen' => 'Tidak',
'pelatihan_pengolahan' => 'Ya', 'pelatihan_keuangan' => 'Ya', 'pelatihan_pemasaran' => 'Tidak',
'status_pembukuan' => 'Sudah', 'format_pembukuan' => 'Digital/file Excel',
'pertemuan_rutin' => 'Lainnya', 'pertemuan_lainnya' => 'Tiga bulan sekali',
'kasus_kredit_macet' => '<10%', 'sanksi_kredit_macet' => 'Ada', 'efektivitas_sanksi' => 'Sangat efektif',
'faktor_manajemen' => ['Kepemimpinan koperasi', 'Lainnya'], 'faktor_manajemen_lainnya' => 'Komunikasi',
'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi', 'Lainnya'],
'agregasi_komoditas_lainnya' => 'Padi', 'fasilitas_koperasi' => ['Gudang penyimpanan', 'Lainnya'],
'fasilitas_lainnya' => 'Timbangan', 'pengolahan_lanjutan' => 'Ya',
'detail_pengolahan_lanjutan' => 'Fermentasi', 'cara_penentuan_harga' => 'Harga pasar dan mutu produk',
'kontrak_offtaker' => 'Ada', 'kontrak_komoditas' => 'Kopi',
];
$this->post(route($prefix.'.store', $profile), $input)
->assertRedirect(route($prefix.'.index', $profile))->assertSessionHasNoErrors();
$profile->refresh();
foreach ($input as $field => $value) {
$stored = $profile->{$field};
if ($stored instanceof BackedEnum) {
$stored = $stored->value;
}
if ($stored instanceof Collection) {
$stored = $stored->map(fn ($item) => $item->value)->all();
}
expect($stored)->toBe($value);
}
$response = $this->get(route($prefix.'.index', $profile))->assertOk()
->assertSee('Dokumen Kelembagaan')->assertDontSee('Dokumen Kelembagaan (AD/ART)')
->assertDontSee('Data dokumen, layanan, pengelolaan, dan kegiatan usaha koperasi.');
$dom = ($this->dom)($response->getContent());
foreach ($dom->query('//form[@data-koperasi-operasional]//label | //form[@data-koperasi-operasional]//legend') as $label) {
expect($label->textContent)->not->toContain('?');
}
expect($dom->query('//select[@name="ad_art"]/option[@value="Ada, lengkap" and @selected]')->length)->toBe(1)
->and($dom->query('//input[@name="layanan_koperasi[]" and @value="Simpan pinjam" and @checked]')->length)->toBe(1)
->and($dom->query('//input[@name="tahun_ad_art" and @value="2024" and not(@disabled)]')->length)->toBe(1);
$this->post(route($prefix.'.store', $profile), [])->assertSessionHasNoErrors();
expect($profile->fresh()->layanan_koperasi)->toHaveCount(0);
});
test('operational separates management and post harvest into independent cards with actions', function () {
$profile = PengurusKoperasi::create($this->input);
$prefix = TabKoperasi::KELEMBAGAAN->routePrefix();
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
foreach (['pengelolaan', 'rantai-nilai'] as $section) {
$form = '//section[@id="section-operasional-'.$section.'"]//form';
expect($dom->query($form)->length)->toBe(1)
->and($dom->query($form.'//input[@name="section" and @value="'.$section.'"]')->length)->toBe(1)
->and($dom->query($form.'//button[@type="submit"]')->length)->toBe(1)
->and($dom->query($form.'//a[@href="'.route('koperasi.pengurus.index').'"]')->length)->toBe(1);
}
expect($dom->query('//form//form')->length)->toBe(0)
->and($dom->query('//section[@id="section-operasional-pengelolaan"]//select[@name="ad_art"]')->length)->toBe(1)
->and($dom->query('//section[@id="section-operasional-pengelolaan"]//input[@name="keterlibatan_rantai_nilai[]"]')->length)->toBe(0)
->and($dom->query('//section[@id="section-operasional-rantai-nilai"]//h5[text()="Rantai Nilai dan Pascapanen"]')->length)->toBe(1)
->and($dom->query('//section[@id="section-operasional-rantai-nilai"]//select[@name="praktik_sortasi"]')->length)->toBe(1)
->and($dom->query('//section[@id="section-operasional-rantai-nilai"]//input[@name="harga_premium_rupiah"]')->length)->toBe(1);
});
test('operational saves each card without changing or validating answers from the other card', function (string $section, array $input, string $changedField, string $changedValue) {
$management = [
'ad_art' => 'Ada, lengkap', 'tahun_ad_art' => 2024,
'layanan_koperasi' => ['Lainnya'], 'layanan_lainnya' => 'Pendampingan', 'pelatihan_produksi' => 'Ya',
'status_pembukuan' => 'Sudah', 'format_pembukuan' => 'Digital/file Excel',
'pertemuan_rutin' => 'Lainnya', 'pertemuan_lainnya' => 'Tiga bulan sekali',
'faktor_manajemen' => ['Lainnya'], 'faktor_manajemen_lainnya' => 'Komunikasi',
];
$postHarvest = [
'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Lainnya'], 'agregasi_komoditas_lainnya' => 'Padi',
'fasilitas_koperasi' => ['Lainnya'], 'fasilitas_lainnya' => 'Timbangan',
'pengolahan_lanjutan' => 'Ya', 'detail_pengolahan_lanjutan' => 'Fermentasi',
'cara_penentuan_harga' => 'Harga pasar', 'kontrak_offtaker' => 'Ada', 'kontrak_komoditas' => 'Padi',
'praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Mutu',
'praktik_pengeringan' => 'Kadang', 'cara_pengeringan' => 'Lantai jemur',
'praktik_fermentasi' => 'Pernah coba', 'model_fermentasi' => ['Kolektif'], 'proses_fermentasi' => 'Kelompok',
'harga_premium_rupiah' => 12500,
];
$profile = PengurusKoperasi::create([...$this->input, ...$management, ...$postHarvest]);
$preserved = $section === 'pengelolaan' ? $postHarvest : $management;
$original = array_intersect_key($profile->fresh()->getRawOriginal(), $preserved);
$prefix = TabKoperasi::KELEMBAGAAN->routePrefix();
$this->post(route($prefix.'.store', $profile), ['section' => $section, ...$input])
->assertSessionHasNoErrors()->assertRedirect(route($prefix.'.index', $profile).'#section-operasional-rantai-nilai');
$profile->refresh();
expect(array_intersect_key($profile->getRawOriginal(), $preserved))->toBe($original)
->and($profile->{$changedField}->value)->toBe($changedValue);
})->with([
'management preserves post harvest including premiums' => [
'pengelolaan', [
'status_pembukuan' => 'Belum ada', 'layanan_koperasi' => ['Simpan pinjam'],
'keterlibatan_rantai_nilai' => ['Agregasi', 'Tidak terlibat kegiatan di atas'],
'praktik_sortasi' => 'Pilihan salah', 'harga_premium_rupiah' => -1,
], 'status_pembukuan', 'Belum ada',
],
'post harvest preserves documents services and management' => [
'rantai-nilai', [
'keterlibatan_rantai_nilai' => ['Tidak terlibat kegiatan di atas'], 'praktik_sortasi' => 'Belum pernah',
'ad_art' => 'Pilihan salah', 'layanan_koperasi' => 'Pilihan salah',
'status_pembukuan' => 'Sudah', 'format_pembukuan' => 'Tidak ada',
], 'praktik_sortasi', 'Belum pernah',
],
]);
test('operational validation stays at its card and keeps saved values visible in the other card', function (string $section, array $input, string $field) {
$profile = PengurusKoperasi::create([
...$this->input, 'layanan_koperasi' => ['Lainnya'], 'layanan_lainnya' => 'Pendampingan',
'praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Mutu',
]);
$original = $profile->fresh()->getRawOriginal();
$prefix = TabKoperasi::KELEMBAGAAN->routePrefix();
$this->post(route($prefix.'.store', $profile), ['section' => $section, ...$input])
->assertSessionHasErrors($field)->assertRedirect(route($prefix.'.index', $profile).'#section-operasional-'.$section);
expect($profile->fresh()->getRawOriginal())->toBe($original);
$otherField = $section === 'pengelolaan' ? 'standar_sortasi' : 'layanan_lainnya';
$otherValue = $section === 'pengelolaan' ? 'Mutu' : 'Pendampingan';
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
expect($dom->query('//input[@name="'.$otherField.'" and @value="'.$otherValue.'" and not(@disabled)]')->length)->toBe(1);
})->with([
'document needs year' => ['pengelolaan', ['ad_art' => 'Ada, lengkap'], 'tahun_ad_art'],
'existing bookkeeping needs its format' => ['pengelolaan', ['status_pembukuan' => 'Sudah', 'format_pembukuan' => 'Tidak ada'], 'format_pembukuan'],
'aggregation needs commodities' => ['rantai-nilai', ['keterlibatan_rantai_nilai' => ['Agregasi']], 'agregasi_komoditas'],
'sorting needs standards' => ['rantai-nilai', ['praktik_sortasi' => 'Ya rutin'], 'standar_sortasi'],
'premium units are exclusive' => ['rantai-nilai', ['praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Mutu', 'harga_premium_rupiah' => 1000, 'harga_premium_persen' => 10], 'harga_premium_persen'],
]);
test('operational rejects unknown card sections without saving', function () {
$profile = PengurusKoperasi::create($this->input);
$this->post(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.store', $profile), [
'section' => 'invalid', 'pelatihan_produksi' => 'Ya',
])->assertSessionHasErrors('section');
expect($profile->fresh()->pelatihan_produksi)->toBeNull();
});
test('koperasi operational validation preserves draft choices and marks invalid fields', function () {
$profile = PengurusKoperasi::create($this->input);
$prefix = TabKoperasi::KELEMBAGAAN->routePrefix();
$url = route($prefix.'.index', $profile);
$this->from($url)->post(route($prefix.'.store', $profile), [
'ad_art' => 'Pilihan salah', 'tahun_ad_art' => 'bukan tahun',
'layanan_koperasi' => ['Simpan pinjam'], 'fasilitas_koperasi' => ['Pilihan salah'],
'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'],
])->assertRedirect($url)->assertSessionHasErrors(['ad_art', 'fasilitas_koperasi.0']);
$response = $this->get($url)->assertOk()->assertSee('Pilihan Status AD/ART tidak valid.');
$dom = ($this->dom)($response->getContent());
expect($dom->query('//input[@name="layanan_koperasi[]" and @value="Simpan pinjam" and @checked]')->length)->toBe(1)
->and($dom->query('//select[@name="ad_art" and contains(@class,"is-invalid")]')->length)->toBe(1)
->and($profile->fresh()->ad_art)->toBeNull();
});
test('operational follow up details are required only for applicable answers', function (array $input, string $field) {
$profile = PengurusKoperasi::create([...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam']);
$url = route(TabKoperasi::KELEMBAGAAN->routePrefix().'.index', $profile);
$this->from($url)->post(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.store', $profile), $input)
->assertRedirect($url)->assertSessionHasErrors($field);
})->with([
'complete documents need an update year' => [['ad_art' => 'Ada, lengkap'], 'tahun_ad_art'],
'update year cannot be zero' => [['ad_art' => 'Ada, lengkap', 'tahun_ad_art' => 0], 'tahun_ad_art'],
'other services need a description' => [['layanan_koperasi' => ['Lainnya']], 'layanan_lainnya'],
'existing bookkeeping needs a format' => [['status_pembukuan' => 'Sudah'], 'format_pembukuan'],
'existing bookkeeping cannot have no format' => [['status_pembukuan' => 'Sudah', 'format_pembukuan' => 'Tidak ada'], 'format_pembukuan'],
'other meeting frequency needs a description' => [['pertemuan_rutin' => 'Lainnya'], 'pertemuan_lainnya'],
'sanctions need an effectiveness answer' => [['sanksi_kredit_macet' => 'Ada'], 'efektivitas_sanksi'],
'other management factors need a description' => [['faktor_manajemen' => ['Lainnya']], 'faktor_manajemen_lainnya'],
'aggregation role needs a commodity answer' => [['keterlibatan_rantai_nilai' => ['Agregasi']], 'agregasi_komoditas'],
'other aggregation commodity needs a description' => [['keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Lainnya']], 'agregasi_komoditas_lainnya'],
'other facilities need a description' => [['keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'], 'fasilitas_koperasi' => ['Lainnya']], 'fasilitas_lainnya'],
'processing needs a description when yes' => [['keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'], 'pengolahan_lanjutan' => 'Ya'], 'detail_pengolahan_lanjutan'],
'offtaker contract needs its commodity' => [['keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'], 'kontrak_offtaker' => 'Ada'], 'kontrak_komoditas'],
]);
test('credit questions use the saved cooperative type instead of selected services', function (string $jenis) {
$profile = PengurusKoperasi::create([
...$this->input, 'jenis_koperasi' => $jenis, 'kasus_kredit_macet' => '<10%',
'sanksi_kredit_macet' => 'Ada', 'efektivitas_sanksi' => 'Efektif',
]);
$prefix = TabKoperasi::KELEMBAGAAN->routePrefix();
$this->post(route($prefix.'.store', $profile), [
'jenis_koperasi' => 'Koperasi Simpan Pinjam', 'layanan_koperasi' => ['Simpan pinjam'],
'kasus_kredit_macet' => 'Pilihan salah', 'sanksi_kredit_macet' => 'Ada', 'efektivitas_sanksi' => 'Pilihan salah',
])->assertRedirect(route($prefix.'.index', $profile))->assertSessionHasNoErrors();
$profile->refresh();
expect($profile->jenis_koperasi)->toBe($jenis)->and($profile->kasus_kredit_macet)->toBeNull()
->and($profile->sanksi_kredit_macet)->toBeNull()->and($profile->efektivitas_sanksi)->toBeNull();
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
expect($dom->query('//select[@name="kasus_kredit_macet" and @disabled]')->length)->toBe(1)
->and($dom->query('//select[@name="sanksi_kredit_macet" and @disabled]')->length)->toBe(1);
})->with(['Koperasi Produksi', 'Koperasi Pemasaran', 'Koperasi Desa', 'Koperasi Jasa']);
test('savings cooperatives see credit questions without selecting savings services and no sanctions skips effectiveness', function () {
$profile = PengurusKoperasi::create([
...$this->input, 'jenis_koperasi' => 'Koperasi Simpan Pinjam', 'layanan_koperasi' => [],
'sanksi_kredit_macet' => 'Ada', 'efektivitas_sanksi' => 'Efektif',
]);
$prefix = TabKoperasi::KELEMBAGAAN->routePrefix();
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
expect($dom->query('//select[@name="kasus_kredit_macet" and not(@disabled)]')->length)->toBe(1);
$this->post(route($prefix.'.store', $profile), [
'kasus_kredit_macet' => '10-25%', 'sanksi_kredit_macet' => 'Tidak ada', 'efektivitas_sanksi' => ['data lama'],
])->assertSessionHasNoErrors();
expect($profile->fresh()->kasus_kredit_macet->value)->toBe('10-25%')
->and($profile->fresh()->efektivitas_sanksi)->toBeNull();
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
expect($dom->query('//select[@name="efektivitas_sanksi" and @disabled]')->length)->toBe(1);
});
test('changing operational answers clears obsolete details including malicious hidden inputs', function () {
$profile = PengurusKoperasi::create([
...$this->input, 'ad_art' => 'Ada, lengkap', 'tahun_ad_art' => 2024,
'layanan_koperasi' => ['Lainnya'], 'layanan_lainnya' => 'Layanan lama',
'status_pembukuan' => 'Sudah', 'format_pembukuan' => 'Digital/file Excel',
'pertemuan_rutin' => 'Lainnya', 'pertemuan_lainnya' => 'Per tiga bulan',
'faktor_manajemen' => ['Lainnya'], 'faktor_manajemen_lainnya' => 'Faktor lama',
'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Lainnya'], 'agregasi_komoditas_lainnya' => 'Padi',
'fasilitas_koperasi' => ['Lainnya'], 'fasilitas_lainnya' => 'Alat lama',
'pengolahan_lanjutan' => 'Ya', 'detail_pengolahan_lanjutan' => 'Fermentasi',
'kontrak_offtaker' => 'Ada', 'kontrak_komoditas' => 'Kopi',
]);
$this->post(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.store', $profile), [
'ad_art' => 'Ada tapi tidak lengkap', 'tahun_ad_art' => 'bukan tahun',
'layanan_koperasi' => ['Pemasaran hasil'], 'layanan_lainnya' => ['payload tersembunyi'],
'status_pembukuan' => 'Belum ada', 'format_pembukuan' => 'Pilihan salah',
'pertemuan_rutin' => 'Satu tahun sekali', 'pertemuan_lainnya' => ['payload tersembunyi'],
'faktor_manajemen' => ['Rasa saling percaya'], 'faktor_manajemen_lainnya' => ['payload tersembunyi'],
'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'], 'agregasi_komoditas_lainnya' => ['payload tersembunyi'],
'fasilitas_koperasi' => ['Gudang penyimpanan'], 'fasilitas_lainnya' => ['payload tersembunyi'],
'pengolahan_lanjutan' => 'Tidak', 'detail_pengolahan_lanjutan' => ['payload tersembunyi'],
'kontrak_offtaker' => 'Tidak ada', 'kontrak_komoditas' => ['payload tersembunyi'],
])->assertSessionHasNoErrors();
$profile->refresh();
foreach (['tahun_ad_art', 'layanan_lainnya', 'pertemuan_lainnya', 'faktor_manajemen_lainnya',
'agregasi_komoditas_lainnya', 'fasilitas_lainnya', 'detail_pengolahan_lanjutan', 'kontrak_komoditas'] as $field) {
expect($profile->{$field})->toBeNull();
}
expect($profile->format_pembukuan->value)->toBe('Tidak ada');
});
test('no aggregation skips remaining operational questions without a notification or changing save destination', function (array $answers) {
$profile = PengurusKoperasi::create([
...$this->input, 'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'],
'fasilitas_koperasi' => ['Gudang penyimpanan'], 'fasilitas_lainnya' => 'Teks lama',
'pengolahan_lanjutan' => 'Ya', 'detail_pengolahan_lanjutan' => 'Fermentasi',
'cara_penentuan_harga' => 'Harga pasar', 'kontrak_offtaker' => 'Ada', 'kontrak_komoditas' => 'Kopi',
]);
$prefix = TabKoperasi::KELEMBAGAAN->routePrefix();
$this->post(route($prefix.'.store', $profile), [
...$answers, 'fasilitas_koperasi' => ['Pilihan salah'], 'pengolahan_lanjutan' => 'Ya',
'detail_pengolahan_lanjutan' => 'Teks lama', 'cara_penentuan_harga' => 'Teks lama',
'kontrak_offtaker' => 'Ada', 'kontrak_komoditas' => 'Teks lama',
])->assertRedirect(route($prefix.'.index', $profile))->assertSessionHasNoErrors();
$profile->refresh();
expect($profile->fasilitas_koperasi)->toHaveCount(0)->and($profile->pengolahan_lanjutan)->toBeNull()
->and($profile->detail_pengolahan_lanjutan)->toBeNull()->and($profile->cara_penentuan_harga)->toBeNull()
->and($profile->kontrak_offtaker)->toBeNull()->and($profile->kontrak_komoditas)->toBeNull();
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
$d6 = route(TabKoperasi::RANTAI_NILAI->routePrefix().'.index', $profile);
expect($dom->query('//*[@data-operasional-skip-agregasi]')->length)->toBe(0)
->and($dom->query('//a[@href="'.$d6.'" and contains(@class,"nav-link")]')->length)->toBe(1)
->and($dom->query('//select[@name="pengolahan_lanjutan" and @disabled]')->length)->toBe(1);
$this->get($d6)->assertOk();
})->with([
'explicitly does not aggregate' => [['keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Tidak terlibat']]],
'different value chain role' => [['keterlibatan_rantai_nilai' => ['Penyediaan input']]],
'not involved in value chain' => [['keterlibatan_rantai_nilai' => ['Tidak terlibat kegiatan di atas']]],
]);
test('exclusive negative operational answers cannot be combined with positive choices', function (array $input, string $field) {
$profile = PengurusKoperasi::create($this->input);
$this->post(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.store', $profile), $input)
->assertSessionHasErrors($field);
expect($profile->fresh()->keterlibatan_rantai_nilai)->toBeNull();
})->with([
'contradictory value chain roles' => [['keterlibatan_rantai_nilai' => ['Agregasi', 'Tidak terlibat kegiatan di atas']], 'keterlibatan_rantai_nilai'],
'contradictory aggregation answer' => [['keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi', 'Tidak terlibat']], 'agregasi_komoditas'],
]);
test('D6 covers purchase sales supplier relationships and prices with the shared profile icon', function () {
$profile = PengurusKoperasi::create($this->input);
$prefix = TabKoperasi::RANTAI_NILAI->routePrefix();
$input = [
'sumber_pasokan' => ['Anggota', 'Petani non-anggota', 'Pengumpul/sub-agen', 'Campuran', 'Lainnya'],
'sumber_pasokan_lainnya' => 'Kebun sendiri',
'bentuk_pembelian' => ['Buah segar hasil petik', 'Biji basah', 'Biji kering/kupas', 'Produk terfermentasi'],
'tujuan_penjualan' => ['Offtaker/Eksportir', 'Pedagang besar', 'Koperasi/BUMDes lain', 'Eceran', 'Lainnya'],
'tujuan_penjualan_lainnya' => 'Hotel', 'bentuk_penjualan' => ['Biji basah', 'Biji kering/kupas'],
'sifat_hubungan_bisnis' => ['Bebas', 'Langganan tetap', 'Berbasis hutang', 'Titip jual', 'Komitmen volume', 'Semi-kontrak', 'Lainnya'],
'sifat_hubungan_lainnya' => 'Kemitraan', 'mekanisme_pembayaran' => 'Campuran', 'status_perjanjian_tertulis' => 'Lisan',
'cara_menetapkan_harga_pasar' => ['Ikut pasar', 'Kalkulasi biaya', 'Harga offtaker', 'Lainnya'],
'cara_menetapkan_harga_lainnya' => 'Musyawarah',
'volume_beli_sekarang' => 1200, 'volume_beli_1_tahun' => 1000, 'volume_beli_2_tahun' => 900,
'volume_jual_sekarang' => 1100, 'volume_jual_1_tahun' => 900, 'volume_jual_2_tahun' => 800,
'harga_segar_sekarang' => '12.000', 'harga_basah_1_tahun' => '15.000', 'harga_kering_2_tahun' => '25.000',
];
$this->post(route($prefix.'.store', $profile), $input)->assertRedirect(route($prefix.'.index', $profile))->assertSessionHasNoErrors();
$profile->refresh();
foreach ($input as $field => $expected) {
$stored = $profile->{$field};
if ($stored instanceof BackedEnum) {
$stored = $stored->value;
}
if ($stored instanceof Collection) {
$stored = $stored->map(fn ($case) => $case->value)->all();
}
if (str_starts_with($field, 'harga_')) {
$expected = (int) str_replace('.', '', $expected);
}
expect($stored)->toBe($expected);
}
$response = $this->get(route($prefix.'.index', $profile))->assertOk()
->assertSee('Saluran Penjualan/Pembelian')->assertSee('Harga di Setiap Titik')
->assertDontSee('Peluang Nilai Tambah');
$dom = ($this->dom)($response->getContent());
expect($dom->query('//a[@href="'.route($prefix.'.index', $profile).'"]//i[contains(@class,"tabler-arrows-exchange")]')->length)->toBe(1)
->and($dom->query('//input[@name="harga_segar_sekarang" and @value="12.000" and not(@disabled)]')->length)->toBe(1)
->and($dom->query('//*[@name="praktik_sortasi" or @name="harga_premium_rupiah"]')->length)->toBe(0);
$conditions = json_decode($dom->query('//*[@data-koperasi-survey-field="bentuk_penjualan"]')->item(0)->getAttribute('data-koperasi-conditions'));
expect($conditions[0]->values)->toBeArray();
});
test('D6 other choices require details and never sold cannot be combined with buyers', function (array $input, string $field) {
$profile = PengurusKoperasi::create($this->input);
$this->post(route(TabKoperasi::RANTAI_NILAI->routePrefix().'.store', $profile), $input)->assertSessionHasErrors($field);
})->with([
'other supplier' => [['sumber_pasokan' => ['Lainnya']], 'sumber_pasokan_lainnya'],
'other buyer' => [['tujuan_penjualan' => ['Lainnya']], 'tujuan_penjualan_lainnya'],
'other relationship' => [['sifat_hubungan_bisnis' => ['Lainnya']], 'sifat_hubungan_lainnya'],
'other price setting' => [['cara_menetapkan_harga_pasar' => ['Lainnya']], 'cara_menetapkan_harga_lainnya'],
'never sold with a buyer' => [['tujuan_penjualan' => ['Belum pernah', 'Eceran']], 'tujuan_penjualan'],
]);
test('D6 never sold clears obsolete sales answers while retaining purchase information', function () {
$profile = PengurusKoperasi::create([
...$this->input, 'tujuan_penjualan' => ['Lainnya'], 'tujuan_penjualan_lainnya' => 'Hotel',
'bentuk_penjualan' => ['Biji basah'], 'volume_jual_sekarang' => 500,
'volume_jual_1_tahun' => 400, 'volume_jual_2_tahun' => 300,
'sumber_pasokan_lainnya' => 'Teks lama', 'sifat_hubungan_lainnya' => 'Teks lama', 'cara_menetapkan_harga_lainnya' => 'Teks lama',
]);
$prefix = TabKoperasi::RANTAI_NILAI->routePrefix();
$this->post(route($prefix.'.store', $profile), [
'sumber_pasokan' => ['Anggota'], 'sumber_pasokan_lainnya' => ['payload tersembunyi'],
'bentuk_pembelian' => ['Biji basah'], 'volume_beli_sekarang' => 600, 'harga_basah_sekarang' => '12.000',
'tujuan_penjualan' => ['Belum pernah'], 'tujuan_penjualan_lainnya' => ['payload tersembunyi'],
'bentuk_penjualan' => ['Pilihan salah'], 'volume_jual_sekarang' => 'bukan angka',
'sifat_hubungan_lainnya' => ['payload tersembunyi'], 'cara_menetapkan_harga_lainnya' => ['payload tersembunyi'],
])->assertSessionHasNoErrors();
$profile->refresh();
expect($profile->bentuk_penjualan)->toHaveCount(0)->and($profile->volume_jual_sekarang)->toBeNull()
->and($profile->volume_jual_1_tahun)->toBeNull()->and($profile->volume_jual_2_tahun)->toBeNull()
->and($profile->tujuan_penjualan_lainnya)->toBeNull()->and($profile->sumber_pasokan_lainnya)->toBeNull()
->and($profile->sifat_hubungan_lainnya)->toBeNull()->and($profile->cara_menetapkan_harga_lainnya)->toBeNull()
->and($profile->volume_beli_sekarang)->toBe(600)->and((int) $profile->harga_basah_sekarang)->toBe(12000);
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
expect($dom->query('//input[@name="bentuk_penjualan[]" and @disabled]')->length)->toBe(4)
->and($dom->query('//input[@name="volume_jual_sekarang" and @disabled]')->length)->toBe(1);
});
test('D6 keeps all historical product prices editable regardless of current purchase forms', function () {
$profile = PengurusKoperasi::create([...$this->input, 'bentuk_pembelian' => ['Biji basah'], 'harga_basah_sekarang' => 12000]);
$prefix = TabKoperasi::RANTAI_NILAI->routePrefix();
$this->post(route($prefix.'.store', $profile), [
'bentuk_pembelian' => ['Buah segar hasil petik'], 'harga_segar_sekarang' => '15.000',
'harga_basah_sekarang' => '12.000', 'harga_kering_1_tahun' => '25.000',
])->assertSessionHasNoErrors();
expect((int) $profile->fresh()->harga_segar_sekarang)->toBe(15000)
->and((int) $profile->fresh()->harga_basah_sekarang)->toBe(12000)->and((int) $profile->fresh()->harga_kering_1_tahun)->toBe(25000);
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
expect($dom->query('//input[@name="harga_segar_sekarang" and not(@disabled)]')->length)->toBe(1)
->and($dom->query('//input[@name="harga_basah_sekarang" and not(@disabled)]')->length)->toBe(1)
->and($dom->query('//input[starts-with(@name,"harga_") and not(@disabled)]')->length)->toBe(12);
$this->post(route($prefix.'.store', $profile), ['bentuk_pembelian' => ['Buah segar hasil petik'], 'harga_segar_sekarang' => -1])->assertSessionHasErrors('harga_segar_sekarang');
expect((int) $profile->fresh()->harga_segar_sekarang)->toBe(15000);
});
test('D6 has two independent cards with actions and the complete three harvest price table', function () {
$profile = PengurusKoperasi::create($this->input);
$prefix = TabKoperasi::RANTAI_NILAI->routePrefix();
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
foreach (['saluran', 'harga'] as $section) {
$form = '//section[@id="section-rantai-'.$section.'"]//form';
expect($dom->query($form)->length)->toBe(1)
->and($dom->query($form.'//input[@name="section" and @value="'.$section.'"]')->length)->toBe(1)
->and($dom->query($form.'//button[@type="submit"]')->length)->toBe(1)
->and($dom->query($form.'//a[@href="'.route('koperasi.pengurus.index').'"]')->length)->toBe(1);
}
expect($dom->query('//form//form')->length)->toBe(0)
->and($dom->query('//h6[text()="Saluran Pembelian/Sumber Pasokan" or text()="Saluran Penjualan Utama" or text()="Hubungan dengan Petani Pemasok"]')->length)->toBe(0)
->and($dom->query('//section[@id="section-rantai-saluran"]//input[starts-with(@name,"harga_")]')->length)->toBe(0)
->and($dom->query('//section[@id="section-rantai-harga"]//table/tbody/tr')->length)->toBe(4)
->and($dom->query('//section[@id="section-rantai-harga"]//table//input[@data-koperasi-rupiah and not(@disabled)]')->length)->toBe(12)
->and($dom->query('//section[@id="section-rantai-harga"]//table//span[text()="/kg"]')->length)->toBe(12);
});
test('D6 saves channels then jumps to prices while preserving every price answer', function () {
$prices = ['cara_menetapkan_harga_pasar' => ['Lainnya'], 'cara_menetapkan_harga_lainnya' => 'Musyawarah'];
foreach (BentukProdukKoperasi::cases() as $product) {
foreach (['sekarang', '1_tahun', '2_tahun'] as $period) {
$prices['harga_'.$product->pricePrefix().'_'.$period] = 12500;
}
}
$profile = PengurusKoperasi::create([...$this->input, ...$prices]);
$prefix = TabKoperasi::RANTAI_NILAI->routePrefix();
$this->post(route($prefix.'.store', $profile), [
'section' => 'saluran', 'sumber_pasokan' => ['Anggota'], 'bentuk_pembelian' => ['Biji basah'],
'tujuan_penjualan' => ['Belum pernah'], 'volume_beli_sekarang' => 200,
'harga_segar_sekarang' => -1, 'cara_menetapkan_harga_pasar' => ['Lainnya'],
])->assertSessionHasNoErrors()->assertRedirect(route($prefix.'.index', $profile).'#section-rantai-harga');
$profile->refresh();
expect($profile->sumber_pasokan->first()->value)->toBe('Anggota')->and($profile->volume_beli_sekarang)->toBe(200)
->and($profile->cara_menetapkan_harga_pasar->first()->value)->toBe('Lainnya')
->and($profile->cara_menetapkan_harga_lainnya)->toBe('Musyawarah');
foreach (array_keys($prices) as $field) {
if (str_starts_with($field, 'harga_')) {
expect((int) $profile->{$field})->toBe(12500);
}
}
});
test('D6 saves prices independently without changing channel answers or validating their submitted values', function () {
$channels = [
'sumber_pasokan' => ['Lainnya'], 'sumber_pasokan_lainnya' => 'Kebun sendiri',
'bentuk_pembelian' => ['Biji basah'], 'tujuan_penjualan' => ['Lainnya'], 'tujuan_penjualan_lainnya' => 'Hotel',
'bentuk_penjualan' => ['Biji kering/kupas'], 'sifat_hubungan_bisnis' => ['Lainnya'],
'sifat_hubungan_lainnya' => 'Kemitraan', 'mekanisme_pembayaran' => 'Tunai', 'status_perjanjian_tertulis' => 'Lisan',
'volume_beli_sekarang' => 200, 'volume_beli_1_tahun' => 150, 'volume_beli_2_tahun' => 100,
'volume_jual_sekarang' => 180, 'volume_jual_1_tahun' => 120, 'volume_jual_2_tahun' => 90,
];
$profile = PengurusKoperasi::create([...$this->input, ...$channels]);
$original = array_intersect_key($profile->fresh()->getRawOriginal(), $channels);
$prefix = TabKoperasi::RANTAI_NILAI->routePrefix();
$this->post(route($prefix.'.store', $profile), [
'section' => 'harga', 'harga_fermentasi_2_tahun' => '1.250.000', 'cara_menetapkan_harga_pasar' => ['Ikut pasar'],
'sumber_pasokan' => 'Pilihan salah', 'tujuan_penjualan' => ['Belum pernah', 'Eceran'], 'volume_beli_sekarang' => -1,
])->assertSessionHasNoErrors()->assertRedirect(route($prefix.'.index', $profile).'#section-rantai-harga');
$profile->refresh();
expect(array_intersect_key($profile->getRawOriginal(), $channels))->toBe($original)
->and((int) $profile->harga_fermentasi_2_tahun)->toBe(1250000)
->and($profile->cara_menetapkan_harga_pasar->first()->value)->toBe('Ikut pasar');
});
test('D6 validation returns to the submitted card and keeps the other card saved values', function (string $section, array $input, string $field) {
$profile = PengurusKoperasi::create([
...$this->input, 'sumber_pasokan' => ['Lainnya'], 'sumber_pasokan_lainnya' => 'Kebun sendiri',
'cara_menetapkan_harga_pasar' => ['Lainnya'], 'cara_menetapkan_harga_lainnya' => 'Musyawarah', 'harga_segar_sekarang' => 12500,
]);
$original = $profile->fresh()->getRawOriginal();
$prefix = TabKoperasi::RANTAI_NILAI->routePrefix();
$this->post(route($prefix.'.store', $profile), ['section' => $section, ...$input])
->assertSessionHasErrors($field)->assertRedirect(route($prefix.'.index', $profile).'#section-rantai-'.$section);
expect($profile->fresh()->getRawOriginal())->toBe($original);
$otherField = $section === 'saluran' ? 'cara_menetapkan_harga_lainnya' : 'sumber_pasokan_lainnya';
$otherValue = $section === 'saluran' ? 'Musyawarah' : 'Kebun sendiri';
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
expect($dom->query('//input[@name="'.$otherField.'" and @value="'.$otherValue.'" and not(@disabled)]')->length)->toBe(1);
})->with([
'supplier detail' => ['saluran', ['sumber_pasokan' => ['Lainnya']], 'sumber_pasokan_lainnya'],
'negative price' => ['harga', ['harga_segar_sekarang' => -1], 'harga_segar_sekarang'],
'price setting detail' => ['harga', ['cara_menetapkan_harga_pasar' => ['Lainnya']], 'cara_menetapkan_harga_lainnya'],
]);
test('D6 rejects unknown form sections without writing any answers', function () {
$profile = PengurusKoperasi::create($this->input);
$this->post(route(TabKoperasi::RANTAI_NILAI->routePrefix().'.store', $profile), [
'section' => 'invalid', 'harga_segar_sekarang' => 10000,
])->assertSessionHasErrors('section');
expect($profile->fresh()->harga_segar_sekarang)->toBeNull();
});
test('K3 existing data moves to operational and D6 writes cannot overwrite post harvest answers', function () {
$profile = PengurusKoperasi::create([
...$this->input, 'praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Ukuran biji',
'praktik_pengeringan' => 'Kadang', 'cara_pengeringan' => 'Lantai jemur',
'praktik_fermentasi' => 'Pernah coba', 'model_fermentasi' => ['Kolektif'], 'proses_fermentasi' => 'Fermentasi kelompok',
'harga_premium_rupiah' => 12500, 'harga_segar_sekarang' => 10000,
]);
$op = TabKoperasi::KELEMBAGAAN->routePrefix();
$d6 = TabKoperasi::RANTAI_NILAI->routePrefix();
$response = $this->get(route($op.'.index', $profile))->assertOk()->assertSee('Peluang Nilai Tambah (Pengolahan Pascapanen)');
$dom = ($this->dom)($response->getContent());
expect($dom->query('//input[@name="harga_premium_rupiah" and @value="12.500"]')->length)->toBe(1)
->and($dom->query('//input[@name="model_fermentasi[]" and @value="Kolektif" and @checked]')->length)->toBe(1);
$this->post(route($d6.'.store', $profile), [
'bentuk_pembelian' => ['Buah segar hasil petik'], 'praktik_sortasi' => 'Belum pernah',
'model_fermentasi' => [], 'harga_premium_rupiah' => 0,
])->assertSessionHasNoErrors();
expect($profile->fresh()->praktik_sortasi->value)->toBe('Ya rutin')
->and((int) $profile->fresh()->harga_premium_rupiah)->toBe(12500)
->and($profile->fresh()->model_fermentasi)->toHaveCount(1);
$profile->update(['harga_segar_sekarang' => 10000]);
$this->post(route($op.'.store', $profile), [
'praktik_sortasi' => 'Belum pernah', 'praktik_pengeringan' => 'Tidak', 'praktik_fermentasi' => 'Belum, direncakan',
'standar_sortasi' => ['payload tersembunyi'], 'cara_pengeringan' => ['payload tersembunyi'],
'model_fermentasi' => ['Pilihan salah'], 'proses_fermentasi' => ['payload tersembunyi'],
'harga_premium_rupiah' => 'bukan angka', 'harga_premium_persen' => 999,
])->assertRedirect(route($op.'.index', $profile))->assertSessionHasNoErrors();
$profile->refresh();
expect($profile->standar_sortasi)->toBeNull()->and($profile->cara_pengeringan)->toBeNull()
->and($profile->model_fermentasi)->toHaveCount(0)->and($profile->proses_fermentasi)->toBeNull()
->and($profile->harga_premium_rupiah)->toBeNull()->and($profile->harga_premium_persen)->toBeNull()
->and((int) $profile->harga_segar_sekarang)->toBe(10000);
});
test('K3 follow ups validate actual practices while planned or absent processing skips them', function (array $input, string $field) {
$profile = PengurusKoperasi::create($this->input);
$this->post(route(TabKoperasi::KELEMBAGAAN->routePrefix().'.store', $profile), $input)->assertSessionHasErrors($field);
})->with([
'routine sorting needs criteria' => [['praktik_sortasi' => 'Ya rutin'], 'standar_sortasi'],
'inconsistent sorting needs criteria' => [['praktik_sortasi' => 'Ya tidak konsisten'], 'standar_sortasi'],
'routine drying needs its method' => [['praktik_pengeringan' => 'Ya rutin'], 'cara_pengeringan'],
'occasional drying needs its method' => [['praktik_pengeringan' => 'Kadang'], 'cara_pengeringan'],
'routine fermentation needs a model' => [['praktik_fermentasi' => 'Ya rutin', 'proses_fermentasi' => 'Proses kelompok'], 'model_fermentasi'],
'trial fermentation needs its process' => [['praktik_fermentasi' => 'Pernah coba', 'model_fermentasi' => ['Individual']], 'proses_fermentasi'],
'premium units are alternatives' => [['praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Mutu', 'harga_premium_rupiah' => '1.000', 'harga_premium_persen' => 10], 'harga_premium_persen'],
'premium percentage is bounded' => [['praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Mutu', 'harga_premium_persen' => 101], 'harga_premium_persen'],
'premium rupiah cannot be negative' => [['praktik_sortasi' => 'Ya rutin', 'standar_sortasi' => 'Mutu', 'harga_premium_rupiah' => -1], 'harga_premium_rupiah'],
]);
test('K3 premium is available for other actual post harvest processing such as packaging', function () {
$profile = PengurusKoperasi::create($this->input);
$prefix = TabKoperasi::KELEMBAGAAN->routePrefix();
$this->post(route($prefix.'.store', $profile), [
'keterlibatan_rantai_nilai' => ['Agregasi'], 'agregasi_komoditas' => ['Kopi'],
'pengolahan_lanjutan' => 'Ya', 'detail_pengolahan_lanjutan' => 'Pengemasan',
'praktik_sortasi' => 'Belum pernah', 'praktik_pengeringan' => 'Tidak', 'praktik_fermentasi' => 'Belum, tidak',
'harga_premium_persen' => 10,
])->assertSessionHasNoErrors();
expect((int) $profile->fresh()->harga_premium_persen)->toBe(10);
$dom = ($this->dom)($this->get(route($prefix.'.index', $profile))->assertOk()->getContent());
expect($dom->query('//input[@name="harga_premium_persen" and not(@disabled)]')->length)->toBe(1);
});