Skip to content

Fix/sr observability aml notification gaps - #1410

Merged
GoodnessJohn merged 4 commits into
Haroldwonder:mainfrom
dami-005:fix/sr-observability-aml-notification-gaps
Aug 28, 2026
Merged

Fix/sr observability aml notification gaps#1410
GoodnessJohn merged 4 commits into
Haroldwonder:mainfrom
dami-005:fix/sr-observability-aml-notification-gaps

Conversation

@dami-005

Copy link
Copy Markdown
Contributor

Summary

  1. fix(tracing): propagate job correlation ID into log context (125 lines) — tracedJob() now runs job bodies inside correlationStorage.run() so logs share the span's correlation ID; wrapped the 3 AML jobs that previously had no tracing at all. Added a test + a README (backend/docs/SR-108-job-correlation-id.md) since the diff was under 150 lines.
  2. fix(aml): run transaction monitoring and travel-rule assessment on remittance creation (275 lines) — POST /api/remittance now calls TransactionMonitoringService.evaluateTransfer() and TravelRuleService.assess() inline, not just the manual AML API. Added an integration test.
  3. fix(aml): make SAR reference numbering atomic (178 lines) — replaced the racy COUNT(*)-based SAR reference generation with an atomic upsert against a new sar_reference_counters table (+ migration). Added a concurrency test.
  4. fix(notifications): wire NotificationService and webhook fan-out into live event paths (190 lines) — wired NotificationService and WebhookService into RemittanceEventEmitter and the KYC approval webhook path, and instantiated both at app startup in index.ts. Added a test asserting email/SMS actually fire on status change.

Linked Issue

Closes #1279
Closes #1277
Closes #1278
Closes #1280

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Chore / infra

Checklist

  • Tests added or updated for the change
  • Documentation updated (README, API docs, guides) where relevant
  • Changeset added (sdk/) if this touches a published package
  • Linked issue referenced above
  • This PR introduces a breaking change (if checked, describe migration steps below)

Breaking Change Notes

tracedJob() generated a correlation ID and attached it only as an OTel
span attribute — it never called correlationStorage.run(), so every
createLogger(...) log line emitted inside a scheduled job read
getCorrelationId() as undefined and could not be cross-referenced
against the span for that run.

- tracedJob() now runs the job body inside
  correlationStorage.run(correlationId, fn), so logs and the span
  share one ID.
- Wrapped the three AML jobs (aml-periodic-rescreening,
  aml-travel-rule-transmit, aml-data-retention) in tracedJob, which
  previously had neither a span nor a correlation ID.
- Added a test asserting a log line emitted inside a tracedJob-wrapped
  function carries the same correlation_id as the span attribute.
…mittance creation

TransactionMonitoringService.evaluateTransfer() and TravelRuleService.assess()
were fully implemented and unit-tested but only reachable via officer-triggered
calls to routes/aml.ts (or the scheduler's transmitPending retry). The live
POST /api/remittance path only ran autoFlagIfAboveThreshold — a simple
threshold check — so structuring/velocity/corridor monitoring and travel-rule
data collection never executed for a real remittance unless an officer
manually replayed it through the AML API.

- POST /api/remittance now calls monitoring.evaluateTransfer() and
  travelRule.assess() synchronously right after the transaction row is
  inserted, mirroring the existing autoFlagIfAboveThreshold best-effort
  pattern (logged, non-fatal to the response).
- Added an integration test asserting that creating a remittance which
  trips a monitoring rule raises an aml_alerts row with no manual call
  to /api/aml/monitoring/evaluate.
SarWorkflowService.nextReference() derived the sequence number from
SELECT COUNT(*) FROM sar_reports WHERE reference LIKE 'SAR-<year>-%',
with no locking, and the subsequent INSERT INTO sar_reports happened in
a separate statement. Two officers (or an officer and a batch process)
calling createFromAlerts() concurrently within the same year could read
the same count before either INSERT committed, so one of the two SAR
drafts would fail with a unique-constraint violation on `reference`
instead of a clean, distinct number — surfacing as an unhandled 500 for
a legally-mandated filing.

- Added a sar_reference_counters table (one row per calendar year) and
  a migration that seeds it from the highest existing reference so
  numbering continues rather than restarting.
- nextReference() now claims its sequence number via a single atomic
  INSERT ... ON CONFLICT DO UPDATE ... RETURNING statement, which
  Postgres serializes at the row level, instead of a racy count-then-
  insert.
- Added a concurrency test that fires createFromAlerts() twice (and
  ten times) in parallel and asserts every reference is distinct.
… live event paths

NotificationService (SendGrid email + Twilio SMS, with SR-035 localized
templates) was only ever referenced inside its own file —
notifyRemittanceStatus()/notifyKycEvent() were never called from
RemittanceEventEmitter, routes/remittance.ts, kyc-webhook-handler.ts, or
the scheduler. Separately, RemittanceEventEmitter.setWebhookService() was
never called in production, so this.webhookService was always undefined
and the webhook fan-out branch in emitStatusChange() never ran.

- RemittanceEventEmitter gained setNotificationService() and now calls
  notifyRemittanceStatus() from emitStatusChange() on completed/failed
  status changes (best-effort, mirrors the existing webhook branch).
- kyc-webhook-handler.ts now calls notifyKycEvent('kyc_approved', ...)
  when an anchor pushes an approval, the one KYC event this handler
  observes.
- index.ts now instantiates NotificationService and WebhookService at
  startup and wires both into remittanceEventEmitter, so the webhook
  fan-out path (setWebhookService) and the notification path
  (setNotificationService) both actually fire in production.
- Added a test asserting a status-change event triggers outbound
  SendGrid/Twilio calls once wired, and that opt-out/unwired states
  correctly send nothing.
@dami-005
dami-005 requested a review from Haroldwonder as a code owner August 28, 2026 06:11
@drips-wave

drips-wave Bot commented Aug 28, 2026

Copy link
Copy Markdown

@dami-005 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

@dami-005 is attempting to deploy a commit to the Harold's projects Team on Vercel.

A member of the Team first needs to authorize it.

@GoodnessJohn
GoodnessJohn merged commit c927e55 into Haroldwonder:main Aug 28, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants