A regulatory customer-contact platform
A time-boxed FCA redress campaign across three customer cohorts, each governed by different regulatory regimes, with audit trail as a non-negotiable.
- Outcome
- An auditable contact flow that holds up to FCA, FOS and CMC scrutiny — designed, built and operated in eight working weeks.
- Stack
-
- Python
- BigQuery
- Cloud Run
- Mailgun
- Tags
-
- FCA
- BigQuery
- Cloud Run
- Python
The problem
A UK motor-finance lender needed to contact roughly seventeen thousand customers under two parallel regulatory regimes: FCA PS26/3 for the customers eligible for redress, and DISP 1.6 for those who had complained but were not owed redress. The proactive-contact obligation under PS26/3 had a hard deadline. The DISP cohort needed final responses that contained the right legal furniture, in the right order, with the right referral rights.
The catch: the firm had to be able to demonstrate, after the fact, exactly what was sent to which customer, when, and what came back.
The cohorts
Three populations, not two — a re-scope early in the engagement clarified the picture:
| Cohort | Approx. size | Regime | What the customer receives |
|---|---|---|---|
| Not previously complained, redress due | 2,300 | PS26/3 | Invitation to engage with the scheme |
| Complained, redress due | 400 | PS26/3 + DISP | Final response with redress offer |
| Complained, no redress due | 14,000 | DISP 1.6 | Final response with reasons and FOS referral rights |
Each cohort needed its own template set, its own send cadence, and — for the redress-due populations — first-class tracing obligations under the scheme.
What we built
A deliberately small system:
- Standalone Python scripts, not Dagster. A four-week campaign does not justify an orchestrator footprint, even though the host stack had one available. Right-sized architecture is a habit, not a compromise.
- One BigQuery dataset holding everything stateful: customer attributes, send records, email events, postal batches, postal returns. State is derived in views — never stored in status columns that can drift from the underlying events.
- A Cloud Run webhook receiving Mailgun delivery and engagement events. Idempotent ingest via
MERGEon the provider event ID, so webhook retries don’t duplicate. - Deterministic send IDs (SHA-256 of cohort, customer, wave and template) so a rerun produces the same identifier and no duplicate sends.
- Address snapshots on every postal send. “Gone away” returns can be attributed correctly even if the customer record is updated later.
What was easy to overlook
- Tracing is a regulatory artefact. Under PS26/3, the firm must be able to show reasonable steps were taken to locate every eligible customer. The end-of-wave trace queue is a first-class output, not a side effect.
- Claims management companies. For the complained cohort, addressing decisions (customer-only, CMC-only, or both) are a legal sign-off question, not a technical one. We made the choice surface-able and reversible — the design didn’t have to be reworked when legal came back.
- The audit trail is the product. Every report we ship has a SQL fingerprint a reviewer can re-run. No opaque steps; no derived numbers without provenance.
What it left behind
A dataset and a small set of Python scripts that any analyst familiar with BigQuery can read in an afternoon. No dashboards we don’t operate. No vendor we don’t need. When the campaign ends, the trail it leaves is exactly the trail a regulator would ask for.