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); });