Files
accounting_dev_v2/tests/Contract/ApplicationContractTest.php
2026-09-11 16:03:00 +07:00

238 lines
15 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
final class ApplicationContractTest extends TestCase
{
public function testInternalControllersUseAuthenticatedBaseController(): void
{
$exceptions = array('Auth.php', 'Migrate.php', 'Accountingrepair.php', 'AttendanceWebhook.php');
foreach (glob(APPPATH . 'controllers/*.php') as $file) {
if (in_array(basename($file), $exceptions, true) || stripos(basename($file), 'check.php') !== false) continue;
$source = file_get_contents($file);
if (strpos($source, 'class ') === false) continue;
$isAuthenticated = preg_match('/extends\s+(MY_Controller|MY_Admin_Controller)/', $source);
$isCliOnly = strpos($source, 'is_cli_request()') !== false || strpos($source, 'is_cli()') !== false;
self::assertTrue((bool) ($isAuthenticated || $isCliOnly), basename($file) . ' melewati kontrol session/permission terpusat dan bukan controller CLI.');
}
}
public function testAjaxSessionExpiryReturnsJson401(): void
{
$source = file_get_contents(APPPATH . 'core/MY_Controller.php');
self::assertStringContainsString('is_ajax_request()', $source);
self::assertStringContainsString('set_status_header(401)', $source);
self::assertStringContainsString("'status' => false", $source);
}
public function testResponsiveShellHasDesktopAndMobileRules(): void
{
$css = file_get_contents(dirname(APPPATH) . '/assets/css/app-final.css');
self::assertStringContainsString('@media', $css);
self::assertMatchesRegularExpression('/\.mobile-action-bar\s*\{display\s*:\s*none!important\}/i', $css, 'Menu bawah wajib disembunyikan pada desktop.');
self::assertMatchesRegularExpression('/max-width\s*:\s*991(?:\.98)?px[\s\S]*\.mobile-action-bar\s*\{[\s\S]*display\s*:\s*grid!important/i', $css, 'Menu bawah wajib tersedia pada HP.');
self::assertMatchesRegularExpression('/max-width\s*:\s*(991|768|767)(?:\.98)?px/i', $css, 'Breakpoint HP belum tersedia.');
}
public function testUiPreventsRepeatedSubmitAndHandlesAjaxFailure(): void
{
$js = file_get_contents(dirname(APPPATH) . '/assets/js/app-ui.min.js');
$network = file_get_contents(dirname(APPPATH) . '/assets/js/app-network.js');
self::assertMatchesRegularExpression('/submitting|is-submitting|disabled/i', $js);
self::assertMatchesRegularExpression('/ajaxError|status\s*===\s*401|\.fail\s*\(/i', $network);
}
public function testMoneyInputsUseCentralParserBeforeAjax(): void
{
$ui = file_get_contents(dirname(APPPATH) . '/assets/js/app-ui.min.js');
$journal = file_get_contents(APPPATH . 'views/jurnal/layout.php');
self::assertStringContainsString('window.parseMoney=', $ui);
self::assertStringContainsString("replace(/,/g,'')", $ui);
self::assertStringContainsString("+(hasDecimal?','+decimal:'')", $ui);
self::assertStringNotContainsString("parseFloat($(this).find('.debit').val())", $journal);
self::assertStringContainsString("window.parseMoney($(this).find('.debit').val())", $journal);
}
public function testMasterAdminHasFullAccessAndApprovalOverride(): void
{
$base = file_get_contents(APPPATH . 'core/MY_Controller.php');
$approval = file_get_contents(APPPATH . 'libraries/ApprovalService.php');
$roles = file_get_contents(APPPATH . 'controllers/Roles.php');
self::assertStringContainsString("userdata('role') === 'Admin'", $base);
self::assertStringContainsString('$masterAdmin', $approval);
self::assertStringContainsString('is_super_admin', $approval);
self::assertStringContainsString('Role wajib mempunyai minimal satu permission.', $roles);
self::assertStringContainsString('Role bawaan aplikasi tidak dapat dihapus.', $roles);
}
public function testProfessionalReportsHaveIndonesianFinancialStatements(): void
{
$navigation = file_get_contents(APPPATH . 'libraries/NavigationService.php');
$view = file_get_contents(APPPATH . 'views/professional_reports/index.php');
self::assertStringContainsString("'label'=>'Laporan Profesional'", $navigation);
self::assertStringContainsString("'balance_sheet'=>'Laporan Neraca'", $view);
self::assertStringContainsString("'profit_loss'=>'Laporan Laba Rugi'", $view);
self::assertStringContainsString('TOTAL AKTIVA', $view);
self::assertStringContainsString('TOTAL PASIVA', $view);
self::assertStringContainsString('Laba Bersih Berjalan', $view);
self::assertStringContainsString('TOTAL PENDAPATAN', $view);
self::assertStringContainsString('TOTAL BEBAN', $view);
self::assertStringContainsString('$accountCode', $view);
}
public function testPurchasePaymentsSeparatePrincipalAndBankCharge(): void
{
$service = file_get_contents(APPPATH . 'libraries/PurchaseService.php');
$purchases = file_get_contents(APPPATH . 'controllers/Purchases.php');
$payables = file_get_contents(APPPATH . 'controllers/Payables.php');
$migration = file_get_contents(APPPATH . 'migrations/20260905000700_add_purchase_payment_bank_charge.php');
self::assertStringContainsString('bank_charge_amount', $migration);
self::assertStringContainsString('total_cash_out', $migration);
self::assertStringContainsString("accountmappingservice->get('bank_charge_expense')", $service);
self::assertStringContainsString('$principal=round($gross-$charge,2)', $service);
self::assertStringContainsString('prepayOrderWithBankCharge', $purchases);
self::assertStringContainsString('settleSupplierInvoiceMultiWithBankCharge', $purchases);
self::assertStringContainsString('paymentMultiWithBankCharge', $payables);
}
public function testPurchaseReturnsAreSeparatedAndIntegrated(): void
{
$navigation=file_get_contents(APPPATH.'libraries/NavigationService.php');
$workflow=file_get_contents(APPPATH.'views/purchases/workflow.php');
$view=file_get_contents(APPPATH.'views/purchase_returns/index.php');
$service=file_get_contents(APPPATH.'libraries/PurchaseAdjustmentService.php');
$controller=file_get_contents(APPPATH.'controllers/Purchasereturns.php');
self::assertStringContainsString("'label'=>'Retur & Refund'",$navigation);
self::assertStringNotContainsString("'adjustment'=>'6",$workflow);
self::assertStringContainsString('purchase-workflow-stepper',$workflow);
self::assertStringContainsString('adjustment-stepper',$view);
self::assertStringContainsString('purchase_return_status_history',$service);
self::assertStringContainsString("movement_type'=>'purchase_return'",$service);
self::assertStringContainsString("writeLedger('purchase_return'",$service);
self::assertStringContainsString("accountmappingservice->get('supplier_refund_receivable')",$service);
self::assertStringContainsString('FOR UPDATE',$service);
self::assertStringContainsString('idempotency_key',$service);
self::assertStringContainsString("requirePermission('purchase_returns'",$controller);
self::assertStringContainsString("requirePermission('purchase_refunds'",$controller);
self::assertStringContainsString("requirePermission('supplier_debit_notes'",$controller);
self::assertStringContainsString('attachment-input',$view);
self::assertStringContainsString('DataTransfer',$view);
self::assertStringContainsString('<iframe',$view);
}
public function testPurchaseAdjustmentsSupportReversalAuditAndProfessionalReports(): void
{
$service=file_get_contents(APPPATH.'libraries/PurchaseAdjustmentService.php');
$purchaseService=file_get_contents(APPPATH.'libraries/PurchaseService.php');
$reportService=file_get_contents(APPPATH.'libraries/ProfessionalReportService.php');
$reportView=file_get_contents(APPPATH.'views/professional_reports/index.php');
$adjustmentView=file_get_contents(APPPATH.'views/purchase_returns/index.php');
self::assertStringContainsString('reverseReturn',$service);
self::assertStringContainsString('reverseRefundReceipt',$service);
self::assertStringContainsString('reverseDebitNote',$service);
self::assertStringContainsString('supplier_refund_status_history',$service);
self::assertStringContainsString('supplier_debit_note_status_history',$service);
self::assertStringContainsString("'purchase_returns'",$reportService);
self::assertStringContainsString("'outstanding_supplier_refunds'",$reportService);
self::assertStringContainsString('Laporan Retur Pembelian',$reportView);
self::assertStringContainsString('Outstanding Refund Supplier',$reportView);
self::assertStringContainsString('preInvoiceReturnDeduction',$purchaseService);
self::assertStringContainsString("join('goods_receipt_lines grl0'",$purchaseService);
self::assertStringContainsString('si0.created_at<=pr.shipped_at',$purchaseService);
self::assertStringNotContainsString('window.confirm(',$adjustmentView);
self::assertStringNotContainsString('window.alert(',$adjustmentView);
}
public function testProfessionalInventoryUsesOneLedgerAndSafeWorkflow(): void
{
$service=file_get_contents(APPPATH.'libraries/ProfessionalInventoryService.php');
$model=file_get_contents(APPPATH.'models/InventoryModel.php');
$controller=file_get_contents(APPPATH.'controllers/Inventoryprofessional.php');
$operations=file_get_contents(APPPATH.'views/inventory_professional/operations.php');
$pending=file_get_contents(APPPATH.'views/items/draft_items.php');
self::assertStringContainsString("FROM inventory_ledger WHERE item_id=?",$model);
self::assertStringContainsString('FOR UPDATE',$service);
self::assertStringContainsString('idempotency_key',$service);
self::assertStringContainsString('reservationAction',$service);
self::assertStringContainsString('reversal_of_id',$service);
self::assertStringContainsString("requirePermission('items','can_post')",$controller);
self::assertStringContainsString('serverSide:true',$operations);
self::assertStringContainsString('Satu dokumen dapat memuat beberapa barang.',$operations);
self::assertStringNotContainsString('Tambah Master Barang',$pending);
self::assertStringNotContainsString('window.alert(',$operations);
self::assertStringNotContainsString('window.confirm(',$operations);
}
public function testInventoryFinalizationCoversRequiredOperationalScenarios(): void
{
$service=file_get_contents(APPPATH.'libraries/ProfessionalInventoryService.php');
$purchase=file_get_contents(APPPATH.'libraries/PurchaseService.php');
$sales=file_get_contents(APPPATH.'libraries/ReceivableService.php');
$returns=file_get_contents(APPPATH.'libraries/PurchaseAdjustmentService.php');
$asset=file_get_contents(APPPATH.'libraries/FixedAssetService.php');
$technician=file_get_contents(APPPATH.'models/PeralatanTeknisi_model.php');
$items=file_get_contents(APPPATH.'controllers/Items.php');
$reports=file_get_contents(APPPATH.'controllers/Inventoryprofessional.php');
self::assertStringContainsString("\$tracking==='UNIT'?1",$purchase,'Penerimaan UNIT wajib menghasilkan satu unit per barcode.');
self::assertStringContainsString('qty_per_barcode',$purchase,'Penerimaan QTY wajib menghormati isi per barcode.');
self::assertStringContainsString("'posted_count'=>\$posted",$items,'Aktivasi barang berserial harus dapat diposting bertahap.');
self::assertStringContainsString("\$doc->document_type==='transfer'",$service,'Transfer gudang harus melalui dua sisi ledger.');
self::assertStringContainsString('reservationAction',$service);
self::assertStringContainsString("array('release','consume','cancel')",$service);
self::assertStringContainsString('stok negatif ditolak',$service);
self::assertStringContainsString('idempotency_key',$service);
self::assertStringContainsString("\$doc->document_type==='opname'",$service);
self::assertStringContainsString('reversal_of_id',$service);
self::assertStringContainsString("writeLedger('purchase_return'",$returns);
self::assertStringContainsString('purchase_replacements',$returns);
self::assertStringContainsString("ib.status = 'available'",$technician);
self::assertStringContainsString('FOR UPDATE',$technician);
self::assertStringContainsString("where('i.company_id'",$technician);
self::assertStringContainsString("'document_type'=>'invoice_details'",$sales);
self::assertStringContainsString("'document_type'=>'asset_capitalization'",$asset);
self::assertStringContainsString("'reconciliation'",$reports);
self::assertStringContainsString("'stock_card'",$reports);
self::assertStringContainsString('limit($length, $start)',$items,'Daftar barcode wajib dipaginasi di server.');
}
public function testInventoryWriteEndpointsAndButtonsArePermissionAware(): void
{
$controller=file_get_contents(APPPATH.'controllers/Inventoryprofessional.php');
$items=file_get_contents(APPPATH.'controllers/Items.php');
$operations=file_get_contents(APPPATH.'views/inventory_professional/operations.php');
$pending=file_get_contents(APPPATH.'views/items/draft_items.php');
self::assertStringContainsString("input->method())!=='POST'",$controller);
self::assertStringContainsString("input->method()) !== 'POST'",$items);
self::assertStringContainsString("requirePermission('items','can_approve')",$controller);
self::assertStringContainsString('permissions.approve',$operations);
self::assertStringContainsString('$canPost',$items);
self::assertStringNotContainsString('btn-adjust',$pending);
self::assertStringNotContainsString('btn-delete',$pending);
}
public function testFixedAssetsUseSingleProfessionalWorkflow(): void
{
$controller=file_get_contents(APPPATH.'controllers/Fixedassets.php');
$legacy=file_get_contents(APPPATH.'controllers/Asset.php');
$service=file_get_contents(APPPATH.'libraries/FixedAssetService.php');
$model=file_get_contents(APPPATH.'models/FixedAssetModel.php');
$view=file_get_contents(APPPATH.'views/fixed_assets/index.php');
self::assertStringContainsString('FixedAssetService',$controller);
self::assertStringContainsString("redirect('fixedassets'",$legacy);
self::assertStringNotContainsString("insert('assets'",$legacy);
self::assertStringContainsString('FOR UPDATE',$service);
self::assertStringContainsString("'document_type'=>'asset_capitalization'",$service);
self::assertStringContainsString("'movement_type'=>'installation'",$service);
self::assertStringContainsString('idempotency_key',$service);
self::assertStringContainsString("event_type==='depreciation'",$service);
self::assertStringContainsString("lifecycle_status NOT IN('disposed','lost','damaged')",$model);
self::assertStringContainsString('serverSide:true',file_get_contents(dirname(APPPATH).'/assets/js/fixed-assets.js'));
self::assertStringContainsString('Laporan & Rekonsiliasi',$view);
self::assertStringNotContainsString('window.alert(',$view);
self::assertStringNotContainsString('window.confirm(',$view);
}
}