@@ -6,31 +6,22 @@ import type {
66 FormatInputResult ,
77 WebhookProviderHandler ,
88} from '@/lib/webhooks/providers/types'
9+ import { SENDBLUE_TRIGGER_IS_OUTBOUND } from '@/triggers/sendblue/utils'
910
1011const logger = createLogger ( 'WebhookProvider:Sendblue' )
1112
12- /**
13- * Maps Sendblue trigger IDs to the expected value of the payload `is_outbound`
14- * flag. Inbound messages are routed to the "message received" trigger and
15- * outbound status callbacks to the "message status updated" trigger.
16- */
17- const TRIGGER_IS_OUTBOUND : Record < string , boolean > = {
18- sendblue_message_received : false ,
19- sendblue_message_status_updated : true ,
20- }
21-
2213export const sendblueHandler : WebhookProviderHandler = {
2314 matchEvent ( { body, webhook, requestId } : EventMatchContext ) : boolean {
2415 const providerConfig = getProviderConfig ( webhook )
2516 const triggerId = providerConfig . triggerId as string | undefined
26- if ( ! triggerId || ! ( triggerId in TRIGGER_IS_OUTBOUND ) ) return true
17+ if ( ! triggerId || ! ( triggerId in SENDBLUE_TRIGGER_IS_OUTBOUND ) ) return true
2718
2819 if ( ! isRecord ( body ) ) {
2920 logger . warn ( `[${ requestId } ] Sendblue webhook payload was not an object` )
3021 return false
3122 }
3223
33- const expected = TRIGGER_IS_OUTBOUND [ triggerId ]
24+ const expected = SENDBLUE_TRIGGER_IS_OUTBOUND [ triggerId ]
3425 const isOutbound = body . is_outbound === true
3526 if ( isOutbound !== expected ) {
3627 logger . info ( `[${ requestId } ] Sendblue event did not match trigger` , { triggerId, isOutbound } )
@@ -43,14 +34,19 @@ export const sendblueHandler: WebhookProviderHandler = {
4334 extractIdempotencyId ( body : unknown ) : string | null {
4435 if ( ! isRecord ( body ) ) return null
4536 const handle = body . message_handle
46- return typeof handle === 'string' && handle . length > 0 ? handle : null
37+ if ( typeof handle !== 'string' || handle . length === 0 ) return null
38+ // A single outbound message emits multiple status callbacks (e.g. SENT then
39+ // DELIVERED) that share one message_handle, so the status is part of the key
40+ // to keep distinct transitions from being deduped as retries.
41+ const status = typeof body . status === 'string' && body . status . length > 0 ? body . status : null
42+ return status ? `${ handle } :${ status } ` : handle
4743 } ,
4844
4945 async formatInput ( { body } : FormatInputContext ) : Promise < FormatInputResult > {
5046 const b = isRecord ( body ) ? body : { }
5147 return {
5248 input : {
53- accountEmail : b . accountEmail ?? b . account_email ?? null ,
49+ account_email : b . accountEmail ?? b . account_email ?? null ,
5450 content : b . content ?? null ,
5551 media_url : b . media_url ?? null ,
5652 is_outbound : b . is_outbound ?? null ,
0 commit comments