backend-manager 5.0.85 → 5.0.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +53 -1
- package/package.json +1 -1
- package/src/cli/commands/base-command.js +5 -1
- package/src/cli/commands/serve.js +1 -2
- package/src/manager/cron/daily/ghostii-auto-publisher.js +10 -19
- package/src/manager/events/firestore/payments-webhooks/on-write.js +351 -56
- package/src/manager/events/firestore/payments-webhooks/transitions/index.js +148 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +16 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +15 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +15 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +18 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +15 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +14 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +16 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +16 -0
- package/src/manager/index.js +26 -36
- package/src/manager/libraries/{stripe.js → payment-processors/stripe.js} +57 -2
- package/src/manager/libraries/payment-processors/test.js +141 -0
- package/src/manager/routes/app/get.js +5 -22
- package/src/manager/routes/payments/intent/post.js +38 -23
- package/src/manager/routes/payments/intent/processors/stripe.js +112 -44
- package/src/manager/routes/payments/intent/processors/test.js +139 -76
- package/src/manager/routes/payments/webhook/post.js +14 -5
- package/src/manager/routes/payments/webhook/processors/stripe.js +75 -9
- package/src/manager/schemas/payments/intent/post.js +1 -1
- package/src/test/test-accounts.js +10 -1
- package/templates/backend-manager-config.json +16 -4
- package/test/events/payments/journey-payments-cancel.js +6 -0
- package/test/events/payments/journey-payments-failure.js +114 -0
- package/test/events/payments/journey-payments-suspend.js +6 -0
- package/test/events/payments/journey-payments-trial.js +12 -0
- package/test/events/payments/journey-payments-upgrade.js +17 -0
- package/test/fixtures/stripe/checkout-session-completed.json +130 -0
- package/test/fixtures/stripe/invoice-payment-failed.json +148 -0
- package/test/fixtures/stripe/invoice-subscription-payment-failed.json +28 -0
- package/test/helpers/stripe-parse-webhook.js +447 -0
- package/test/helpers/stripe-to-unified.js +59 -59
- package/test/routes/payments/intent.js +3 -3
- package/test/routes/payments/webhook.js +2 -2
- package/src/manager/libraries/test.js +0 -27
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: Stripe parseWebhook()
|
|
3
|
+
* Unit tests for the Stripe webhook processor's event categorization and routing
|
|
4
|
+
*
|
|
5
|
+
* Verifies that parseWebhook() correctly determines category, resourceType, resourceId,
|
|
6
|
+
* and uid for each supported event type. Uses real Stripe CLI fixtures where available.
|
|
7
|
+
*/
|
|
8
|
+
const stripeProcessor = require('../../src/manager/routes/payments/webhook/processors/stripe.js');
|
|
9
|
+
|
|
10
|
+
// Real Stripe CLI fixtures
|
|
11
|
+
const FIXTURE_INVOICE_MANUAL = require('../fixtures/stripe/invoice-payment-failed.json');
|
|
12
|
+
const FIXTURE_CHECKOUT_PAYMENT = require('../fixtures/stripe/checkout-session-completed.json');
|
|
13
|
+
|
|
14
|
+
// Hand-crafted fixture (subscription-related invoice failure)
|
|
15
|
+
const FIXTURE_INVOICE_SUB = require('../fixtures/stripe/invoice-subscription-payment-failed.json');
|
|
16
|
+
|
|
17
|
+
function parseWebhook(event) {
|
|
18
|
+
return stripeProcessor.parseWebhook({ body: event });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
description: 'Stripe parseWebhook() event categorization',
|
|
23
|
+
type: 'group',
|
|
24
|
+
|
|
25
|
+
tests: [
|
|
26
|
+
// ─── isSupported() ───
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
name: 'supports-subscription-created',
|
|
30
|
+
async run({ assert }) {
|
|
31
|
+
assert.ok(stripeProcessor.isSupported('customer.subscription.created'), 'Should support customer.subscription.created');
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
name: 'supports-subscription-updated',
|
|
37
|
+
async run({ assert }) {
|
|
38
|
+
assert.ok(stripeProcessor.isSupported('customer.subscription.updated'), 'Should support customer.subscription.updated');
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
name: 'supports-subscription-deleted',
|
|
44
|
+
async run({ assert }) {
|
|
45
|
+
assert.ok(stripeProcessor.isSupported('customer.subscription.deleted'), 'Should support customer.subscription.deleted');
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
name: 'supports-invoice-payment-failed',
|
|
51
|
+
async run({ assert }) {
|
|
52
|
+
assert.ok(stripeProcessor.isSupported('invoice.payment_failed'), 'Should support invoice.payment_failed');
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
name: 'supports-checkout-session-completed',
|
|
58
|
+
async run({ assert }) {
|
|
59
|
+
assert.ok(stripeProcessor.isSupported('checkout.session.completed'), 'Should support checkout.session.completed');
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
name: 'rejects-unsupported-event',
|
|
65
|
+
async run({ assert }) {
|
|
66
|
+
assert.equal(stripeProcessor.isSupported('charge.succeeded'), false, 'Should not support charge.succeeded');
|
|
67
|
+
assert.equal(stripeProcessor.isSupported('customer.created'), false, 'Should not support customer.created');
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// ─── Validation ───
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
name: 'throws-on-empty-body',
|
|
75
|
+
async run({ assert }) {
|
|
76
|
+
try {
|
|
77
|
+
parseWebhook(null);
|
|
78
|
+
assert.fail('Should have thrown');
|
|
79
|
+
} catch (e) {
|
|
80
|
+
assert.match(e.message, /Invalid/, 'Should mention invalid payload');
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
name: 'throws-on-missing-id',
|
|
87
|
+
async run({ assert }) {
|
|
88
|
+
try {
|
|
89
|
+
parseWebhook({ type: 'customer.subscription.updated', data: { object: {} } });
|
|
90
|
+
assert.fail('Should have thrown');
|
|
91
|
+
} catch (e) {
|
|
92
|
+
assert.match(e.message, /Invalid/, 'Should mention invalid payload');
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
name: 'throws-on-missing-type',
|
|
99
|
+
async run({ assert }) {
|
|
100
|
+
try {
|
|
101
|
+
parseWebhook({ id: 'evt_123', data: { object: {} } });
|
|
102
|
+
assert.fail('Should have thrown');
|
|
103
|
+
} catch (e) {
|
|
104
|
+
assert.match(e.message, /Invalid/, 'Should mention invalid payload');
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// ─── customer.subscription.* events ───
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
name: 'subscription-created-category',
|
|
113
|
+
async run({ assert }) {
|
|
114
|
+
const result = parseWebhook({
|
|
115
|
+
id: 'evt_sub_created',
|
|
116
|
+
type: 'customer.subscription.created',
|
|
117
|
+
data: { object: { id: 'sub_123', metadata: { uid: 'user-abc' } } },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
assert.equal(result.category, 'subscription', 'Category should be subscription');
|
|
121
|
+
assert.equal(result.resourceType, 'subscription', 'Resource type should be subscription');
|
|
122
|
+
assert.equal(result.resourceId, 'sub_123', 'Resource ID should be subscription ID');
|
|
123
|
+
assert.equal(result.uid, 'user-abc', 'UID should come from metadata');
|
|
124
|
+
assert.equal(result.eventId, 'evt_sub_created', 'Event ID should match');
|
|
125
|
+
assert.equal(result.eventType, 'customer.subscription.created', 'Event type should match');
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
{
|
|
130
|
+
name: 'subscription-updated-category',
|
|
131
|
+
async run({ assert }) {
|
|
132
|
+
const result = parseWebhook({
|
|
133
|
+
id: 'evt_sub_updated',
|
|
134
|
+
type: 'customer.subscription.updated',
|
|
135
|
+
data: { object: { id: 'sub_456', metadata: { uid: 'user-def' } } },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
assert.equal(result.category, 'subscription', 'Category should be subscription');
|
|
139
|
+
assert.equal(result.resourceType, 'subscription', 'Resource type should be subscription');
|
|
140
|
+
assert.equal(result.resourceId, 'sub_456', 'Resource ID should match');
|
|
141
|
+
assert.equal(result.uid, 'user-def', 'UID should match');
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
{
|
|
146
|
+
name: 'subscription-deleted-category',
|
|
147
|
+
async run({ assert }) {
|
|
148
|
+
const result = parseWebhook({
|
|
149
|
+
id: 'evt_sub_deleted',
|
|
150
|
+
type: 'customer.subscription.deleted',
|
|
151
|
+
data: { object: { id: 'sub_789', metadata: { uid: 'user-ghi' } } },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
assert.equal(result.category, 'subscription', 'Category should be subscription');
|
|
155
|
+
assert.equal(result.resourceType, 'subscription', 'Resource type should be subscription');
|
|
156
|
+
assert.equal(result.resourceId, 'sub_789', 'Resource ID should match');
|
|
157
|
+
assert.equal(result.uid, 'user-ghi', 'UID should match');
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
{
|
|
162
|
+
name: 'subscription-event-null-uid-when-missing-metadata',
|
|
163
|
+
async run({ assert }) {
|
|
164
|
+
const result = parseWebhook({
|
|
165
|
+
id: 'evt_no_uid',
|
|
166
|
+
type: 'customer.subscription.created',
|
|
167
|
+
data: { object: { id: 'sub_no_uid', metadata: {} } },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
assert.equal(result.uid, null, 'UID should be null when not in metadata');
|
|
171
|
+
assert.equal(result.category, 'subscription', 'Category should still be subscription');
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// ─── invoice.payment_failed — subscription-related ───
|
|
176
|
+
|
|
177
|
+
{
|
|
178
|
+
name: 'invoice-sub-failure-category',
|
|
179
|
+
async run({ assert }) {
|
|
180
|
+
const result = parseWebhook({
|
|
181
|
+
id: 'evt_inv_sub_fail',
|
|
182
|
+
type: 'invoice.payment_failed',
|
|
183
|
+
data: {
|
|
184
|
+
object: {
|
|
185
|
+
id: 'in_sub_fail',
|
|
186
|
+
billing_reason: 'subscription_cycle',
|
|
187
|
+
parent: {
|
|
188
|
+
subscription_details: {
|
|
189
|
+
subscription: 'sub_target',
|
|
190
|
+
metadata: { uid: 'user-sub-fail' },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
assert.equal(result.category, 'subscription', 'Subscription invoice failure → subscription category');
|
|
198
|
+
assert.equal(result.resourceType, 'subscription', 'Should fetch the subscription, not the invoice');
|
|
199
|
+
assert.equal(result.resourceId, 'sub_target', 'Resource ID should be the subscription ID');
|
|
200
|
+
assert.equal(result.uid, 'user-sub-fail', 'UID should come from subscription metadata');
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
{
|
|
205
|
+
name: 'invoice-sub-failure-from-fixture',
|
|
206
|
+
async run({ assert }) {
|
|
207
|
+
const result = parseWebhook({
|
|
208
|
+
id: 'evt_fixture_sub_fail',
|
|
209
|
+
type: 'invoice.payment_failed',
|
|
210
|
+
data: { object: FIXTURE_INVOICE_SUB },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
assert.equal(result.category, 'subscription', 'Fixture: should be subscription category');
|
|
214
|
+
assert.equal(result.resourceType, 'subscription', 'Fixture: should fetch subscription');
|
|
215
|
+
assert.equal(result.resourceId, 'sub_test_failed_sub', 'Fixture: resource ID from parent.subscription_details');
|
|
216
|
+
assert.equal(result.uid, 'test-uid-sub-fail', 'Fixture: UID from subscription metadata');
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
{
|
|
221
|
+
name: 'invoice-sub-failure-subscription-create-reason',
|
|
222
|
+
async run({ assert }) {
|
|
223
|
+
const result = parseWebhook({
|
|
224
|
+
id: 'evt_inv_sub_create',
|
|
225
|
+
type: 'invoice.payment_failed',
|
|
226
|
+
data: {
|
|
227
|
+
object: {
|
|
228
|
+
id: 'in_sub_create_fail',
|
|
229
|
+
billing_reason: 'subscription_create',
|
|
230
|
+
parent: {
|
|
231
|
+
subscription_details: {
|
|
232
|
+
subscription: 'sub_new',
|
|
233
|
+
metadata: { uid: 'user-new' },
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
assert.equal(result.category, 'subscription', 'subscription_create billing reason → subscription');
|
|
241
|
+
assert.equal(result.resourceId, 'sub_new', 'Should use subscription ID');
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
{
|
|
246
|
+
name: 'invoice-sub-failure-legacy-subscription-field',
|
|
247
|
+
async run({ assert }) {
|
|
248
|
+
// Older Stripe API versions use data.object.subscription instead of parent
|
|
249
|
+
const result = parseWebhook({
|
|
250
|
+
id: 'evt_inv_legacy',
|
|
251
|
+
type: 'invoice.payment_failed',
|
|
252
|
+
data: {
|
|
253
|
+
object: {
|
|
254
|
+
id: 'in_legacy',
|
|
255
|
+
billing_reason: 'subscription_cycle',
|
|
256
|
+
subscription: 'sub_legacy',
|
|
257
|
+
metadata: { uid: 'user-legacy' },
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
assert.equal(result.category, 'subscription', 'Legacy format → subscription');
|
|
263
|
+
assert.equal(result.resourceType, 'subscription', 'Should still fetch subscription');
|
|
264
|
+
assert.equal(result.resourceId, 'sub_legacy', 'Should use legacy subscription field');
|
|
265
|
+
assert.equal(result.uid, 'user-legacy', 'UID falls back to invoice metadata');
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
// ─── invoice.payment_failed — one-time ───
|
|
270
|
+
|
|
271
|
+
{
|
|
272
|
+
name: 'invoice-onetime-failure-category',
|
|
273
|
+
async run({ assert }) {
|
|
274
|
+
const result = parseWebhook({
|
|
275
|
+
id: 'evt_inv_manual_fail',
|
|
276
|
+
type: 'invoice.payment_failed',
|
|
277
|
+
data: {
|
|
278
|
+
object: {
|
|
279
|
+
id: 'in_manual_fail',
|
|
280
|
+
billing_reason: 'manual',
|
|
281
|
+
metadata: { uid: 'user-manual' },
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
assert.equal(result.category, 'one-time', 'Manual billing reason → one-time');
|
|
287
|
+
assert.equal(result.resourceType, 'invoice', 'Should fetch the invoice');
|
|
288
|
+
assert.equal(result.resourceId, 'in_manual_fail', 'Resource ID should be invoice ID');
|
|
289
|
+
assert.equal(result.uid, 'user-manual', 'UID from invoice metadata');
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
{
|
|
294
|
+
name: 'invoice-onetime-failure-from-fixture',
|
|
295
|
+
async run({ assert }) {
|
|
296
|
+
const result = parseWebhook({
|
|
297
|
+
id: 'evt_fixture_manual',
|
|
298
|
+
type: 'invoice.payment_failed',
|
|
299
|
+
data: { object: FIXTURE_INVOICE_MANUAL },
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
assert.equal(result.category, 'one-time', 'Fixture: manual billing → one-time');
|
|
303
|
+
assert.equal(result.resourceType, 'invoice', 'Fixture: should fetch invoice');
|
|
304
|
+
assert.equal(result.resourceId, FIXTURE_INVOICE_MANUAL.id, 'Fixture: resource ID is invoice ID');
|
|
305
|
+
assert.equal(result.uid, null, 'Fixture: no uid in metadata (CLI-generated)');
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
{
|
|
310
|
+
name: 'invoice-failure-no-billing-reason-defaults-to-onetime',
|
|
311
|
+
async run({ assert }) {
|
|
312
|
+
const result = parseWebhook({
|
|
313
|
+
id: 'evt_inv_no_reason',
|
|
314
|
+
type: 'invoice.payment_failed',
|
|
315
|
+
data: {
|
|
316
|
+
object: {
|
|
317
|
+
id: 'in_no_reason',
|
|
318
|
+
metadata: { uid: 'user-no-reason' },
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
assert.equal(result.category, 'one-time', 'No billing reason → one-time');
|
|
324
|
+
assert.equal(result.resourceType, 'invoice', 'Should fetch invoice');
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
// ─── checkout.session.completed — one-time payment ───
|
|
329
|
+
|
|
330
|
+
{
|
|
331
|
+
name: 'checkout-payment-category',
|
|
332
|
+
async run({ assert }) {
|
|
333
|
+
const result = parseWebhook({
|
|
334
|
+
id: 'evt_cs_payment',
|
|
335
|
+
type: 'checkout.session.completed',
|
|
336
|
+
data: {
|
|
337
|
+
object: {
|
|
338
|
+
id: 'cs_test_payment',
|
|
339
|
+
mode: 'payment',
|
|
340
|
+
metadata: { uid: 'user-checkout' },
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
assert.equal(result.category, 'one-time', 'Payment mode → one-time');
|
|
346
|
+
assert.equal(result.resourceType, 'session', 'Should fetch session');
|
|
347
|
+
assert.equal(result.resourceId, 'cs_test_payment', 'Resource ID should be session ID');
|
|
348
|
+
assert.equal(result.uid, 'user-checkout', 'UID from session metadata');
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
{
|
|
353
|
+
name: 'checkout-payment-from-fixture',
|
|
354
|
+
async run({ assert }) {
|
|
355
|
+
const result = parseWebhook({
|
|
356
|
+
id: 'evt_fixture_checkout',
|
|
357
|
+
type: 'checkout.session.completed',
|
|
358
|
+
data: { object: FIXTURE_CHECKOUT_PAYMENT },
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
assert.equal(result.category, 'one-time', 'Fixture: payment mode → one-time');
|
|
362
|
+
assert.equal(result.resourceType, 'session', 'Fixture: should fetch session');
|
|
363
|
+
assert.equal(result.resourceId, FIXTURE_CHECKOUT_PAYMENT.id, 'Fixture: resource ID is session ID');
|
|
364
|
+
assert.equal(result.uid, null, 'Fixture: no uid in metadata (CLI-generated)');
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
// ─── checkout.session.completed — subscription mode (skipped) ───
|
|
369
|
+
|
|
370
|
+
{
|
|
371
|
+
name: 'checkout-subscription-skipped',
|
|
372
|
+
async run({ assert }) {
|
|
373
|
+
const result = parseWebhook({
|
|
374
|
+
id: 'evt_cs_sub',
|
|
375
|
+
type: 'checkout.session.completed',
|
|
376
|
+
data: {
|
|
377
|
+
object: {
|
|
378
|
+
id: 'cs_test_sub',
|
|
379
|
+
mode: 'subscription',
|
|
380
|
+
metadata: { uid: 'user-cs-sub' },
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
assert.equal(result.category, null, 'Subscription checkout → null (skipped)');
|
|
386
|
+
assert.equal(result.resourceType, null, 'No resource type for skipped events');
|
|
387
|
+
assert.equal(result.resourceId, null, 'No resource ID for skipped events');
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
{
|
|
392
|
+
name: 'checkout-unknown-mode-skipped',
|
|
393
|
+
async run({ assert }) {
|
|
394
|
+
const result = parseWebhook({
|
|
395
|
+
id: 'evt_cs_unknown',
|
|
396
|
+
type: 'checkout.session.completed',
|
|
397
|
+
data: {
|
|
398
|
+
object: {
|
|
399
|
+
id: 'cs_test_unknown',
|
|
400
|
+
mode: 'setup',
|
|
401
|
+
metadata: {},
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
assert.equal(result.category, null, 'Unknown checkout mode → null');
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
// ─── Unsupported event type passthrough ───
|
|
411
|
+
|
|
412
|
+
{
|
|
413
|
+
name: 'unsupported-event-returns-null-category',
|
|
414
|
+
async run({ assert }) {
|
|
415
|
+
const result = parseWebhook({
|
|
416
|
+
id: 'evt_unsupported',
|
|
417
|
+
type: 'charge.succeeded',
|
|
418
|
+
data: { object: { id: 'ch_123' } },
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
assert.equal(result.category, null, 'Unsupported event → null category');
|
|
422
|
+
assert.equal(result.resourceType, null, 'No resource type');
|
|
423
|
+
assert.equal(result.resourceId, null, 'No resource ID');
|
|
424
|
+
assert.equal(result.eventId, 'evt_unsupported', 'Should still return event ID');
|
|
425
|
+
assert.equal(result.eventType, 'charge.succeeded', 'Should still return event type');
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
// ─── raw passthrough ───
|
|
430
|
+
|
|
431
|
+
{
|
|
432
|
+
name: 'raw-contains-full-event',
|
|
433
|
+
async run({ assert }) {
|
|
434
|
+
const event = {
|
|
435
|
+
id: 'evt_raw',
|
|
436
|
+
type: 'customer.subscription.updated',
|
|
437
|
+
data: { object: { id: 'sub_raw', metadata: { uid: 'user-raw' } } },
|
|
438
|
+
extra_field: 'preserved',
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const result = parseWebhook(event);
|
|
442
|
+
assert.equal(result.raw, event, 'Raw should be the full event object');
|
|
443
|
+
assert.equal(result.raw.extra_field, 'preserved', 'Extra fields preserved in raw');
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
};
|