Manual DSR Handling — Standard Operating Procedure
Version: 1.1.0 Last Updated: 2026-05-18 UTC Owner: Operator (Kristerpher) SLA: 45 days from request receipt to response (CPRA/CCPA Cal. Civ. Code § 1798.100(b))
Status: This SOP is ACTIVE as of v1 launch. Automated DSR tooling is deferred to 2026-Q3 (#1630). During the deferral window, this manual process funds the privacy policy commitment at
docs/legal/policies/privacy-policy-v1.mdSection 8.This document does NOT constitute legal advice. See BLR DIY privacy memo PR #1646 and ADR-0076 §OQ-4.
Cross-references
- Privacy policy:
docs/legal/policies/privacy-policy-v1.md(Section 8 — DSR commitment) - Retention policy:
docs/ops/policies/data-retention.md - Automated DSR tooling (deferred): issue #1630
- Legal basis for retention:
docs/legal/artifacts/ropa-template.md
1. Intake
1.1 How requests arrive
Customers email support@raxx.app to initiate a DSR. Three request types are in scope:
- Access — "What data do you hold about me?"
- Correction — "Please correct this inaccurate data."
- Deletion (anonymization) — "Please delete / anonymize my data."
Portability requests ("give me my data in machine-readable format") are treated as a sub-type of Access.
1.2 FreeScout ticket creation
- The inbound email creates a FreeScout ticket automatically (via the
support@raxx.appmailbox). - If no auto-ticket is created (e.g., the request arrives via another channel), create a ticket manually in FreeScout.
- Apply the tag
dsr-requestto the ticket. - Note the receipt timestamp (UTC) in the ticket body — this is Day 0 of the 45-day SLA.
- Acknowledge receipt to the requester within 48 hours:
Subject: We received your privacy request — [Ticket #XXXX]
Hi [Name],
We received your request on [DATE UTC]. We will respond within 45 days. If we need additional information to verify your identity, we will contact you.
Questions? Reply to this email or reference ticket #XXXX.
Raxx Support support@raxx.app
2. Verification
Before processing any DSR, confirm the requester is the account holder.
2.1 Verification levels
| Request type | Verification required |
|---|---|
| Access | Email match: inbound email matches account email in billing_customer table |
| Correction | Same as Access |
| Deletion / anonymization | Stronger verification: (a) email match, AND (b) for sensitive deletions, send a one-time verification code to the account email and require the requester to reply with it |
2.2 Verification steps
- Query
billing_customerby email. Confirm the account exists and the email matches. - If the requester's email does NOT match an account, reply:
"We could not locate an account associated with this email address. If you have a different email on file, please reply with it. If you believe this is an error, contact us at support@raxx.app."
- Document the verification outcome (confirmed / unable to verify) in the FreeScout ticket notes.
- For deletion requests: send a confirmation email to the account address:
"We received a deletion request for the account associated with this email. If you did not submit this request, contact support@raxx.app immediately. If you did submit this request, no action is needed — we will proceed in 48 hours."
3. Process per DSR Type
3.1 Access Request
Scope: Export all personal data Raxx holds for the account, including:
- Identity: name, email
- Billing: billing address, last-4 card digits, card brand, Stripe customer ID, payment status
- Payment event history: late_payment_count, chargeback_count, failed_charge_count
- Acquisition source, customer segment
- Invoice history (invoice amounts, dates, status)
- Account metadata (created_at, plan, account status)
- Strategy configuration data (if stored in scope)
Steps:
- Run the access export query (see Section 6.1).
- Format results as a JSON or CSV file.
- Send via secure channel (attach to FreeScout reply, or use a secure file share if volume is large).
- Include a plain-language cover note explaining each field.
- Log audit row (see Section 4).
- Mark FreeScout ticket resolved; note completion date.
Deadline: 45 days from receipt (CPRA/CCPA Cal. Civ. Code § 1798.100(b)); extendable by an additional 45 days with notice where reasonably necessary. For reference: GDPR Art. 12(3) sets a 30-day baseline with up to 2-month extension — if EU residents are in scope, the shorter GDPR window is the binding deadline for those requests.
3.2 Correction Request
Scope: Correct inaccurate personal data the customer identifies.
Steps:
- Verify the specific data field and the correction requested.
- For billing fields (email, name, address): update directly in the
billing_customertable. - For payment event counts (
late_payment_countetc.): verify the count against Stripe event history (Stripe Dashboard → Customer → Payment history). If a Stripe event was erroneously recorded, update the count and note the discrepancy. - For Stripe-sourced data (card brand, last 4): update in Stripe Dashboard, then sync to
billing_customer. - Send confirmation to the requester with the corrected values.
- Log audit row (see Section 4).
- Mark FreeScout ticket resolved.
Edge case — disputed payment event counts: If the requester disputes a failed_charge_count but Stripe records confirm the event, document the Stripe event ID in the ticket and explain to the requester that the count reflects events recorded by our payment processor. Escalate to the operator if the discrepancy is unresolvable.
3.3 Deletion / Anonymization Request
Scope: Anonymize customer PII on the billing_customer record. Preserve invoice and transaction records for tax compliance (7-year floor). See docs/ops/policies/data-retention.md.
Steps:
- Check the decision tree (Section 7) for edge cases before proceeding.
- Run the anonymization SQL (see Section 6.2).
- Confirm the anonymization completed (query the row; verify
anonymized_at IS NOT NULL). - Confirm invoice rows are intact (they must NOT be deleted).
- Send confirmation to the requester:
"We have anonymized the personal information associated with your account. Invoice and transaction records required for tax compliance have been retained as described in our privacy policy. A record of this request is retained for compliance purposes."
- Log audit row (see Section 4).
- Mark FreeScout ticket resolved.
4. Audit Trail
Every completed DSR must generate an audit row in billing_action_log.
4.1 billing_action_log entry template
INSERT INTO billing_action_log (
customer_id,
action,
actor,
metadata,
created_at
)
VALUES (
'<customer_id>',
'dsr_manual_processed',
'<operator_github_handle>',
jsonb_build_object(
'dsr_type', '<access|correction|deletion>',
'freescout_ticket', '<ticket_id>',
'completed_at', NOW()::text,
'notes', '<brief summary>'
),
NOW()
);
4.2 Manual tracking during the manual era
Until automated tooling ships (#1630), also track each DSR in the DSR tracking spreadsheet:
| Column | Value |
|---|---|
| Ticket ID | FreeScout ticket number |
| Request type | access / correction / deletion |
| Received (UTC) | Day 0 timestamp |
| Verified (UTC) | Timestamp of identity verification |
| Completed (UTC) | Timestamp of completion |
| SLA deadline | Day 0 + 45 days |
| Processed by | GitHub handle |
| Notes | Any edge cases or escalations |
5. 45-Day SLA Monitoring
- Day 0: ticket received; acknowledgment sent within 48 hours.
- Day 40: if the ticket is still open, the FreeScout SLA reminder fires (configure FreeScout SLA at 40 days with notification to
support@raxx.app). - Day 40: if FreeScout SLA alert is not configured, add a calendar reminder manually on Day 0 for Day 40 to check status.
- Day 44: final escalation deadline. If not complete, notify the requester of the delay and provide an expected completion date within the statutory extension window.
CPRA/CCPA extension rule (primary): Cal. Civ. Code § 1798.100(b) — the initial response window is 45 days. A single 45-day extension is permitted (total 90 days) if the operator notifies the requester of the extension and the reason before the initial 45-day period expires.
GDPR extension note: GDPR Art. 12(3) sets a 30-day baseline with up to 2-month extension (total 3 months) for complex or numerous requests, with notice within the first month. If EU residents are in scope in a future release, the GDPR window is shorter and is the binding deadline for those requests. For v1 (US-only, California geo-target), CPRA governs.
6. SQL Templates
6.1 Access Export Query
-- DSR Access Export — returns all PII fields for one customer
-- Replace '<customer_id>' with the actual customer UUID or ID
SELECT
id,
email,
name,
address_line1,
address_line2,
address_city,
address_state,
address_postal,
address_country,
acquisition_source,
customer_segment,
late_payment_count,
chargeback_count,
failed_charge_count,
stripe_customer_id,
created_at,
updated_at,
anonymized_at
FROM billing_customer
WHERE id = '<customer_id>';
Export the result as JSON or CSV and include in the access response.
For invoice history, additionally query:
-- Invoice history for DSR access export
SELECT
id,
amount,
currency,
status,
created_at,
period_start,
period_end
FROM invoices
WHERE customer_id = '<customer_id>'
ORDER BY created_at DESC;
6.2 Deletion / Anonymization SQL
Run both statements in a single transaction. The idempotency guard (AND anonymized_at IS NULL) prevents double-anonymization.
BEGIN;
-- Step 1: Anonymize PII on billing_customer
-- Idempotency guard: only runs if anonymized_at is NULL (not already anonymized)
UPDATE billing_customer
SET
email = 'anonymized-' || gen_random_uuid() || '@deleted.local',
name = NULL,
address_line1 = NULL,
address_line2 = NULL,
address_city = NULL,
address_state = NULL,
address_postal = NULL,
address_country = NULL,
anonymized_at = NOW()
WHERE id = '<customer_id>'
AND anonymized_at IS NULL;
-- Step 2: Verify anonymization completed (row count should be 1)
-- If 0 rows updated, the row was already anonymized — check anonymized_at
SELECT id, email, anonymized_at
FROM billing_customer
WHERE id = '<customer_id>';
-- Step 3: Confirm invoice rows are intact (must NOT be deleted)
SELECT COUNT(*) AS invoice_count
FROM invoices
WHERE customer_id = '<customer_id>';
COMMIT;
After running: Verify anonymized_at is set and the email is in anonymized-<uuid>@deleted.local format. Confirm invoice count is as expected (invoices are never deleted).
Note on address_country: Per docs/ops/policies/data-retention.md, address_country may need to be retained for tax nexus purposes. If the CPA confirms this, replace address_country = NULL above with a comment that this field is retained. Review this with the CPA before first deletion.
6.3 Audit Log Insert (full example)
-- After completing anonymization, insert audit row
INSERT INTO billing_action_log (
customer_id,
action,
actor,
metadata,
created_at
)
VALUES (
'<customer_id>',
'dsr_manual_processed',
'kristerpher',
jsonb_build_object(
'dsr_type', 'deletion',
'freescout_ticket', 'FS-1234',
'completed_at', NOW()::text,
'notes', 'Anonymization completed per DSR request received 2026-05-XX UTC'
),
NOW()
);
7. Decision Tree — Edge Cases
7.1 Customer has an open/active subscription
Question: Can we delete a customer who is still subscribed?
Decision: 1. First, cancel the subscription in Stripe (customer must confirm they want to cancel, or operator cancels per their request). 2. Confirm any pending payments are settled (or note them as outstanding disputes). 3. Then proceed with anonymization per Section 3.3.
Do not: Anonymize a billing_customer row that has an active Stripe subscription without first canceling in Stripe — this will create a broken billing state.
7.2 Customer has pending or disputed payments
Question: Can we anonymize a customer who has an open Stripe dispute or unpaid invoice?
Decision: 1. If a Stripe chargeback dispute is open: defer anonymization until the dispute is resolved. Notify the requester:
"Your deletion request is acknowledged. We are unable to complete anonymization while an open billing dispute is pending. We will process your request within [X] days of dispute resolution. This is permitted under GDPR Art. 17(3)(e) (legal claims)." 2. If an invoice is unpaid: confirm with the operator before proceeding. The outstanding amount may represent a legal claim that justifies retention. 3. Document the reason for deferral in the FreeScout ticket and
billing_action_log.
7.3 GDPR Article 17(3) Exemptions — When to refuse erasure with explanation
GDPR Art. 17(3) permits refusing erasure where retention is necessary for:
| Exemption | Art. 17(3) basis | Example for Raxx |
|---|---|---|
| Compliance with a legal obligation | Art. 17(3)(b) | 7-year tax retention — invoice records must be retained |
| Establishment, exercise, or defense of legal claims | Art. 17(3)(e) | Open Stripe chargeback dispute; unpaid invoice; pending litigation |
| Public interest (archiving, research, statistics) | Art. 17(3)(d) | Not applicable for Raxx at v1 |
| Freedom of expression | Art. 17(3)(a) | Not applicable |
When to use Art. 17(3): 1. Always use it for invoice records — explain that invoices must be retained for 7 years per tax law. 2. Use it for open disputes — explain that records are retained to exercise/defend a legal claim. 3. Do NOT use it to refuse PII anonymization (name, email, address) — these can and should be anonymized even when invoice records are retained.
Response template for partial refusal (tax retention):
"We have anonymized your personal information (name, email address, and billing address) in our systems. We are required by law to retain invoice and transaction records for 7 years for tax and accounting compliance. These retained records are no longer linked to your name or contact details."
7.4 Requester is not the account holder (proxy request)
If a person claims to act on behalf of an account holder (e.g., a lawyer, family member):
- Require written authorization from the account holder.
- Verify the authorization before proceeding.
- Document the authorization in the FreeScout ticket.
8. Checklist — Per-DSR Completion
- [ ] Ticket created and tagged
dsr-requestin FreeScout - [ ] Receipt acknowledged within 48 hours
- [ ] Identity verified (email match + secondary for deletion)
- [ ] Day 40 SLA reminder set
- [ ] Request processed per type (access / correction / deletion)
- [ ]
billing_action_logrow inserted - [ ] DSR tracking spreadsheet updated
- [ ] Confirmation sent to requester
- [ ] FreeScout ticket marked resolved
This SOP must be reviewed annually. Next review: 2027-05-01 UTC. Automated DSR tooling target: 2026-Q3 (#1630) — update this SOP when that ships.