The client approved version four, and something moved
In short
If a client quotes a detail your current itinerary does not contain, the system almost certainly edited the trip in place and kept no snapshot of what was approved. Reconstruct what you can from the sent proposal, then start recording every change against a frozen revision — who, when, which field, old value, new value — so the next argument is answerable instead of arguable.
Key takeaways
- The symptom is a client quoting something real. Start by believing them and prove where your record went thin.
- An itinerary storing current state only cannot say what was approved, at any level of goodwill from your team.
- In 2 of the 4 causes nobody edited the trip at all — the component or the template underneath it moved instead.
- The minimum audit row is 5 fields: actor, timestamp, field path, old value, new value, plus the revision it belonged to.
- Freeze a render at every revision. Re-generating a PDF today reproduces the fault you are trying to diagnose.
- A supplier content sync can rewrite a proposal agreed 6 weeks earlier, with no internal user to blame.
A client says the itinerary they approved included a private airport transfer, and the trip in front of you shows a shared shuttle. Nobody remembers changing it and nobody can prove it was ever there. That is not a memory problem. It is a storage problem: the itinerary holds current state and nothing else, so the version they approved no longer exists anywhere you can query.
Begin by assuming the client is right. They usually are, because they are reading from something — a PDF, an email, a link they opened in March — and you are reading from a record that has been written over since. The useful work is finding exactly where your reconstruction of the approved state runs out. That boundary names which of 4 causes you are looking at, and 2 of them involve nobody touching the trip at all.
Reconstruct the approved state, and mark where it fails
Do this before touching any code. It takes under 60 minutes on one trip and it produces the finding, which is rarely the one anybody expected.
- Find the artefact the client is quoting — the sent email, the attachment, the proposal link. Note its date and which revision label it carries, if any.
- Put it beside the trip as it stands now, day by day. Record every difference, not only the disputed one. The other 3 or 4 are the evidence.
- For each difference, ask whether the itinerary row changed or the thing it points at changed. A day still referencing the same transfer component, whose description now reads differently, is a different fault from a day whose transfer row was replaced.
- Try to render the trip as at the approval date. If the answer is 'we can show you the current version', you have confirmed there are no snapshots and can stop hunting for a hidden log.
- List the derived values that moved without an edit: service dates, transfer legs, per-day costing lines. These change on their own when anything upstream moves, which is the recalculation set in what has to recalculate when a day moves.
- Write down the last point at which the 2 records agree. That timestamp, and the fields that diverge after it, are the diagnosis.
Four ways an approved detail leaves without a trace
Ranked by how often each turns out to be the one. The first is the most common and the least interesting. Rows 3 and 4 are what catch experienced teams.
| Cause | What you observe | Confirming test | Whose change it was |
|---|---|---|---|
| Edited in place, no snapshot taken | Current trip differs from the sent PDF; no stored revision matches that PDF | Try to render the trip as at the approval date, and fail | Someone on your team, unrecorded |
| Component record mutated underneath the trip | The day still points at the same component, but its title, inclusions or duration read differently | Compare the component's own updated timestamp with the approval date | Whoever maintains the product library |
| Template edited and pushed into live trips | Several unrelated trips changed the same way on the same day | Search for the identical difference across other trips built from that template | Whoever edited the template |
| Supplier sync overwrote the description | Wording changed with no internal user involved; often a whole category moves at once | Check the last import run against the date the wording changed | The supplier, through your integration |
The day did not change; the thing it points at did
This is the cause most teams never suspect, because the itinerary genuinely was not edited. A trip day holds a reference to a component — 1 purchasable service with its own supplier, duration and cancellation term, as the itinerary component defines it. If the day stores a pointer and the component is mutable, editing that component changes every trip that ever referenced it, including trips that ran 2 years ago.
- Descriptions and inclusions. A product manager tidies the wording of a city tour, and 40 open proposals now describe something slightly different from what was agreed.
- Durations and timings. A 4-hour half-day becomes a 8-hour full-day in the library, and the affected days quietly stop being physically possible.
- Cancellation terms. The term the client accepted is replaced by the one the supplier offers today, which is what gets quoted at them if they cancel.
- Supplier identity. A component reassigned to a different operator changes who actually delivers the service, with no visible change on the itinerary line.
- Long trips multiply the exposure: a 14-day, 3-country trip carries dozens of component references across several suppliers and currencies, as modelling a fourteen-day, three-country trip sets out row by row.
The fix is a modelling decision, not a process one. Copy the fields that appear in the proposal — title, inclusions, duration, cancellation term, supplier name — onto the revision at the moment it is sent, and keep the pointer alongside for operations. The proposal then says what it said, and the live product stays free to move.
One template edit, thirty live trips
The same failure at a larger blast radius. A saved composition is edited to correct an error — a wrong hotel, a missing meal — and the edit propagates into trips already quoted, already approved, sometimes operating. The tell is 30 unrelated trips changing identically on 1 day, which no in-place edit by a single consultant produces.
The supplier rewrote the product and your proposal followed
If any inventory is imported — bedbank content, activity descriptions, ticketing product names — the supplier can rewrite text you have already sent. There is no internal user to attribute it to, and the sync run that overwrote the text usually keeps no record of what it replaced. That makes it the hardest of the 4 to detect after the fact.
- Stage imported content and promote it into proposal-facing fields on an explicit action, rather than writing it straight through.
- Keep the previous value on every import for fields that appear in client documents. 2 generations answers nearly every question of this kind.
- Stamp each imported record with the run identifier that last wrote it, so a wording change traces to a dated job rather than to a person who does not exist.
The minimum audit record, field by field
Full event sourcing is not required and rarely earns its place in an itinerary builder. What is required is 1 row per change, written by the same code path that performs the change, carrying 5 fields plus the revision it belonged to.
| Field | What it holds | Why it is not optional |
|---|---|---|
| actor | The user, or the named integration or scheduled job, that made the change | 'The system' is not an answer. An import run needs an identity exactly as a person does. |
| changed_at | Server timestamp with offset, in UTC, not a date | 2 edits on the same day are ordinary, and the order between them is usually the whole question |
| target | A stable path: trip, revision, day offset, component reference, field name | A free-text note about what changed cannot be diffed, filtered or replayed later |
| old_value and new_value | Both sides, serialised, for the single field that moved | Storing only the new value gives you a list of edits you cannot run backwards |
| revision_id | The client-visible revision current at the moment of the change | It is what turns a list of edits into 'this happened after they approved' |
A version history that stores only the current state is not a history. It is one row that has been overwritten a number of times nobody counted.
Two additions make it genuinely useful. Freeze a render at every client-visible revision — a stored file, not a re-generation — because a re-generation reads today's data and reproduces the fault you are diagnosing, the subject of the exported proposal and the live itinerary disagreeing. And keep version, variant and revision distinct in the schema; collapsing the 3 is why so many builders can show a change list but cannot say which changes a client ever saw, which version, variant, revision takes apart.
What the reconstruction will not recover, and what to tell the client
Be plain about the limit. Without stored revisions you can recover what was sent, from the sent document, and roughly when the divergence began, from updated timestamps. You cannot recover who changed it, why, or the intermediate states. If 3 people edited the trip across 2 weeks, the database holds the result of all 3 and evidence of none.
Operationally: honour the sent document as agreed scope for this trip, then fix the model. It matters beyond the itinerary too — a client disputing a charge compares the document they hold against a line on a card statement, and the 2 have to be reconcilable, the point of whose name appears on the traveller's statement. Where they read the detail matters as well, since identical data reads differently by layout, as how day-by-day itineraries were laid out in our travel concepts shows.
Retrofitting revisions and an audit table into a live itinerary builder is contained work — a single write path, a snapshot table, a frozen render and a consultant-facing diff view — and it is the shape of narrow internal build we scope under internal tools and ops. The rest of this silo sits under itinerary building and the trip data model, inside our travel and tourism work.
Frequently asked questions
Short answers to the follow-ups this page tends to raise.
How do you track itinerary changes after a client has approved a proposal?
Freeze a revision at the moment of sending and write 1 audit row per change afterwards. The revision stores client-visible fields as copies plus a rendered document; the audit row records actor, timestamp, field path, old value, new value and the current revision. Copying rather than pointing is what makes the snapshot survive later edits to the product library.
The itinerary changed but nobody edited it. How is that possible?
Because the trip is almost certainly storing pointers to components and templates that other people can edit. A product manager rewriting a tour description, a template correction pushed into live trips, or an overnight inventory import from a supplier will each change what a proposal says without anyone opening the trip. Check the component's updated timestamp before assuming a person edited the itinerary.
Is a full version history worth building, or is an audit log enough?
An audit log plus a frozen snapshot at each client-visible revision answers almost every real question and costs far less than event sourcing. Event sourcing earns its place when you need to replay a trip to any arbitrary point in time. Answering 'what did they approve, and what moved since' does not require that.
Should the client be able to see the change history themselves?
Show differences between client-visible revisions, never the internal edit log. A client benefits from a clear list of what moved between revision 3 and revision 4. They do not benefit from watching a consultant try 4 hotels before settling on one. Keep the internal log complete and derive the client-facing view from revision boundaries only.
- itinerary builder
- audit trail
- versioning
- trip data model
The work behind this page
Builds from our portfolio that this page draws on.
QuoteForge
An AI CPQ and proposal platform that builds enterprise quotes from your catalog, guards every discount against the margin floor, routes approvals, and generates the proposal.
Sales AIAskVault
An AI internal knowledge-search platform that answers employee questions from your own docs — grounded in citations, with knowledge gaps surfaced and deflection tracked.
Productivity AIRead next
- Version, variant, revision: three itinerary objects, not oneA version is a snapshot, a variant is a parallel option, a revision is what the client has actually seen. Collapse them and the invoice, the proposal and the voucher start disagreeing.definition
- The itinerary component: the smallest thing a trip can be built fromOne purchasable service carrying its own supplier, date, duration, pricing basis and cancellation term — and if it cannot be cancelled or repriced alone, it is a field on a component.definition
- The trip day: why day three is not a date until a departure is chosenDay 3 becomes 14 March only once a departure exists. Storing the offset instead of the date is what makes shifting a departure and inserting a day survivable operations.definition
- Bookings confirm on your site and the supplier has no recordA confirmation email proves the checkout finished, not that a supplier accepted anything. Here is the timed comparison that shows whether your booking script reads live availability or a copy.diagnostic
- Drafts that put a closed property on day four: fixing seasonal blindnessSeasonal blindness is a metadata failure, not a prompting failure: the closure lives in a sentence nobody can filter on, so the shut property stays in the candidate set the model draws from.diagnostic
- Enquiry-to-booking conversion: the metric that lies at six monthsGroup enquiries by the month they arrived and follow them until they close. The calendar-month ratio moves when volume moves, so a growing month looks like a bad one.definition
Working on something in this space?
Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.
Start the conversation