- ID
- f91522e8-0342-4313-89bd-5f810510091a
BE-7215 Move Braze SMS opt-in from address endpoint to a dedicated customer endpoint
- source :: https://hungryroot.atlassian.net/browse/BE-7215
- tags :: Hungryroot API Project
- epic :: EP-411 Eng Parking Lot
- sprint :: HR BE Sprint 104
- status :: Status - Code Review
Tasks
DONE Add new DynamicConfig enum entry: api.braze_sms_optin
- ID
- c67ed4a4-3414-3f39-640b-b11156589017
DONE Add new SmsConsentRecord model
- ID
- 7049707e-ba94-6548-cab5-a5ea6899b3e8
DONE Create new SMS opt-in endpoint in app/rest/customer.py
- ID
- c8c72d87-767b-e4d5-8002-37ae92dd2e3b
DONE Add guard in old customer address endpoint and testing
- ID
- b2877612-1bc7-1321-1631-79769075edba
DONE Fix tests and address PR issues
- ID
- 6c9c12a4-7efc-7624-9c0b-50db5dd85af9
Description
Background
The customer address PATCH endpoint (CustomerAddressDetail in
app/rest/customer_address.py) currently handles Braze SMS opt-in as a side
effect of updating the shipping address. The sms_opt_in write-only field on the
address payload triggers braze.sms_opt_in(customer, phone) inline during the
address update flow (lines 196-198).
This couples SMS consent collection to the address update, which:
Makes the address endpoint responsible for unrelated concerns (address
persistence vs. marketing consent)
Prevents the client from opting a user into SMS at other points in the flow
without also touching the address
Makes it harder to test and reason about each responsibility independently
Additionally, we currently have no audit trail for SMS consent. For TCPA
compliance and general best practice, we need to persist a durable record of
each consent event.
Scope
1. Add dynamic config api.braze_sms_optin
+ Add a new DynamicConfig enum entry: api.braze_sms_optin
+ Default value: False (disabled — address endpoint continues handling SMS opt-in)
+ When True: the address endpoint skips the braze.sms_opt_in call, and the new endpoint is active
2. New SmsConsentRecord model
+ Create a new model (in app/models or a new app if preferred) to persist an immutable audit log of every SMS consent event. Fields:
Field
Type
Notes
customer
ForeignKey(Customer)
The customer granting consent
phone
CharField
Phone number collected (E.164 format, e.g. +12125556666)
ip_address
GenericIPAddressField
Client IP at time of consent
consent_language
TextField
Exact disclaimer text the user was shown at time of consent
consent_categories
JSONField
Which checkbox(es) were checked, e.g. ["marketing", "transactional"]
consent_method
CharField(choices=...)
How consent was collected: web_form, checkout_flow, account_settings, etc.
consented_at
DateTimeField
Timestamp of when the user consented (server-side, UTC)
created_at
DateTimeField(auto_now_add)
Row creation timestamp
This table is append-only — records should never be updated or soft-deleted so
the audit trail is tamper-evident.
3. Create new SMS opt-in endpoint in app/rest/customer.py
+ New endpoint accepts the following payload:
+ phone (required) — phone number
+ sms_opt_in (required, boolean) — whether the user is opting in
+ consent_language (required, string) — the exact disclaimer text shown to the user
consent_categories (required, list of strings) — which checkboxes were checked
(e.g. ["marketing"], ["transactional"], ["marketing", "transactional"])
consent_method (required, string) — one of: web_form, checkout_flow,
account_settings
Gated by api.braze_sms_optin dynamic config — returns 404 or 400 when config is
False
When sms_opt_in is True:
Create an SmsConsentRecord row with all collected fields plus the client IP and
server timestamp
Call braze.sms_opt_in(customer=customer, phone=phone)
Authenticated, customer-scoped (reuse existing permission patterns from
app/rest/customer.py)
Wire up URL route in the API URL config
4. Update address endpoint to respect dynamic config
In CustomerAddressDetail.update(), wrap the existing braze.sms_opt_in call with
a check:
If api.braze_sms_optin config is active (True), skip the Braze SMS call on the
address endpoint
If config is inactive (False, default), preserve current behavior
5. Tests
Unit tests for the new endpoint (success, missing fields, config disabled)
Unit tests verifying SmsConsentRecord is created with correct data on successful
opt-in
Unit tests for the address endpoint confirming SMS opt-in is skipped when config
is active
Verify backward compatibility when config is off (default)
Rollout Plan
Deploy with api.braze_sms_optin = False (no behavior change)
Client integrates the new endpoint, passing consent metadata in the payload
Enable api.braze_sms_optin = True to cut over
Follow-up: remove the sms_opt_in field from the address serializer and clean up
the dynamic config once fully migrated
Files to Touch
app/models/ — new SmsConsentRecord model + migration
app/rest/customer_address.py — gate existing SMS opt-in behind config
app/rest/customer.py — new endpoint + serializer
app/constants/dynamic_config.py — new config enum
hrplatform/urls/api.py — route for new endpoint
app/tests/ — tests for both endpoints and the new model