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:
@@ -25,6 +25,16 @@ serve(async (req) => {
|
||||
|
||||
// Helper: retrieve fee from a payment intent and mark invoice paid
|
||||
async function markPaid(paymentIntentId: string, invoice_id: string) {
|
||||
// Idempotency: Stripe retries events, and card payments fire both
|
||||
// checkout.session.completed and payment_intent.succeeded. Never
|
||||
// overwrite paid_at or re-send the receipt for an already-paid invoice.
|
||||
const { data: existing } = await supabase
|
||||
.from('invoices')
|
||||
.select('status')
|
||||
.eq('id', invoice_id)
|
||||
.single();
|
||||
if (existing?.status === 'paid') return;
|
||||
|
||||
let stripe_fee: number | null = null;
|
||||
try {
|
||||
const pi = await stripe.paymentIntents.retrieve(paymentIntentId, {
|
||||
|
||||
Reference in New Issue
Block a user