From f183f58b0c8ef340c9c2e12d33f210963ff3aefc Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 13 May 2026 12:27:08 +0200 Subject: [PATCH] =?UTF-8?q?fix(audit-wave-10):=20types-auditor=20fixes=20?= =?UTF-8?q?=E2=80=94=20Tx=20type,=20BerthDetailData,=20parseBody,=20toAudi?= =?UTF-8?q?tJson?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address the CRITICAL + high-leverage HIGH items from the types-auditor: **C1 — `tx: any` in client-restore.service** Export a canonical `Tx` type from `lib/db/utils.ts` (derived from Drizzle's `db.transaction` callback shape) and use it in `applyReversal` so the 12+ downstream tx writes get full inference. **C2 — berth-detail page stacked `useQuery` escape hatches** Export `BerthDetailData` from berth-detail-header and consume it through useQuery + apiFetch. Removed three `any` escapes in the highest-traffic detail page. Also collapsed the duplicate `BerthData` in berth-tabs.tsx to import from berth-detail-header so the two types can't drift. **C3 — parseBody migration for portal/public routes** Replace raw `await req.json() + schema.parse(body)` with the project-standard `parseBody(req, schema)` helper across 7 routes: - portal/auth/{change-password, activate, reset-password} - auth/set-password - public/{interests, residential-inquiries} Skipped the three anti-enumeration routes (forgot-password, sign-in, sign-in-by-identifier) where the manual validation gives opaque errors on purpose. website-inquiries already wraps the parse in a custom 400 — left as-is. **HIGH #5 — `toAuditJson` helper (21 → 0 inline casts)** Introduce `toAuditJson(row: T): Record` in lib/audit.ts (mirrors gdpr-bundle-builder's `toJsonRow` that already exists for the same reason). Codemod 21 ` as unknown as Record` sites across: - invoices.ts × 6 - expenses.ts × 6 - berths.service × 2 - documents.service × 2 - ocr-config.service × 2 - ai-budget.service × 2 - yachts.service, companies.service, company-memberships.service × 1 each document-templates' `payload as unknown as Record<...>` is a different shape (Documenso form-values widening, not an audit log) — kept the manual cast there. Tests stay 1315/1315. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/auth/set-password/route.ts | 24 ++---------- src/app/api/portal/auth/activate/route.ts | 19 ++------- .../api/portal/auth/change-password/route.ts | 11 ++---- .../api/portal/auth/reset-password/route.ts | 19 ++------- src/app/api/public/interests/route.ts | 4 +- .../api/public/residential-inquiries/route.ts | 4 +- src/components/berths/berth-detail-header.tsx | 4 +- src/components/berths/berth-detail.tsx | 11 ++---- src/components/berths/berth-tabs.tsx | 39 +------------------ src/lib/audit.ts | 11 ++++++ src/lib/db/utils.ts | 7 ++++ src/lib/services/ai-budget.service.ts | 5 ++- src/lib/services/berths.service.ts | 6 +-- src/lib/services/client-restore.service.ts | 8 +--- src/lib/services/companies.service.ts | 7 +--- .../services/company-memberships.service.ts | 7 +--- src/lib/services/documents.service.ts | 6 +-- src/lib/services/expenses.ts | 14 +++---- src/lib/services/invoices.ts | 15 +++---- src/lib/services/ocr-config.service.ts | 5 ++- src/lib/services/yachts.service.ts | 7 +--- 21 files changed, 78 insertions(+), 155 deletions(-) diff --git a/src/app/api/auth/set-password/route.ts b/src/app/api/auth/set-password/route.ts index 05c7fbad..2fa0b288 100644 --- a/src/app/api/auth/set-password/route.ts +++ b/src/app/api/auth/set-password/route.ts @@ -1,9 +1,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; -import { errorResponse, ValidationError } from '@/lib/errors'; +import { errorResponse } from '@/lib/errors'; import { consumeCrmInvite } from '@/lib/services/crm-invite.service'; -import { enforcePublicRateLimit } from '@/lib/api/route-helpers'; +import { enforcePublicRateLimit, parseBody } from '@/lib/api/route-helpers'; const bodySchema = z.object({ token: z.string().min(1), @@ -16,24 +16,8 @@ export async function POST(req: NextRequest): Promise { if (limited) return limited; try { - let body: unknown; - try { - body = await req.json(); - } catch { - // Use {error} via errorResponse so the envelope matches every other - // route (auditor-F §32 — was emitting {message} as a third variant). - throw new ValidationError('Invalid request body'); - } - - const parsed = bodySchema.safeParse(body); - if (!parsed.success) { - throw new ValidationError(parsed.error.issues[0]?.message ?? 'Invalid input'); - } - - const result = await consumeCrmInvite({ - token: parsed.data.token, - password: parsed.data.password, - }); + const { token, password } = await parseBody(req, bodySchema); + const result = await consumeCrmInvite({ token, password }); return NextResponse.json({ data: { email: result.email } }); } catch (err) { return errorResponse(err); diff --git a/src/app/api/portal/auth/activate/route.ts b/src/app/api/portal/auth/activate/route.ts index c2a202cf..4037da47 100644 --- a/src/app/api/portal/auth/activate/route.ts +++ b/src/app/api/portal/auth/activate/route.ts @@ -1,8 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; -import { enforcePublicRateLimit } from '@/lib/api/route-helpers'; -import { errorResponse, ValidationError } from '@/lib/errors'; +import { enforcePublicRateLimit, parseBody } from '@/lib/api/route-helpers'; +import { errorResponse } from '@/lib/errors'; import { activateAccount } from '@/lib/services/portal-auth.service'; const bodySchema = z.object({ @@ -16,19 +16,8 @@ export async function POST(req: NextRequest): Promise { if (limited) return limited; try { - let body: unknown; - try { - body = await req.json(); - } catch { - throw new ValidationError('Invalid request body'); - } - - const parsed = bodySchema.safeParse(body); - if (!parsed.success) { - throw new ValidationError(parsed.error.issues[0]?.message ?? 'Invalid input'); - } - - await activateAccount(parsed.data.token, parsed.data.password); + const { token, password } = await parseBody(req, bodySchema); + await activateAccount(token, password); return NextResponse.json({ success: true }); } catch (err) { return errorResponse(err); diff --git a/src/app/api/portal/auth/change-password/route.ts b/src/app/api/portal/auth/change-password/route.ts index 277185d4..dc5e4c1e 100644 --- a/src/app/api/portal/auth/change-password/route.ts +++ b/src/app/api/portal/auth/change-password/route.ts @@ -4,7 +4,8 @@ import { eq } from 'drizzle-orm'; import { db } from '@/lib/db'; import { portalUsers } from '@/lib/db/schema/portal'; -import { errorResponse, UnauthorizedError, ValidationError } from '@/lib/errors'; +import { parseBody } from '@/lib/api/route-helpers'; +import { errorResponse, UnauthorizedError } from '@/lib/errors'; import { getPortalSession } from '@/lib/portal/auth'; import { changePortalPassword } from '@/lib/services/portal-auth.service'; @@ -18,13 +19,7 @@ export async function POST(req: NextRequest): Promise { const session = await getPortalSession(); if (!session) throw new UnauthorizedError('Portal session required'); - let body: unknown; - try { - body = await req.json(); - } catch { - throw new ValidationError('Invalid request body'); - } - const { currentPassword, newPassword } = bodySchema.parse(body); + const { currentPassword, newPassword } = await parseBody(req, bodySchema); const user = await db.query.portalUsers.findFirst({ where: eq(portalUsers.email, session.email), diff --git a/src/app/api/portal/auth/reset-password/route.ts b/src/app/api/portal/auth/reset-password/route.ts index c5a76873..107d0aa3 100644 --- a/src/app/api/portal/auth/reset-password/route.ts +++ b/src/app/api/portal/auth/reset-password/route.ts @@ -1,8 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; -import { enforcePublicRateLimit } from '@/lib/api/route-helpers'; -import { errorResponse, ValidationError } from '@/lib/errors'; +import { enforcePublicRateLimit, parseBody } from '@/lib/api/route-helpers'; +import { errorResponse } from '@/lib/errors'; import { resetPassword } from '@/lib/services/portal-auth.service'; const bodySchema = z.object({ @@ -16,19 +16,8 @@ export async function POST(req: NextRequest): Promise { if (limited) return limited; try { - let body: unknown; - try { - body = await req.json(); - } catch { - throw new ValidationError('Invalid request body'); - } - - const parsed = bodySchema.safeParse(body); - if (!parsed.success) { - throw new ValidationError(parsed.error.issues[0]?.message ?? 'Invalid input'); - } - - await resetPassword(parsed.data.token, parsed.data.password); + const { token, password } = await parseBody(req, bodySchema); + await resetPassword(token, password); return NextResponse.json({ success: true }); } catch (err) { return errorResponse(err); diff --git a/src/app/api/public/interests/route.ts b/src/app/api/public/interests/route.ts index a11ce483..1e95554b 100644 --- a/src/app/api/public/interests/route.ts +++ b/src/app/api/public/interests/route.ts @@ -11,6 +11,7 @@ import { ports } from '@/lib/db/schema/ports'; import { yachts, yachtOwnershipHistory } from '@/lib/db/schema/yachts'; import { companies, companyMemberships } from '@/lib/db/schema/companies'; import { createAuditLog } from '@/lib/audit'; +import { parseBody } from '@/lib/api/route-helpers'; import { errorResponse, RateLimitError, ValidationError } from '@/lib/errors'; import { checkRateLimit, rateLimiters } from '@/lib/rate-limit'; import { publicInterestSchema } from '@/lib/validators/interests'; @@ -44,8 +45,7 @@ export async function POST(req: NextRequest) { const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'; await gateRateLimit(ip); - const body = await req.json(); - const data = publicInterestSchema.parse(body); + const data = await parseBody(req, publicInterestSchema); // Resolve portId from query param or header (public endpoints need explicit port) const portId = req.nextUrl.searchParams.get('portId') ?? req.headers.get('X-Port-Id'); diff --git a/src/app/api/public/residential-inquiries/route.ts b/src/app/api/public/residential-inquiries/route.ts index 7bb865f0..4494d111 100644 --- a/src/app/api/public/residential-inquiries/route.ts +++ b/src/app/api/public/residential-inquiries/route.ts @@ -14,6 +14,7 @@ import { import { resolveSubject } from '@/lib/email/resolve-subject'; import { getBrandingShell } from '@/lib/email/branding-resolver'; import { env } from '@/lib/env'; +import { parseBody } from '@/lib/api/route-helpers'; import { errorResponse, RateLimitError, ValidationError } from '@/lib/errors'; import { logger } from '@/lib/logger'; import { checkRateLimit, rateLimiters } from '@/lib/rate-limit'; @@ -48,8 +49,7 @@ export async function POST(req: NextRequest) { const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'; await gateRateLimit(ip); - const body = await req.json(); - const data = publicResidentialInquirySchema.parse(body); + const data = await parseBody(req, publicResidentialInquirySchema); const portId = req.nextUrl.searchParams.get('portId') ?? req.headers.get('X-Port-Id'); if (!portId) { diff --git a/src/components/berths/berth-detail-header.tsx b/src/components/berths/berth-detail-header.tsx index 976e6952..6b4697e5 100644 --- a/src/components/berths/berth-detail-header.tsx +++ b/src/components/berths/berth-detail-header.tsx @@ -45,7 +45,7 @@ import { CommandList, } from '@/components/ui/command'; -type BerthDetailData = { +export type BerthDetailData = { id: string; mooringNumber: string; area: string | null; @@ -80,6 +80,8 @@ type BerthDetailData = { mooringType: string | null; access: string | null; berthApproved: boolean | null; + statusLastChangedReason: string | null; + statusLastModified: string | null; tags: Array<{ id: string; name: string; color: string }>; }; diff --git a/src/components/berths/berth-detail.tsx b/src/components/berths/berth-detail.tsx index 8dc5e089..ebbc2282 100644 --- a/src/components/berths/berth-detail.tsx +++ b/src/components/berths/berth-detail.tsx @@ -8,7 +8,7 @@ import { DetailLayout } from '@/components/shared/detail-layout'; import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider'; import { apiFetch } from '@/lib/api/client'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; -import { BerthDetailHeader } from './berth-detail-header'; +import { BerthDetailHeader, type BerthDetailData } from './berth-detail-header'; import { BerthForm } from './berth-form'; import { buildBerthTabs } from './berth-tabs'; @@ -17,12 +17,10 @@ interface BerthDetailProps { } export function BerthDetail({ berthId }: BerthDetailProps) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data, isLoading } = useQuery({ + const { data, isLoading } = useQuery({ queryKey: ['berth', berthId], queryFn: () => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - apiFetch<{ data: any }>(`/api/v1/berths/${berthId}`).then((r) => r.data), + apiFetch<{ data: BerthDetailData }>(`/api/v1/berths/${berthId}`).then((r) => r.data), }); useRealtimeInvalidation({ @@ -58,8 +56,7 @@ export function BerthDetail({ berthId }: BerthDetailProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams]); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const berth = data as any; + const berth = data; return ( <> diff --git a/src/components/berths/berth-tabs.tsx b/src/components/berths/berth-tabs.tsx index 9452c50a..9d12a40c 100644 --- a/src/components/berths/berth-tabs.tsx +++ b/src/components/berths/berth-tabs.tsx @@ -29,44 +29,7 @@ import { BerthInterestPulse } from './berth-interest-pulse'; import { BerthDocumentsTab } from './berth-documents-tab'; import { BerthDealDocumentsTab } from './berth-deal-documents-tab'; -type BerthData = { - id: string; - mooringNumber: string; - area: string | null; - status: string; - lengthFt: string | null; - lengthM: string | null; - widthFt: string | null; - widthM: string | null; - draftFt: string | null; - draftM: string | null; - widthIsMinimum: boolean | null; - nominalBoatSize: string | null; - nominalBoatSizeM: string | null; - waterDepth: string | null; - waterDepthM: string | null; - waterDepthIsMinimum: boolean | null; - sidePontoon: string | null; - powerCapacity: string | null; - voltage: string | null; - mooringType: string | null; - cleatType: string | null; - cleatCapacity: string | null; - bollardType: string | null; - bollardCapacity: string | null; - access: string | null; - price: string | null; - priceCurrency: string; - bowFacing: string | null; - berthApproved: boolean | null; - tenureType: string; - tenureYears: number | null; - tenureStartDate: string | null; - tenureEndDate: string | null; - statusLastChangedReason: string | null; - statusLastModified: string | null; - tags: Array<{ id: string; name: string; color: string }>; -}; +import type { BerthDetailData as BerthData } from './berth-detail-header'; /** * Compact ft/m segmented control for the Specifications card. Two diff --git a/src/lib/audit.ts b/src/lib/audit.ts index 138b5958..05428ba8 100644 --- a/src/lib/audit.ts +++ b/src/lib/audit.ts @@ -2,6 +2,17 @@ import { db } from '@/lib/db'; import { auditLogs } from '@/lib/db/schema'; import { logger } from '@/lib/logger'; +/** + * Widen a Drizzle row (or any object) to the shape audit_logs.oldValue / + * newValue expects. Centralizes the structurally-safe `Record` cast 20+ services were doing inline via + * `as unknown as Record`. Mirrors gdpr-bundle-builder's + * `toJsonRow` helper (same audit-found motivation). + */ +export function toAuditJson(row: T): Record { + return row as unknown as Record; +} + export type AuditAction = | 'create' | 'update' diff --git a/src/lib/db/utils.ts b/src/lib/db/utils.ts index 86c4fa4d..6b479928 100644 --- a/src/lib/db/utils.ts +++ b/src/lib/db/utils.ts @@ -2,6 +2,13 @@ import { eq, sql } from 'drizzle-orm'; import type { PgTable, PgColumn } from 'drizzle-orm/pg-core'; import { db } from './index'; +/** + * Drizzle transaction client type — the argument shape `db.transaction`'s + * callback receives. Exported so service helpers that take a `tx` + * parameter can spell the type instead of falling back to `any`. + */ +export type Tx = Parameters[0]>[0]; + /** * Wraps a database operation in a transaction. * Rolls back automatically on error. diff --git a/src/lib/services/ai-budget.service.ts b/src/lib/services/ai-budget.service.ts index 1e916974..de753843 100644 --- a/src/lib/services/ai-budget.service.ts +++ b/src/lib/services/ai-budget.service.ts @@ -12,6 +12,7 @@ import { and, eq, gte, sql } from 'drizzle-orm'; +import { toAuditJson } from '@/lib/audit'; import { db } from '@/lib/db'; import { aiUsageLedger } from '@/lib/db/schema/ai-usage'; import { systemSettings } from '@/lib/db/schema/system'; @@ -85,13 +86,13 @@ export async function setAiBudget( .values({ key: KEY, portId, - value: next as unknown as Record, + value: toAuditJson(next), updatedBy: userId, }) .onConflictDoUpdate({ target: [systemSettings.key, systemSettings.portId], set: { - value: next as unknown as Record, + value: toAuditJson(next), updatedBy: userId, updatedAt: new Date(), }, diff --git a/src/lib/services/berths.service.ts b/src/lib/services/berths.service.ts index 9412548f..d27e28c0 100644 --- a/src/lib/services/berths.service.ts +++ b/src/lib/services/berths.service.ts @@ -6,7 +6,7 @@ import { clients } from '@/lib/db/schema/clients'; import { interestBerths, interests } from '@/lib/db/schema/interests'; import { tags } from '@/lib/db/schema/system'; import { PIPELINE_STAGES } from '@/lib/constants'; -import { createAuditLog, type AuditMeta } from '@/lib/audit'; +import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit'; import { diffEntity } from '@/lib/entity-diff'; import { NotFoundError, ValidationError } from '@/lib/errors'; import { buildListQuery } from '@/lib/db/query-builder'; @@ -287,8 +287,8 @@ export async function updateBerth( action: 'update', entityType: 'berth', entityId: id, - oldValue: diff as unknown as Record, - newValue: data as unknown as Record, + oldValue: toAuditJson(diff), + newValue: toAuditJson(data), ipAddress: meta.ipAddress, userAgent: meta.userAgent, }); diff --git a/src/lib/services/client-restore.service.ts b/src/lib/services/client-restore.service.ts index 72f13f4b..fa141a49 100644 --- a/src/lib/services/client-restore.service.ts +++ b/src/lib/services/client-restore.service.ts @@ -19,6 +19,7 @@ import { and, eq, isNull, ne, sql } from 'drizzle-orm'; import { db } from '@/lib/db'; +import type { Tx } from '@/lib/db/utils'; import { clients } from '@/lib/db/schema/clients'; import { interests, interestBerths } from '@/lib/db/schema/interests'; import { berths } from '@/lib/db/schema/berths'; @@ -356,12 +357,7 @@ export async function restoreClientWithSelections(args: { }; } -async function applyReversal( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - tx: any, - r: RestoreReversal, - clientId: string, -): Promise { +async function applyReversal(tx: Tx, r: RestoreReversal, clientId: string): Promise { switch (r.kind) { case 'berth_released': { // Re-link the berth to whichever interest originally owned it diff --git a/src/lib/services/companies.service.ts b/src/lib/services/companies.service.ts index 4121f8f5..79164256 100644 --- a/src/lib/services/companies.service.ts +++ b/src/lib/services/companies.service.ts @@ -10,7 +10,7 @@ import type { Company } from '@/lib/db/schema/companies'; import { yachts } from '@/lib/db/schema/yachts'; import { withTransaction } from '@/lib/db/utils'; import { buildListQuery } from '@/lib/db/query-builder'; -import { createAuditLog, type AuditMeta } from '@/lib/audit'; +import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit'; import { NotFoundError, ConflictError } from '@/lib/errors'; import { logger } from '@/lib/logger'; import { @@ -149,10 +149,7 @@ export async function updateCompany( throw new NotFoundError('Company'); } - const { diff } = diffEntity( - existing as unknown as Record, - data as Record, - ); + const { diff } = diffEntity(toAuditJson(existing), data as Record); let updated: Company | undefined; try { diff --git a/src/lib/services/company-memberships.service.ts b/src/lib/services/company-memberships.service.ts index 2bd9e94c..b4ef275e 100644 --- a/src/lib/services/company-memberships.service.ts +++ b/src/lib/services/company-memberships.service.ts @@ -4,7 +4,7 @@ import { companies, companyMemberships } from '@/lib/db/schema/companies'; import type { CompanyMembership } from '@/lib/db/schema/companies'; import { clients } from '@/lib/db/schema/clients'; import { withTransaction } from '@/lib/db/utils'; -import { createAuditLog, type AuditMeta } from '@/lib/audit'; +import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit'; import { NotFoundError, ConflictError, ValidationError } from '@/lib/errors'; import { emitToRoom } from '@/lib/socket/server'; import { diffEntity } from '@/lib/entity-diff'; @@ -127,10 +127,7 @@ export async function updateMembership( ): Promise { const existing = await loadMembershipScoped(membershipId, portId); - const { diff } = diffEntity( - existing as unknown as Record, - data as Record, - ); + const { diff } = diffEntity(toAuditJson(existing), data as Record); const rows = await db .update(companyMemberships) diff --git a/src/lib/services/documents.service.ts b/src/lib/services/documents.service.ts index 0a2cc056..cddc331f 100644 --- a/src/lib/services/documents.service.ts +++ b/src/lib/services/documents.service.ts @@ -16,7 +16,7 @@ import { berthReservations } from '@/lib/db/schema/reservations'; import { ports } from '@/lib/db/schema/ports'; import { userProfiles, userPortRoles } from '@/lib/db/schema/users'; import { buildListQuery } from '@/lib/db/query-builder'; -import { createAuditLog, type AuditMeta } from '@/lib/audit'; +import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit'; import { diffEntity } from '@/lib/entity-diff'; import { CodedError, NotFoundError, ValidationError, ConflictError } from '@/lib/errors'; import { emitToRoom } from '@/lib/socket/server'; @@ -580,8 +580,8 @@ export async function updateDocument( action: 'update', entityType: 'document', entityId: id, - oldValue: existing as unknown as Record, - newValue: updated as unknown as Record, + oldValue: toAuditJson(existing), + newValue: toAuditJson(updated!), ipAddress: meta.ipAddress, userAgent: meta.userAgent, }); diff --git a/src/lib/services/expenses.ts b/src/lib/services/expenses.ts index c715654c..df3657b1 100644 --- a/src/lib/services/expenses.ts +++ b/src/lib/services/expenses.ts @@ -4,7 +4,7 @@ import type { PgColumn } from 'drizzle-orm/pg-core'; import { db } from '@/lib/db'; import { expenses, invoices, invoiceExpenses } from '@/lib/db/schema/financial'; import { buildListQuery } from '@/lib/db/query-builder'; -import { createAuditLog, type AuditMeta } from '@/lib/audit'; +import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit'; import { diffEntity } from '@/lib/entity-diff'; import { softDelete, restore } from '@/lib/db/utils'; import { CodedError, NotFoundError } from '@/lib/errors'; @@ -171,7 +171,7 @@ export async function createExpense(portId: string, data: CreateExpenseInput, me action: 'create', entityType: 'expense', entityId: expense.id, - newValue: expense as unknown as Record, + newValue: toAuditJson(expense), ipAddress: meta.ipAddress, userAgent: meta.userAgent, }); @@ -250,7 +250,7 @@ export async function updateExpense( if (data.amount !== undefined) updateData.amount = String(data.amount); - const { diff } = diffEntity(existing as unknown as Record, updateData); + const { diff } = diffEntity(toAuditJson(existing), updateData); const [updated] = await db .update(expenses) @@ -266,8 +266,8 @@ export async function updateExpense( action: 'update', entityType: 'expense', entityId: id, - oldValue: existing as unknown as Record, - newValue: updated as unknown as Record, + oldValue: toAuditJson(existing), + newValue: toAuditJson(updated), metadata: { diff }, ipAddress: meta.ipAddress, userAgent: meta.userAgent, @@ -304,7 +304,7 @@ export async function archiveExpense(id: string, portId: string, meta: AuditMeta action: 'archive', entityType: 'expense', entityId: id, - oldValue: existing as unknown as Record, + oldValue: toAuditJson(existing), ipAddress: meta.ipAddress, userAgent: meta.userAgent, }); @@ -325,7 +325,7 @@ export async function restoreExpense(id: string, portId: string, meta: AuditMeta action: 'restore', entityType: 'expense', entityId: id, - newValue: restored as unknown as Record, + newValue: toAuditJson(restored), ipAddress: meta.ipAddress, userAgent: meta.userAgent, }); diff --git a/src/lib/services/invoices.ts b/src/lib/services/invoices.ts index ccd00dde..7bf2a427 100644 --- a/src/lib/services/invoices.ts +++ b/src/lib/services/invoices.ts @@ -7,7 +7,7 @@ import { systemSettings } from '@/lib/db/schema/system'; import { clients, clientAddresses } from '@/lib/db/schema/clients'; import { companies, companyAddresses } from '@/lib/db/schema/companies'; import { buildListQuery } from '@/lib/db/query-builder'; -import { createAuditLog, type AuditMeta } from '@/lib/audit'; +import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit'; import { diffEntity } from '@/lib/entity-diff'; import { withTransaction } from '@/lib/db/utils'; import { CodedError, NotFoundError, ConflictError, ValidationError } from '@/lib/errors'; @@ -372,7 +372,7 @@ export async function createInvoice(portId: string, data: CreateInvoiceInput, me action: 'create', entityType: 'invoice', entityId: invoice.id, - newValue: invoice as unknown as Record, + newValue: toAuditJson(invoice), ipAddress: meta.ipAddress, userAgent: meta.userAgent, }); @@ -524,10 +524,7 @@ export async function updateInvoice( return result; }); - const { diff } = diffEntity( - existing as unknown as Record, - updated as unknown as Record, - ); + const { diff } = diffEntity(toAuditJson(existing), toAuditJson(updated)); void createAuditLog({ userId: meta.userId, @@ -535,8 +532,8 @@ export async function updateInvoice( action: 'update', entityType: 'invoice', entityId: id, - oldValue: existing as unknown as Record, - newValue: updated as unknown as Record, + oldValue: toAuditJson(existing), + newValue: toAuditJson(updated), metadata: { diff }, ipAddress: meta.ipAddress, userAgent: meta.userAgent, @@ -570,7 +567,7 @@ export async function deleteInvoice(id: string, portId: string, meta: AuditMeta) action: 'delete', entityType: 'invoice', entityId: id, - oldValue: existing as unknown as Record, + oldValue: toAuditJson(existing), ipAddress: meta.ipAddress, userAgent: meta.userAgent, }); diff --git a/src/lib/services/ocr-config.service.ts b/src/lib/services/ocr-config.service.ts index df190138..6f2374b5 100644 --- a/src/lib/services/ocr-config.service.ts +++ b/src/lib/services/ocr-config.service.ts @@ -6,6 +6,7 @@ import { and, eq, isNull } from 'drizzle-orm'; +import { toAuditJson } from '@/lib/audit'; import { db } from '@/lib/db'; import { systemSettings } from '@/lib/db/schema/system'; import { encrypt, decrypt } from '@/lib/utils/encryption'; @@ -76,13 +77,13 @@ async function writeRow(portId: string | null, value: StoredOcrConfig, userId: s .values({ key: KEY, portId, - value: value as unknown as Record, + value: toAuditJson(value), updatedBy: userId, }) .onConflictDoUpdate({ target: [systemSettings.key, systemSettings.portId], set: { - value: value as unknown as Record, + value: toAuditJson(value), updatedBy: userId, updatedAt: new Date(), }, diff --git a/src/lib/services/yachts.service.ts b/src/lib/services/yachts.service.ts index 53496731..b1e9717a 100644 --- a/src/lib/services/yachts.service.ts +++ b/src/lib/services/yachts.service.ts @@ -3,7 +3,7 @@ import { db } from '@/lib/db'; import { yachts, yachtOwnershipHistory, yachtTags, clients } from '@/lib/db/schema'; import type { Yacht } from '@/lib/db/schema/yachts'; import { companies } from '@/lib/db/schema/companies'; -import { createAuditLog, type AuditMeta } from '@/lib/audit'; +import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit'; import { NotFoundError, ValidationError } from '@/lib/errors'; import { logger } from '@/lib/logger'; import { @@ -138,10 +138,7 @@ export async function updateYacht( throw new NotFoundError('Yacht'); } - const { diff } = diffEntity( - existing as unknown as Record, - data as Record, - ); + const { diff } = diffEntity(toAuditJson(existing), data as Record); const [updated] = await db .update(yachts)