From c7ad805e1ecef10c60e7939bbc43a0257f1111b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 23:01:41 +0000 Subject: [PATCH] fix(finance): amended invoices born OVERDUE when due date is past; localise SUPERSEDED pill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bugbot findings on #256: the amend replacement was always created OPEN, dropping an overdue correction out of the overdue bucket/reminders until the next sweepOverdue run — status now derives from the same rule (unpaid + dueAt < now) at creation, with a regression test. The status pill rendered hard-coded English labels; it now uses the existing finance.statusLabel EN/FR translations. https://claude.ai/code/session_01VzJJTUcvzZgS9jN8aap9iv --- .../invoice-amendment.service.test.ts | 13 ++++- components/finance/InvoiceStatusPill.tsx | 47 ++++++------------- lib/services/invoice-amendment.service.ts | 6 ++- 3 files changed, 31 insertions(+), 35 deletions(-) diff --git a/__tests__/unit/services/invoice-amendment.service.test.ts b/__tests__/unit/services/invoice-amendment.service.test.ts index a9de3a4c..6ee4d1e7 100644 --- a/__tests__/unit/services/invoice-amendment.service.test.ts +++ b/__tests__/unit/services/invoice-amendment.service.test.ts @@ -57,7 +57,8 @@ const LINES = [ function amendArgs(overrides: Partial> = {}) { return { invoiceId: 'inv-1', - dueAt: new Date('2026-07-15T00:00:00Z'), + // Runtime-relative so the born-OPEN assertions never rot into the past. + dueAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), lines: LINES, notes: 'Corrected quantity', ...overrides, @@ -175,6 +176,16 @@ describe('invoiceAmendmentService.amend — supersede chain', () => { expect(mockTx.invoice.create.mock.calls[0][0].data.visitOutcomeId).toBe('vo-1'); }); + it('replacement is born OVERDUE when the amended due date is already past (Bugbot #256)', async () => { + mockTx.invoice.findUnique.mockResolvedValue(makeOriginal({ status: 'OVERDUE' })); + + await invoiceAmendmentService.amend( + amendArgs({ dueAt: new Date('2026-05-01T00:00:00Z') }), + ); + + expect(mockTx.invoice.create.mock.calls[0][0].data.status).toBe('OVERDUE'); + }); + it('amending an OVERDUE unpaid invoice is allowed', async () => { mockTx.invoice.findUnique.mockResolvedValue(makeOriginal({ status: 'OVERDUE' })); diff --git a/components/finance/InvoiceStatusPill.tsx b/components/finance/InvoiceStatusPill.tsx index baba8838..195eef9d 100644 --- a/components/finance/InvoiceStatusPill.tsx +++ b/components/finance/InvoiceStatusPill.tsx @@ -1,34 +1,14 @@ +import { useTranslations } from 'next-intl'; import type { FinanceInvoiceStatus } from '@prisma/client'; -const STATUS_STYLES: Record = { - DRAFT: { - label: 'Draft', - className: 'bg-gray-100 text-gray-700 ring-gray-200', - }, - OPEN: { - label: 'Open', - className: 'bg-blue-50 text-blue-700 ring-blue-200', - }, - PARTIAL: { - label: 'Partial', - className: 'bg-amber-50 text-amber-800 ring-amber-200', - }, - PAID: { - label: 'Paid', - className: 'bg-emerald-50 text-emerald-700 ring-emerald-200', - }, - OVERDUE: { - label: 'Overdue', - className: 'bg-red-50 text-red-700 ring-red-200', - }, - CANCELLED: { - label: 'Cancelled', - className: 'bg-gray-100 text-gray-500 ring-gray-200 line-through', - }, - SUPERSEDED: { - label: 'Superseded', - className: 'bg-violet-50 text-violet-700 ring-violet-200', - }, +const STATUS_STYLES: Record = { + DRAFT: 'bg-gray-100 text-gray-700 ring-gray-200', + OPEN: 'bg-blue-50 text-blue-700 ring-blue-200', + PARTIAL: 'bg-amber-50 text-amber-800 ring-amber-200', + PAID: 'bg-emerald-50 text-emerald-700 ring-emerald-200', + OVERDUE: 'bg-red-50 text-red-700 ring-red-200', + CANCELLED: 'bg-gray-100 text-gray-500 ring-gray-200 line-through', + SUPERSEDED: 'bg-violet-50 text-violet-700 ring-violet-200', }; export function InvoiceStatusPill({ @@ -38,14 +18,15 @@ export function InvoiceStatusPill({ status: FinanceInvoiceStatus; size?: 'sm' | 'md'; }) { - const config = STATUS_STYLES[status]; + const t = useTranslations('finance.statusLabel'); + const label = t(status); const padding = size === 'md' ? 'px-2.5 py-1 text-sm' : 'px-2 py-0.5 text-xs'; return ( - {config.label} + {label} ); } diff --git a/lib/services/invoice-amendment.service.ts b/lib/services/invoice-amendment.service.ts index 5ca472fc..07bab71c 100644 --- a/lib/services/invoice-amendment.service.ts +++ b/lib/services/invoice-amendment.service.ts @@ -135,7 +135,11 @@ export const invoiceAmendmentService = { total: subtotal, currency: original.currency, notes: input.notes ?? null, - status: 'OPEN', + // Born OVERDUE when the chosen due date is already past — the + // sweepOverdue rule (unpaid + dueAt < now) applied at creation, + // so an amended overdue invoice never drops out of the overdue + // bucket/reminders until the next sweep. + status: input.dueAt < new Date() ? 'OVERDUE' : 'OPEN', amendedFromId: original.id, lines: { create: lines }, },