backend-manager 5.0.169 → 5.0.171
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/TODO-2.md +3 -0
- package/package.json +1 -1
- package/src/manager/cron/daily/ghostii-auto-publisher.js +1 -1
- package/src/manager/events/firestore/payments-disputes/on-write.js +186 -52
- package/src/manager/helpers/user.js +0 -1
- package/src/manager/routes/payments/dispute-alert/post.js +3 -3
- package/src/manager/routes/payments/dispute-alert/processors/chargeblast.js +12 -3
package/TODO-2.md
CHANGED
|
@@ -10,6 +10,9 @@ payments/reactivate
|
|
|
10
10
|
payments/upgrade
|
|
11
11
|
* takes a subscription id and a new plan id and upgrades the user's subscription to the new plan. this can only be done if the user has an active subscription.
|
|
12
12
|
|
|
13
|
+
-------
|
|
14
|
+
UPSELL
|
|
15
|
+
* products in BEM can have an UPSELL where you link another product ID and it allows you to add it to your cart OR shows you after checkout?
|
|
13
16
|
|
|
14
17
|
-------
|
|
15
18
|
USER OBJECT UPDGRADE --> INSTANCE?
|
package/package.json
CHANGED
|
@@ -235,7 +235,7 @@ function uploadPost(assistant, settings, article) {
|
|
|
235
235
|
author: settings.author,
|
|
236
236
|
categories: article.categories,
|
|
237
237
|
tags: article.keywords,
|
|
238
|
-
|
|
238
|
+
postPath: 'ghostii',
|
|
239
239
|
githubUser: settings.brand.github.user,
|
|
240
240
|
githubRepo: settings.brand.github.repo,
|
|
241
241
|
},
|
|
@@ -5,9 +5,9 @@ const powertools = require('node-powertools');
|
|
|
5
5
|
* Firestore trigger: payments-disputes/{alertId} onWrite
|
|
6
6
|
*
|
|
7
7
|
* Processes pending dispute alerts:
|
|
8
|
-
* 1.
|
|
9
|
-
* 2.
|
|
10
|
-
* 3. Issues full refund on matched
|
|
8
|
+
* 1. Tries direct match via charge ID or payment intent (from Chargeblast alert.updated)
|
|
9
|
+
* 2. Falls back to searching Stripe invoices by date range + amount + card last4
|
|
10
|
+
* 3. Issues full refund on matched charge
|
|
11
11
|
* 4. Cancels subscription immediately (Stripe fires webhook → existing pipeline handles user doc)
|
|
12
12
|
* 5. Sends email alert to brand contact
|
|
13
13
|
* 6. Updates dispute document with results
|
|
@@ -33,14 +33,14 @@ module.exports = async ({ assistant, change, context }) => {
|
|
|
33
33
|
const alert = dataAfter.alert;
|
|
34
34
|
const processor = alert.processor || 'stripe';
|
|
35
35
|
|
|
36
|
-
assistant.log(`Processing dispute ${alertId}: processor=${processor}, amount=${alert.amount}, card=****${alert.card.last4}, date=${alert.transactionDate}`);
|
|
36
|
+
assistant.log(`Processing dispute ${alertId}: processor=${processor}, amount=${alert.amount}, card=****${alert.card.last4}, date=${alert.transactionDate}, chargeId=${alert.chargeId || 'none'}, paymentIntentId=${alert.paymentIntentId || 'none'}`);
|
|
37
37
|
|
|
38
38
|
// Only Stripe is supported for now
|
|
39
39
|
if (processor !== 'stripe') {
|
|
40
40
|
throw new Error(`Unsupported processor: ${processor}. Only 'stripe' is currently supported.`);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// Search for the matching
|
|
43
|
+
// Search for the matching charge
|
|
44
44
|
const match = await searchAndMatch({ alert, assistant });
|
|
45
45
|
|
|
46
46
|
// Build timestamps
|
|
@@ -48,9 +48,10 @@ module.exports = async ({ assistant, change, context }) => {
|
|
|
48
48
|
const nowUNIX = powertools.timestamp(now, { output: 'unix' });
|
|
49
49
|
|
|
50
50
|
if (!match) {
|
|
51
|
-
// No matching
|
|
51
|
+
// No matching charge found
|
|
52
52
|
await disputeRef.set({
|
|
53
53
|
status: 'no-match',
|
|
54
|
+
match: null,
|
|
54
55
|
actions: {
|
|
55
56
|
refund: 'skipped',
|
|
56
57
|
cancel: 'skipped',
|
|
@@ -64,11 +65,11 @@ module.exports = async ({ assistant, change, context }) => {
|
|
|
64
65
|
},
|
|
65
66
|
}, { merge: true });
|
|
66
67
|
|
|
67
|
-
assistant.log(`Dispute ${alertId}: no matching
|
|
68
|
+
assistant.log(`Dispute ${alertId}: no matching charge found`);
|
|
68
69
|
|
|
69
70
|
// Still send email to alert brand about unmatched dispute
|
|
70
71
|
if (!assistant.isTesting() || process.env.TEST_EXTENDED_MODE) {
|
|
71
|
-
sendDisputeEmail({ alert, match: null, alertId, assistant });
|
|
72
|
+
sendDisputeEmail({ alert, match: null, result: null, alertId, assistant });
|
|
72
73
|
await disputeRef.set({ actions: { email: 'success' } }, { merge: true });
|
|
73
74
|
} else {
|
|
74
75
|
assistant.log(`Dispute ${alertId}: skipping email (testing mode)`);
|
|
@@ -85,10 +86,12 @@ module.exports = async ({ assistant, change, context }) => {
|
|
|
85
86
|
await disputeRef.set({
|
|
86
87
|
status: 'resolved',
|
|
87
88
|
match: {
|
|
88
|
-
|
|
89
|
+
method: match.method,
|
|
90
|
+
invoiceId: match.invoiceId || null,
|
|
89
91
|
subscriptionId: match.subscriptionId || null,
|
|
90
92
|
customerId: match.customerId,
|
|
91
93
|
uid: match.uid || null,
|
|
94
|
+
email: match.email || null,
|
|
92
95
|
chargeId: match.chargeId,
|
|
93
96
|
refundId: result.refundId || null,
|
|
94
97
|
amountRefunded: result.amountRefunded || null,
|
|
@@ -129,7 +132,12 @@ module.exports = async ({ assistant, change, context }) => {
|
|
|
129
132
|
};
|
|
130
133
|
|
|
131
134
|
/**
|
|
132
|
-
*
|
|
135
|
+
* Try to match the dispute alert to a Stripe charge.
|
|
136
|
+
*
|
|
137
|
+
* Strategy (in order):
|
|
138
|
+
* 1. Direct lookup via charge ID (externalOrder from Chargeblast)
|
|
139
|
+
* 2. Direct lookup via payment intent ID (metadata from Chargeblast)
|
|
140
|
+
* 3. Fallback: search invoices by date range + amount + card last4
|
|
133
141
|
*
|
|
134
142
|
* @param {object} options
|
|
135
143
|
* @param {object} options.alert - Normalized alert data
|
|
@@ -140,6 +148,103 @@ async function searchAndMatch({ alert, assistant }) {
|
|
|
140
148
|
const StripeLib = require('../../../libraries/payment/processors/stripe.js');
|
|
141
149
|
const stripe = StripeLib.init();
|
|
142
150
|
|
|
151
|
+
// Strategy 1: Direct charge lookup
|
|
152
|
+
if (alert.chargeId && alert.chargeId.startsWith('ch_')) {
|
|
153
|
+
assistant.log(`Trying direct charge lookup: ${alert.chargeId}`);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const charge = await stripe.charges.retrieve(alert.chargeId, {
|
|
157
|
+
expand: ['invoice', 'customer'],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const match = await resolveMatchFromCharge({ charge, stripe, assistant, method: 'charge-id' });
|
|
161
|
+
|
|
162
|
+
if (match) {
|
|
163
|
+
return match;
|
|
164
|
+
}
|
|
165
|
+
} catch (e) {
|
|
166
|
+
assistant.log(`Direct charge lookup failed for ${alert.chargeId}: ${e.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Strategy 2: Direct payment intent lookup
|
|
171
|
+
if (alert.paymentIntentId && alert.paymentIntentId.startsWith('pi_')) {
|
|
172
|
+
assistant.log(`Trying direct payment intent lookup: ${alert.paymentIntentId}`);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const pi = await stripe.paymentIntents.retrieve(alert.paymentIntentId, {
|
|
176
|
+
expand: ['latest_charge', 'latest_charge.invoice', 'latest_charge.customer'],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const charge = pi.latest_charge;
|
|
180
|
+
if (charge) {
|
|
181
|
+
const match = await resolveMatchFromCharge({ charge, stripe, assistant, method: 'payment-intent' });
|
|
182
|
+
|
|
183
|
+
if (match) {
|
|
184
|
+
return match;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
assistant.log(`Direct payment intent lookup failed for ${alert.paymentIntentId}: ${e.message}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Strategy 3: Fallback — search invoices by date range + amount + card last4
|
|
193
|
+
return searchInvoicesFallback({ alert, stripe, assistant });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Build a match object from a Stripe charge
|
|
198
|
+
*/
|
|
199
|
+
async function resolveMatchFromCharge({ charge, stripe, assistant, method }) {
|
|
200
|
+
if (!charge || charge.status !== 'succeeded') {
|
|
201
|
+
assistant.log(`Charge ${charge?.id} status=${charge?.status}, skipping`);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Resolve UID from customer metadata
|
|
206
|
+
let uid = null;
|
|
207
|
+
let email = null;
|
|
208
|
+
const customerId = typeof charge.customer === 'string' ? charge.customer : charge.customer?.id;
|
|
209
|
+
|
|
210
|
+
if (charge.customer && typeof charge.customer === 'object') {
|
|
211
|
+
uid = charge.customer.metadata?.uid || null;
|
|
212
|
+
email = charge.customer.email || null;
|
|
213
|
+
} else if (customerId) {
|
|
214
|
+
try {
|
|
215
|
+
const customer = await stripe.customers.retrieve(customerId);
|
|
216
|
+
uid = customer.metadata?.uid || null;
|
|
217
|
+
email = customer.email || null;
|
|
218
|
+
} catch (e) {
|
|
219
|
+
assistant.error(`Failed to retrieve customer ${customerId}: ${e.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Resolve invoice/subscription
|
|
224
|
+
const invoiceId = typeof charge.invoice === 'string'
|
|
225
|
+
? charge.invoice
|
|
226
|
+
: charge.invoice?.id || null;
|
|
227
|
+
const subscriptionId = typeof charge.invoice === 'object'
|
|
228
|
+
? charge.invoice?.subscription || null
|
|
229
|
+
: null;
|
|
230
|
+
|
|
231
|
+
assistant.log(`Matched via ${method}: charge=${charge.id}, customer=${customerId}, uid=${uid}, invoice=${invoiceId}`);
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
method: method,
|
|
235
|
+
invoiceId: invoiceId,
|
|
236
|
+
subscriptionId: subscriptionId,
|
|
237
|
+
customerId: customerId,
|
|
238
|
+
uid: uid,
|
|
239
|
+
email: email,
|
|
240
|
+
chargeId: charge.id,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Fallback: search Stripe invoices by date range + amount and match card last4
|
|
246
|
+
*/
|
|
247
|
+
async function searchInvoicesFallback({ alert, stripe, assistant }) {
|
|
143
248
|
const amountCents = Math.round(alert.amount * 100);
|
|
144
249
|
const alertDate = moment(alert.transactionDate);
|
|
145
250
|
|
|
@@ -150,7 +255,7 @@ async function searchAndMatch({ alert, assistant }) {
|
|
|
150
255
|
const start = alertDate.clone().subtract(2, 'days').unix();
|
|
151
256
|
const end = alertDate.clone().add(2, 'days').unix();
|
|
152
257
|
|
|
153
|
-
assistant.log(`
|
|
258
|
+
assistant.log(`Fallback: searching Stripe invoices: amount=${amountCents} cents, range=${moment.unix(start).format('YYYY-MM-DD')} to ${moment.unix(end).format('YYYY-MM-DD')}`);
|
|
154
259
|
|
|
155
260
|
// Search invoices by date range and amount
|
|
156
261
|
const invoices = await stripe.invoices.search({
|
|
@@ -184,21 +289,25 @@ async function searchAndMatch({ alert, assistant }) {
|
|
|
184
289
|
|
|
185
290
|
// Resolve UID from customer metadata
|
|
186
291
|
let uid = null;
|
|
292
|
+
let email = null;
|
|
187
293
|
const customerId = invoice.customer;
|
|
188
294
|
if (customerId) {
|
|
189
295
|
try {
|
|
190
296
|
const customer = await stripe.customers.retrieve(customerId);
|
|
191
297
|
uid = customer.metadata?.uid || null;
|
|
298
|
+
email = customer.email || null;
|
|
192
299
|
} catch (e) {
|
|
193
300
|
assistant.error(`Failed to retrieve customer ${customerId}: ${e.message}`);
|
|
194
301
|
}
|
|
195
302
|
}
|
|
196
303
|
|
|
197
304
|
return {
|
|
305
|
+
method: 'invoice-search',
|
|
198
306
|
invoiceId: invoice.id,
|
|
199
307
|
subscriptionId: invoice.subscription || null,
|
|
200
308
|
customerId: customerId,
|
|
201
309
|
uid: uid,
|
|
310
|
+
email: email,
|
|
202
311
|
chargeId: invoice.charge || null,
|
|
203
312
|
};
|
|
204
313
|
}
|
|
@@ -243,11 +352,11 @@ async function processDispute({ match, alert, assistant }) {
|
|
|
243
352
|
result.currency = refund.currency;
|
|
244
353
|
result.refundStatus = 'success';
|
|
245
354
|
|
|
246
|
-
assistant.log(`Refund success: refundId=${refund.id}, amount=${amountCents},
|
|
355
|
+
assistant.log(`Refund success: refundId=${refund.id}, amount=${amountCents}, charge=${match.chargeId}`);
|
|
247
356
|
} catch (e) {
|
|
248
357
|
result.refundStatus = 'failed';
|
|
249
358
|
result.errors.push(`Refund failed: ${e.message}`);
|
|
250
|
-
assistant.error(`Refund failed for
|
|
359
|
+
assistant.error(`Refund failed for charge ${match.chargeId}: ${e.message}`);
|
|
251
360
|
}
|
|
252
361
|
}
|
|
253
362
|
|
|
@@ -290,47 +399,72 @@ function sendDisputeEmail({ alert, match, result, alertId, assistant }) {
|
|
|
290
399
|
}
|
|
291
400
|
|
|
292
401
|
const matched = match ? 'Matched' : 'Unmatched';
|
|
293
|
-
const subject = `Dispute
|
|
294
|
-
|
|
295
|
-
const disputeDetails = {
|
|
296
|
-
id: alertId,
|
|
297
|
-
card: `****${alert.card.last4} (${alert.card.brand || 'unknown'})`,
|
|
298
|
-
amount: `$${alert.amount}`,
|
|
299
|
-
date: alert.transactionDate,
|
|
300
|
-
processor: alert.processor,
|
|
301
|
-
};
|
|
402
|
+
const subject = `Dispute Alert: ${matched} — $${alert.amount} on ****${alert.card.last4} [${alertId}]`;
|
|
302
403
|
|
|
303
|
-
const
|
|
304
|
-
? {
|
|
305
|
-
invoiceId: match.invoiceId,
|
|
306
|
-
subscriptionId: match.subscriptionId || 'N/A',
|
|
307
|
-
uid: match.uid || 'unknown',
|
|
308
|
-
refund: result?.refundStatus || 'N/A',
|
|
309
|
-
cancel: result?.cancelStatus || 'N/A',
|
|
310
|
-
}
|
|
311
|
-
: null;
|
|
404
|
+
const messageLines = [];
|
|
312
405
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
'
|
|
317
|
-
`<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (matchDetails) {
|
|
321
|
-
messageLines.push(
|
|
322
|
-
'',
|
|
323
|
-
'<strong>Match & Actions:</strong>',
|
|
324
|
-
`<pre><code>${JSON.stringify(matchDetails, null, 2)}</code></pre>`,
|
|
325
|
-
);
|
|
406
|
+
// Status banner
|
|
407
|
+
if (match && result) {
|
|
408
|
+
const hasErrors = result.errors?.length > 0;
|
|
409
|
+
const banner = hasErrors ? 'Partially processed (see errors below)' : 'Automatically processed';
|
|
410
|
+
messageLines.push(`<strong>${banner}</strong>`);
|
|
411
|
+
} else {
|
|
412
|
+
messageLines.push('<strong>Could not be matched to a charge — manual review required.</strong>');
|
|
326
413
|
}
|
|
327
414
|
|
|
415
|
+
messageLines.push('');
|
|
416
|
+
|
|
417
|
+
// Alert details
|
|
418
|
+
messageLines.push('<strong>Alert Details:</strong>');
|
|
419
|
+
messageLines.push('<ul>');
|
|
420
|
+
messageLines.push(`<li><strong>Alert ID:</strong> ${alertId}</li>`);
|
|
421
|
+
messageLines.push(`<li><strong>Type:</strong> ${alert.alertType || 'N/A'}</li>`);
|
|
422
|
+
messageLines.push(`<li><strong>Card:</strong> ****${alert.card.last4} (${alert.card.brand || 'unknown'})</li>`);
|
|
423
|
+
messageLines.push(`<li><strong>Amount:</strong> $${alert.amount}</li>`);
|
|
424
|
+
messageLines.push(`<li><strong>Transaction Date:</strong> ${alert.transactionDate}</li>`);
|
|
425
|
+
messageLines.push(`<li><strong>Processor:</strong> ${alert.processor}</li>`);
|
|
426
|
+
messageLines.push(`<li><strong>Reason:</strong> ${alert.reasonCode || 'N/A'}</li>`);
|
|
427
|
+
messageLines.push(`<li><strong>Network:</strong> ${alert.subprovider || 'N/A'}</li>`);
|
|
428
|
+
messageLines.push(`<li><strong>Customer Email:</strong> ${alert.customerEmail || 'N/A'}</li>`);
|
|
429
|
+
messageLines.push(`<li><strong>Already Refunded:</strong> ${alert.isRefunded ? 'Yes' : 'No'}</li>`);
|
|
430
|
+
messageLines.push('</ul>');
|
|
431
|
+
|
|
432
|
+
// Match & action details
|
|
433
|
+
if (match) {
|
|
434
|
+
messageLines.push('<strong>Match Details:</strong>');
|
|
435
|
+
messageLines.push('<ul>');
|
|
436
|
+
messageLines.push(`<li><strong>Method:</strong> ${match.method}</li>`);
|
|
437
|
+
messageLines.push(`<li><strong>Charge:</strong> ${match.chargeId || 'N/A'}</li>`);
|
|
438
|
+
messageLines.push(`<li><strong>Invoice:</strong> ${match.invoiceId || 'N/A'}</li>`);
|
|
439
|
+
messageLines.push(`<li><strong>Subscription:</strong> ${match.subscriptionId || 'N/A'}</li>`);
|
|
440
|
+
messageLines.push(`<li><strong>Customer:</strong> ${match.customerId || 'N/A'}</li>`);
|
|
441
|
+
messageLines.push(`<li><strong>Customer Email:</strong> ${match.email || 'N/A'}</li>`);
|
|
442
|
+
messageLines.push(`<li><strong>UID:</strong> ${match.uid || 'unknown'}</li>`);
|
|
443
|
+
messageLines.push('</ul>');
|
|
444
|
+
|
|
445
|
+
if (result) {
|
|
446
|
+
messageLines.push('<strong>Actions Taken:</strong>');
|
|
447
|
+
messageLines.push('<ul>');
|
|
448
|
+
messageLines.push(`<li><strong>Refund:</strong> ${result.refundStatus}${result.refundId ? ` (${result.refundId})` : ''}${result.amountRefunded ? ` — $${(result.amountRefunded / 100).toFixed(2)} ${result.currency || ''}` : ''}</li>`);
|
|
449
|
+
messageLines.push(`<li><strong>Cancel Subscription:</strong> ${result.cancelStatus}</li>`);
|
|
450
|
+
messageLines.push('</ul>');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Stripe link
|
|
455
|
+
if (alert.stripeUrl) {
|
|
456
|
+
messageLines.push(`<br><a href="${alert.stripeUrl}">View in Stripe Dashboard</a>`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Errors
|
|
328
460
|
if (result?.errors?.length) {
|
|
329
|
-
messageLines.push(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
461
|
+
messageLines.push('');
|
|
462
|
+
messageLines.push('<strong>Errors:</strong>');
|
|
463
|
+
messageLines.push('<ul>');
|
|
464
|
+
result.errors.forEach((err) => {
|
|
465
|
+
messageLines.push(`<li>${err}</li>`);
|
|
466
|
+
});
|
|
467
|
+
messageLines.push('</ul>');
|
|
334
468
|
}
|
|
335
469
|
|
|
336
470
|
email.send({
|
|
@@ -342,11 +476,11 @@ function sendDisputeEmail({ alert, match, result, alertId, assistant }) {
|
|
|
342
476
|
copy: true,
|
|
343
477
|
data: {
|
|
344
478
|
email: {
|
|
345
|
-
preview: `Dispute
|
|
479
|
+
preview: `Dispute Alert: ${matched} — $${alert.amount} on ****${alert.card.last4}`,
|
|
346
480
|
},
|
|
347
481
|
body: {
|
|
348
|
-
title:
|
|
349
|
-
message: messageLines.join('
|
|
482
|
+
title: `Dispute Alert: ${matched}`,
|
|
483
|
+
message: messageLines.join('\n'),
|
|
350
484
|
},
|
|
351
485
|
},
|
|
352
486
|
})
|
|
@@ -2,12 +2,12 @@ const path = require('path');
|
|
|
2
2
|
const powertools = require('node-powertools');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* POST /payments/dispute-alert?
|
|
5
|
+
* POST /payments/dispute-alert?provider=chargeblast&key=XXX
|
|
6
6
|
* Receives dispute alert webhooks (e.g., from Chargeblast), validates them,
|
|
7
7
|
* and saves to Firestore for async processing via onWrite trigger
|
|
8
8
|
*
|
|
9
9
|
* Query params:
|
|
10
|
-
* -
|
|
10
|
+
* - provider: alert provider name (default: 'chargeblast')
|
|
11
11
|
* - key: must match BACKEND_MANAGER_KEY
|
|
12
12
|
*/
|
|
13
13
|
module.exports = async ({ assistant, Manager, libraries }) => {
|
|
@@ -22,7 +22,7 @@ module.exports = async ({ assistant, Manager, libraries }) => {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// Determine alert provider (default: chargeblast)
|
|
25
|
-
const provider = query.
|
|
25
|
+
const provider = query.provider || 'chargeblast';
|
|
26
26
|
|
|
27
27
|
// Load the processor module
|
|
28
28
|
let processorModule;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Chargeblast dispute alert processor
|
|
3
3
|
* Normalizes Chargeblast webhook payloads into a standard dispute alert shape
|
|
4
4
|
*
|
|
5
|
-
* Chargeblast sends:
|
|
6
|
-
* id, card
|
|
7
|
-
*
|
|
5
|
+
* Chargeblast sends two event types:
|
|
6
|
+
* alert.created: id, card, cardBrand, amount, transactionDate, processor, etc.
|
|
7
|
+
* alert.updated: same + externalOrder (charge ID), metadata (payment intent), customerEmail, etc.
|
|
8
8
|
*/
|
|
9
9
|
module.exports = {
|
|
10
10
|
/**
|
|
@@ -38,6 +38,15 @@ module.exports = {
|
|
|
38
38
|
amount: parseFloat(body.amount),
|
|
39
39
|
transactionDate: String(body.transactionDate).split(' ')[0], // date only, no time
|
|
40
40
|
processor: body.processor ? String(body.processor).toLowerCase() : 'stripe',
|
|
41
|
+
alertType: body.alertType || null,
|
|
42
|
+
customerEmail: body.customerEmail || null,
|
|
43
|
+
// Stripe-specific IDs provided by Chargeblast on alert.updated events
|
|
44
|
+
chargeId: body.externalOrder || null,
|
|
45
|
+
paymentIntentId: body.metadata || null,
|
|
46
|
+
stripeUrl: body.externalUrl || null,
|
|
47
|
+
reasonCode: body.reasonCode || null,
|
|
48
|
+
subprovider: body.subprovider || null,
|
|
49
|
+
isRefunded: body.isRefunded || false,
|
|
41
50
|
};
|
|
42
51
|
},
|
|
43
52
|
};
|