ADR-037: CRM Identity Model โ PersonProfile and Booker Concept โ
Status: โ Approved โ 2026-05-20 Impacts: domain-backoffice.md, schema-backoffice.md, domain-commerce.md, schema-commerce.md, domain-communications.md, domain-customer-intelligence.md, adr-021, event-contracts-commerce.md, invoice-service-protocol.md, booking-lifecycle-protocol.md, cancellation-protocol.md
Context โ
The domain model uses four different names for the same real-world person across four bounded contexts:
| Context | Entity | Meaning |
|---|---|---|
| Backoffice | PersonProfile | CRM person record |
| Commerce | Passenger | Traveler on a booking |
| Communications | Contact | Messaging identity |
| Customer Intelligence | CustomerProfile | Behavioral analytics root |
Three structural problems triggered this ADR:
PassengerProfilebakes in the wrong assumption. The entity functions as a universal person record (de-duplication by email/phone, dietary needs, CRM history), but its name implies every person is a traveler. A company secretary booking 50 employee tours, or a grandmother booking for her grandchildren, receives a "PassengerProfile" despite never boarding a bus.No explicit Booker identity. The person who pays (booker) hides behind
passengers.is_primary_contact = true. This forces the booker into aPassengerrow even when they don't travel. Customer Intelligence cannot distinguish "this person booked 8 times" (commercial value) from "this person rode 8 times" (travel behavior). The booker drives revenue โ CI needs to identify them directly.CI identity anchor is unspecified. ADR-021 says CI "reads PassengerProfile via
PersonProfileUpdatedevents" but never declares the FK column connectingCustomerProfileto the identity graph.
Decision โ
1. Rename PassengerProfile โ PersonProfile โ
The Backoffice CRM record becomes PersonProfile โ a role-neutral anchor for any person the operator has a relationship with (booker, traveler, or both).
- Traveler-specific fields (
dietary_needs,date_of_birth) remain onPersonProfileas nullable attributes. They are populated only when the person also travels. Even for travelers,dietary_needsare optional (not every tour type cares). - Profile creation triggers when a person first appears as either a booker or a passenger on any booking.
- De-duplication matches by email or phone number. Profiles without contact identifiers (e.g., children) cannot de-duplicate โ manual merge is the operator's responsibility via the CRM interface.
- Physical table rename (
passenger_profilesโperson_profiles) and column renames (passenger_profile_idโperson_profile_id) are complete. Hasura metadata, SQL schema, and documentation are aligned.
2. Add booker_profile_id on Booking [planned โ Phase 1] โ
A new soft FK on commerce.bookings identifies the person with billing authority:
booker_profile_id UUID NULLABLE
Soft FK โ backoffice.person_profiles
CHECK (booker_profile_id IS NOT NULL OR status = 'DRAFT' OR reseller_id IS NOT NULL)- B2C checkout (Phase 1): The form submitter is assumed to also be the primary contact passenger. The
submitCheckouthandler resolvesbooker_profile_idfrom that passenger'sPersonProfile. Capturing a non-traveler booker (the grandmother case) requires a checkout UI change โ deferred to Phase 1.x. The data model already supports it via the booker/passenger separation. - Dispatcher-created bookings: The dispatcher sets
booker_profile_iddirectly. The booker need not appear as aPassengerrow. - B2B bookings (Reseller):
booker_profile_idis optional whenreseller_idis present. Thereseller_idcaptures the primary commercial relationship; the booker provides optional CRM depth on the agency contact. CI limitation: B2B bookings withoutbooker_profile_iddo not attribute lifetime-value or booking-count metrics to individual end-customers. Aggregate revenue is attributed at theResellerlevel. A future enhancement may capture the end-customer's identity at quote conversion. - Migration backfill:
UPDATE bookings SET booker_profile_id = (SELECT person_profile_id FROM passengers WHERE booking_id = bookings.id AND is_primary_contact = true LIMIT 1).
3. Redefine is_primary_contact on Passenger โ
is_primary_contact no longer indicates billing authority. It now means: "the on-trip communication recipient and driver's on-site contact." Billing authority lives exclusively on bookings.booker_profile_id.
4. Communication Routing Split โ
| Message type | Recipient | Resolution |
|---|---|---|
| Transactional (confirmations, invoices, payment reminders, cancellation refunds) | Booker | bookings.booker_profile_id โ person_profiles |
| Operational (pre-departure reminders, boarding details) | Primary contact passenger | passengers WHERE is_primary_contact = true |
Communications auto-creates or matches a Contact for the booker's PersonProfile via email/phone de-duplication.
5. CI Identity Anchor โ
CustomerProfilecarriesperson_profile_id(soft FK to BackofficePersonProfile). CI creates aCustomerProfileonly when aPersonProfileexists.BookingConfirmedevents carrybooker_profile_id. CI attributeslifetime_revenue,booking_count, andaverage_booking_lead_timeto the booker'sCustomerProfile. Travelers receive travel-specific metrics only.- Other lifecycle events (
BookingCancelled,BookingRefunded,BookingFullyPaid,BookingCompleted,BookingNoShow) do not carrybooker_profile_idin their payloads. CI resolves the booker by joiningbooking_idโbookings.booker_profile_idat consumption time. - CI event consumers discard communication events from
Contactrecords without aperson_profile_idlink.
6. Authorization Update โ
Self-service cancelBooking authorizes on the booker (billing authority), not on is_primary_contact. A child listed as primary contact should not cancel a booking their grandmother paid for.
7. Border Document Authority and Pre-Fill โ
Border manifest fields (document_number, nationality) exist only on Commerce Passenger โ not on PersonProfile. These are point-in-time travel credentials, not identity attributes:
- Passport numbers expire and renew between bookings. The border manifest requires the document the traveler carries on this departure, not the one from a previous trip.
- PersonProfile deliberately omits these fields to avoid a stale-data trap where the CRM claims "passport X123" while the traveler renewed to "Y456."
- Conditional requirement.
document_numberandnationalityare required only for cross-border tours. They are nullable in the schema โ domestic tours (e.g., Nordsee day trips) do not need border manifest data. The operator bears responsibility for collecting the right fields for their specific routes.
DOB sync direction: date_of_birth is the one shared field โ it exists on both PersonProfile (CRM, demographic pricing) and Passenger (border manifests). On BookingConfirmed, the PersonProfile updater writes back the Passenger's DOB to PersonProfile.date_of_birth โ the most recent booking input wins. If a Passenger row omits DOB (domestic tour), the system does not overwrite an existing PersonProfile DOB. Rationale: the traveler actively re-entered it at booking time and may have corrected a typo.
Pre-fill strategy (returning travelers): The checkout and dispatcher UIs query the traveler's most recent Passenger row (passengers WHERE person_profile_id = :id ORDER BY created_at DESC LIMIT 1) and offer the previous document_number, nationality, and date_of_birth as pre-filled defaults. The traveler confirms or updates before submission. This is a UI-layer read-only suggestion โ the Passenger row remains the authoritative, booking-scoped record. Pre-fill requires a linked person_profile_id โ children and other passengers without contact identifiers may lack a stable profile link (see ยง2 de-duplication limits) and require manual data entry.
GDPR note: document_number, nationality, and date_of_birth are included in the scrub_passengers tombstone set alongside first_name, last_name, email, and phone. A passport number is a government-issued personal identifier โ it must be redacted on the same schedule as other PII.
Consequences โ
Positive โ
- Identity-role separation. "Who is this person?" (
PersonProfile) and "what role do they play on this booking?" (booker vs. traveler) are distinct questions with distinct answers. - CI revenue attribution. CI can attribute lifetime value to the person who pays, not the person who rides.
- CRM accuracy. Booker-only records (e.g., company secretaries) no longer carry empty traveler fields with an incorrect "Passenger" label.
Negative โ
- ~28 documentation files require updates. One-time cost, no code changes.
- Physical renames complete.
passenger_profilesโperson_profiles(table),passenger_profile_idโperson_profile_id(columns oncommerce.passengersandcommunications.contacts),trip_idโservice_leg_id(oncommunications.conversations). Hasura metadata, SQL schema, index migrations, and all documentation are aligned. - GDPR cascade required.
bookings.booker_profile_idfollows the same GDPR cascade aspassengers.person_profile_idโ soft-nullified onPersonProfileerasure.invoices.recipient_snapshotretains the frozen booker PII per GoBD ยง 147 AO (10-year retention) โ the snapshot is NOT redacted on PersonProfile erasure.
Neutral โ
- CI entity names unchanged.
CustomerProfile,CustomerActivityLog,CustomerSegmentstay โ "customer" is CI's own ubiquitous language, industry-standard for analytics contexts. Passengerentity name stays. It correctly means "a traveler on a booking."
Rejected Alternatives โ
Option B โ Separate BookerProfile alongside PassengerProfile โ
Create a new BookerProfile entity for the payer, keep PassengerProfile for travelers. Rejected because:
- De-duplication splits. A person who is both booker and traveler exists in two tables. Merge logic becomes a cross-entity join problem.
- Contact resolution ambiguity. Communications would need to link to both
BookerProfileandPassengerProfileโ polymorphic FK or union query. - The CRM already functions as a universal record. The existing de-duplication logic (email/phone match) treats the profile as person-centric, not role-centric. Renaming aligns the name with the existing behavior.
Cross-References โ
| Document | Section | Change |
|---|---|---|
| domain-backoffice.md | Entity dictionary | PassengerProfile โ PersonProfile |
| schema-backoffice.md | person_profiles | Physical rename complete |
| domain-commerce.md | Booking entity | Add booker_profile_id |
| schema-commerce.md | bookings table | Add booker_profile_id column |
| domain-model.md | Soft FK map | New row: Booking.booker_profile_id โ PersonProfile |
| invoice-service-protocol.md | ยง 14 UStG mapping | Recipient source โ booker |
| booking-lifecycle-protocol.md | cancelBooking guard | Auth on booker |
| event-contracts-commerce.md | Payloads | passenger_email โ booker_email |
| adr-021 | Identity anchor | person_profile_id specification |