Skip to content

The invoice and the trip it pays for were separate records, so nobody could say which trips were unpaid

A booking and invoicing system for a travel agency where every invoice line points at the trip component it bills, and every payment points at the bank account it landed in. "Which trips are unpaid" becomes a query instead of an afternoon.

TR-2291 · Marenca Coast, 9 nights

Dana & Miles Ferrante · 2 travellers · 10/08/2026 – 10/17/2026 · booked by Rosa Iverson

Add componentTrips, invoices, receiptsThu 09/17/2026
Components
6
Supplier cost
$4,646.00
Sell price
$5,623.00
Margin
$977.0017.4%
Invoiced
$5,623.006 lines
Payment status · derived
Part-paid2,123.00 open

Components

Each with its own supplier, cost and sell price
ComponentSupplierCostSellMarginBilled on
Return flights · 2 adultsBoston – Marenca · out 10/08, back 10/17CMP-8812
Corvane AirwaysCV-7Q2KLM1,684.001,920.00236.0012.3%INV-4127 · L1Paid
Airport transfer, both waysPrivate car · 10/08 and 10/17CMP-8813
Litoral TransfersLT-4091796.00140.0044.0031.4%INV-4127 · L2Paid
Casa Morvelli · 5 nights10/08–10/13 · sea-view double, breakfastCMP-8814
Casa MorvelliCM-2291/051,450.001,775.00325.0018.3%INV-4127 · L3Part-paid
Albergo Punta Brisa · 4 nights10/13–10/17 · garden double, breakfastCMP-8815
Punta Brisa HotelsPB-883411,088.001,340.00252.0018.8%INV-4127 · L4Open
Coast boat day · 2 guests10/11 · lunch aboardCMP-8816
Marenca Sea ToursMST-1129210.00290.0080.0027.6%INV-4127 · L5Open
Travel cover · 2 adults10/08–10/17 · cancellation and medicalCMP-8817
Lynmoor CoverLYN-554120118.00158.0040.0025.3%INV-4127 · L6Open
Trip total · 6 components6 suppliers4,646.005,623.00977.006 of 6 billed
Sell price, split into supplier cost and margin, component by component4,646.00 + 977.00 = 5,623.00

Margin, as the trip is built

running
1+ 236.00sell so far 1,920.00236.00
2+ 44.00sell so far 2,060.00280.00
3+ 325.00sell so far 3,835.00605.00
4+ 252.00sell so far 5,175.00857.00
5+ 80.00sell so far 5,465.00937.00
6+ 40.00sell so far 5,623.00977.00
Trip margin17.4% of sell$977.00
Payments on this tripPart-paid
09/02··44181,500.00
09/14··09312,000.00
Paid 3,500.00Open 2,123.00

Due 09/24/2026. Worked out from 6 lines and 2 allocations; no status is saved on the trip.

The brief, in specifics

Industry
Travel & Tourism
Duration
10 weeks
Cooperation model
Fixed price
Services
Platform engineeringInvoicingBack office
Integrations
PDF document generationEmail deliveryBank statement importCurrency reference rates
Technologies
LaravelMySQLBladePDF generationBootstrap
Team
1 Project lead1 Backend engineer1 Full-stack engineer

Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.

The hard problem

01
  1. 01

    Trips lived in one place and invoices in another, joined by a reference typed by hand. Part-payments across two bank accounts were normal, and a customer who had paid two of three components looked identical to one who had paid nothing. Chasing ran on memory, and the wrong people got chased.

    A hand-typed reference is a join that fails silently: abbreviate it once, and the trip and its invoice are no longer connected, with nothing to show it. Part-payments across two accounts were routine, so a customer who had paid two of three components looked identical to one who had paid nothing. Chasing ran on memory, and memory chased the wrong people.

    Make the invoice line the join. Each line points at the trip component it bills, so a trip knows what it has been invoiced for and what has been paid. Payments carry the bank account, which is what makes the month-end match against a statement possible at all.

The solution

How the pieces fit

  1. 01

    Made every invoice line reference the trip component it bills, replacing free-text descriptions

    Migrating the existing book to real references surfaced forty trips whose hand-typed codes matched nothing, most of them abbreviated by one person.

  2. 02

    Recorded the receiving bank account on each payment, since part-payments arrive in different places

    Each payment records the bank account that received it, the field that turns a month-end match against two statements from a search into a mechanical check.

  3. 03

    Derived trip payment status from its lines instead of storing a status somebody sets

    Deriving status meant auditing every trip the old system had marked paid, and thirty-one of them turned out not to be.

  4. 04

    Generated the customer-facing document from the same records, so the PDF can't disagree with the ledger

    The customer-facing PDF generates from the same records the ledger reads, so regenerating an invoice reflects corrections instead of reprinting a stored file.

  5. 05

    Made the unpaid view the default landing screen, because that's the question being asked

    The unpaid and overdue view is the landing screen, because that's the question the office asks every morning.

Introduction

The system we were asked to build

The travel agency books trips with several components each (flights, transfers, accommodation) and invoices them in pieces. We built the system that keeps the trip and its invoicing joined up, with payments recorded against the bank account that received them.

A travel agency booking trips of several components each (flights, transfers, accommodation) and invoicing them in pieces as they're confirmed. Trips lived in one system and invoices in another, joined by a reference typed by hand. The engagement was commissioned after a month-end where reconciliation across two bank accounts took four days and still didn't balance.

Full-Stack Engineering

Lines point at components

The join between a trip and its invoice is a real reference, where it used to be a hand-typed code that drifted.

Each invoice line holds a reference to the trip component it bills (this flight, this transfer, this night), where the old system relied on a hand-typed code that drifted the moment someone abbreviated it. Because the join is a reference, a trip knows exactly what has and hasn't been invoiced, and a component can't be billed twice without the second attempt showing.

What shipped
  • Lines reference the component they bill, not a typed code
  • A trip knows what has and hasn't been invoiced
  • Double-billing a component shows up instead of slipping through
INV-4127 · Dana & Miles Ferrante

TR-2291 Marenca Coast, 9 nights · issued 08/28/2026 · due 09/24/2026

Part-paid · derivedTrips, invoices, receiptsThu 09/17/2026
Lines
6 each pointing at a component
Invoice total
$5,623.00
Allocated
$3,500.00
Balance due
$2,123.00by 09/24/2026

Invoice lines

A line bills a component; it is never a typed description
#Line · the component it billsAmountAllocatedLine
L1Return flights · 2 adultsCMP-8812FlightCorvane Airways1,920.001,920.00Paid
L2Airport transfer, both waysCMP-8813TransferLitoral Transfers140.00140.00Paid
L3Casa Morvelli · 5 nightsCMP-8814StayCasa Morvelli1,775.001,440.00Part-paid
L4Albergo Punta Brisa · 4 nightsCMP-8815StayPunta Brisa Hotels1,340.000.00Open
L5Coast boat day · 2 guestsCMP-8816ExcursionMarenca Sea Tours290.000.00Open
L6Travel cover · 2 adultsCMP-8817CoverLynmoor Cover158.000.00Open
Invoice total5,623.00
Allocated from ··4418 and ··0931−3,500.00
Balance due2,123.00
A second line billing any of these components would show here against the one already billing it.
INV-4127.pdfGenerated 09/17 8:52 AM from ledger v14 · same 6 recordsPDFSend
Lanternway Travel41 Wharf Street, Suite 3 · accounts@lanternway.exampleInvoiceINV-4127
Bill toDana & Miles FerranteMarenca Coast, 9 nightsTravel 10/08/2026 – 10/17/2026Issued 08/28/2026Due 09/24/2026Trip TR-2291
DescriptionAmount
Return flights · 2 adultsCMP-8812$1,920.00
Airport transfer, both waysCMP-8813$140.00
Casa Morvelli · 5 nightsCMP-8814$1,775.00
Albergo Punta Brisa · 4 nightsCMP-8815$1,340.00
Coast boat day · 2 guestsCMP-8816$290.00
Travel cover · 2 adultsCMP-8817$158.00
Total$5,623.00Received to date−$3,500.00Balance due$2,123.00
Pay toArden Federal · Client deposits account ending 0931Quote INV-4127 so the payment can be allocated to its lines
ledger v14 · 6 lines · INV-4127page 1 of 1
On screen

Every line on INV-4127 carries a pill naming the component it bills, and the customer PDF beside it is generated from those same six records, down to the $2,123.00 balance due.

Receipts

September · grouped by the account the money actually landed in

September 2026Trips, invoices, receiptsThu 09/17/2026
Received in September
$23,805.00
Bank accounts
2 every receipt records one
Allocated to invoice lines
$19,755.00
Waiting for an invoice
$4,050.002 credits

Operating ··4418Keelhaven Bank

6 of 6 on statement
Received13,436.00Allocated12,796.00Waiting for invoice640.00
DateStatement lineAmountAllocated to
09/02ACH CREDIT FERRANTE DRC-7761 · Dana & Miles Ferrante1,500.00INV-4127
09/04CHECK DEPOSIT #2207RC-7764 · The Salgado family2,500.00INV-4102
09/08ACH CREDIT PRYCE-HOLLISRC-7769 · Mara Pryce-Hollis2,000.00INV-4134
09/11WIRE IN VARGA ERC-7779 · Eszter Varga5,000.00INV-4140
09/15ACH CREDIT HOLM ARC-7790 · Anders Holm1,796.00INV-4146
09/15CHECK DEPOSIT HOLLIS AMALFIRC-7793 · no invoice quoted640.00Waiting
September statement · 6 credits12,796.00 + 640.00 = 13,436.00

Client deposits ··0931Arden Federal

5 of 5 on statement
Received10,369.00Allocated6,959.00Waiting for invoice3,410.00
DateStatement lineAmountAllocated to
09/01ACH CREDIT OKONJO ARC-7758 · Adaeze Okonjo1,500.00INV-4089
09/09ACH CREDIT TANAKA JRC-7772 · Joel Tanaka2,275.00INV-4121
09/10ACH CREDIT DESCHAMPS LRC-7775 · Luc Deschamps1,184.00INV-4137
09/14ACH CREDIT FERRANTE MRC-7786 · Dana & Miles Ferrante2,000.00INV-4127
09/16ACH CREDIT REF 88213047RC-7795 · no invoice quoted3,410.00Waiting
September statement · 5 credits6,959.00 + 3,410.00 = 10,369.00
On screen

September's receipts grouped by the account the money actually landed in, operating and client deposits, with two bank credits still waiting for an invoice to attach to.

Payments carry a bank

Each payment records where it landed, which is what makes a month-end match against two statements possible.

Every payment records the bank account it landed in, the field that makes a month-end match against two statements possible at all. Without it, reconciliation is guesswork across accounts; with it, the match is mechanical and the exceptions are a short list instead of the whole month.

What shipped
  • Bank account recorded on every payment
  • Month-end match becomes mechanical, no guessing
  • Exceptions are a short list, never the whole month
Reconcile RC-7795

Client deposits ··0931 · credited 09/16/2026 · the statement carries a reference and nothing else

Save allocationsTrips, invoices, receiptsThu 09/17/2026
ACH CREDIT REF 88213047Reference 88213047 · no name, no invoice number
3,410.00 of 3,410.00 allocatedacross 2 invoices
Unallocated0.00

Split across open invoices

Harriet Whitcombe · TR-2264
Two open invoices on one customer sum to exactly 3,410.00. The reference matched no invoice.
INV-4118due 09/15/2026 · total 2,860.00 · 1,000.00 already allocatedPaid
CMP-9112FlightReturn flights · 2 adultsopen 980.00+980.00
CMP-9113StayHotel Alfama Pátio · 3 nightsopen 880.00+880.00
Allocate from RC-77951,860.00open after0.00
INV-4131due 09/29/2026 · total 1,550.00 · 0.00 already allocatedPaid
CMP-9117StayQuinta das Rosas · 3 nightsopen 1,140.00+1,140.00
CMP-9118ExcursionSintra palaces day tour · 2open 410.00+410.00
Allocate from RC-77951,550.00open after0.00
Nothing left unallocated1,860.00 + 1,550.00 = 3,410.00 · left 0.00

What this changes

all derived
INV-41182,860.00 totalOverduePaid
INV-41311,550.00 totalUnpaidPaid
TR-2264Lisbon & Sintra, 6 nights · 4,410.00OverduePaid
Status is a queryopen = Σ line.amount − Σ allocation.amount
paid when open = 0
overdue when open > 0 past due
part-paid when some is allocated
On the unpaid screen
Invoices to chase97
Difference29,436.5026,026.50
Unallocated receipts4,050.00640.00

Status is derived

On screen

A $3,410.00 credit that arrived with nothing but a reference number, split across two invoices until none of it is left unallocated. Both invoices, and the trip, change status because the lines now say so.

A trip's payment state comes from its lines, so it can't be set to something the money doesn't support.

A trip's payment status is computed from its lines and their payments; there's no field for anyone to set. So it can't say paid when the money doesn't support it. That was the previous system's most expensive failure, because a trip marked paid stops being chased and the discrepancy only appears at year end.

What shipped
  • Status computed from lines and payments, never set
  • Can't claim paid without the money behind it
  • Removes the marked-paid-but-unpaid failure entirely
Unpaid, this morning

Worked out from 42 invoice lines and every allocation · nothing read from a stored status

Send 4 remindersTrips, invoices, receiptsThu 09/17/2026
Invoiced · 12 invoices
$59,691.50
Paid · allocated to lines
$30,255.00
Difference · still to collect
$29,436.50
Received, not yet allocated
$4,050.00not counted as paid
Overdue10,615.50· 4Due in the next 14 days8,813.00· 3Due later10,008.00· 2

Who to chase

Ordered by age, then amount · the landing screen
DueTripCustomerInvoiceInvoicedPaidOpenExpect inStatus
08/31 17d lateKyoto & Nara, 8 nightsTR-2231Adaeze OkonjoINV-40896,480.004,000.002,480.00··0931Overdue
09/04 13d lateBanff lodges, 5 nightsTR-2238Cal & Ruth BrennanINV-40963,915.500.003,915.50··4418Overdue
09/09 8d lateCosta Rica, 10 nightsTR-2245The Salgado familyINV-41029,860.007,500.002,360.00··0931Overdue
09/15 2d lateLisbon & Sintra, 6 nightsTR-2264Harriet WhitcombeINV-41182,860.001,000.001,860.00··0931Overdue
09/24 in 7dMarenca Coast, 9 nightsTR-2291Dana & Miles FerranteINV-41275,623.003,500.002,123.00··0931Part-paid
09/29 in 12dLisbon & Sintra, 6 nightsTR-2264Harriet WhitcombeINV-41311,550.000.001,550.00··0931Unpaid
10/01 in 14dAmalfi, 7 nightsTR-2296Mara Pryce-HollisINV-41347,140.002,000.005,140.00··4418Part-paid
10/08 in 21dPatagonia, 12 nightsTR-2302Eszter VargaINV-414011,620.005,000.006,620.00··4418Part-paid
10/12 in 25dScottish Highlands, 6 nightsTR-2305Thandi Mbeki-RossINV-41443,388.000.003,388.00··4418Unpaid
9 invoices open · 3 of 12 fully allocated and off this list59,691.50 − 30,255.00 = 29,436.50
On screen

The morning report and landing screen: $59,691.50 invoiced, $30,255.00 paid, and the $29,436.50 between them, worked out from the lines rather than read off a stored status, with the chase list ordered by age and amount.

Unpaid is the landing screen

The default view answers the question the agency actually asks every morning.

The application opens on unpaid and overdue trips, because that's the question the agency asks every morning, and the previous landing screen answered a different one. Ordering is by age and amount, with the bank the payment is expected in shown alongside, so the screen itself is the morning's chasing list.

What shipped
  • Opens on unpaid and overdue, the actual morning question
  • Ordered by age and amount, with expected bank shown
  • The chasing list is the screen itself

One source for the PDF

The document a traveler receives is generated from the same records the ledger reads.

The PDF a traveler receives is generated from the same records the ledger reads, so the document and the accounts can't describe different amounts. Regenerating an invoice reflects any correction (there's no stored file to reprint), and each generated document records the version of the data it came from.

What shipped
  • Documents generated from the ledger's own records
  • Regeneration reflects corrections; nothing is reprinted from a stored file
  • Each PDF records the data version it came from
Dana & Miles Ferrante · CU-0418

Customer since 05/06/2025 · 3 trips · every entry below is an invoice or an allocated receipt

StatementTrips, invoices, receiptsThu 09/17/2026
Invoiced, all time
$9,179.00
Received, all time
$7,056.00
Balance
$2,123.00on INV-4127
Paid into
··4418 ··0931

History, as a running balance

8 entries
DateEntryAmountBalance
05/06/2025INV-3411Santa Fe, 4 nights2,316.002,316.00
05/09/2025RC-5120··4418Payment received−800.001,516.00
06/02/2025RC-5261··0931Payment received−1,516.000.00
03/12/2026INV-3906Charleston, 3 nights1,240.001,240.00
03/20/2026RC-6904··4418Payment received−1,240.000.00
08/28/2026INV-4127Marenca Coast, 9 nights5,623.005,623.00
09/02/2026RC-7761··4418Payment received−1,500.004,123.00
09/14/2026RC-7786··0931Payment received−2,000.002,123.00
09/17Balance due · 9,179.00 − 7,056.002,123.00
Correct an entry and the statement regenerates from it; nothing is reprinted from a stored file.
statement-CU-0418-2026-09.pdfGenerated 09/17 9:14 AM from ledger v15 · 8 entriesPDFSend
Lanternway Travel41 Wharf Street, Suite 3 · accounts@lanternway.exampleStatementCU-0418 · Sep 2026
Statement forDana & Miles FerranteCustomer CU-0418Statement date 09/17/2026Entries 8
DateEntryAmountBalance
05/06/2025INV-3411 Santa Fe, 4 nights$2,316.00$2,316.00
05/09/2025RC-5120 Payment received−$800.00$1,516.00
06/02/2025RC-5261 Payment received−$1,516.00$0.00
03/12/2026INV-3906 Charleston, 3 nights$1,240.00$1,240.00
03/20/2026RC-6904 Payment received−$1,240.00$0.00
08/28/2026INV-4127 Marenca Coast, 9 nights$5,623.00$5,623.00
09/02/2026RC-7761 Payment received−$1,500.00$4,123.00
09/14/2026RC-7786 Payment received−$2,000.00$2,123.00
Balance due 09/24$2,123.00
Pay toArden Federal · Client deposits account ending 0931Quote INV-4127
ledger v15 · 8 entries · CU-0418page 1 of 1
On screen

One customer's whole history as a running balance, ending at $2,123.00 after $3,500.00 received against the Marenca trip, and the statement the agency sends out of it.

Process

Phase by phase

  1. Phase 1: Trips And Components

    A Trip Is Several Things

    Modeled a trip as its components (flights, transfers, accommodation), which is what makes partial invoicing and partial payment describable.

    • Trip Model
    • Components
    • Traveller Records
  2. Phase 2: Invoicing

    Lines That Point Somewhere

    Built invoices whose lines reference components. A hand-typed reference was the old join and it broke constantly; a foreign key doesn't.

    • Invoices
    • Invoice Lines
    • Component Linking
  3. Phase 3: Payments And Banks

    Where The Money Landed

    Built payments against invoice lines with the receiving bank account recorded, because reconciling a month against two statements is impossible without knowing which one to look at.

    • Payments
    • Bank Accounts
    • Allocation
  4. Phase 4: The Unpaid View

    The Question, As A Screen

    Made outstanding balance the landing view, derived from lines instead of a stored status, so the list of who to chase is always current and never someone's recollection.

    • Outstanding View
    • Ageing
    • Chase List
  5. Phase 5: Documents

    One Source, One PDF

    Generated the customer-facing invoice from the same records the ledger reads, so a document sent to a traveler can't say something the system disagrees with.

    • PDF Invoices
    • Receipts
    • Statements
Migration check

The old book, moved onto real references · typed codes resolved to components, stored statuses re-derived

Export exceptionsTrips, invoices, receiptsThu 09/17/2026
Typed codes that joined nothing
40trips · mostly abbreviated by one person
Marked paid by the old system, not paid
31trips · the money didn't support it
Stored statuses now
Noneevery status is derived

Codes that matched nothing

showing 12 of 40
Typed codeTyped byOn invoiceNow references
MRNC-CST-0412JMINV-2217CMP-4410 · Stay
LSB SNT 6NJMINV-2231CMP-4468 · Flight
KYO-NARA-FLTJMINV-2240CMP-4502 · Flight
BNF LDGJMINV-2246CMP-4519 · Stay
CRICA-10-TRFDKINV-2252CMP-4551 · Transfer
AMLF BOATJMINV-2263CMP-4590 · Excursion
ICE-RR-CARJMINV-2270CMP-4613 · Transfer
PTG ESTNCJMINV-2281CMP-4647 · Stay
NOLA 3N HTLRIINV-2288CMP-4672 · Stay
SCT-HGHL-INSJMINV-2296CMP-4701 · Cover
MAINE LOBSTJMINV-2305CMP-4733 · Excursion
STFE-4NJMINV-2311CMP-4750 · Stay
10 of these 12 typed by JM · each line now holds a component id, not a code

Marked paid, re-derived

showing 12 of 31
TripOld fieldInvoicedAllocatedOpen
Tuscany, 7 nightsTR-1874PAID6,240.004,240.002,000.00
Yellowstone, 5 nightsTR-1880PAID3,185.002,385.00800.00
Crete, 8 nightsTR-1893PAID5,470.005,020.00450.00
Quebec City, 4 nightsTR-1901PAID1,960.000.001,960.00
Sedona, 3 nightsTR-1912PAID1,245.001,000.00245.00
Dolomites, 6 nightsTR-1926PAID4,890.003,890.001,000.00
Savannah, 3 nightsTR-1933PAID1,380.00690.00690.00
Azores, 7 nightsTR-1947PAID4,115.003,615.00500.00
Big Sur, 4 nightsTR-1958PAID2,260.002,000.00260.00
Oaxaca, 6 nightsTR-1964PAID3,350.001,675.001,675.00
Copenhagen, 5 nightsTR-1971PAID3,920.003,420.00500.00
Kauai, 8 nightsTR-1985PAID7,310.006,310.001,000.00
Open = invoiced − allocations found · back on the chase list
On screen

The migration check: 40 trips whose hand-typed codes matched nothing, relinked to real components, and the 31 trips the old system had marked paid that the money did not support.

Outcome

What it carries now

0

Hand-typed joins

All

Bank accounts reconciled

None

Stored payment statuses

Immediate

Time to list unpaid trips

Hand-typed joins is zero: lines reference components. Bank accounts reconciled is all of them, because every payment records where it landed. Stored payment statuses is none: status is derived. Time to list unpaid trips is immediate, because it's the default view and not a report to run.

Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.

What it settled

What the architecture settled

A hand-typed reference is a join that fails quietly and gets discovered at month end.

A typed reference is a join that fails quietly, and the failure only surfaces at month end, when it's expensive and cold.

Partial payment is the normal case in travel, so the schema has to describe it exactly.

Partial payment is the normal case in travel, not an edge, so a schema that approximates it will misrepresent most of the book most of the time.

Record which account received the money, or reconciliation becomes a manual search across statements.

Without the receiving account on the payment, reconciliation is a manual search across statements: exactly the four days the agency was spending.

Derive status from the records. A stored status is a second source of truth waiting to disagree.

A stored status is a second source of truth, and the direction it fails in (marked paid, actually unpaid) is the one that stops the chasing.

How the work was run

01
  1. 01

    The agency's bookkeeper spent a morning walking us through a month-end close, and that's where the bank-account-per-payment requirement came from. Nobody had asked for it. It was simply impossible to do her job without it, and she'd been doing it by hand.

    Ten weeks, fixed, with the invoice-line-to-component reference settled first because every other improvement depends on it. The unpaid view became the landing screen at the agency's request in week two: they pointed out that the previous system opened on a dashboard answering a question nobody in the office asked.

One credit, followed through

Nobody marks an invoice paid. The lines work it out.

A bank credit arrives with nothing but a reference number. Follow it as it's split across two invoices, spent to the last cent, and taken off the chase list, without a status being written anywhere. Switch tabs, or use the arrow keys once one is focused.

The deposits account shows a credit with a reference number and nothing else: no name, no invoice number. It's recorded against the account it landed in and sits, visible, in the unallocated list. Nobody guesses it onto a customer.

The bank credit
$3,410.00Client deposits ··0931 · 09/16/2026
ACH CREDIT REF 88213047Reference 88213047 matches no invoice number.
Unallocated$3,410.00
0.00 allocated
INV-4118Harriet Whitcombe · due 09/15/2026Overdue
  • CMP-9112Return flights · 2 adults1,000.00 / 1,980.00
  • CMP-9113Hotel Alfama Pátio · 3 nights0.00 / 880.00
open = 2,860.00 − 1,000.00= 1,860.00
INV-4131Harriet Whitcombe · due 09/29/2026Unpaid
  • CMP-9117Quinta das Rosas · 3 nights0.00 / 1,140.00
  • CMP-9118Sintra palaces day tour · 20.00 / 410.00
open = 1,550.00 − 0.00= 1,550.00
Unpaid, derived9 open
  • Adaeze Okonjo2,480.00
  • Cal & Ruth Brennan3,915.50
  • The Salgado family2,360.00
  • Harriet Whitcombe1,860.00
  • Dana & Miles Ferrante2,123.00
  • Harriet Whitcombe1,550.00
  • Mara Pryce-Hollis5,140.00
  • Eszter Varga6,620.00
  • Thandi Mbeki-Ross3,388.00
Still to collect29,436.50

Stored payment statuses: none. Every status above is computed from invoice lines and allocations each time it is shown. Step timing in the split tab is illustrative.

Architecture

From a booked component to a trip that knows whether it's paid

A Laravel application over MySQL where the invoice line is the join. Everything after it, from reconciliation to the chase list to the document a traveler receives, reads the same records instead of keeping a copy.

  1. 01 · Source
    Trip componentsA trip is modeled as its flights, transfers and accommodation, so partial invoicing and partial payment can be described at all.
  2. 02 · Invoicing
    Lines that reference componentsEach invoice line holds a foreign key to the component it bills, never a typed code, so billing a component twice is visible.
  3. 03 · Payments
    Payments with their bank accountEvery payment records the account that received it and is recorded against invoice lines, so the month-end match is mechanical.
  4. 04 · State
    MySQL, no status columnPayment status is computed from lines and their payments. There's no field to set, so it can't say paid without the money.
  5. 05 · Delivery
    Unpaid view and PDFsBlade renders the unpaid view as the landing screen; PDFs generate from the ledger's own records and note the data version they used.

Where the money could slip

Reconciliation that holds at month end

Money received can't go quietly unmatched

Every payment records the bank account it landed in, so each month is matched against the right statement mechanically, and whatever doesn't match is a short list of exceptions.

A paid customer drops off the chase list by itself

Status is computed from invoice lines and their payments, never stored. It can't say paid before the money supports it, so the list of who to chase is always current, never someone's recollection.

Margins stay tied to the components they came from

Each invoice line references the trip component it bills, so a trip knows what has been invoiced, a component billed twice is visible, and the PDF is generated from those same records.

Still working out which trips are unpaid from memory? Scope your build in three minutes.

Scope your build
Have a project?

Let's talk

Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.