From 23feecbe67098e4597317d589e40a03692624584 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Mon, 12 Oct 2026 22:17:32 +0800 Subject: [PATCH] fix(transactions): declare the tables the cached transactions list embeds TransactionFilter::queryForInternal eager loads line items, the journal with its debit and credit accounts, and the subject/payer/payee/ initiator/context morphs inside the cached list query, so edits to any of them were served stale until the cache expired. Declare them as $apiCacheDependencies so core-api's list cache key carries their versions: the relation paths, plus the morph target tables ledger writes (ledger_invoices, contacts, vendors, companies). Orders and drivers are left out because they are written constantly. --- server/src/Models/Transaction.php | 26 ++++++++++++++++++++++ server/tests/Models/ModelContractsTest.php | 25 +++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/server/src/Models/Transaction.php b/server/src/Models/Transaction.php index 9cd6586..251945a 100644 --- a/server/src/Models/Transaction.php +++ b/server/src/Models/Transaction.php @@ -58,6 +58,32 @@ class Transaction extends BaseTransaction 'expires_at', ]; + /** + * Tables the cached transactions list embeds, so writes to them invalidate it. + * + * TransactionFilter::queryForInternal eager loads the line items, the journal + * with its debit and credit accounts, and the subject, payer, payee, initiator + * and context morphs. Morph targets have no single table, so the ones ledger + * writes are listed: ledger invoices, the invoice customer types (contacts, vendors) + * and companies. Orders and drivers are left out on purpose: they are written + * constantly, so declaring them would leave the list almost never cached; their + * snapshots in the list may lag by up to the list cache TTL. + * + * Requires fleetbase/core-api's cache dependency support; inert before it. + * + * @var string[] + */ + protected $apiCacheDependencies = [ + 'items', + 'journal', + 'journal.debitAccount', + 'journal.creditAccount', + 'ledger_invoices', + 'contacts', + 'vendors', + 'companies', + ]; + /** * Merge ledger-specific fillable fields into the parent's list. */ diff --git a/server/tests/Models/ModelContractsTest.php b/server/tests/Models/ModelContractsTest.php index 044083f..6abda3c 100644 --- a/server/tests/Models/ModelContractsTest.php +++ b/server/tests/Models/ModelContractsTest.php @@ -124,6 +124,31 @@ public function where(...$arguments): self ->and($gateway->toArray())->not->toHaveKey('config'); }); +test('ledger transaction declares the relations its cached list eager loads as cache dependencies', function () { + $transaction = new Transaction(); + $dependencies = (fn () => $this->apiCacheDependencies)->call($transaction); + + expect($dependencies)->toBe(['items', 'journal', 'journal.debitAccount', 'journal.creditAccount', 'ledger_invoices', 'contacts', 'vendors', 'companies']) + // Raw entries name the morph targets' tables; contacts and vendors belong to FleetOps + ->and((new Invoice())->getTable())->toBe('ledger_invoices') + ->and((new Fleetbase\Models\Company())->getTable())->toBe('companies'); + + // Every relation path must resolve, or core's cache rejects the declaration + $tables = []; + foreach (array_filter($dependencies, fn ($path) => method_exists($transaction, explode('.', $path)[0])) as $path) { + $model = $transaction; + foreach (explode('.', $path) as $segment) { + $relation = $model->{$segment}(); + expect($relation)->toBeInstanceOf(Illuminate\Database\Eloquent\Relations\Relation::class) + ->and($relation)->not->toBeInstanceOf(MorphTo::class); + $model = $relation->getRelated(); + $tables[] = $model->getTable(); + } + } + + expect(array_values(array_unique($tables)))->toBe(['transaction_items', 'ledger_journals', 'ledger_accounts']); +}); + test('wallet relationships computed values and state rules cover every owner type', function () { $wallet = new WalletStateProbe(['balance' => 1050, 'status' => Wallet::STATUS_ACTIVE]);