Manual DSR Handling — Standard Operating Procedure
Version: 1.2.0 Last Updated: 2026-10-31 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.As of 2026-10-31 UTC the automated erasure service (
billing_dsr_service.execute_dsr) is not live: its routes answer 404 whilebilling_dsr_erasure_flowis OFF. A request that arrives by email (policy Section 8) is processed by hand with this SOP, or with the Console customer delete. See Section 3.3 Step 4 for the detail.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 the account email in the billing_customer table, or, for an account with no billing record, users.email (Section 2.3) |
| 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 query returns nothing, do not conclude there is no account yet: a free-tier account has no billing record. Run theusers.emailcheck in Section 2.3 first. - If the requester's email matches neither a
billing_customerrow nor a Raptorusersrow (Section 2.3), 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."
2.3 Account with no billing customer row
A free-tier account has a Raptor users row but no billing record: a person who never
subscribed has no billing customer row. If that person answered the setup quiz, the
account also holds a trader_profiles row and quiz answers in onboarding_state
(#4837), and the privacy policy tells them that deleting their account removes those
answers. So an empty billing_customer result is not "no account", and
the Section 2.2 step 2 reply must not be sent until this check has also come back empty.
Verify against the Raptor users table (read-only):
SELECT id, email, created_at FROM users WHERE lower(email) = lower('<requester_email>');
- One row: the requester has a Raptor account. The email match on
users.emailis the verification, and a deletion request takes the same extra step as Section 2.1 (the one-time code goes to that address).idis the<raptor_user_id>used in Section 3.3. Note in the FreeScout ticket: "verified against users.email, no billing customer row". - No row in
usersand none inbilling_customer: send the Section 2.2 step 2 reply.
What to do next for this account, by request type:
- Access / Correction: Sections 3.1, 3.2 and 6.1 are written for the billing record
and return nothing for this account. The personal data held is the
usersrow and the setup answers (trader_profiles, and the quiz slugs inonboarding_state.step_data). Include those in the response; do not answer "no data held". - Deletion: follow the "Account with no billing customer row" branch at the top of Section 3.3.
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).
billing_action_loghas no Access action, and an account with no billing customer row has no row to log against; either way the record is the FreeScout ticket and the tracking spreadsheet (Section 4.2). - 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).
billing_action_loghas no Correction action, and an account with no billing customer row has no row to log against; either way the record is the FreeScout ticket and the tracking spreadsheet (Section 4.2). - 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.
Account with no billing customer row (Section 2.3). There is no billing_customer
row to anonymize and there are no invoices, so steps 2, 3 and 5 below do not apply.
Take the <raptor_user_id> from Section 2.3, then do ONE of the following:
- Full account deletion (the requester wants the account gone): the Console
customer delete. Open
/console/customers/<raptor_user_id>/danger-zone; the form posts toPOST /console/customers/<raptor_user_id>/delete. It is the admin hard delete: it removes theusersrow and, throughON DELETE CASCADE,trader_profiles,onboarding_stateandidempotency_keys, so Step 4 is not needed afterwards. It is the only route in this SOP that removes the account itself. It needs the Console customer-admin flag (console_customer_admin) ON, theconsole-managerrole and TOTP elevation, and it cannot be undone. The Console writes its own audit row (console.customer.deleted). - Delete the stored data and keep the account: run Step 4 below with that
<raptor_user_id>: 4a/4b foridempotency_keysand 4c for the setup answers. 4c is the step that keeps the policy's "deleting your account removes them" promise when the account is not hard-deleted.
Then use the variant confirmation in step 6 and skip step 7's billing_action_log row
(that table is keyed on a billing customer; there is none). The compliance record for
this account is the dsr_log rows from Step 4 (or the Console audit row), the FreeScout
ticket and the tracking spreadsheet.
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). - Erase the Raptor-side data for the user account:
idempotency_keys(4a/4b) and the stored setup-quiz answers (4c). For anonymize-path erasures theusersrow is not deleted, soON DELETE CASCADEdoes not fire on any table that hangs off it; each of these is an explicit sweep.idempotency_keys: SC-IK-1 (#4145), ADR-0138 §Security/GDPR, issue #4150. Setup answers (trader_profiles,onboarding_stateand the advisory personal rule inpersonal_rules, #4828): #4837 — the privacy policy and the setup quiz notice tell people that deleting their account removes these answers, so this is a promise to keep, not an optional tidy-up.
Automated path (only when billing_dsr_erasure_flow is ON): If the request is
processed via billing_dsr_service.execute_dsr(), steps 4a, 4b and 4c all run
automatically, in the same transaction as the anonymization: the service calls
erase_user_idempotency_keys() and then erase_user_setup_answers() from
raptor_user_dsr_service. Do not run the manual SQL below on top of it.
This path is not what runs today. execute_dsr() is reachable only through
/api/billing/dsr/* and POST /api/internal/jobs/billing-dsr/execute. Those routes
answer 404 unless billing_dsr_erasure_flow is ON (it is default: false,
risk: high). Nothing in the repo schedules or calls the batch route, and no
application code creates the Raptor billing_customers rows that execute_dsr()
works from. So a request that arrives by email (policy Section 8) is processed by hand:
the manual path below, or the Console customer delete (see the top of this section).
Treat the automated path as available only once the flag is ON and the flow has been
exercised end to end; until then do not wait for it. When it has run, verify
both compliance rows after the request completes — a dsr_log row for each
sweep, whose request_id starts with the DSR id and carries the sweep name:
sql
SELECT request_id, tables_affected, rows_pseudonymized, status
FROM dsr_log
WHERE customer_id = <raptor_user_id>
AND (request_id LIKE '%#dsr.idempotency_keys.erased#%'
OR request_id LIKE '%#dsr.setup_answers.erased#%')
ORDER BY id;
Expect two rows (dsr.idempotency_keys.erased and dsr.setup_answers.erased),
both status = 'completed'. A dsr.idempotency_keys.erased row without a
dsr.setup_answers.erased row means the setup answers were NOT erased: run
Step 4c by hand. (The two sweeps share one transaction, so this only happens if
the request was processed by something other than execute_dsr().)
Manual path (operator-executed SQL; the path used today): Look up the Raptor
users.id for the account email (Section 2.3 has the query). <ticket_ref> is the
FreeScout ticket number; it becomes part of the dsr_log.request_id, which is unique,
and the NOW() suffix keeps a re-run from colliding. Each statement below deletes and writes its compliance row in
one statement, so the row count is the real count; wrap each in a transaction.
dsr_log has the columns request_id, customer_id, requested_at,
completed_at, tables_affected (JSON array), rows_pseudonymized (the rows
deleted, whatever the column is called) and status. There is no action or
context column.
sql
BEGIN;
-- Step 4a: delete idempotency_keys rows for the subject.
-- Step 4b: write the compliance audit row, in the same statement.
-- Replace <raptor_user_id> with the integer users.id from the users table.
WITH gone AS (
DELETE FROM idempotency_keys WHERE scope_user_id = <raptor_user_id> RETURNING 1
)
INSERT INTO dsr_log (request_id, customer_id, requested_at, completed_at,
tables_affected, rows_pseudonymized, status)
SELECT '<ticket_ref>#dsr.idempotency_keys.erased#' || NOW()::text,
<raptor_user_id>, NOW(), NOW(),
'["idempotency_keys"]'::jsonb, (SELECT COUNT(*) FROM gone), 'completed';
COMMIT;
If the account has no Raptor users row (billing-only customer), this step
is a no-op — skip it and note in the FreeScout ticket.
Step 4c — setup answers (#4837). Needed on the manual path, which is the path
that runs today (the automated path above does it only when it is live). The answers
live in trader_profiles and in onboarding_state (the quiz slugs in step_data),
and the personal rule the user wrote (a habit, their own number and unit) is in
personal_rules; all three are keyed on users.id with ON DELETE CASCADE. The admin hard delete
(DELETE /api/internal/customers/<id>) removes them through the cascade. An
anonymize-path erasure leaves the users row, so the cascade does not fire.
For a deletion request, run this in one transaction. It deletes the rows of all
three tables and writes the dsr.setup_answers.erased compliance row with the
combined row count, like 4a/4b, so there is no separate bookkeeping; also note
the count in the FreeScout ticket:
sql
BEGIN;
WITH p AS (
DELETE FROM trader_profiles WHERE user_id = <raptor_user_id> RETURNING 1
), o AS (
DELETE FROM onboarding_state WHERE user_id = <raptor_user_id> RETURNING 1
), r AS (
DELETE FROM personal_rules WHERE user_id = <raptor_user_id> RETURNING 1
)
INSERT INTO dsr_log (request_id, customer_id, requested_at, completed_at,
tables_affected, rows_pseudonymized, status)
SELECT '<ticket_ref>#dsr.setup_answers.erased#' || NOW()::text,
<raptor_user_id>, NOW(), NOW(),
'["trader_profiles","onboarding_state","personal_rules"]'::jsonb,
(SELECT COUNT(*) FROM p) + (SELECT COUNT(*) FROM o) + (SELECT COUNT(*) FROM r),
'completed';
COMMIT;
Skip it for a billing-only customer with no Raptor users row.
dsr_log is audit data: it is not deleted, and it holds table names and counts,
never an answer.
- 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."
For an account with no billing customer row, send instead (Console delete):
"We have deleted your account and the data associated with it, including any setup answers. A record of this request is retained for compliance purposes."
or, if the account was kept and only Step 4 was run:
"We have deleted the setup answers and other stored data you asked us to remove. Your account remains open. A record of this request is retained for compliance purposes." 7. Log audit row (Section 4.1). Not applicable to an account with no billing customer row; see the note at the top of this section. 8. Mark FreeScout ticket resolved.
4. Audit Trail
Every completed deletion / anonymization DSR must generate an audit row in billing_action_log
(the Raptor table from backend_v2/alembic/versions/0058_billing_customers_pg.py; the Console's
console_billing_action_log is a different table). This is not applicable to an account with
no billing customer row (Section 2.3): the record is the dsr_log rows or the
console.customer.deleted audit row, the FreeScout ticket and the tracking spreadsheet
(Section 4.2).
Access and Correction requests get no billing_action_log row on any account. The table's
ck_bal_action CHECK admits only dsr_initiated, dsr_confirmation_sent,
dsr_customer_confirmed, dsr_scheduled, dsr_executing, dsr_anonymize_completed,
dsr_full_delete_completed, dsr_canceled and retention_anonymize, and none of them
describes an Access or a Correction. Do not borrow a deletion action for them; the record is the
FreeScout ticket and the tracking spreadsheet (Section 4.2). Adding an action for them is a schema
change (extend ck_bal_action and its SQLite twin), not an edit to this SOP.
4.1 billing_action_log entry template
For a deletion / anonymization of an account with a billing customer row. Run it once the
anonymization is done and checked: the table is append-only, so a wrong row cannot be edited
afterwards. <customer_id> is billing_customers.id. id and recorded_at fill themselves in
(recorded_at is an ISO-8601 UTC text timestamp). payload is JSON text and must not hold the
email, name or address.
INSERT INTO billing_action_log (
action,
actor_id,
entity_type,
entity_id,
payload
)
VALUES (
'dsr_anonymize_completed',
'<operator_github_handle>',
'billing_customer',
'<customer_id>',
jsonb_build_object(
'dsr_type', 'deletion',
'freescout_ticket', '<ticket_id>',
'completed_at', to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"'),
'notes', '<brief summary, no personal data>'
)::text
);
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 of an account with a billing customer row, insert the audit row.
-- Not for an account with no billing customer row, nor for Access / Correction (Section 4).
INSERT INTO billing_action_log (
action,
actor_id,
entity_type,
entity_id,
payload
)
VALUES (
'dsr_anonymize_completed',
'kristerpher',
'billing_customer',
'<customer_id>',
jsonb_build_object(
'dsr_type', 'deletion',
'freescout_ticket', 'FS-1234',
'completed_at', to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"'),
'notes', 'Anonymization completed per DSR request received 2026-05-XX UTC'
)::text
);
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.
billing_action_loghas no action for a deferral (none of the actions listed in Section 4 fits), so do not write a row there.
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)
- [ ] For a deletion on an account with a Raptor
usersrow (including one with no billing customer row, Section 2.3): Step 4a/4b/4c compliance rows present indsr_log(or, if the Console customer delete was used instead, theconsole.customer.deletedaudit row present) - [ ]
billing_action_logrow inserted (Section 4.1; deletion on an account with a billing customer row only). Not applicable to an account with no billing customer row: the record is thedsr_logrows or theconsole.customer.deletedaudit row, the FreeScout ticket and the tracking spreadsheet. Not applicable to Access or Correction on any account:billing_action_loghas no action for them, so the record is the FreeScout ticket and the tracking spreadsheet - [ ] 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.