fix(audit): H5 — keep yacht ownership-history ledger consistent on archive/restore
Extracts transferOwnershipTx (close open yacht_ownership_history row + open a new one + update denormalized owner) from transferOwnership, and uses it in client-archive + client-restore instead of writing only the denormalized columns — which left the ledger showing the old owner as current and let the next real transfer close the wrong row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ import { portalUsers } from '@/lib/db/schema/portal';
|
||||
import { documents } from '@/lib/db/schema/documents';
|
||||
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { activeInterestsWhere } from '@/lib/services/active-interest';
|
||||
import { transferOwnershipTx } from '@/lib/services/yachts.service';
|
||||
import { ConflictError, NotFoundError } from '@/lib/errors';
|
||||
import type { ArchiveMetadata } from '@/lib/services/client-archive.service';
|
||||
|
||||
@@ -310,14 +311,14 @@ export async function restoreClientWithSelections(args: {
|
||||
|
||||
// Apply auto-reversals.
|
||||
for (const r of dossier.autoReversible) {
|
||||
await applyReversal(tx, r, args.clientId);
|
||||
await applyReversal(tx, r, args.clientId, args.meta.userId);
|
||||
autoReversed += 1;
|
||||
}
|
||||
|
||||
// Apply opted-in prompts.
|
||||
for (const r of dossier.reversibleWithPrompt) {
|
||||
if (!opted.has(r.id)) continue;
|
||||
await applyReversal(tx, r, args.clientId);
|
||||
await applyReversal(tx, r, args.clientId, args.meta.userId);
|
||||
promptedReversed += 1;
|
||||
}
|
||||
|
||||
@@ -357,7 +358,12 @@ export async function restoreClientWithSelections(args: {
|
||||
};
|
||||
}
|
||||
|
||||
async function applyReversal(tx: Tx, r: RestoreReversal, clientId: string): Promise<void> {
|
||||
async function applyReversal(
|
||||
tx: Tx,
|
||||
r: RestoreReversal,
|
||||
clientId: string,
|
||||
actorUserId: string,
|
||||
): Promise<void> {
|
||||
switch (r.kind) {
|
||||
case 'berth_released': {
|
||||
// Re-link the berth to whichever interest originally owned it
|
||||
@@ -397,11 +403,18 @@ async function applyReversal(tx: Tx, r: RestoreReversal, clientId: string): Prom
|
||||
break;
|
||||
}
|
||||
case 'yacht_transferred': {
|
||||
// Transfer back to the restored client.
|
||||
await tx
|
||||
.update(yachts)
|
||||
.set({ currentOwnerType: 'client', currentOwnerId: clientId })
|
||||
.where(eq(yachts.id, r.refId));
|
||||
// Transfer back to the restored client through the shared
|
||||
// ledger-aware helper: closes the open yacht_ownership_history row
|
||||
// (whoever the archive transfer pointed it at) and opens a fresh one
|
||||
// for the restored client, keeping the ledger and denorm columns in
|
||||
// sync. Writing only the denorm columns (the prior bug) re-corrupted
|
||||
// the history on every restore.
|
||||
await transferOwnershipTx(tx as unknown as typeof db, {
|
||||
yachtId: r.refId,
|
||||
newOwner: { type: 'client', id: clientId },
|
||||
transferReason: 'Smart-restore: ownership returned to restored client',
|
||||
createdBy: actorUserId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'yacht_marked_sold_away':
|
||||
|
||||
Reference in New Issue
Block a user