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
| Persona | Role | Source |
|---|---|---|
| Data Principal (Guest) | Submits DSAR via web form / email / staff intake | EPIC EP-004 US-004-010 / US-004-012 |
| DPO / Privacy Officer | Reviews + 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 check | hmp-web-dashboard/src/app/(dashboard)/crm/dsar/page.tsx |
| Guardian (DPDPA §9) | Submits DSAR for a minor or person with disability | EP-004 Gotcha 9 |
| Erasure Coordinator (system) | Cron / event-loop that publishes guest.erased + waits for acks | RetentionCronService + GdprService.eraseGuestData |
Entry points
| Entry | URL / Surface | Backed by |
|---|---|---|
| Staff DSAR intake | https://dashboard.<env>.keynova.ai/crm/dsar | hmp-web-dashboard/src/app/(dashboard)/crm/dsar/page.tsx + [id]/page.tsx |
| Staff DSAR detail / status transition | https://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/requests | hmp-guest-service/src/dpdp/dpdp.controller.ts |
| Guest self-service web form | [SME-PROPOSED] — does NOT exist today; staff-only intake | n/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 built | n/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()(persistsDsarRequestwithtrackingId=DSAR-<base36ts>-<uuid8>,slaDeadline=now+30d,contactEmailfrom JWT or guest row,ipAddressfromx-forwarded-for,status='RECEIVED'). WritesDsarAuditEntry(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
Guesttable for the requesting tenant (cross-tenant DSAR is impossible —DpdpService.createRequest404s on guest not in tenant). Submission channel known (web form / email / phone — recorded indescription). - Post-conditions:
DsarRequestrow inserted,trackingIdreturned to caller (guest receives an acknowledgement email via notification-service[SME-PROPOSED]— not currently wired).slaDeadlineset 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
slaDeadlinecolumn tracks it. Theescalatedboolean is set externally by the (deferred) escalation cron — not bycreateRequest. Guardian/minor consent path (§9): ifguest.dateOfBirthputs age < 18, the intake form MUST require guardian's signed consent OR reject withGUARDIAN_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;DpdpServiceadvancesRECEIVED→VERIFIEDviaupdateStatus()but the verification evidence (which ID was shown, who verified, when, via what challenge) is captured only as free-text in theDsarAuditEntry.detailsblob. DPDPA §11 + §12 require "verifiable" identity — a free-text note from staff is not auditable. - Code:
dpdp.controller.tsPATCH /api/dpdp/requests/:id/statusbody{ status: 'IN_REVIEW' }(maps DB-internalVERIFIED). The DB-enum state machine (dpdp.service.ts:74-80) enforcesRECEIVED → VERIFIED → IN_PROGRESS → COMPLETED | REJECTED. Audit row written byaudit()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 publicIN_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+DsarAuditEntryin 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 separatelogRequest()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.eraseGuestDataenforces ONE eligibility check: noCONFIRMED/CHECKED_INreservation. 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'] } } }); throwsConflictExceptionwhen > 0. Open folio balance is NOT checked (frontdesk-service'sFolio.balanceis a cross-service field accessible only via service-client — not invoked here). Tax retention is NOT checked (notaxRetentionUntilcolumn on Guest or Folio). RBI payment retention (Paymentrows 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 noPOST /api/dpdp/requests/:id/eligibility-checkpreview endpoint. - Post-conditions: If ineligible →
ConflictExceptionreturned; DSAR stays inIN_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/erasurefor a guest with closed stays from last week would happily anonymise theGuestrow but the linkedFolio.guestNamewould 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. TheGdprService.eraseGuestDataALREADY 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 callsgdprService.eraseGuestData()and marks the DSARCOMPLETEDin one transaction. No call toexportGuestData(). Compare to the ACCESS path (line 213-221):updateStatus(COMPLETED)for an ACCESS request does invokeexportGuestData()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 fireguest.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-forgetkafkaService.publish('guest.erased' as any, { guestId, tenantId, email, erasedAt, requestedBy }). The publish is OUTSIDE theprisma.$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.erasedtopic with envelope{eventId, tenantId, eventType, occurredAt, payload}. Eight consumers (next step) pick it up viakafkaService.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 beforeXACKcauses 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 publishesguest.erased.ackon success. Coverage matrix:Service File PII scrubbed Strategy reservation hmp-reservation-service/src/guest-erased/guest-erased.consumer.tsReservation.specialRequests,internalNotes,formCDataEncrypted(encrypted Form-C blob)null frontdesk hmp-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; Foliojoined viareservation.guestIdbecause Folio carries no directguestIdpos hmp-pos-service/src/consumers/guest-erased.consumer.tsPOSOrder.guestName(→null).PreOrderhas NOguestIdcolumn today (gap AP-CLEANUP-POS-001)per CLAUDE.md housekeeping hmp-housekeeping-service/src/consumers/guest-erased.consumer.spec.ts(consumer exists; specific scrub TBD via code-read — likely RoomServiceRequest.guestName)sentinel notification hmp-notification-service/src/consumers/guest-erased.consumer.tsNotificationLog.recipient(→[erased]— email/phone),Guestread-model (firstName/lastName→[erased], email/phone→null),WhatsAppConversation.guestName(null),guestPhone(→[erased])sentinel for NOT NULL crm hmp-crm-service/src/consumers/guest-erased.consumer.spec.tsper CRM CLAUDE.md — campaigns / loyalty / segments per service payment hmp-payment-service/src/consumers/guest-erased.consumer.spec.ts[SME-PROPOSED]per EP-015 — RazorpayPaymentLink.recipient_email/recipient_phonewere sent UPSTREAM (cannot un-send); localPayment.guestIdsoft-reference nulled. Razorpay-side erasure requires a DPO-portal API call out-of-scope here.local null only analytics hmp-analytics-service/src/consumers/guest-erased.consumer.tsMvGuestSpend.deleteMany({where:{guestId,tenantId}})— the only MV that names a specific guestDELETE spa hmp-spa-service/src/consumers/guest-erased.consumer.spec.tsper spa CLAUDE.md sentinel banquet hmp-banquet-service/src/consumers/guest-erased.consumer.spec.tsEventBooking.clientName/clientEmail/clientPhoneperGdprService(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 Stringsoft-ref, notguestId)n/a -
Code: Each consumer subscribes on
OnModuleInit. All wrap their writes inprisma.$transaction. All are idempotent (a second delivery is a no-op — PrismaupdateManyreturns count=0 without error). All publishguest.erased.ackwith{guestId, tenantId, service, erasedAt}. -
Test: 8
.spec.tsfiles (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.ackpublished 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.guestNameis 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 forPayment.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'sretention-untilfield 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 noGuestErasedAckConsumerin any service, noGdprErasureAcktable, no compliance-ledger view of "service X acked at T". The ledger built intoGdprErasureLog(lines 199-208 ofgdpr.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.ackRedis 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: shipGuestErasedAckConsumerin guest-service that writes oneGuestErasedAckrow per(guestId, service, erasedAt), then surfaceGET /api/gdpr/guests/:id/erasure-ledgerfor 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-158marks the DSARCOMPLETEDimmediately aftergdprService.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
COMPLETEDregardless 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,DsarRequestshould transition toCOMPLETED_WITH_PENDING_ACKS(new status value) and the missing consumer's row stays in apending_acksview 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
DsarCertificatetable +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
BackupSnapshotRetentiontable + cron).
Step 12: Audit log entry retained (anonymized PK, retention 6+ years)
- Story: US-004-010 (Built — partial).
GdprErasureLogtable pergdpr.service.ts:198-208is 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) + multipleDsarAuditEntry(per state transition).guestIdis preserved as a soft reference — the correspondingGuestrow is anonymised but the FK remains valid (no cascade-delete). TherequestedByfield capturesuserIdfor 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 POLICYor S3 archival job. An ADMIN couldDELETE FROM gdpr_erasure_logand we'd lose the evidence. Recommendation: (1) add WORM (write-once-read-many) constraint via a Postgres trigger + (2) daily CloudTrail-style archival tokeynova-cloudtrail-mgmtbucket (the same bucket holds AWS CloudTrail data events for 7-year Object Lock per Plan 0). Drift D-011.
Failure modes worth testing
| # | Scenario | Expected behaviour | Coverage today | Owning story |
|---|---|---|---|---|
| F1 | Consumer 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 published | Per-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 |
| F2 | Partial ack timeout (5 of 8 services acked within 5s, 3 stalled) | DSAR transitions to COMPLETED_WITH_PENDING_ACKS; ops dashboard surfaces the pending services | Not implemented — coordinator doesn't exist | [SME-PROPOSED] US-004-NEW-DSAR-ACK-LEDGER + Step 9 |
| F3 | Regulatory 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_REVIEW | Not implemented — only active-reservation check exists | [SME-PROPOSED] US-004-NEW-DSAR-ELIGIBILITY-MATRIX |
| F4 | Backup snapshot S3 PUT fails (KMS quota exceeded) | DSAR remains in IN_PROGRESS; alert fires to DPO; retry queue picks up after 5 min | Not implemented — Step 5 isn't built | [SME-PROPOSED] US-004-NEW-DSAR-ERASURE-INCLUDES-EXPORT |
| F5 | Ack 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-op | Not implemented — no consumer for the ack topic | F2 cascade |
| F6 | DPDPA 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 audit | No escalation cron exists; escalated boolean defined but never written to | [SME-PROPOSED] US-004-NEW-DSAR-SLA-ESCALATION |
| F7 | Certificate generation fails (pdfkit error after all 8 acks received) | DSAR transitions to COMPLETED_AWAITING_CERTIFICATE; nightly cron retries up to 3× then emails DPO | Not implemented — no certificate generator | Step 10 |
| F8 | Retention 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 time | Not implemented (Step 11) | Step 11 |
| F9 | Secondary 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 PII | Not implemented — secondary identifier re-creation is the #1 GDPR violation pattern in hospitality | [SME-PROPOSED] US-004-NEW-DSAR-IDENTIFIER-BLOCKLIST |
| F10 | Razorpay 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 copy | Not implemented; EP-015 RTM not yet built | [SME-PROPOSED] US-015-NEW-DSAR-UPSTREAM-COPY-DISCLOSURE |
| F11 | Auto-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 run | retention-cron.service.spec.ts covers happy path. No spec for cron-vs-new-reservation race. | US-004-010 follow-up |
| F12 | Guardian/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 accepting | Not 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.)
| ID | Type | Severity | Description | Surfaced by step |
|---|---|---|---|---|
| D-001 | Catalog-gap | P1 | EP-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-002 | Catalog-gap + UX | P0 | No 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-003 | Catalog-gap + Compliance | P1 | No 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-004 | Test-vs-code + Compliance | P1 | Identity 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-005 | Cross-EPIC + Compliance | P0 | Cascade 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-006 | Cross-flow + Story-incomplete | P0 | ERASURE 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-007 | Cross-service-handoff | P1 | guest.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-008 | Cross-service-handoff + Compliance | P0 | guest.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-009 | Cross-service-handoff | P0 | No 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-010 | Story-incomplete + Compliance | P1 | No 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-011 | Compliance + Retention | P1 | GdprErasureLog 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
| Step | Spans | Why cross-EPIC |
|---|---|---|
| 1 → 3 | EP-004 (guest) | Catalog gap — public DSAR intake doesn't exist; staff-only intake is the de facto path. |
| 4 | EP-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). |
| 5 | EP-004 (guest) | §11 export is in EP-004 but not wired into the ERASURE path. |
| 6 → 8 | EP-004 → all 8 consumer EPICs | The cascade is the most cross-EPIC piece of the platform. EP-014 (notifications) + EP-015 (payment) own the highest-risk PII surfaces. |
| 9 → 10 | EP-004 + ops | Coordinator + certificate are EP-004's responsibility but operationally surface in /crm/dsar (dashboard) and audit dashboards (EP-012C). |
| 12 | EP-004 ↔ EP-012C | Audit 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
GdprErasureLogfor monthly DPB reporting. - Razorpay DPO-API integration — programmatic erasure-request to upstream payment systems (Razorpay holds
PaymentLink.recipient_email/recipient_phoneindependently). - 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.formCDataEncryptedis 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
RECTIFICATIONDsarRequestType but UI is read-only; no edit screen. - Right-to-Nominate (DPDPA §14) —
NOMINATIONtype accepted butnomineeDetailsis free-text; needs structured persistence + nominee-acknowledgement email. - Annual DPDPA compliance attestation — auto-generated from
GdprErasureLogaggregates; 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.mdGotcha 30 — frontdesk consumer scrub matrix.hmp-notification-service/CLAUDE.mdConsumers table — notification consumer scrub matrix.hmp-analytics-service/CLAUDE.mdConsumers table — analytics consumer (DELETE strategy, only MV withguestId).