Reading additional input from stdin... OpenAI Codex v0.142.5 -------- workdir: /home/moonui/moon-erp-be model: gpt-5.5 provider: openai approval: never sandbox: read-only reasoning effort: none reasoning summaries: none session id: 019f2334-dd51-7233-aae9-bb78aaf8197b -------- user You are re-reviewing a FIX diff (on STDIN) that applied 4 agreed review findings to a Laravel money-data remediation command 'clinic:remediate-stranded-receipts'. Do NOT run shell (blocked); review the diff on stdin directly. The 4 findings the fix must correctly implement WITHOUT introducing new defects: (1) surface JE Posted/Draft status per receipt (capture the returned JE, print POSTED vs 'left as DRAFT — post it manually', add a Draft count, and make the verify re-scan flag a receipt whose linked JE is Draft — WITHOUT force-posting); (2) tighten the pre-L2 revenue reconstruction to also constrain by patient_id AND source_id IN (the receipt's order line ids), keeping the balance assert; (3) guard payment.amount>0 (skip+report a receipt with any non-positive payment line, post no negative JE lines); (4) --company scoping uses !== null so --company=0 doesn't mean 'all'. Judge: does the fix correctly implement all 4? Did it break the balanced-JE / idempotency / read-only-scan / CN-untouched invariants? Any NEW defect? Report defects as CRITICAL/IMPORTANT/MINOR with line + fix, else 'APPROVED — fix correct'. One-line caveat for anything needing unshown code; do NOT run shell. ### FIX COMMIT (29c72a436..14b8f378f) 14b8f378f fix(clinic): م0 review — surface JE posted/draft status + tighten reconstruction + positive-payment guard ### DIFF -U8 diff --git a/Modules/Clinic/app/Console/RemediateStrandedReceipts.php b/Modules/Clinic/app/Console/RemediateStrandedReceipts.php index d9a113d97..2a2a95473 100644 --- a/Modules/Clinic/app/Console/RemediateStrandedReceipts.php +++ b/Modules/Clinic/app/Console/RemediateStrandedReceipts.php @@ -1,22 +1,24 @@ 0) but NO journal_entry_id. This is the * historical damage the م0 C4 code fix stops creating going forward; this * command repairs what already happened. * @@ -61,18 +63,19 @@ public function __construct( public function handle(): int { $force = (bool) $this->option('force'); $companyId = $this->option('company') !== null ? (int) $this->option('company') : null; $stranded = $this->strandedReceipts($companyId); $orphanCreditNotes = $this->orphanCreditNotes($companyId); $deletedOrders = $this->deletedOrdersWithMoney($companyId); + $draftJeReceipts = $this->receiptsWithDraftJe($companyId); - $this->renderScan($stranded, $orphanCreditNotes, $deletedOrders); + $this->renderScan($stranded, $orphanCreditNotes, $deletedOrders, $draftJeReceipts); if (! $force) { $remediable = $stranded->where('remediable', true)->count(); $manual = $stranded->count() - $remediable; $this->warn( "DRY RUN — nothing written. Re-run with --force to remediate {$remediable} receipt(s); " ."{$manual} need a manual decision. BACK UP THE DATABASE FIRST." ); @@ -98,17 +101,17 @@ private function strandedReceipts(?int $companyId): Collection { $query = ReceptionReceipt::query() ->where('status', 'pending') ->where('paid_total', '>', 0) ->whereNull('journal_entry_id') ->orderBy('company_id') ->orderBy('id'); - if ($companyId) { + if ($companyId !== null) { $query->where('company_id', $companyId); } return $query->get()->map(function (ReceptionReceipt $receipt) { $arId = (int) $this->settings->get('clinic.ar_account_id', $receipt->company_id); $revId = (int) $this->settings->get('clinic.revenue_account_id', $receipt->company_id); $glConfigured = $arId > 0 && $revId > 0; @@ -138,17 +141,17 @@ private function orphanCreditNotes(?int $companyId): Collection $query = ClinicCreditNote::query() ->where('status', 'posted') ->whereNotNull('reception_receipt_id') ->whereHas('receipt', fn ($q) => $q->whereNull('journal_entry_id')) ->with('receipt') ->orderBy('company_id') ->orderBy('id'); - if ($companyId) { + if ($companyId !== null) { $query->where('company_id', $companyId); } return $query->get(); } /** * (c) M4 retro-detector: soft-deleted service orders that still carry a @@ -156,37 +159,70 @@ private function orphanCreditNotes(?int $companyId): Collection * Report-only. * * @return Collection> */ private function deletedOrdersWithMoney(?int $companyId): Collection { $query = ServiceOrder::withTrashed()->whereNotNull('deleted_at'); - if ($companyId) { + if ($companyId !== null) { $query->where('company_id', $companyId); } return $query->get() ->map(function (ServiceOrder $order) { $receipts = ReceptionReceipt::withTrashed() ->where('service_order_id', $order->id)->count(); $creditNotes = ClinicCreditNote::withTrashed() ->where('service_order_id', $order->id)->count(); return ['order' => $order, 'receipts' => $receipts, 'credit_notes' => $creditNotes]; }) ->filter(fn ($row) => $row['receipts'] > 0 || $row['credit_notes'] > 0) ->values(); } + /** + * (d) Verify-time honesty: receipts that carry a journal_entry_id but whose + * linked JE is NOT posted (Draft/Approved). CreateJournalEntry deliberately + * leaves a safe Draft when a company has auto-post off (or approve/post + * throws) — the receipt still shows paid, but the JE never hit the ledger + * (reports read POSTED only). The scan drops these from detector (a) because + * journal_entry_id IS set, so surface them here or "healed" would mask an + * unposted JE. Report-only — we NEVER force a post (that would override a + * company's deliberate manual-posting policy). + * + * @return Collection + */ + private function receiptsWithDraftJe(?int $companyId): Collection + { + $query = ReceptionReceipt::query() + ->where('status', '!=', 'voided') + ->whereNotNull('journal_entry_id') + ->whereHas('journalEntry', fn ($j) => $j->where('status', '!=', JournalEntryStatus::Posted->value)) + ->with('journalEntry') + ->orderBy('company_id') + ->orderBy('id'); + + if ($companyId !== null) { + $query->where('company_id', $companyId); + } + + return $query->get(); + } + // ── Scan output ────────────────────────────────────────────────────────── - private function renderScan(Collection $stranded, Collection $orphanCreditNotes, Collection $deletedOrders): void - { + private function renderScan( + Collection $stranded, + Collection $orphanCreditNotes, + Collection $deletedOrders, + Collection $draftJeReceipts, + ): void { $this->info('(a) Stranded pending receipts (paid, no journal entry)'); if ($stranded->isEmpty()) { $this->line(' none'); } else { $this->table( ['id', 'receipt_number', 'company_id', 'paid_total', 'ar/revenue set?', 'payments have account?', 'remediable?'], $stranded->map(fn ($row) => [ $row['receipt']->id, @@ -227,44 +263,81 @@ private function renderScan(Collection $stranded, Collection $orphanCreditNotes, $row['order']->id, $row['order']->order_number, $row['receipts'], $row['credit_notes'], ])->all() ); } + $this->newLine(); + $this->info('(d) Receipts linked to a NON-POSTED (Draft) journal entry — look paid but are NOT in the ledger'); + if ($draftJeReceipts->isEmpty()) { + $this->line(' none'); + } else { + $this->table( + ['id', 'receipt_number', 'company_id', 'receipt status', 'je id', 'je status'], + $draftJeReceipts->map(fn (ReceptionReceipt $receipt) => [ + $receipt->id, + $receipt->receipt_number, + $receipt->company_id, + $receipt->status, + $receipt->journal_entry_id, + $receipt->journalEntry?->status->value ?? '—', + ])->all() + ); + $this->warn( + " ⚠ {$draftJeReceipts->count()} receipt(s) have a Draft JE — post those journal entries manually " + .'(this company likely uses manual posting; they are NOT fully healed until posted).' + ); + } + $this->newLine(); $remediable = $stranded->where('remediable', true)->count(); $manual = $stranded->count() - $remediable; $this->info( "Summary: {$stranded->count()} stranded receipt(s) — {$remediable} remediable, {$manual} need a manual decision; " - ."{$orphanCreditNotes->count()} orphan credit note(s); {$deletedOrders->count()} soft-deleted order(s) with money." + ."{$orphanCreditNotes->count()} orphan credit note(s); {$deletedOrders->count()} soft-deleted order(s) with money; " + ."{$draftJeReceipts->count()} receipt(s) with a Draft (non-posted) JE." ); } // ── Remediation (--force) ──────────────────────────────────────────────── private function remediate(Collection $stranded, Collection $orphanCreditNotes): void { $this->newLine(); $this->info('REMEDIATING — posting the missing journal entries…'); $remediated = 0; + $draftJes = 0; $skipped = []; foreach ($stranded as $row) { /** @var ReceptionReceipt $receipt */ $receipt = $row['receipt']; $result = $this->remediateReceipt($row); if ($result['status'] === 'remediated') { $remediated++; - $this->line(" ✔ {$receipt->receipt_number}: journal entry posted, receipt marked paid."); + + // Surface the ACTUAL posting outcome — CreateJournalEntry may leave + // the JE as Draft (auto-post off / post threw). Do not claim "posted" + // when it is not in the ledger. + if ($result['je_status'] === JournalEntryStatus::Posted->value) { + $this->line(" ✔ {$receipt->receipt_number}: JE #{$result['je_id']} POSTED, receipt marked paid."); + } else { + $draftJes++; + $jeStatusLabel = strtoupper((string) $result['je_status']); + $this->warn( + " ⚠ {$receipt->receipt_number}: JE #{$result['je_id']} left as {$jeStatusLabel} " + .'(NOT posted) — post it manually; receipt marked paid.' + ); + } } else { $skipped[] = "{$receipt->receipt_number}: {$result['reason']}"; $this->warn(" ⚠ {$receipt->receipt_number}: SKIPPED — {$result['reason']}"); } } // Credit-note linkage heal (report-only — never modify the credit note). $healed = 0; @@ -272,28 +345,36 @@ private function remediate(Collection $stranded, Collection $orphanCreditNotes): $receipt = $cn->receipt()->first(); if ($receipt && $receipt->journal_entry_id) { $healed++; $this->line(" ✔ CN {$cn->credit_note_number}: linkage healed (receipt {$receipt->receipt_number} now posted)."); } } $this->newLine(); - $this->table(['Remediated', 'Skipped (needs manual)', 'Credit notes healed'], [[ - $remediated, count($skipped), $healed, - ]]); + $this->table( + ['Remediated', 'of which JE left Draft', 'Skipped (needs manual)', 'Credit notes healed'], + [[$remediated, $draftJes, count($skipped), $healed]] + ); + + if ($draftJes > 0) { + $this->warn( + "{$draftJes} journal entry(ies) were created but left as Draft (this company uses manual posting, " + .'or auto-post failed). POST THEM MANUALLY — until posted they are not in the ledger.' + ); + } if ($skipped !== []) { $this->warn('Needs manual decision:'); foreach ($skipped as $reason) { $this->warn(" - {$reason}"); } } - $this->info('Done. Re-run without --force to verify (healed items drop out of the scan).'); + $this->info('Done. Re-run without --force to verify (detector (d) surfaces any Draft JEs still needing a post).'); } /** * Heal a single receipt inside its own transaction. Returns * ['status' => 'remediated'|'skipped'|'error', 'reason' => ?string]. * * @param array $row * @return array @@ -312,25 +393,30 @@ private function remediateReceipt(array $row): array $revId = $row['rev_id']; $payments = $receipt->payments()->get(); if ($payments->isEmpty()) { return ['status' => 'skipped', 'reason' => 'no payments found on a receipt with money']; } // (2) Never guess an account — skip the whole receipt if any payment - // has no GL account. + // has no GL account. (3) Never post a non-positive money line — skip if + // any payment amount is <= 0, even if the payments net to paid_total (a + // negative debit/credit line is always wrong). foreach ($payments as $payment) { if (! (int) $payment->account_id) { return ['status' => 'skipped', 'reason' => 'a payment has no GL account']; } + if ((float) $payment->amount <= 0) { + return ['status' => 'skipped', 'reason' => 'a payment amount is not positive']; + } } try { - DB::transaction(function () use ($receipt, $payments, $arId, $revId) { + $jeInfo = DB::transaction(function () use ($receipt, $payments, $arId, $revId): array { $lines = []; $paymentTotal = 0.0; // Payment side: DR the receiving account, CR AR. foreach ($payments as $payment) { $amount = (float) $payment->amount; $paymentTotal += $amount; @@ -362,22 +448,35 @@ private function remediateReceipt(array $row): array continue; } $revenueTotal += $amount; $this->appendRevenueLines($lines, $arId, $revId, $amount, $receipt->receipt_number); } } else { // Pre-L2 receipt (no receipt-line linkage) — reconstruct from the // Charge patient-ledger rows WritePatientLedgerEntries wrote at - // collect time (source_type=service_order_line, reference=receipt#). + // collect time. Constrain tightly to THIS patient AND the line ids + // of THIS receipt's service order (not just the receipt-number + // reference) so a stray Charge that happens to share a reference / + // company can never leak into the revenue side. A genuine mismatch + // still trips the balance assert below → rolls back → needs-manual. + $orderLineIds = $receipt->service_order_id + ? ServiceOrderLine::query() + ->where('service_order_id', $receipt->service_order_id) + ->pluck('id') + ->all() + : []; + $charges = PatientLedgerEntry::query() ->where('company_id', $receipt->company_id) + ->where('patient_id', $receipt->patient_id) ->where('entry_type', LedgerEntryType::Charge->value) ->where('source_type', 'service_order_line') ->where('reference', $receipt->receipt_number) + ->whereIn('source_id', $orderLineIds) ->get(); foreach ($charges as $charge) { $amount = (float) $charge->amount; if ($amount <= 0) { continue; } $revenueTotal += $amount; @@ -418,22 +517,27 @@ private function remediateReceipt(array $row): array 'idempotency_key' => 'clinic_receipt_'.$receipt->id, ], $lines); // (6) Flip the receipt. $receipt->update([ 'journal_entry_id' => $je->id, 'status' => 'paid', ]); + + // Surface the REAL posting outcome — the JE may be Draft when the + // company has auto-post off (or approve/post threw). The caller must + // not claim "posted" when it is not in the ledger. + return ['id' => $je->id, 'status' => $je->status->value]; }); } catch (\Throwable $e) { return ['status' => 'error', 'reason' => $e->getMessage()]; } - return ['status' => 'remediated']; + return ['status' => 'remediated', 'je_id' => $jeInfo['id'], 'je_status' => $jeInfo['status']]; } /** * Append the DR-AR / CR-revenue pair for one unit of recognised revenue. * * @param array> $lines */ private function appendRevenueLines(array &$lines, int $arId, int $revId, float $amount, string $receiptNumber): void diff --git a/Modules/Clinic/tests/Feature/RemediateStrandedReceiptsTest.php b/Modules/Clinic/tests/Feature/RemediateStrandedReceiptsTest.php index f075102db..fd829b108 100644 --- a/Modules/Clinic/tests/Feature/RemediateStrandedReceiptsTest.php +++ b/Modules/Clinic/tests/Feature/RemediateStrandedReceiptsTest.php @@ -49,18 +49,19 @@ function remActor(): array $user->branches()->attach($branch->id, ['is_primary' => true]); return [$company, $user, $branch]; } /** * Fiscal period + AR/revenue/cash accounts. When $configureGl is false the two * clinic.*_account_id settings are deliberately left unset (the GL-unset case). + * When $autoPost is false the created JE is deliberately left as Draft. */ -function remSetupAccounting(int $companyId, bool $configureGl = true): array +function remSetupAccounting(int $companyId, bool $configureGl = true, bool $autoPost = true): array { $now = now(); $fy = FiscalYear::factory()->create([ 'company_id' => $companyId, 'start_date' => $now->copy()->startOfYear()->toDateString(), 'end_date' => $now->copy()->endOfYear()->toDateString(), ]); FiscalPeriod::factory() @@ -77,17 +78,17 @@ function remSetupAccounting(int $companyId, bool $configureGl = true): array $revenueAccount = Account::factory()->create([ 'company_id' => $companyId, 'name' => 'Revenue', 'account_type' => 'detail', ]); $cashAccount = Account::factory()->create([ 'company_id' => $companyId, 'name' => 'Cash Box', 'account_type' => 'detail', ]); $settings = app(SettingsService::class); - $settings->set('accounting.auto_post_entries', true, $companyId); + $settings->set('accounting.auto_post_entries', $autoPost, $companyId); if ($configureGl) { $settings->set('clinic.ar_account_id', $arAccount->id, $companyId); $settings->set('clinic.revenue_account_id', $revenueAccount->id, $companyId); } return compact('arAccount', 'revenueAccount', 'cashAccount'); } @@ -216,17 +217,20 @@ function remJeCountFor(ReceptionReceipt $receipt): int test('--force posts a balanced JE and flips the receipt to paid', function () { [$company, $user] = remActor(); $accounts = remSetupAccounting($company->id); $receipt = remMakeStrandedReceipt($company->id, $user->id, 100.0, $accounts); $ledgerBefore = PatientLedgerEntry::query()->where('company_id', $company->id)->count(); - $this->artisan('clinic:remediate-stranded-receipts --force')->assertExitCode(0); + // The command surfaces the ACTUAL posting outcome (Fix 1). + $this->artisan('clinic:remediate-stranded-receipts --force') + ->assertExitCode(0) + ->expectsOutputToContain('POSTED'); $fresh = $receipt->fresh(); expect($fresh->status)->toBe('paid'); expect($fresh->journal_entry_id)->not->toBeNull(); $je = JournalEntry::query()->find($fresh->journal_entry_id); expect($je)->not->toBeNull(); expect((int) $je->source_id)->toBe($receipt->id); @@ -343,8 +347,130 @@ function remJeCountFor(ReceptionReceipt $receipt): int expect($receipt->fresh()->journal_entry_id)->not->toBeNull(); // Credit note untouched $cnFresh = $cn->fresh(); expect($cnFresh->status)->toBe('posted'); expect($cnFresh->journal_entry_id)->toBeNull(); expect(round((float) $cnFresh->amount, 3))->toBe(40.0); }); + +// ── 8. Review Fix 1 — Draft JE is surfaced (not masked as "posted") ────────── + +test('--force surfaces a Draft JE when auto-post is off and the verify scan flags it', function () { + [$company, $user] = remActor(); + $accounts = remSetupAccounting($company->id, autoPost: false); + $receipt = remMakeStrandedReceipt($company->id, $user->id, 100.0, $accounts); + + // The command must report DRAFT, not a bare "POSTED". + $this->artisan('clinic:remediate-stranded-receipts --force') + ->assertExitCode(0) + ->expectsOutputToContain('DRAFT'); + + $fresh = $receipt->fresh(); + expect($fresh->status)->toBe('paid'); + expect($fresh->journal_entry_id)->not->toBeNull(); + + $je = JournalEntry::query()->find($fresh->journal_entry_id); + expect($je->status)->toBe(JournalEntryStatus::Draft); + + // The verify re-scan must NOT silently treat it as fully healed — detector (d) + // surfaces the receipt whose linked JE is still Draft. + $this->artisan('clinic:remediate-stranded-receipts') + ->assertExitCode(0) + ->expectsOutputToContain($receipt->receipt_number) + ->expectsOutputToContain('Draft'); +}); + +// ── 9. Review Fix 2 — reconstruction ignores a stray Charge sharing the ref ── + +test('--force reconstruction excludes a decoy Charge with a different patient/order but the same reference', function () { + [$company, $user] = remActor(); + $accounts = remSetupAccounting($company->id); + $receipt = remMakeStrandedReceipt($company->id, $user->id, 100.0, $accounts, revenueVia: 'ledger'); + + // Decoy: same company + same receipt-number reference, but a DIFFERENT patient + // and a line from a DIFFERENT order. It must never leak into the revenue side. + [$otherOrder, $otherLine] = remMakeOrderWithLine($company->id, $user->id, 500.0); + PatientLedgerEntry::create([ + 'company_id' => $company->id, + 'patient_id' => 9999, + 'entry_type' => LedgerEntryType::Charge->value, + 'amount' => 500.0, + 'source_type' => 'service_order_line', + 'source_id' => $otherLine->id, + 'reference' => $receipt->receipt_number, + 'occurred_at' => now(), + 'created_by' => $user->id, + ]); + + $this->artisan('clinic:remediate-stranded-receipts --force')->assertExitCode(0); + + // Remediation SUCCEEDED → the balance held → revenue was reconstructed at 100, + // not 600 (had the decoy leaked in, the balance assert would have skipped it). + $fresh = $receipt->fresh(); + expect($fresh->status)->toBe('paid'); + expect($fresh->journal_entry_id)->not->toBeNull(); + + $je = JournalEntry::query()->find($fresh->journal_entry_id); + // Payment side (cash 100 / AR 100) + revenue side (AR 100 / revenue 100) = 200/200. + expect(round((float) $je->total_debit, 3))->toBe(200.0); + expect(round((float) $je->total_credit, 3))->toBe(200.0); +}); + +// ── 10. Review Fix 3 — a non-positive payment line is refused ──────────────── + +test('--force skips a receipt that has a non-positive payment line', function () { + [$company, $user] = remActor(); + $accounts = remSetupAccounting($company->id); + [$order, $line] = remMakeOrderWithLine($company->id, $user->id, 100.0); + + $receipt = ReceptionReceipt::factory()->create([ + 'company_id' => $company->id, + 'service_order_id' => $order->id, + 'patient_id' => $order->patient_id, + 'services_total' => 100.0, + 'patient_due' => 100.0, + 'paid_total' => 100.0, + 'status' => 'pending', + 'journal_entry_id' => null, + 'created_by' => $user->id, + ]); + + // Two payments that NET to paid_total (150 + -50 = 100) but one is negative. + ReceptionPayment::create([ + 'company_id' => $company->id, 'reception_receipt_id' => $receipt->id, + 'payment_method' => 'cash', 'amount' => 150.0, + 'account_id' => $accounts['cashAccount']->id, 'created_by' => $user->id, + ]); + ReceptionPayment::create([ + 'company_id' => $company->id, 'reception_receipt_id' => $receipt->id, + 'payment_method' => 'cash', 'amount' => -50.0, + 'account_id' => $accounts['cashAccount']->id, 'created_by' => $user->id, + ]); + ReceptionReceiptLine::create([ + 'company_id' => $company->id, 'reception_receipt_id' => $receipt->id, + 'service_order_line_id' => $line->id, 'amount' => 100.0, + ]); + + $this->artisan('clinic:remediate-stranded-receipts --force')->assertExitCode(0); + + $fresh = $receipt->fresh(); + expect($fresh->status)->toBe('pending'); + expect($fresh->journal_entry_id)->toBeNull(); + expect(remJeCountFor($receipt))->toBe(0); +}); + +// ── 11. Review Fix 4 — --company=0 does not scope to all companies ─────────── + +test('--company=0 does not silently scope to all companies', function () { + [$company, $user] = remActor(); + $accounts = remSetupAccounting($company->id); + $receipt = remMakeStrandedReceipt($company->id, $user->id, 100.0, $accounts); + + // Company 0 never exists → nothing in scope → the real receipt is untouched. + $this->artisan('clinic:remediate-stranded-receipts --force --company=0')->assertExitCode(0); + + $fresh = $receipt->fresh(); + expect($fresh->status)->toBe('pending'); + expect($fresh->journal_entry_id)->toBeNull(); + expect(remJeCountFor($receipt))->toBe(0); +}); codex I’m using the code-review stance and will inspect only the provided diff, as requested. No shell commands. codex APPROVED — fix correct. All 4 requested findings are implemented in the shown diff: - JE status is captured from the returned JE, POSTED vs non-posted is reported, Draft count is summarized, and dry-run verification now surfaces receipts linked to non-posted JEs without force-posting. - Pre-L2 reconstruction is tightened with `patient_id` and `source_id IN` this receipt’s service order line IDs, while relying on the existing balance assert to reject mismatches. - Non-positive payment lines now skip the entire receipt before JE creation. - `--company` scoping now uses `!== null`, so `--company=0` no longer means all companies. No new defect found in the shown diff. Balanced-JE, idempotency, read-only scan, and CN-untouched invariants appear preserved. Caveat: this assumes the unshown `JournalEntry::$status` cast returns `JournalEntryStatus`, as the diff uses `$je->status->value` and tests expect enum instances. tokens used 18,685 APPROVED — fix correct. All 4 requested findings are implemented in the shown diff: - JE status is captured from the returned JE, POSTED vs non-posted is reported, Draft count is summarized, and dry-run verification now surfaces receipts linked to non-posted JEs without force-posting. - Pre-L2 reconstruction is tightened with `patient_id` and `source_id IN` this receipt’s service order line IDs, while relying on the existing balance assert to reject mismatches. - Non-positive payment lines now skip the entire receipt before JE creation. - `--company` scoping now uses `!== null`, so `--company=0` no longer means all companies. No new defect found in the shown diff. Balanced-JE, idempotency, read-only scan, and CN-untouched invariants appear preserved. Caveat: this assumes the unshown `JournalEntry::$status` cast returns `JournalEntryStatus`, as the diff uses `$je->status->value` and tests expect enum instances.