Repository navigation
fix: UK GDPR/PECR erasure, deletion callback and SMS opt-out - #41
Open
Mcgyver-ai wants to merge 12 commits into
Open
Mcgyver-ai wants to merge 12 commits into
Mcgyver-ai wants to merge 12 commits into
Conversation
…el failures
eraseUserData also deletes agencies/{uid}, inbound_tokens, ghl_events, invites, orphan users docs sharing the stripeCustomerId, and removes the uid from another owner's agency. cancelSubscriptionBeforeErasure aborts deletion unless the subscription is cancelled or already gone.
smsSuppressed is keyed by Twilio's E.164 From number. Reminders are now validated as E.164 at schedule time, and the cron fails any queued non-E.164 reminder instead of sending past an opt-out.
…S fixes into routes
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Invites hold the invitee's email and acceptedBy uid even when another user owns them.
An unverified token email could belong to anyone, so deleting that account would wipe pending invites meant for the real owner. Drops the stored-email fallback, which came from the same unverified source.
This was referenced Oct 17, 2026
Resolves the route.js conflicts with #42 by taking main's side: GET /api/result/:id stays deleted, and main's retrieve-first Stripe cancel in DELETE /api/account replaces the branch's version. toPublicResult and its test go with the deleted route. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018sQyAmPXx4AVoEa3XZmC6b
…hook recreating erased users
cancelSubscriptionBeforeErasure now retrieves first and skips the cancel
for canceled or incomplete_expired subscriptions. The cancel-first
version called cancel on incomplete_expired, which Stripe rejects, so
those users could never delete their account. The route uses the tested
helper again instead of main's inline copy.
customer.subscription.deleted lands right after erasure. It used
set(merge) on users/{uid}, which recreated the doc for a deleted user;
update() with NOT_FOUND ignored can't.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sQyAmPXx4AVoEa3XZmC6b
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018sQyAmPXx4AVoEa3XZmC6b
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Audit fixes against
e493421, for UK GDPR/PECR. Branched frommain(independent of #40).main(with #42) is merged in; conflicts were resolved infe29ce5.GET /api/result/:idoutright (nothing called it) along with its OpenAPI entry. The merge takes main's side and drops this PR'stoPublicResultfilter and its test.app/r/[id]reads Firestore server-side and never rendersownerEmail/ownerUid.eraseUserDatanow also deletesagencies/{uid},inbound_tokens(uid),ghl_events(uid) andinvites(agencyId,acceptedBy, andemailwhen the token email is verified). It readsusers/{uid}first: any otherusersdocs with the samestripeCustomerId(the webhook's customer-id-keyed orphans) are deleted too. IfagencyIdpoints at someone else's agency, the uid is removed from that agency'smemberUidsviaFieldValue.arrayRemove(an already-deleted agency is tolerated).accountService.test.jsextendedcancelSubscriptionBeforeErasurenow uses #42's retrieve-first rules: it skips the cancel forcanceledorincomplete_expired, continues onresource_missing, and returns 502Could not cancel your subscription. Please retry or contact support.for anything else, before any erasure. The route calls the helper instead of #42's inline copy, so the logic is unit-tested. This PR's original cancel-first version calledcancelonincomplete_expiredsubscriptions, which Stripe rejects, so those users could never delete their account. That's fixed inf6419f8's parent. Also: the cancel firescustomer.subscription.deletedright after erasure, and the webhook'sset(merge)recreatedusers/{uid}for the deleted user. It now usesupdate()and ignores NOT_FOUND.accountService.test.js: no sub, active cancelled,canceled/incomplete_expiredskip the cancel,resource_missingcontinues, cancel failure → 502, status check failure → 502 with no cancel. The webhook change has no test (no webhook test harness exists).smsSuppressedis keyed by Twilio's E.164From, so a reminder stored as07700 900123could never match an opt-out./reminders/schedulestrips spaces and requires/^\+[1-9]\d{7,14}$/, otherwise 400to must be E.164, e.g. +447700900123(no country-code guessing). The cron marks any queued non-E.164 reminderstatus: "failed", error: "invalid number"and skips it, and checks and sends using the normalized number. The same rule is inreminderService.js(normalizeE164), which the live route imports.reminderService.test.jsItem 2: Meta deletion callback is blocked
Meta's
user_idis the app-scoped Facebook user ID. The Instagram and Messenger connect flows only take a pasted Page access token and validate it with page/account field lookups (lib/meta.js), so no user ID is ever available to store. The callback is unchanged: it still deletes nothing and still recordsstatus: "completed". Suggested follow-up (in TASKS.md): callGET /debug_tokenat connect time with an app access token (META_APP_ID|META_APP_SECRET), or add Facebook Login. Until then, the callback should stop reportingcompleted.Other notes
reminderServicegets the same E.164 fix but isn't wired into the route's cron wholesale: the service comparesCRON_SECRETwith!==, while the live route uses timing-safesafeEqual.Test plan
yarn test:Test Files 20 passed (20)/Tests 160 passed (160)yarn buildwith CI's placeholder env:✓ Compiled successfully/✓ Generating static pages (76/76)🤖 Generated with Claude Code
https://claude.ai/code/session_018sQyAmPXx4AVoEa3XZmC6b