Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion __tests__/unit/services/invoice-amendment.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ const LINES = [
function amendArgs(overrides: Partial<Record<string, unknown>> = {}) {
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,
Expand Down Expand Up @@ -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' }));

Expand Down
47 changes: 14 additions & 33 deletions components/finance/InvoiceStatusPill.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,14 @@
import { useTranslations } from 'next-intl';
import type { FinanceInvoiceStatus } from '@prisma/client';

const STATUS_STYLES: Record<FinanceInvoiceStatus, { label: string; className: string }> = {
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<FinanceInvoiceStatus, string> = {
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({
Expand All @@ -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 (
<span
className={`inline-flex items-center rounded-full font-medium ring-1 ring-inset ${padding} ${config.className}`}
aria-label={`Invoice status: ${config.label}`}
className={`inline-flex items-center rounded-full font-medium ring-1 ring-inset ${padding} ${STATUS_STYLES[status]}`}
aria-label={`Invoice status: ${label}`}
>
{config.label}
{label}
</span>
);
}
6 changes: 5 additions & 1 deletion lib/services/invoice-amendment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
Expand Down
Loading