fix: invoice integrity — atomic numbering, safe delete, single create path

- 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>
This commit is contained in:
Krao Hasanee
2026-06-12 10:02:28 -04:00
parent aff3d98929
commit 8eacd86b04
13 changed files with 116 additions and 596 deletions
+3 -2
View File
@@ -96,7 +96,7 @@ export default function TeamReports() {
.select('id, task_id, submission_id, invoice:invoices(id, status), submission:submissions(id, task_id, version_number)'),
supabase
.from('subcontractor_invoices')
.select('status, items:subcontractor_invoice_items(task_id, description)')
.select('status, items:subcontractor_invoice_items(task_id, version_number, description)')
.in('status', ['submitted', 'approved', 'paid'])
]);
@@ -150,7 +150,8 @@ export default function TeamReports() {
const status = subInvoice.status;
for (const item of (subInvoice.items || [])) {
if (!item.task_id) continue;
const version = parseVersionFromItemDescription(item.description);
// Stored version preferred; legacy rows (null) fall back to description parsing
const version = item.version_number != null ? Number(item.version_number) : parseVersionFromItemDescription(item.description);
const key = `${item.task_id}:${version}`;
if (!subBillingGroups.has(key)) subBillingGroups.set(key, []);
subBillingGroups.get(key).push({ invoice_status: status });