Skip to content

Commit c6e3f9b

Browse files
feature #61778 [Notifier] Add support for building SmsEvent by dlr_code and RemoteEvent for other LOX24 webhook event types (alebedev80)
This PR was squashed before being merged into the 7.4 branch. Discussion ---------- [Notifier] Add support for building SmsEvent by dlr_code and RemoteEvent for other LOX24 webhook event types | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | yes | New feature? | yes | Deprecations? | no | Issues | Fix #... (if applicable) | License | MIT ## Summary This PR enhances the LOX24RequestParser to provide robust SMS delivery webhook parsing with improved validation, dual status code support (DLR codes and status codes), and better error handling for SMS delivery events. ## What it does **Before**: The parser had basic SMS delivery webhook handling with limited validation and error handling. **After**: The parser provides enhanced SMS delivery webhook processing: - `sms.delivery` & `sms.delivery.dryrun` - SMS delivery receipts with improved validation - Dual status code support: DLR codes (0-16) and transmission status codes (0, 100, etc.) - Better payload validation with detailed error messages - Non-delivery events are handled as generic RemoteEvent objects ## Example Usage ### Enhanced SMS Delivery Processing: ```php // ✅ SMS delivery with status code $deliveryPayload = [ 'id' => 'webhook-123', 'name' => 'sms.delivery', 'data' => ['id' => '123', 'status_code' => 100] ]; $deliveryEvent = $parser->parse($deliveryRequest, $secret); // Returns: SmsEvent with DELIVERED status // ✅ SMS delivery with DLR code (takes priority over status_code) $dlrPayload = [ 'id' => 'webhook-456', 'name' => 'sms.delivery', 'data' => ['id' => '456', 'dlr_code' => 1, 'status_code' => 500] ]; $dlrEvent = $parser->parse($dlrRequest, $secret); // Returns: SmsEvent with DELIVERED status (DLR code 1 overrides status 500) // ✅ Pending delivery (returns null) $pendingPayload = [ 'id' => 'webhook-789', 'name' => 'sms.delivery', 'data' => ['id' => '789', 'dlr_code' => 0] ]; $pendingEvent = $parser->parse($pendingRequest, $secret); // Returns: null (pending delivery) // ✅ Non-delivery events handled as RemoteEvent $otherPayload = [ 'id' => 'webhook-000', 'name' => 'custom.event', 'data' => ['some' => 'data'] ]; $otherEvent = $parser->parse($otherRequest, $secret); // Returns: RemoteEvent with 'custom.event' name ``` ## Key Changes ### 1. **Improved Payload Validation** - Enhanced validation for required webhook fields: `id`, `name`, `data` - Better error messages with specific missing field details - Proper array validation for data payload ### 2. **Smart Return Types** - SMS delivery events (`sms.delivery`, `sms.delivery.dryrun`) return `SmsEvent` objects with DELIVERED/FAILED status - Non-delivery events return `RemoteEvent` objects for generic webhook handling - Maintains `null` return for pending deliveries (status_code=0 or dlr_code=0/2/4) ### 3. **Enhanced SMS Delivery Validation** ```php // SMS delivery events validate required fields case 'sms.delivery': // Requires: payload.id, data.id, and either data.status_code OR data.dlr_code // Supports both LOX24 status codes and DLR codes // DLR codes take priority when both are present ``` ### 4. **Dual Status Code Support** - **DLR Codes**: Priority support for LOX24's Delivery Report codes (0-16) with proper pending state handling - **Status Codes**: Fallback to transmission status codes (0, 100, 208, 400, etc.) when DLR unavailable - **Smart Mapping**: DLR codes 1→DELIVERED, 8/16→FAILED, 0/2/4→null (pending) ### 4. **Comprehensive Testing** - Tests for SMS delivery events with success/failure scenarios - DLR code and status code validation scenarios - Payload validation tests for required fields - Backward compatibility verification for existing delivery events - Type safety assertions (`SmsEvent` vs `RemoteEvent`) ## Backward Compatibility ✅ **Fully backward compatible** - existing consumers continue working without changes: - Same `sms.delivery` event handling behavior - Same status code and DLR code mapping logic - Same null return for pending messages (status_code=0, dlr_code=0/2/4) - Same SmsEvent object structure and methods - Enhanced support for both LOX24 status codes and DLR codes ## Consumer Integration Example ```php #[AsRemoteEventConsumer('lox24_notifier')] class LOX24WebhookConsumer implements ConsumerInterface { public function consume(RemoteEvent $event): void { if ($event instanceof SmsEvent) { match($event->getName()) { SmsEvent::DELIVERED => $this->handleDelivered($event), SmsEvent::FAILED => $this->handleFailed($event), }; } else { // Handle other webhook events as generic RemoteEvent $this->handleGenericWebhook($event); } } } ``` ## Breaking Changes ⚠️ **Minor breaking changes** (unlikely to affect most users): - Parser return type signature changed from `?SmsEvent` to `SmsEvent|RemoteEvent|null` - Exception messages updated with more detailed validation errors - Requires webhook payload to include `id` field (LOX24 standard) - Non-delivery events now return `RemoteEvent` instead of being rejected ## Technical Implementation - **Improved Validation**: Enhanced payload validation with detailed error messages for missing fields - **Factory Methods**: Implemented specialized methods (`createDeliveryEvent`, `createRemoteEvent`, etc.) - **Dual Code Support**: Enhanced delivery event parsing to handle both LOX24 status codes and DLR codes - **Smart Precedence**: DLR codes take priority over status codes when both are present - **Error Handling**: Enhanced error messages with specific validation details - **Null Handling**: Proper handling of pending states (DLR codes 0/2/4, status code 0) ## Testing - ✅ All existing tests pass (backward compatibility) - ✅ Added comprehensive test coverage for SMS delivery scenarios - ✅ Payload validation and error handling tests - ✅ DLR code and status code priority testing This enhancement improves LOX24 SMS delivery webhook processing with better validation, dual status code support, and robust error handling while maintaining complete backward compatibility. Commits ------- d9eaa27 [Notifier] Add support for building SmsEvent by dlr_code and RemoteEvent for other LOX24 webhook event types
2 parents b1ef2a4 + d9eaa27 commit c6e3f9b

File tree

3 files changed

+312
-34
lines changed

3 files changed

+312
-34
lines changed
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
CHANGELOG
22
=========
33

4+
7.4
5+
---
6+
7+
* Add support for building SmsEvent by dlr_code and RemoteEvent for other LOX24 webhook event types
8+
49
7.1
510
---
611

7-
* Add the bridge
12+
* Add the bridge

src/Symfony/Component/Notifier/Bridge/Lox24/Tests/Webhook/Lox24RequestParserTest.php

Lines changed: 228 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\HttpFoundation\Request;
1616
use Symfony\Component\Notifier\Bridge\Lox24\Webhook\Lox24RequestParser;
1717
use Symfony\Component\RemoteEvent\Event\Sms\SmsEvent;
18+
use Symfony\Component\RemoteEvent\RemoteEvent;
1819
use Symfony\Component\Webhook\Exception\RejectWebhookException;
1920

2021
/**
@@ -29,80 +30,284 @@ protected function setUp(): void
2930
$this->parser = new Lox24RequestParser();
3031
}
3132

32-
public function testInvalidNotificationName()
33+
public function testMissingBasicPayloadStructure()
3334
{
3435
$this->expectException(RejectWebhookException::class);
35-
$this->expectExceptionMessage('Notification name is not \'sms.delivery\'');
36-
$request = $this->getRequest(['name' => 'invalid_name', 'data' => ['status_code' => 100]]);
36+
$this->expectExceptionMessage('The required fields "id", "data" are missing from the payload.');
3737

38+
$request = $this->getRequest(['name' => 'sms.delivery']);
3839
$this->parser->parse($request, '');
3940
}
4041

41-
public function testMissingMsgId()
42+
public function testSmsDeliveryMissingMsgId()
4243
{
4344
$this->expectException(RejectWebhookException::class);
44-
$this->expectExceptionMessage('Payload is malformed.');
45-
$request = $this->getRequest(['name' => 'sms.delivery', 'data' => ['status_code' => 100]]);
45+
$this->expectExceptionMessage('The required field "id" is missing from the delivery event payload.');
4646

47+
$request = $this->getRequest([
48+
'id' => 'webhook-id',
49+
'name' => 'sms.delivery',
50+
'data' => ['status_code' => 100],
51+
]);
4752
$this->parser->parse($request, '');
4853
}
4954

50-
public function testMissingMsgStatusCode()
55+
public function testSmsDeliveryMissingBothCodes()
5156
{
5257
$this->expectException(RejectWebhookException::class);
53-
$this->expectExceptionMessage('Payload is malformed.');
54-
$request = $this->getRequest(['name' => 'sms.delivery', 'data' => ['id' => '123']]);
58+
$this->expectExceptionMessage('The required field "status_code" or "dlr_code" is missing from the delivery event payload.');
5559

60+
$request = $this->getRequest([
61+
'id' => 'webhook-id',
62+
'name' => 'sms.delivery',
63+
'data' => ['id' => '123'],
64+
]);
5665
$this->parser->parse($request, '');
5766
}
5867

59-
public function testStatusCode100()
68+
public function testSmsDeliveryStatusCode100()
6069
{
6170
$payload = [
71+
'id' => 'webhook-id',
6272
'name' => 'sms.delivery',
6373
'data' => [
6474
'id' => '123',
6575
'status_code' => 100,
6676
],
6777
];
68-
$request = $this->getRequest($payload);
6978

79+
$request = $this->getRequest($payload);
7080
$event = $this->parser->parse($request, '');
81+
82+
$this->assertInstanceOf(SmsEvent::class, $event);
7183
$this->assertSame('123', $event->getId());
7284
$this->assertSame(SmsEvent::DELIVERED, $event->getName());
7385
$this->assertSame($payload, $event->getPayload());
7486
}
7587

76-
public function testStatusCode0()
88+
public function testSmsDeliveryStatusCode0()
7789
{
78-
$request = $this->getRequest(
79-
[
80-
'name' => 'sms.delivery',
81-
'data' => [
82-
'id' => '123',
83-
'status_code' => 0,
84-
],
85-
]
86-
);
90+
$request = $this->getRequest([
91+
'id' => 'webhook-id',
92+
'name' => 'sms.delivery',
93+
'data' => [
94+
'id' => '123',
95+
'status_code' => 0,
96+
],
97+
]);
8798

8899
$event = $this->parser->parse($request, '');
89100
$this->assertNull($event);
90101
}
91102

92-
public function testStatusCodeUnknown()
103+
public function testSmsDeliveryWithDlrCodeDelivered()
93104
{
94105
$payload = [
106+
'id' => 'webhook-id',
95107
'name' => 'sms.delivery',
96108
'data' => [
97109
'id' => '123',
98-
'status_code' => 410,
110+
'dlr_code' => 1,
111+
'callback_data' => 'test-callback',
99112
],
100113
];
114+
101115
$request = $this->getRequest($payload);
116+
$event = $this->parser->parse($request, '');
102117

118+
$this->assertInstanceOf(SmsEvent::class, $event);
119+
$this->assertSame('123', $event->getId());
120+
$this->assertSame(SmsEvent::DELIVERED, $event->getName());
121+
$this->assertSame($payload, $event->getPayload());
122+
}
123+
124+
public function testSmsDeliveryWithDlrCodeFailed()
125+
{
126+
$payload = [
127+
'id' => 'webhook-id',
128+
'name' => 'sms.delivery',
129+
'data' => [
130+
'id' => '123',
131+
'dlr_code' => 16,
132+
],
133+
];
134+
135+
$request = $this->getRequest($payload);
103136
$event = $this->parser->parse($request, '');
137+
138+
$this->assertInstanceOf(SmsEvent::class, $event);
104139
$this->assertSame('123', $event->getId());
105140
$this->assertSame(SmsEvent::FAILED, $event->getName());
141+
}
142+
143+
public function testSmsDeliveryWithDlrCodePending()
144+
{
145+
$request = $this->getRequest([
146+
'id' => 'webhook-id',
147+
'name' => 'sms.delivery',
148+
'data' => [
149+
'id' => '123',
150+
'dlr_code' => 2,
151+
],
152+
]);
153+
154+
$event = $this->parser->parse($request, '');
155+
$this->assertNull($event);
156+
}
157+
158+
public function testSmsDeliveryDryrun()
159+
{
160+
$payload = [
161+
'id' => 'webhook-id',
162+
'name' => 'sms.delivery.dryrun',
163+
'data' => [
164+
'id' => '123',
165+
'status_code' => 100,
166+
],
167+
];
168+
169+
$request = $this->getRequest($payload);
170+
$event = $this->parser->parse($request, '');
171+
172+
$this->assertInstanceOf(SmsEvent::class, $event);
173+
$this->assertSame('123', $event->getId());
174+
$this->assertSame(SmsEvent::DELIVERED, $event->getName());
175+
}
176+
177+
public function testMissingIdField()
178+
{
179+
$this->expectException(RejectWebhookException::class);
180+
$this->expectExceptionMessage('The required fields "id" are missing from the payload.');
181+
182+
$request = $this->getRequest([
183+
'name' => 'sms.delivery',
184+
'data' => ['id' => '123'],
185+
]);
186+
$this->parser->parse($request, '');
187+
}
188+
189+
public function testMissingNameField()
190+
{
191+
$this->expectException(RejectWebhookException::class);
192+
$this->expectExceptionMessage('The required fields "name" are missing from the payload.');
193+
194+
$request = $this->getRequest([
195+
'id' => 'webhook-id',
196+
'data' => ['id' => '123'],
197+
]);
198+
$this->parser->parse($request, '');
199+
}
200+
201+
public function testMissingDataField()
202+
{
203+
$this->expectException(RejectWebhookException::class);
204+
$this->expectExceptionMessage('The required fields "data" are missing from the payload.');
205+
206+
$request = $this->getRequest([
207+
'id' => 'webhook-id',
208+
'name' => 'sms.delivery',
209+
]);
210+
$this->parser->parse($request, '');
211+
}
212+
213+
public function testInvalidDataFieldNotArray()
214+
{
215+
$this->expectException(RejectWebhookException::class);
216+
$this->expectExceptionMessage('The "data" field must be an array.');
217+
218+
$request = $this->getRequest([
219+
'id' => 'webhook-id',
220+
'name' => 'sms.delivery',
221+
'data' => 'invalid-data',
222+
]);
223+
$this->parser->parse($request, '');
224+
}
225+
226+
public function testRemoteEventForUnknownEventType()
227+
{
228+
$payload = [
229+
'id' => 'webhook-id',
230+
'name' => 'custom.event',
231+
'data' => ['some' => 'data'],
232+
];
233+
234+
$request = $this->getRequest($payload);
235+
$event = $this->parser->parse($request, '');
236+
237+
$this->assertInstanceOf(RemoteEvent::class, $event);
238+
$this->assertSame('webhook-id', $event->getId());
239+
$this->assertSame('custom.event', $event->getName());
240+
$this->assertSame($payload, $event->getPayload());
241+
}
242+
243+
public function testSmsDeliveryStatusCodeFailed()
244+
{
245+
$payload = [
246+
'id' => 'webhook-id',
247+
'name' => 'sms.delivery',
248+
'data' => [
249+
'id' => '123',
250+
'status_code' => 500,
251+
],
252+
];
253+
254+
$request = $this->getRequest($payload);
255+
$event = $this->parser->parse($request, '');
256+
257+
$this->assertInstanceOf(SmsEvent::class, $event);
258+
$this->assertSame('123', $event->getId());
259+
$this->assertSame(SmsEvent::FAILED, $event->getName());
260+
$this->assertSame($payload, $event->getPayload());
261+
}
262+
263+
public function testSmsDeliveryWithDlrCode0()
264+
{
265+
$request = $this->getRequest([
266+
'id' => 'webhook-id',
267+
'name' => 'sms.delivery',
268+
'data' => [
269+
'id' => '123',
270+
'dlr_code' => 0,
271+
],
272+
]);
273+
274+
$event = $this->parser->parse($request, '');
275+
$this->assertNull($event);
276+
}
277+
278+
public function testSmsDeliveryWithDlrCode4()
279+
{
280+
$request = $this->getRequest([
281+
'id' => 'webhook-id',
282+
'name' => 'sms.delivery',
283+
'data' => [
284+
'id' => '123',
285+
'dlr_code' => 4,
286+
],
287+
]);
288+
289+
$event = $this->parser->parse($request, '');
290+
$this->assertNull($event);
291+
}
292+
293+
public function testSmsDeliveryDlrCodePriorityOverStatusCode()
294+
{
295+
$payload = [
296+
'id' => 'webhook-id',
297+
'name' => 'sms.delivery',
298+
'data' => [
299+
'id' => '123',
300+
'dlr_code' => 1,
301+
'status_code' => 500,
302+
],
303+
];
304+
305+
$request = $this->getRequest($payload);
306+
$event = $this->parser->parse($request, '');
307+
308+
$this->assertInstanceOf(SmsEvent::class, $event);
309+
$this->assertSame('123', $event->getId());
310+
$this->assertSame(SmsEvent::DELIVERED, $event->getName());
106311
$this->assertSame($payload, $event->getPayload());
107312
}
108313

0 commit comments

Comments
 (0)