A unique database index, not a UI check. Same org, same project, same total, same issue date collides. Equal progress draws are still legal — three identical instalments on one job is a real billing pattern — because an accidental repeat always lands on the same day with the same figure, which is what the index actually outlaws.
Also called: invoiced twice · duplicated the invoice · double billed the customer · clicked save twice
The second identical invoice is refused by the database, not warned about on screen
Same business, same project, same total, same day collides and the save is rejected. A check in the interface is bypassed by a second browser tab, a retry after a timeout, a background sync, and every code path written after it — and the failure here is a retry: the save looked like it had not worked, so it was done again. Only the database is present for every write.
Three equal instalments on one job are still legal — you say so on the record
Billing the same job in equal draws is ordinary, so the block offers a second, deliberately differently worded action: this is a separate charge, save it as well. That writes an acknowledgement against the row and the invoice is allowed through. Duplication stops being something that happens to you and becomes something a person states on purpose.
The refusal names the invoice you already have
Instead of a failed save you are told that INV-2026-0041, for this exact amount, was issued today — with two ways out: keep the one you have, or confirm this is a separate charge. The draft the blocked attempt created is removed, so a refused save does not leave half an invoice behind for you to find later.
The same cheque cannot be recorded twice against an invoice
A payment carrying the same reference and amount on the same invoice is refused, and the entry form stays open with an explanation saying it was not added a second time — because closing it would look like the entry went through, and “try again” is exactly how $10,205.06 got recorded twice. Cash and unreferenced payments are deliberately not constrained: the same customer paying the same round sum twice is ordinary.
- 1invoices_no_accidental_dup_idx is unique over (org_id, project_id, total, issued_date, coalesce(duplicate_ack,'')), excluding voided, deleted, project-less and zero-total rows.
- 2The total first lands on the row during the line-item save, so that is where the index fires and returns Postgres error 23505.
- 3The client surfaces the code intact rather than converting it to a generic failure, and looks up the colliding invoice by the same predicate so it can be named.
- 4The draft the blocked attempt just created is removed, so a refused save leaves no half-invoice behind.
A builder hit save on an invoice, the screen lagged, he closed out, came back and did it again — and the job ended up billed twice. A check in the screen would not have caught it: two browser tabs, a retry after a timeout and a background sync all step around one, and the failure is a retry by definition, because the save looked like it had not worked. The rule lives in the database instead, where every write has to pass it, so an identical invoice for the same amount on the same job on the same day cannot be created at all.
- Retries after an apparent failure creating a second identical invoice.
- Two tabs or a background sync racing to create the same bill.
- Duplicates only discovered when the customer complains.
See it on your own jobs
Twenty minutes, your numbers, no slide deck. We’ll build one of your real buildings in front of you and send you the estimate link at the end — yours to keep either way.
or keep browsing features →