8eacd86b04
- Invoice numbers now come from DB function next_invoice_number() (max+1 per year under advisory lock) with a unique index; the old row-count method reused numbers after deletes and raced concurrent creates, which broke public pay links - Remove dead standalone TeamCreateInvoice page; the TeamInvoices modal is the single create path (page had already drifted) - Invoice delete now asks for confirmation and only un-bills tasks/ submissions not billed on another invoice - Created invoice shows correct "sent" status in list without reload - Invoice/due dates computed at save time, not module load - Reopening a paid invoice clears stale stripe_fee - Stripe webhook markPaid is idempotent (no double receipts) - subcontractor_invoice_items.version_number column stores billing version explicitly; description parsing kept as legacy fallback - Drop unused buildInvoiceStatusByKey/deriveVersionStatus Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
38 lines
1.2 KiB
PL/PgSQL
38 lines
1.2 KiB
PL/PgSQL
-- Invoice numbers were generated client-side from a row count, which reuses
|
|
-- numbers after deletes and collides under concurrency. Replace with a DB
|
|
-- function that takes max+1 for the year under an advisory lock, and enforce
|
|
-- uniqueness at the schema level.
|
|
|
|
create or replace function public.next_invoice_number()
|
|
returns text
|
|
language plpgsql
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
declare
|
|
yr text := to_char(now(), 'YYYY');
|
|
next_n int;
|
|
begin
|
|
-- Serialize concurrent invoice creation within this transaction
|
|
perform pg_advisory_xact_lock(hashtext('invoice_number_' || yr));
|
|
|
|
select coalesce(
|
|
max((regexp_match(invoice_number, '^INV-' || yr || '-(\d+)$'))[1]::int),
|
|
0
|
|
) + 1
|
|
into next_n
|
|
from public.invoices
|
|
where invoice_number like 'INV-' || yr || '-%';
|
|
|
|
return 'INV-' || yr || '-' || lpad(next_n::text, 3, '0');
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.next_invoice_number() from public;
|
|
grant execute on function public.next_invoice_number() to authenticated;
|
|
|
|
-- Enforce uniqueness going forward (fails if historical duplicates exist —
|
|
-- those must be reviewed manually first, never auto-renumbered).
|
|
create unique index if not exists invoices_invoice_number_key
|
|
on public.invoices (invoice_number);
|