Org Web Adapter

hungryroot/jira/be_7215_move_braze_sms_opt_in_from_address_endpoint_to_a_dedicated_customer_endpoint.org

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

workhungryrootfeaturebrazeep411be7215smssprint104
ID
c67ed4a4-3414-3f39-640b-b11156589017

DONE Add new SmsConsentRecord model

workhungryrootbe7215brazeep411modelssmssprint104feature
ID
7049707e-ba94-6548-cab5-a5ea6899b3e8

DONE Create new SMS opt-in endpoint in app/rest/customer.py

workhungryrootbe7215brazeep411modelssmssprint104feature
ID
c8c72d87-767b-e4d5-8002-37ae92dd2e3b

DONE Add guard in old customer address endpoint and testing

workhungryrootbe7215brazeep411modelssmssprint104feature
ID
b2877612-1bc7-1321-1631-79769075edba

DONE Fix tests and address PR issues

workhungryrootbe7215brazeep411modelssmssprint104feature
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