Skip to main content

BF-013 — DSAR / GDPR / DPDPA Erasure

Business flow (cross-EPIC user journey). Authored 2026-05-25 as Wave 2 of the RTM Reconciliation Scale-Out (1 of 12 parallel flows), matching the BF-001 template. Spans EP-004 (Guest Management — owns DSAR + erasure publisher), EP-014 (Notifications — must scrub PII from queues + delivery logs), EP-015 (Integrations / Payment — Razorpay PaymentLink recipient_email/recipient_phone), plus 6 more consumer services (reservation, frontdesk, pos, housekeeping, crm, spa, banquet, analytics, asset-service audit-log soft-mask).

Each step names (a) the user story that owns the requirement, (b) the primary code path that runs it, (c) the closest automated test that exercises it, and (d) pre/post-conditions and notes. Where the spec contains no matching catalog story, the step is tagged [SME-PROPOSED] as a NEW story for the SME's intake.

India regulatory frame is DPDPA Act 2023 (in force 2024-2025) — §11 Data Principal access, §12 correction/erasure, §13(3) 30-day SLA, §28 penalties. EU GDPR Art. 17 + 20 still applies for tourist guests with EU residency. Retention floors that block erasure: CBIC 7-year GST/invoice retention (Rule 56 CGST Act), RBI 10-year payment-instrument retention (UPI/card scheme rules), and state-level Hotel Guest Register laws (e.g. Maharashtra BPSS 1969 §41 — 1-year minimum).

Personas​

PersonaRoleSource
Data Principal (Guest)Submits DSAR via web form / email / staff intakeEPIC EP-004 US-004-010 / US-004-012
DPO / Privacy OfficerReviews + decides DSAR (verify, eligibility, accept/reject)[SME-PROPOSED] (no US-NNN-NNN row today; AP-T08-004 + P1-C4 ship parts)
Staff (FRONT_DESK / CRM_MANAGER)Intakes DSAR on guest's behalf, runs identity checkhmp-web-dashboard/src/app/(dashboard)/crm/dsar/page.tsx
Guardian (DPDPA §9)Submits DSAR for a minor or person with disabilityEP-004 Gotcha 9
Erasure Coordinator (system)Cron / event-loop that publishes guest.erased + waits for acksRetentionCronService + GdprService.eraseGuestData

Entry points​

EntryURL / SurfaceBacked by
Staff DSAR intakehttps://dashboard.<env>.keynova.ai/crm/dsarhmp-web-dashboard/src/app/(dashboard)/crm/dsar/page.tsx + [id]/page.tsx
Staff DSAR detail / status transitionhttps://dashboard.<env>.keynova.ai/crm/dsar/[id]dpdp.controller.ts::updateStatus
Manual erasure (legacy GDPR Art. 17 entry-point)DELETE /api/gdpr/guests/:id/erasure (ADMIN+)hmp-guest-service/src/gdpr/gdpr.controller.ts:24-38
Submit DPDP request (admin-portal / dashboard backend call)POST /api/dpdp/requestshmp-guest-service/src/dpdp/dpdp.controller.ts
Guest self-service web form[SME-PROPOSED] — does NOT exist today; staff-only intaken/a — gap D-002
Auto-erasure (90-day post-checkout DPDPA cron)n/a — @Cron('10 2 * * *')hmp-guest-service/src/gdpr/retention-cron.service.ts
Compliance audit log query[SME-PROPOSED] — GET /api/gdpr/audit-log?from=&to= not builtn/a — gap D-003

The 12-step journey​

Step 1: Guest submits DSAR (web form or email or staff intake)​

  • Story: US-004-010 (GDPR Right to Erasure — catalog "Built") + US-004-012 (Consent Mgmt — "Partial"). Both treat the surface as a staff-administered action. The guest-facing self-service intake form is [SME-PROPOSED] — US-004-NEW-DSAR-PUBLIC-INTAKE "Public DSAR submission form on booking-engine + post-stay email footer". Today every DSAR is opened by a staff member on the guest's behalf via /crm/dsar, which violates DPDPA §6(5) ("ease of withdrawal / request submission").
  • Code: Staff intake — POST /api/dpdp/requests → hmp-guest-service/src/dpdp/dpdp.service.ts::createRequest() (persists DsarRequest with trackingId=DSAR-<base36ts>-<uuid8>, slaDeadline=now+30d, contactEmail from JWT or guest row, ipAddress from x-forwarded-for, status='RECEIVED'). Writes DsarAuditEntry (action='CREATED').
  • Test: hmp-guest-service/src/dpdp/dpdp.service.spec.ts — createRequest specs (typed status mapping + audit-trail write). E2E: hmp-web-dashboard/e2e/tests/crm-dsar-lifecycle.spec.ts.
  • Pre-conditions: Guest exists in Guest table for the requesting tenant (cross-tenant DSAR is impossible — DpdpService.createRequest 404s on guest not in tenant). Submission channel known (web form / email / phone — recorded in description).
  • Post-conditions: DsarRequest row inserted, trackingId returned to caller (guest receives an acknowledgement email via notification-service [SME-PROPOSED] — not currently wired). slaDeadline set 30 days out.
  • Notes: India DPDPA §13(3) 30-day SLA is the hard ceiling; missing the deadline triggers §28 penalties up to ₹250 crore. The slaDeadline column tracks it. The escalated boolean is set externally by the (deferred) escalation cron — not by createRequest. Guardian/minor consent path (§9): if guest.dateOfBirth puts age < 18, the intake form MUST require guardian's signed consent OR reject with GUARDIAN_CONSENT_REQUIRED — not enforced today (Step 2 elaborates).

Step 2: Identity verification (govt ID + 2FA)​

  • Story: [SME-PROPOSED] new story — US-004-NEW-DSAR-IDENTITY-VERIFICATION "DSAR identity verification — govt-ID check + 2-factor confirmation". The catalog has NO story for this; DpdpService advances RECEIVED→VERIFIED via updateStatus() but the verification evidence (which ID was shown, who verified, when, via what challenge) is captured only as free-text in the DsarAuditEntry.details blob. DPDPA §11 + §12 require "verifiable" identity — a free-text note from staff is not auditable.
  • Code: dpdp.controller.ts PATCH /api/dpdp/requests/:id/status body { status: 'IN_REVIEW' } (maps DB-internal VERIFIED). The DB-enum state machine (dpdp.service.ts:74-80) enforces RECEIVED → VERIFIED → IN_PROGRESS → COMPLETED | REJECTED. Audit row written by audit() helper.
  • Test: dpdp.service.spec.ts :: updateStatus specs (transition validation, invalid-transition rejection, audit-entry write).
  • Pre-conditions: Step 1 complete; DSAR in RECEIVED/PENDING. Staff has access to /crm/dsar/[id]. Guest's govt ID type captured during intake (Aadhaar masked per UIDAI; passport for foreign guests).
  • Post-conditions: DsarRequest.status='VERIFIED' (mapped to public IN_REVIEW). DsarAuditEntry(action='STATUS_CHANGED', details='to IN_REVIEW') written. NO structured verification metadata (idType, idMaskedLast4, verifierUserId, verificationMethod) persisted — gap D-004.
  • Notes: Guardian-DSAR (Guest.dateOfBirth < 18) per DPDPA §9 must verify BOTH the guardian's identity AND the relationship (guardianRelation column on Guest is populated at create-time per Gotcha 9 — but the DSAR flow does not re-check it). 2FA on the verification (SMS OTP to guest's phone on record before the DPO accepts the request) is industry standard for DPO workflows; not implemented today.

Step 3: Request logged (DSAR.received status=PENDING)​

  • Story: US-004-010 (Built; persistence layer). Already covered by Step 1 — this step is redundant in the current implementation because Step 1 atomically creates DsarRequest + DsarAuditEntry in one call. Catalog conflates "received" (intake) with "logged" (persistence). The grand-plan charter step says they should be separable (intake on a public surface → queue → DPO review queue ingest); today they are one DB transaction.
  • Code: dpdp.service.ts::createRequest() is the single atomic insert. No separate logRequest() method.
  • Test: Same coverage as Step 1.
  • Pre-conditions / Post-conditions: identical to Step 1 today.
  • Notes: If the public-form ([SME-PROPOSED] US-004-NEW-DSAR-PUBLIC-INTAKE) ever ships, intake (no-auth web POST) and logging (DPO queue ingest after spam / bot check) must split. The Plan B captcha + bot-detection + rate-limit-per-IP layer becomes the gate between Steps 1 and 3.

Step 4: Eligibility check (no active reservation, no open folio balance, no regulatory hold)​

  • Story: US-004-010 (Built — partial coverage). GdprService.eraseGuestData enforces ONE eligibility check: no CONFIRMED/CHECKED_IN reservation. The two regulatory-hold checks (CBIC 7-yr GST, RBI 10-yr payments, state hotel-register law) are NOT enforced — [SME-PROPOSED] US-004-NEW-DSAR-ELIGIBILITY-MATRIX.
  • Code: hmp-guest-service/src/gdpr/gdpr.service.ts:46-58 — xPrisma.reservation.count({ where: { guestId, status: { in: ['CONFIRMED', 'CHECKED_IN'] } } }); throws ConflictException when > 0. Open folio balance is NOT checked (frontdesk-service's Folio.balance is a cross-service field accessible only via service-client — not invoked here). Tax retention is NOT checked (no taxRetentionUntil column on Guest or Folio). RBI payment retention (Payment rows are owned by payment-service, not joined here).
  • Test: hmp-guest-service/src/gdpr/gdpr.service.spec.ts :: erasure-with-active-reservation throws spec. No spec for open-folio refusal or regulatory-hold refusal (because the checks don't exist).
  • Pre-conditions: Step 2 (verified) complete. Eligibility runs synchronously inside eraseGuestData — not as a separate "preview" call. There is no POST /api/dpdp/requests/:id/eligibility-check preview endpoint.
  • Post-conditions: If ineligible → ConflictException returned; DSAR stays in IN_REVIEW; DPO must escalate or wait. If eligible → falls through into Step 6.
  • Notes: This is the biggest compliance gap in the current flow. India tax law (CBIC Rule 56) requires invoices + supporting records be retained 6 years from the end of the financial year — for a guest whose last stay was 2026-03-31, the earliest legal erasure of folio + e-invoice rows is 2032-03-31. Today an admin clicking DELETE /api/gdpr/guests/:id/erasure for a guest with closed stays from last week would happily anonymise the Guest row but the linked Folio.guestName would be scrubbed by the frontdesk consumer (Step 7) — that's WRONG. The folio guest-name is part of the supply document and must survive the 7-year hold. The GdprService.eraseGuestData ALREADY documents this: "preserves financial records" — but the cascade consumers don't respect it uniformly. Drift D-005.

Step 5: Backup snapshot taken (DPDPA §11 right-of-access fulfilled before erasure)​

  • Story: US-004-011 (GDPR Data Portability Export — catalog "Built", verified "Partial" per EP-004 RTM). The cross-flow expectation is that an ERASURE request implicitly satisfies §11 access first — the guest receives the export bundle and then the data is anonymised. Today the two requests are independent: an ERASURE DSAR does NOT auto-trigger the export; the guest has to file a separate ACCESS request first. [SME-PROPOSED] US-004-NEW-DSAR-ERASURE-INCLUDES-EXPORT.
  • Code: dpdp.service.ts:147-158 — when an ERASURE request lands, createRequest() immediately calls gdprService.eraseGuestData() and marks the DSAR COMPLETED in one transaction. No call to exportGuestData(). Compare to the ACCESS path (line 213-221): updateStatus(COMPLETED) for an ACCESS request does invoke exportGuestData() synchronously. Inconsistent.
  • Test: dpdp.service.spec.ts :: ERASURE auto-completion spec verifies the GDPR delegate is called but does NOT assert the export was first written to S3 / mailed.
  • Pre-conditions: Step 4 complete (eligibility passed).
  • Post-conditions: Today — no snapshot. The guest's last copy of their own data vanishes silently when ERASURE fires. Compliance gap.
  • Notes: DPDPA §11 explicitly says the Data Principal "shall have the right to obtain… a summary of personal data being processed and the processing activities undertaken." An ERASURE request without a final export bundle delivered to the guest's verified email leaves the guest with no copy of their own data — and no audit trail proving the platform sent it. Recommended fix: the DSAR ERASURE workflow MUST queue an export job + email the signed S3 URL to DsarRequest.contactEmail, wait for delivery confirmation, AND THEN fire guest.erased. Drift D-006.

Step 6: guest.erased event published (Redis Streams via @hmp/messaging)​

  • Story: US-004-010 (Built — publisher path). The eight-service cascade is the most operationally-mature piece of EP-004.
  • Code: hmp-guest-service/src/gdpr/gdpr.service.ts:218-232 — fire-and-forget kafkaService.publish('guest.erased' as any, { guestId, tenantId, email, erasedAt, requestedBy }). The publish is OUTSIDE the prisma.$transaction — the erasure on the owning DB commits first, then the event publishes asynchronously. A publish failure is logged as a WARN but the erasure is NOT rolled back.
  • Test: gdpr.service.spec.ts :: erasure-publishes-guest-erased spec. Cross-service: hmp-infrastructure/testing/flow-tests/src/flows/guest-lifecycle.flow.spec.ts :: Scenario 6 (erasure anonymises PII end-to-end — pass-rate TBD).
  • Pre-conditions: Step 4 + (ideally Step 5) complete. Owning-DB transaction succeeded.
  • Post-conditions: Event lands on Redis Streams guest.erased topic with envelope {eventId, tenantId, eventType, occurredAt, payload}. Eight consumers (next step) pick it up via kafkaService.subscribe.
  • Notes: The event uses per-(topic,subscriberId) consumer groups since @hmp/messaging@1.5.0 — every consumer service sees every event (fan-out). A consumer crash before XACK causes a redelivery — every consumer MUST be idempotent (they are; see Step 7). The publish-after-commit pattern means a transient bus failure leaves the OWN db updated but downstream services with stale PII for the guest until the manual sweep ([SME-PROPOSED] US-004-NEW-DSAR-MANUAL-SWEEP — does not exist). The transactional-outbox pattern that hmp-payment-service uses (PaymentOutboxService) would close this gap — drift D-007.

Step 7: 8+ consumer services null/redact (the cascade)​

  • Story: US-004-010 (Built — consumer cascade). Eight services subscribe to guest.erased; each scrubs its locally-owned PII and publishes guest.erased.ack on success. Coverage matrix:

    ServiceFilePII scrubbedStrategy
    reservationhmp-reservation-service/src/guest-erased/guest-erased.consumer.tsReservation.specialRequests, internalNotes, formCDataEncrypted (encrypted Form-C blob)null
    frontdeskhmp-frontdesk-service/src/consumers/guest-erased.consumer.tsFolio.guestName (→[erased]), Reservation.specialRequests/internalNotes/formCDataEncrypted (null), HospitalAuthorization.patientName/guestName (→[erased]), aadhaarHash (→''), aadhaarLast4 (→'****')sentinel for NOT NULL, null elsewhere; Folio joined via reservation.guestId because Folio carries no direct guestId
    poshmp-pos-service/src/consumers/guest-erased.consumer.tsPOSOrder.guestName (→null). PreOrder has NO guestId column today (gap AP-CLEANUP-POS-001)per CLAUDE.md
    housekeepinghmp-housekeeping-service/src/consumers/guest-erased.consumer.spec.ts(consumer exists; specific scrub TBD via code-read — likely RoomServiceRequest.guestName)sentinel
    notificationhmp-notification-service/src/consumers/guest-erased.consumer.tsNotificationLog.recipient (→[erased] — email/phone), Guest read-model (firstName/lastName→[erased], email/phone→null), WhatsAppConversation.guestName (null), guestPhone (→[erased])sentinel for NOT NULL
    crmhmp-crm-service/src/consumers/guest-erased.consumer.spec.tsper CRM CLAUDE.md — campaigns / loyalty / segmentsper service
    paymenthmp-payment-service/src/consumers/guest-erased.consumer.spec.ts[SME-PROPOSED] per EP-015 — Razorpay PaymentLink.recipient_email/recipient_phone were sent UPSTREAM (cannot un-send); local Payment.guestId soft-reference nulled. Razorpay-side erasure requires a DPO-portal API call out-of-scope here.local null only
    analyticshmp-analytics-service/src/consumers/guest-erased.consumer.tsMvGuestSpend.deleteMany({where:{guestId,tenantId}}) — the only MV that names a specific guestDELETE
    spahmp-spa-service/src/consumers/guest-erased.consumer.spec.tsper spa CLAUDE.mdsentinel
    banquethmp-banquet-service/src/consumers/guest-erased.consumer.spec.tsEventBooking.clientName/clientEmail/clientPhone per GdprService (currently scrubbed inline in guest-service; should move here)sentinel
    asset[CITED-BUT-NOT-FOUND] — guest-PII on engineering work-ordersn/a today (work-orders carry requestedBy String soft-ref, not guestId)n/a
  • Code: Each consumer subscribes on OnModuleInit. All wrap their writes in prisma.$transaction. All are idempotent (a second delivery is a no-op — Prisma updateMany returns count=0 without error). All publish guest.erased.ack with {guestId, tenantId, service, erasedAt}.

  • Test: 8 .spec.ts files (hmp-{reservation,frontdesk,pos,housekeeping,notification,crm,payment,spa,banquet,analytics}-service/src/consumers/guest-erased.consumer.spec.ts). All passing per CLAUDE.md gotchas.

  • Pre-conditions: Step 6 published successfully. Each consumer service is healthy + has consumed the event from its consumer-group offset.

  • Post-conditions: Per-service-owned PII columns scrubbed. guest.erased.ack published per service. Total PII surface goes from "guest name + email + phone + Aadhaar in 10 services" down to "anonymised tenant-bound row + audit log entry".

  • Notes: The cascade is the gold-standard architectural piece of EP-004. Every consumer was shipped in 2026-04 (AP-T08-005). However the FOLIO scrub is wrong for tax purposes — Folio.guestName is a supply-document field protected by the CBIC 7-yr retention floor; it should NOT be scrubbed for closed folios until the retention window expires. Same for Payment.invoiceRecipientName. This is the cross-EPIC compliance bug that the SME pass found (drift D-005 also surfaces here). Recommendation: cascade consumers must read each service's retention-until field per row before scrubbing — null = scrub now, future date = defer to a retention-cron sweep.

Step 8: Each service publishes guest.erased.ack​

  • Story: [SME-PROPOSED] new story — US-004-NEW-DSAR-ACK-LEDGER "Erasure acknowledgement ledger". Today the acks are PUBLISHED but nothing CONSUMES them — there is no GuestErasedAckConsumer in any service, no GdprErasureAck table, no compliance-ledger view of "service X acked at T". The ledger built into GdprErasureLog (lines 199-208 of gdpr.service.ts) records ONLY the originating erasure, not the per-service acks.
  • Code: 8 await this.kafka.publish('guest.erased.ack', { guestId, tenantId, service, erasedAt: new Date().toISOString() }) calls — one per consumer file. No subscriber to this topic exists.
  • Test: Every consumer spec asserts the publish call. No spec verifies the ack-consumption path (because the consumer is absent).
  • Pre-conditions: Step 7 per-service scrub succeeded.
  • Post-conditions: Ack published to guest.erased.ack Redis Streams topic. No persistence, no ledger row, no observable confirmation in any dashboard.
  • Notes: This is a critical gap — DPDPA §28 penalties hinge on the platform proving every consumer scrubbed. With no ack-ledger, an auditor cannot answer "did notification-service scrub guest X on 2026-04-15?" except by greping pod logs (which roll off after the cluster's log-retention window). Recommendation: ship GuestErasedAckConsumer in guest-service that writes one GuestErasedAck row per (guestId, service, erasedAt), then surface GET /api/gdpr/guests/:id/erasure-ledger for compliance evidence packs. Drift D-008.

Step 9: Coordinator waits for all 8+ acks (timeout)​

  • Story: [SME-PROPOSED] — see Step 8. The coordinator pattern does not exist today.
  • Code: There is no coordinator. dpdp.service.ts:147-158 marks the DSAR COMPLETED immediately after gdprService.eraseGuestData() returns, which is BEFORE any consumer service has even started processing the event (the publish was fire-and-forget on the previous tick of the Redis Streams loop). The "ack" semantically means nothing in the current flow — it's published but not awaited.
  • Test: No coverage.
  • Pre-conditions: Step 6 published.
  • Post-conditions: DSAR is COMPLETED regardless of downstream state.
  • Notes: This is the biggest functional gap in the BF-013 flow. A guest receiving the "your data has been deleted" confirmation email might still appear in CRM campaigns 30s later (read-model lag), in analytics MvGuestSpend if the consumer crashed (replay would catch it eventually), or — worst — in a Razorpay PaymentLink already sent to their phone (un-recoverable). Recommended timeout: 5s p95 to ack-complete (per the EP-004 RTM [SME-PROPOSED] "erasure cascade SHALL complete within 5 seconds p95"). After timeout, DsarRequest should transition to COMPLETED_WITH_PENDING_ACKS (new status value) and the missing consumer's row stays in a pending_acks view for ops to manually verify. Drift D-009.

Step 10: Once all ack: DSAR.status=COMPLETED, certificate generated​

  • Story: [SME-PROPOSED] new story — US-004-NEW-DSAR-COMPLETION-CERTIFICATE "Generate DPDPA §13 completion certificate (PDF, IRN-style numbered, signed)". Today the DSAR completion is just a status flip + audit entry — no certificate, no PDF, no guest-facing artifact.
  • Code: dpdp.service.ts:149-152 — update DsarRequest set status='COMPLETED'. audit(id, 'COMPLETED', actor, 'auto-completed via GDPR erasure'). No PDF generation, no signed artifact, no email to guest.
  • Test: dpdp.service.spec.ts asserts the status update + audit row only.
  • Pre-conditions: Step 9 (all acks received within SLA). Today: just Step 6 done.
  • Post-conditions: DSAR row terminal at COMPLETED. No deliverable for the guest — they receive no email, no certificate, no closure summary.
  • Notes: DPDPA §13(2) requires "a confirmation of the action taken" be delivered to the Data Principal. A status flip in our internal DB does not satisfy that. Recommended: ship a DsarCertificate table + GET /api/dpdp/requests/:id/certificate.pdf (signed JSON-LD + pdfkit) + auto-email on completion. The certificate should enumerate: trackingId, request date, action taken (ACCESS/CORRECTION/ERASURE/NOMINATION), services confirmed scrubbed, retention-hold fields preserved + their legal basis, DPO contact. Drift D-010.

Step 11: Backup snapshot retention countdown (7 days)​

  • Story: [SME-PROPOSED] — covered by Step 5 SME proposal. The snapshot doesn't exist, so the retention countdown doesn't exist either.
  • Code: None today.
  • Test: None.
  • Pre-conditions: Step 5 (snapshot taken) — not built.
  • Post-conditions: If snapshot existed, it should auto-delete from S3 7 days post-delivery via S3 Lifecycle Policy + an audit row in GdprErasureLog (event='snapshot_retention_expired').
  • Notes: Industry standard for §11 fulfillment is "snapshot available 7 days, then permanently deleted". This guards against the snapshot itself becoming a parallel PII surface (a guest's full export bundle on S3 is exactly the kind of data DPDPA §6(2) requires you to minimise). When Step 5 is built, Step 11 is a half-day add-on (S3 Lifecycle rule + a BackupSnapshotRetention table + cron).

Step 12: Audit log entry retained (anonymized PK, retention 6+ years)​

  • Story: US-004-010 (Built — partial). GdprErasureLog table per gdpr.service.ts:198-208 is the closest analog — but it's a single row per erasure (not per-service-ack), and there's no enforced retention floor or query API.
  • Code: gdpr.service.ts:198-208 — tx.gdprErasureLog.create({ tenantId, guestId, requestedBy, erasedFields[], legalBasis ('RIGHT_TO_ERASURE'|'RETENTION_EXPIRY'), status, completedAt }). Lives in guest-service DB. DsarAuditEntry (per-state-transition) sits alongside.
  • Test: gdpr.service.spec.ts asserts the GdprErasureLog row write inside the same transaction.
  • Pre-conditions: Step 10 complete.
  • Post-conditions: Two rows persist for the erasure event: one GdprErasureLog (compliance ledger) + multiple DsarAuditEntry (per state transition). guestId is preserved as a soft reference — the corresponding Guest row is anonymised but the FK remains valid (no cascade-delete). The requestedBy field captures userId for manual erasures and 'SYSTEM_CRON' for the 90-day retention cron.
  • Notes: DPDPA §28 + CBIC + SEBI all imply 6-year minimum retention for compliance audit records (vs. the data they describe). The retention is NOT enforced at the DB level today — there's no Postgres RETENTION POLICY or S3 archival job. An ADMIN could DELETE FROM gdpr_erasure_log and we'd lose the evidence. Recommendation: (1) add WORM (write-once-read-many) constraint via a Postgres trigger + (2) daily CloudTrail-style archival to keynova-cloudtrail-mgmt bucket (the same bucket holds AWS CloudTrail data events for 7-year Object Lock per Plan 0). Drift D-011.

Failure modes worth testing​

#ScenarioExpected behaviourCoverage todayOwning story
F1Consumer crashes mid-erasure (e.g. notification-service OOM during $transaction)Redis Streams XACK not sent → redelivery on next tick; idempotent updateMany makes redelivery safe; ack eventually publishedPer-consumer specs assert idempotency (Prisma updateMany count=0 on second call). No e2e flow test for crash-then-replay.[SME-PROPOSED] US-004-NEW-DSAR-CRASH-REPLAY
F2Partial ack timeout (5 of 8 services acked within 5s, 3 stalled)DSAR transitions to COMPLETED_WITH_PENDING_ACKS; ops dashboard surfaces the pending servicesNot implemented — coordinator doesn't exist[SME-PROPOSED] US-004-NEW-DSAR-ACK-LEDGER + Step 9
F3Regulatory hold blocks erasure (last folio closed 2026-03-31; CBIC 7-yr hold until 2032-03-31)Eligibility check (Step 4) returns ERASURE_BLOCKED_BY_TAX_RETENTION with hold-until date; DSAR remains in IN_REVIEWNot implemented — only active-reservation check exists[SME-PROPOSED] US-004-NEW-DSAR-ELIGIBILITY-MATRIX
F4Backup snapshot S3 PUT fails (KMS quota exceeded)DSAR remains in IN_PROGRESS; alert fires to DPO; retry queue picks up after 5 minNot implemented — Step 5 isn't built[SME-PROPOSED] US-004-NEW-DSAR-ERASURE-INCLUDES-EXPORT
F5Ack idempotency violation (consumer publishes 2 acks for the same event due to non-atomic XACK)Coordinator deduplicates by (guestId, service, eventId) — second ack is a no-opNot implemented — no consumer for the ack topicF2 cascade
F6DPDPA 30-day SLA missed (DPO on leave, DSAR sits 31 days at IN_REVIEW)escalated=true flag set by a daily escalation cron; PagerDuty alert to Privacy Officer; non-action past 30d auto-rejects with SLA_MISSED reason captured for §28 auditNo escalation cron exists; escalated boolean defined but never written to[SME-PROPOSED] US-004-NEW-DSAR-SLA-ESCALATION
F7Certificate generation fails (pdfkit error after all 8 acks received)DSAR transitions to COMPLETED_AWAITING_CERTIFICATE; nightly cron retries up to 3× then emails DPONot implemented — no certificate generatorStep 10
F8Retention countdown miscount (snapshot expires 8 days post-delivery due to clock drift across pods)S3 Lifecycle rule is the source of truth (uses S3-side clock, not pod clock); audit row captures the actual delete timeNot implemented (Step 11)Step 11
F9Secondary identifier re-creation (guest erased; tomorrow they book again via OTA with same email; OTA payload recreates the Guest row with all PII)EmailHashBlocklist table seeded by erasure (sha256(email)); reservation-service's GuestServiceClient.createOrUpdateGuest checks blocklist before creating; auto-anonymises new PIINot implemented — secondary identifier re-creation is the #1 GDPR violation pattern in hospitality[SME-PROPOSED] US-004-NEW-DSAR-IDENTIFIER-BLOCKLIST
F10Razorpay PaymentLink recipient_email cannot be un-sent (upstream OTA / payment system)Outbound consent log row written at send-time; erasure cascade includes an "external systems that received PII" enumeration in the certificate (Step 10); guest is informed via the certificate that Razorpay holds a separate copyNot implemented; EP-015 RTM not yet built[SME-PROPOSED] US-015-NEW-DSAR-UPSTREAM-COPY-DISCLOSURE
F11Auto-erasure cron fires for guest who started a new reservation after checkout (RetentionCronService runs at 02:10 + new booking arrived at 23:30)Cron's call to GdprService.eraseGuestData hits the active-reservation eligibility check (Step 4) and throws ConflictException; timer row stays processedAt IS NULL for the next day's runretention-cron.service.spec.ts covers happy path. No spec for cron-vs-new-reservation race.US-004-010 follow-up
F12Guardian/minor DSAR submitted by adult who is NOT the recorded guardian (impersonation)createRequest should verify guardianName + guardianRelation + guardianPhone against the Guest record's stored values before acceptingNot enforced[SME-PROPOSED] US-004-NEW-DSAR-GUARDIAN-VERIFICATION

Drift summary​

The 11 BF-013-surfaced drifts that Task 6 must adjudicate. (P0 = blocks DPDPA §28 audit defense; P1 = ships next sprint; P2 = backlog.)

IDTypeSeverityDescriptionSurfaced by step
D-001Catalog-gapP1EP-014 (Notifications) RTM is empty — no ### D- entries in the file. PII scrub by notification-service/guest-erased.consumer.ts (NotificationLog.recipient + WhatsAppConversation.guestPhone + Guest read-model) is not represented in any EP-014 user story.Step 7
D-002Catalog-gap + UXP0No guest-facing DSAR intake form exists. All DSARs are staff-administered via /crm/dsar. DPDPA §6(5) "ease of withdrawal" is structurally hard to satisfy without a public, no-auth submission surface (signed-URL via post-stay email + bot/captcha gate).Step 1
D-003Catalog-gap + ComplianceP1No DPO compliance-audit query API. DsarAuditEntry + GdprErasureLog rows exist in the guest-service DB but no GET /api/gdpr/audit-log?from=&to=&legalBasis= endpoint surfaces them. An auditor cannot self-serve evidence; ops has to run raw SQL.Step 12
D-004Test-vs-code + ComplianceP1Identity verification (Step 2) advances RECEIVED→VERIFIED via a status-only PATCH. No structured evidence (idType, idMaskedLast4, verifierUserId, verificationMethod, 2FA token id) is persisted. DPDPA §11 + §12 require "verifiable" identity.Step 2
D-005Cross-EPIC + ComplianceP0Cascade consumers (Step 7) scrub Folio.guestName, HospitalAuthorization.patientName, Payment.invoiceRecipient* — these are supply-document fields under CBIC 7-yr GST retention + RBI 10-yr payment retention. Scrubbing them violates Indian tax + RBI law. Today the scrub is unconditional; correct behaviour is "scrub for closed folios past the retention floor, defer otherwise".Steps 4, 7
D-006Cross-flow + Story-incompleteP0ERASURE DSAR does NOT auto-trigger §11 export. Guest's last copy of their own data vanishes silently. The ACCESS path DOES trigger the export — inconsistent. DPDPA §11 requires the data summary be available before erasure.Step 5
D-007Cross-service-handoffP1guest.erased event published OUTSIDE the prisma.$transaction (fire-and-forget). A transient bus failure leaves owning-DB updated + downstream services with stale PII. Transactional-outbox pattern (already shipped in hmp-payment-service PaymentOutboxService) would close this.Step 6
D-008Cross-service-handoff + ComplianceP0guest.erased.ack is PUBLISHED by 8 consumers but NO subscriber exists, no ledger table exists. A compliance auditor asking "did notification-service scrub guest X on 2026-04-15?" can only grep ephemeral pod logs. DPDPA §28 audit requires durable per-service confirmation.Step 8
D-009Cross-service-handoffP0No coordinator awaits acks before flipping DSAR COMPLETED. DSAR is marked done the moment guest-service's local DB updates, BEFORE any consumer has processed the event. The guest receives "your data is deleted" confirmation while CRM campaigns still hold their email.Step 9
D-010Story-incomplete + ComplianceP1No DSAR completion certificate. DPDPA §13(2) "confirmation of the action taken" not satisfied by a status-flip in an internal DB. Need PDF certificate + emailed signed S3 URL + DPO-counter-signature.Step 10
D-011Compliance + RetentionP1GdprErasureLog retention is not enforced at the DB level (no WORM, no S3 archival). Admin could DELETE FROM gdpr_erasure_log and lose evidence. DPDPA §28 implies 6-year minimum retention for compliance audit records.Step 12

Cross-EPIC narrative summary​

StepSpansWhy cross-EPIC
1 → 3EP-004 (guest)Catalog gap — public DSAR intake doesn't exist; staff-only intake is the de facto path.
4EP-004 ↔ EP-005A (frontdesk) ↔ EP-005B (payment) ↔ EP-012C (compliance)Eligibility check today reads only the local reservation count; should join folio balance (frontdesk-service), payment retention (payment-service), and the GST tax-retention floor (compliance).
5EP-004 (guest)§11 export is in EP-004 but not wired into the ERASURE path.
6 → 8EP-004 → all 8 consumer EPICsThe cascade is the most cross-EPIC piece of the platform. EP-014 (notifications) + EP-015 (payment) own the highest-risk PII surfaces.
9 → 10EP-004 + opsCoordinator + certificate are EP-004's responsibility but operationally surface in /crm/dsar (dashboard) and audit dashboards (EP-012C).
12EP-004 ↔ EP-012CAudit retention is owned by EP-012C compliance but the underlying tables live in EP-004's guest-service DB.

The 3 P0 drifts (D-002, D-005, D-006, D-008, D-009) are the material gaps the SME must escalate. D-005 (cascade scrubbing tax-retention fields) is the highest-risk because it actively violates Indian tax law; the other P0s are gaps in §28 audit defense that surface only under DPB scrutiny but are functionally invisible day-to-day.


Day-2 hardening (not in BF-013 scope)​

Items the Privacy / DPO team typically tackles in the first 60 days post DPDPA-compliance go-live. Flagged so cross-flow BFs know where to pick up:

  • DPO-portal dashboard — per-tenant DSAR queue + SLA timer + bulk-export of GdprErasureLog for monthly DPB reporting.
  • Razorpay DPO-API integration — programmatic erasure-request to upstream payment systems (Razorpay holds PaymentLink.recipient_email/recipient_phone independently).
  • OTA-side erasure — Booking.com / MakeMyTrip / Agoda all have their own PII stores; outbound DSAR forwarding via the channel-service is a Day-90+ item.
  • Encrypted Form-C archival — Reservation.formCDataEncrypted is scrubbed by the cascade BUT the corresponding C-Form PDFs already submitted to FRRO are unreachable. Disclosure in the certificate (Step 10) is the only mitigation.
  • DPDPA Sec 16 cross-border — guests resident in countries the Central Govt restricts transfer to need a flag at intake so their data is geo-pinned to ap-south-1.
  • Children's-data (DPDPA §9) auto-purge — anything under 18 should be auto-erased 30 days after the guardian's consent is withdrawn (vs the standard 90-day post-checkout cron).
  • Right-to-Correct (DPDPA §12) workflow — covered by the existing RECTIFICATION DsarRequestType but UI is read-only; no edit screen.
  • Right-to-Nominate (DPDPA §14) — NOMINATION type accepted but nomineeDetails is free-text; needs structured persistence + nominee-acknowledgement email.
  • Annual DPDPA compliance attestation — auto-generated from GdprErasureLog aggregates; needs DPO signature flow.

See also​

  • BF-001-tenant-property-onboarding.md — template followed by this doc.
  • ../reconciliation/PLAYBOOK.md — drift criteria, SME-hat rules, sidecar pattern.
  • ../reconciliation/EP-004-rtm.md — Guest Management RTM (owns the erasure publisher + 12 drift findings on PII/consent surface).
  • ../reconciliation/EP-014-rtm.md — Notifications RTM (consumer side; drift findings live in this BF until EP-014's own SME pass refreshes them).
  • ../reconciliation/EP-015-rtm.md — Integrations / Payment RTM (Razorpay PaymentLink recipient PII gap).
  • hmp-guest-service/CLAUDE.md — service ground-truth (Gotchas 9-11 cover DPDPA §9 guardian consent + retention timer + cascade idempotency).
  • hmp-frontdesk-service/CLAUDE.md Gotcha 30 — frontdesk consumer scrub matrix.
  • hmp-notification-service/CLAUDE.md Consumers table — notification consumer scrub matrix.
  • hmp-analytics-service/CLAUDE.md Consumers table — analytics consumer (DELETE strategy, only MV with guestId).