Update besar apk finance
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class AccountingDatabaseTest extends TestCase
|
||||
{
|
||||
public function testAccountingInvariantsAgainstDevelopmentDatabase(): void
|
||||
{
|
||||
$root = dirname(__DIR__, 2);
|
||||
$command = array(PHP_BINARY, $root . '/index.php', 'stage17check');
|
||||
$descriptor = array(1 => array('pipe', 'w'), 2 => array('pipe', 'w'));
|
||||
$process = proc_open($command, $descriptor, $pipes, $root);
|
||||
self::assertIsResource($process, 'Tidak dapat menjalankan pemeriksaan integrasi CI3.');
|
||||
$stdout = stream_get_contents($pipes[1]);
|
||||
$stderr = stream_get_contents($pipes[2]);
|
||||
fclose($pipes[1]); fclose($pipes[2]);
|
||||
$exit = proc_close($process);
|
||||
self::assertSame(0, $exit, trim($stderr . PHP_EOL . $stdout));
|
||||
$result = json_decode($stdout, true);
|
||||
self::assertIsArray($result, 'Output Stage17check bukan JSON: ' . $stdout);
|
||||
self::assertSame('ok', $result['status']);
|
||||
foreach ($result['checks'] as $name => $check) {
|
||||
self::assertSame('pass', $check['status'], $name . ': ' . $check['detail']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testPurchaseAdjustmentInvariantsAgainstDevelopmentDatabase(): void
|
||||
{
|
||||
$root=dirname(__DIR__,2);$command=array(PHP_BINARY,$root.'/index.php','purchaseadjustmentcheck','integrity');
|
||||
$descriptor=array(1=>array('pipe','w'),2=>array('pipe','w'));$process=proc_open($command,$descriptor,$pipes,$root);
|
||||
self::assertIsResource($process);$stdout=stream_get_contents($pipes[1]);$stderr=stream_get_contents($pipes[2]);fclose($pipes[1]);fclose($pipes[2]);$exit=proc_close($process);
|
||||
self::assertSame(0,$exit,trim($stderr.PHP_EOL.$stdout));$result=json_decode($stdout,true);self::assertIsArray($result,'Output Purchaseadjustmentcheck bukan JSON: '.$stdout);self::assertSame('ok',$result['status']);
|
||||
}
|
||||
|
||||
public function testPurchaseAdjustmentQueriesAndReportsRunAgainstActualSchema(): void
|
||||
{
|
||||
$root=dirname(__DIR__,2);$command=array(PHP_BINARY,$root.'/index.php','purchaseadjustmentcheck','runtime');
|
||||
$descriptor=array(1=>array('pipe','w'),2=>array('pipe','w'));$process=proc_open($command,$descriptor,$pipes,$root);
|
||||
self::assertIsResource($process);$stdout=stream_get_contents($pipes[1]);$stderr=stream_get_contents($pipes[2]);fclose($pipes[1]);fclose($pipes[2]);$exit=proc_close($process);
|
||||
self::assertSame(0,$exit,trim($stderr.PHP_EOL.$stdout));$result=json_decode($stdout,true);self::assertIsArray($result,'Output runtime Retur/Refund bukan JSON: '.$stdout);self::assertSame('ok',$result['status']);self::assertArrayHasKey('purchase_returns',$result['reports']);self::assertArrayHasKey('supplier_refunds',$result['reports']);self::assertArrayHasKey('supplier_debit_notes',$result['reports']);
|
||||
}
|
||||
|
||||
public function testProfessionalInventoryRuntimeAgainstDevelopmentDatabase(): void
|
||||
{
|
||||
$root=dirname(__DIR__,2);$command=array(PHP_BINARY,$root.'/index.php','inventorycheck','professional');
|
||||
$descriptor=array(1=>array('pipe','w'),2=>array('pipe','w'));$process=proc_open($command,$descriptor,$pipes,$root);
|
||||
self::assertIsResource($process);$stdout=stream_get_contents($pipes[1]);$stderr=stream_get_contents($pipes[2]);fclose($pipes[1]);fclose($pipes[2]);$exit=proc_close($process);
|
||||
self::assertSame(0,$exit,trim($stderr.PHP_EOL.$stdout));$result=json_decode($stdout,true);self::assertIsArray($result,'Output Inventorycheck bukan JSON: '.$stdout);self::assertSame('ok',$result['status']);self::assertArrayHasKey('reconciliation',$result);
|
||||
}
|
||||
|
||||
public function testInventoryIntegrityAndNegativeStockGuardAgainstDevelopmentDatabase(): void
|
||||
{
|
||||
$root=dirname(__DIR__,2);
|
||||
foreach(array('integrity','trigger_test') as$action){$command=array(PHP_BINARY,$root.'/index.php','inventorycheck',$action);$descriptor=array(1=>array('pipe','w'),2=>array('pipe','w'));$process=proc_open($command,$descriptor,$pipes,$root);self::assertIsResource($process);$stdout=stream_get_contents($pipes[1]);$stderr=stream_get_contents($pipes[2]);fclose($pipes[1]);fclose($pipes[2]);$exit=proc_close($process);self::assertSame(0,$exit,$action.': '.trim($stderr.PHP_EOL.$stdout));self::assertStringContainsString('[OK]',$stdout,$action.' tidak menghasilkan bukti pemeriksaan.');}
|
||||
}
|
||||
|
||||
public function testInventoryReconciliationAlwaysProducesAuditableNumbers(): void
|
||||
{
|
||||
$root=dirname(__DIR__,2);$command=array(PHP_BINARY,$root.'/index.php','inventorycheck','reconciliation');$descriptor=array(1=>array('pipe','w'),2=>array('pipe','w'));$process=proc_open($command,$descriptor,$pipes,$root);self::assertIsResource($process);$stdout=stream_get_contents($pipes[1]);$stderr=stream_get_contents($pipes[2]);fclose($pipes[1]);fclose($pipes[2]);$exit=proc_close($process);self::assertContains($exit,array(0,1),trim($stderr.PHP_EOL.$stdout));self::assertStringContainsString('ledger_value=',$stdout);self::assertStringContainsString('gl_value=',$stdout);self::assertStringContainsString('difference=',$stdout);
|
||||
}
|
||||
|
||||
public function testFixedAssetIntegrityAndRuntimeQueries(): void
|
||||
{
|
||||
$root=dirname(__DIR__,2);$command=array(PHP_BINARY,$root.'/index.php','fixedassetcheck');
|
||||
$descriptor=array(1=>array('pipe','w'),2=>array('pipe','w'));$process=proc_open($command,$descriptor,$pipes,$root);
|
||||
self::assertIsResource($process);$stdout=stream_get_contents($pipes[1]);$stderr=stream_get_contents($pipes[2]);fclose($pipes[1]);fclose($pipes[2]);$exit=proc_close($process);
|
||||
self::assertSame(0,$exit,trim($stderr.PHP_EOL.$stdout));self::assertStringContainsString('Fixed asset integrity checks passed.',$stdout);self::assertStringContainsString('model_query_smoke_company_',$stdout);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# QA perangkat Tahap 17
|
||||
|
||||
Automated suite memeriksa kontrak responsive, session AJAX, double-submit, permission, serta invariant database. Sebelum rilis, jalankan juga exploratory test berikut karena rendering Safari/iOS dan perilaku jaringan nyata tidak dapat dibuktikan hanya dari unit test.
|
||||
|
||||
| Target | Viewport / kondisi | Alur wajib |
|
||||
|---|---:|---|
|
||||
| Laptop | 1366×768 | Login, menu, invoice, pembayaran, jurnal, laporan |
|
||||
| Desktop | 1920×1080 | Pastikan bottom navigation tidak tampil dan tabel tetap nyaman |
|
||||
| Android kecil | 360×640 | Menu lengkap, form bertahap, scanner, sticky action |
|
||||
| Android besar | 412×915 | Tabel responsive detail dan modal fullscreen |
|
||||
| iPhone Safari | 390×844 | Safe-area bottom nav, input nominal, download laporan |
|
||||
| Browser | Chrome, Edge, Firefox | Semua alur utama dan ekspor |
|
||||
| Jaringan | Fast 3G, latency 400 ms | Loading terlihat, timeout informatif, submit tidak ganda |
|
||||
| Session | Hapus cookie saat halaman terbuka | AJAX menerima 401 dan diarahkan login tanpa menampilkan HTML di tabel |
|
||||
| Idempotency | Double-click / kirim request dua kali | Hanya satu invoice, pembayaran, stok, atau jurnal terbentuk |
|
||||
|
||||
Perintah standar:
|
||||
|
||||
```powershell
|
||||
C:\xampp\php\php.exe vendor\bin\phpunit
|
||||
C:\xampp\php\php.exe index.php stage17check
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class FormulaServiceTest extends TestCase
|
||||
{
|
||||
private $formula;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->formula = new FormulaService();
|
||||
}
|
||||
|
||||
public function testPayrollFormulaHonorsPrecedenceAndVariables(): void
|
||||
{
|
||||
$value = $this->formula->calculate('basic_salary + overtime_hours * overtime_rate', array(
|
||||
'basic_salary' => 5000000,
|
||||
'overtime_hours' => 4,
|
||||
'overtime_rate' => 50000,
|
||||
));
|
||||
self::assertSame(5200000.0, $value);
|
||||
}
|
||||
|
||||
public function testProrataFormulaAndRounding(): void
|
||||
{
|
||||
self::assertSame(333333.33, $this->formula->calculate('salary / work_days', array(
|
||||
'salary' => 7000000,
|
||||
'work_days' => 21,
|
||||
)));
|
||||
}
|
||||
|
||||
public function testUnknownVariableIsZeroForBackwardCompatibleFormula(): void
|
||||
{
|
||||
self::assertSame(1000.0, $this->formula->calculate('base + optional_bonus', array('base' => 1000)));
|
||||
}
|
||||
|
||||
public function testRejectsDivisionByZero(): void
|
||||
{
|
||||
$this->expectException(BusinessException::class);
|
||||
$this->formula->calculate('salary / days', array('salary' => 1000, 'days' => 0));
|
||||
}
|
||||
|
||||
public function testRejectsUnsafeTokens(): void
|
||||
{
|
||||
$this->expectException(BusinessException::class);
|
||||
$this->formula->calculate('system("whoami")', array());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
define('BASEPATH', dirname(__DIR__) . DIRECTORY_SEPARATOR . 'system' . DIRECTORY_SEPARATOR);
|
||||
define('APPPATH', dirname(__DIR__) . DIRECTORY_SEPARATOR . 'application' . DIRECTORY_SEPARATOR);
|
||||
require_once APPPATH . 'exceptions/BusinessException.php';
|
||||
require_once APPPATH . 'libraries/FormulaService.php';
|
||||
Reference in New Issue
Block a user