Right now an outgoing SMS gets exactly one shot. If it ends up in failed, or gets stuck in pending or dispatched (device offline, app killed, FCM message lost), the only option is to send a brand new message, which creates a duplicate record and loses the connection to the original request.
Resend
Add a way to resend an existing outgoing SMS. Initially allowed for messages in failed, pending, or dispatched status. We can open it up to other statuses later (e.g. resending a sent message that never reached delivered), so the status check should be easy to extend rather than baked into several places.
A resend re-dispatches the same message (same recipient, same content, same device by default) rather than creating a new SMS document.
Per-attempt tracking
This is the part that needs a schema change. Today the SMS document has a single set of status fields that get overwritten as the message moves along: status, requestedAt, dispatchedAt, sentAt, deliveredAt, failedAt, errorCode, errorMessage. Once we allow multiple attempts, overwriting is lossy: after a resend succeeds you can no longer see that the first attempt failed, when, or why.
So each SMS should carry an attempts array, one entry per attempt, where every entry records its own lifecycle:
- attempt number and when it was requested/dispatched
- the status transitions for that attempt (sent, delivered, failed) with their timestamps
- on failure: the error code and error message for that specific attempt
The existing top-level fields (status, sentAt, failedAt, errorCode, etc.) should keep being overwritten to reflect the latest attempt, so existing consumers (API responses, webhooks, dashboard, batch status rollup) keep working unchanged. The attempts array is the full history underneath.
To work out during implementation
- how the status-report path (updateSMSStatus) knows which attempt a report belongs to, since delayed delivery reports for attempt 1 can arrive after attempt 2 was dispatched
- whether existing single-attempt messages get a backfilled attempts entry or the array just starts empty until the first resend
- how batch status rollup should treat a message that failed once but succeeded on resend
- webhook payloads: whether to expose attempt info or keep emitting only the latest state
Right now an outgoing SMS gets exactly one shot. If it ends up in failed, or gets stuck in pending or dispatched (device offline, app killed, FCM message lost), the only option is to send a brand new message, which creates a duplicate record and loses the connection to the original request.
Resend
Add a way to resend an existing outgoing SMS. Initially allowed for messages in failed, pending, or dispatched status. We can open it up to other statuses later (e.g. resending a sent message that never reached delivered), so the status check should be easy to extend rather than baked into several places.
A resend re-dispatches the same message (same recipient, same content, same device by default) rather than creating a new SMS document.
Per-attempt tracking
This is the part that needs a schema change. Today the SMS document has a single set of status fields that get overwritten as the message moves along: status, requestedAt, dispatchedAt, sentAt, deliveredAt, failedAt, errorCode, errorMessage. Once we allow multiple attempts, overwriting is lossy: after a resend succeeds you can no longer see that the first attempt failed, when, or why.
So each SMS should carry an attempts array, one entry per attempt, where every entry records its own lifecycle:
The existing top-level fields (status, sentAt, failedAt, errorCode, etc.) should keep being overwritten to reflect the latest attempt, so existing consumers (API responses, webhooks, dashboard, batch status rollup) keep working unchanged. The attempts array is the full history underneath.
To work out during implementation