backend-manager 5.0.73 → 5.0.74

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.
Files changed (50) hide show
  1. package/CLAUDE.md +70 -0
  2. package/README.md +81 -7
  3. package/package.json +1 -1
  4. package/src/manager/cron/daily/reset-usage.js +5 -32
  5. package/src/manager/events/firestore/payments-webhooks/on-write.js +126 -0
  6. package/src/manager/functions/core/actions/api/admin/get-stats.js +3 -3
  7. package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +1 -1
  8. package/src/manager/functions/core/actions/api/user/delete.js +5 -3
  9. package/src/manager/functions/core/actions/api/user/get-subscription-info.js +25 -9
  10. package/src/manager/functions/core/actions/api/user/validate-settings.js +1 -1
  11. package/src/manager/helpers/analytics.js +4 -4
  12. package/src/manager/helpers/api-manager.js +25 -42
  13. package/src/manager/helpers/middleware.js +1 -1
  14. package/src/manager/helpers/usage.js +24 -93
  15. package/src/manager/helpers/user.js +29 -38
  16. package/src/manager/index.js +22 -10
  17. package/src/manager/libraries/stripe.js +293 -0
  18. package/src/manager/routes/admin/stats/get.js +3 -3
  19. package/src/manager/routes/marketing/contact/post.js +1 -1
  20. package/src/manager/routes/payments/intent/post.js +94 -0
  21. package/src/manager/routes/payments/intent/providers/stripe.js +66 -0
  22. package/src/manager/routes/payments/webhook/post.js +87 -0
  23. package/src/manager/routes/payments/webhook/providers/stripe.js +35 -0
  24. package/src/manager/routes/test/schema/post.js +5 -5
  25. package/src/manager/routes/user/delete.js +5 -3
  26. package/src/manager/routes/user/settings/validate/post.js +3 -3
  27. package/src/manager/routes/user/subscription/get.js +25 -9
  28. package/src/manager/schemas/payments/intent/post.js +22 -0
  29. package/src/manager/schemas/payments/webhook/post.js +6 -0
  30. package/src/manager/schemas/test/schema/post.js +1 -1
  31. package/src/test/test-accounts.js +63 -25
  32. package/src/test/utils/firestore-rules-client.js +5 -5
  33. package/templates/backend-manager-config.json +32 -0
  34. package/templates/firestore.rules +1 -1
  35. package/test/_init/accounts-validation.js +3 -3
  36. package/test/functions/user/delete.js +1 -1
  37. package/test/functions/user/get-subscription-info.js +18 -24
  38. package/test/payments/intent.js +104 -0
  39. package/test/payments/journey-payment-cancel.js +166 -0
  40. package/test/payments/journey-payment-suspend.js +162 -0
  41. package/test/payments/journey-payment-trial.js +167 -0
  42. package/test/payments/journey-payment-upgrade.js +136 -0
  43. package/test/payments/webhook.js +128 -0
  44. package/test/routes/test/schema.js +1 -1
  45. package/test/routes/user/delete.js +1 -1
  46. package/test/routes/user/subscription.js +18 -24
  47. package/test/routes/user/user.js +14 -14
  48. package/test/rules/user.js +8 -8
  49. package/src/manager/helpers/subscription-resolver-new.js +0 -827
  50. package/src/manager/helpers/subscription-resolver.js +0 -841
@@ -1,841 +0,0 @@
1
- const moment = require('moment');
2
- const { arrayify } = require('node-powertools');
3
-
4
- function SubscriptionResolver(Manager, profile, resource) {
5
- const self = this;
6
-
7
- self.Manager = Manager;
8
- self.profile = profile || {};
9
- self.resource = resource || {};
10
-
11
- return self;
12
- }
13
-
14
- SubscriptionResolver.prototype.resolve = function (options) {
15
- const self = this;
16
-
17
- const datePast = moment(0);
18
-
19
- const resolved = {
20
- status: '',
21
- frequency: '',
22
- resource: {
23
- id: '',
24
- },
25
- payment: {
26
- completed: false,
27
- refunded: false,
28
- },
29
- start: {
30
- timestamp: datePast,
31
- timestampUNIX: datePast,
32
- },
33
- expires: {
34
- timestamp: datePast,
35
- timestampUNIX: datePast,
36
- },
37
- cancelled: {
38
- timestamp: datePast,
39
- timestampUNIX: datePast,
40
- },
41
- lastPayment: {
42
- amount: 0,
43
- date: {
44
- timestamp: datePast,
45
- timestampUNIX: datePast,
46
- }
47
- },
48
- trial: {
49
- claimed: false,
50
- active: false,
51
- daysLeft: 0,
52
- expires: {
53
- timestamp: datePast,
54
- timestampUNIX: datePast,
55
- },
56
- },
57
- details: {
58
- message: '',
59
- }
60
- }
61
-
62
- // Set
63
- const profile = self.profile;
64
- const resource = self.resource;
65
-
66
- // Set defaults
67
- profile.type = profile.type || null;
68
- profile.details = profile.details || {};
69
- profile.details.planFrequency = profile.details.planFrequency || null;
70
- profile.authorization = profile.authorization || {};
71
- profile.authorization.status = profile.authorization.status || 'pending';
72
-
73
- // Set
74
- options = options || {};
75
- options.log = typeof options.log === 'undefined' ? false : options.log;
76
- options.resolveProcessor = typeof options.resolveProcessor === 'undefined' ? false : options.resolveProcessor;
77
- options.resolveType = typeof options.resolveType === 'undefined' ? false : options.resolveType;
78
- options.today = typeof options.today === 'undefined' ? moment() : moment(options.today);
79
- options.message = typeof options.message === 'undefined' ? true : options.message;
80
-
81
- // Set provider if not set
82
- if (!profile.processor) {
83
- /*** PayPal ***/
84
- // Order
85
- if (
86
- resource.purchase_units
87
- ) {
88
- profile.processor = 'paypal';
89
- profile.type = profile.type || 'order';
90
- // Subscription
91
- } else if (
92
- // resource.billing_info
93
- resource.create_time
94
- ) {
95
- profile.processor = 'paypal';
96
- profile.type = profile.type || 'subscription';
97
-
98
- /*** Chargebee ***/
99
- // Order
100
- } else if (
101
- resource.line_items
102
- ) {
103
- profile.processor = 'chargebee';
104
- profile.type = profile.type || 'order';
105
- // Subscription
106
- } else if (
107
- resource.billing_period_unit
108
- ) {
109
- profile.processor = 'chargebee';
110
- profile.type = profile.type || 'subscription';
111
-
112
- /*** Stripe ***/
113
- // Order
114
- } else if (
115
- resource.object === 'charge'
116
- ) {
117
- profile.processor = 'stripe';
118
- profile.type = profile.type || 'order';
119
- // Subscription
120
- } else if (
121
- resource.object === 'subscription'
122
- ) {
123
- profile.processor = 'stripe';
124
- profile.type = profile.type || 'subscription';
125
-
126
- /*** Coinbase ***/
127
- // Order AND Subscription
128
- } else if (
129
- resource.addresses
130
- ) {
131
- profile.processor = 'coinbase';
132
- // profile.type = profile.type || 'subscription';
133
-
134
- /*** Error ***/
135
- } else {
136
- throw new Error('Unable to determine subscription provider');
137
- }
138
- }
139
-
140
- // Set profile.type
141
- if (!profile.type) {
142
- profile.type = profile.type || 'subscription';
143
- }
144
-
145
- // Set processor if needed
146
- if (options.resolveProcessor) {
147
- resolved.processor = profile.processor;
148
- }
149
-
150
- // Set type if needed
151
- if (options.resolveType) {
152
- resolved.type = profile.type;
153
- }
154
-
155
- // Set frequency if order
156
- if (profile.type === 'order') {
157
- resolved.frequency = 'single';
158
- }
159
-
160
- // Log if requested
161
- if (options.log) {
162
- console.log('profile', profile);
163
- console.log('resource', resource);
164
- }
165
-
166
- // Resolve
167
- const processor = self[`resolve_${profile.processor}`];
168
- if (processor) {
169
- processor(profile, resource, resolved, options);
170
- } else {
171
- throw new Error('Unknown processor');
172
- }
173
-
174
- // console.log('---resolved', resolved);
175
-
176
- // Check for frequency
177
- if (!resolved.frequency) {
178
- throw new Error('Unknown frequency');
179
- }
180
-
181
- // Fix expiry by adding time to the date of last payment
182
- // console.log('----expires 2', resolved.resource.id, resolved.status, resolved.frequency, resolved.trial.active, resolved.expires.timestamp.toISOString ? resolved.expires.timestamp.toISOString() : resolved.expires.timestamp);
183
- if (resolved.status === 'active') {
184
- // Set days left
185
- if (resolved.trial.active) {
186
- resolved.trial.daysLeft = Math.abs(resolved.expires.timestamp.diff(options.today, 'days'));
187
- resolved.trial.expires.timestamp = moment(resolved.start.timestamp).add(14, 'days');
188
- }
189
-
190
- // Set expiration
191
- // resolved.expires.timestamp.add(1, 'year').add(30, 'days');
192
- resolved.expires.timestamp.add(1, 'year').add(2, 'months');
193
- } else {
194
- // If trial, it's already set to the trial end above
195
- if (!resolved.trial.active) {
196
- if (resolved.frequency === 'annually') {
197
- resolved.expires.timestamp.add(1, 'year');
198
- } else if (resolved.frequency === 'monthly') {
199
- resolved.expires.timestamp.add(1, 'month');
200
- } else if (resolved.frequency === 'weekly') {
201
- resolved.expires.timestamp.add(1, 'week');
202
- } else if (resolved.frequency === 'daily') {
203
- resolved.expires.timestamp.add(1, 'day');
204
- }
205
- }
206
- }
207
- // console.log('----expires 3', resolved.resource.id, resolved.status, resolved.frequency, resolved.trial.active, resolved.expires.timestamp.toISOString ? resolved.expires.timestamp.toISOString() : resolved.expires.timestamp);
208
-
209
- // If they are not trialing AND there was NEVER any payment sent OR the last payment failed, then set the expiration to 0
210
- if (
211
- !resolved.trial.active
212
- && (!resolved.payment.completed || resolved.lastPayment.amount === 0)
213
- ) {
214
- resolved.expires.timestamp = moment(0);
215
- // resolved.cancelled.timestamp = moment(0);
216
- resolved.details.message = 'Most recent payment failed because there is no working payment method on file.'
217
- }
218
-
219
- // If they are trialing and the authorization charge is failed, set to suspended
220
- if (
221
- resolved.trial.active
222
- && profile.authorization.status === 'failed'
223
- ) {
224
- resolved.status = 'suspended';
225
- resolved.details.message = 'Pre-payment authorization failed because there is no working payment method on file.'
226
- }
227
-
228
- // If they got a refund (AND cancelled), set the expiration to 0
229
- // This allows for partial refunds without disabling the subscription
230
- if (resolved.payment.refunded && resolved.status === 'cancelled') {
231
- resolved.expires.timestamp = moment(0);
232
- resolved.details.message = 'Refund was issued so subscription is inactive.'
233
- }
234
-
235
- // If they are suspended, set the expiration to 0
236
- if (resolved.status === 'suspended') {
237
- resolved.expires.timestamp = moment(0);
238
- }
239
-
240
- // console.log('----expires 4', resolved.resource.id, resolved.status, resolved.frequency, resolved.trial.active, resolved.expires.timestamp.toISOString ? resolved.expires.timestamp.toISOString() : resolved.expires.timestamp);
241
-
242
- // Fix timestamps
243
- resolved.start.timestampUNIX = resolved.start.timestamp.unix();
244
- resolved.start.timestamp = resolved.start.timestamp.toISOString();
245
-
246
- resolved.expires.timestampUNIX = resolved.expires.timestamp.unix();
247
- resolved.expires.timestamp = resolved.expires.timestamp.toISOString ? resolved.expires.timestamp.toISOString() : resolved.expires.timestamp;
248
-
249
- resolved.cancelled.timestampUNIX = resolved.cancelled.timestamp.unix();
250
- resolved.cancelled.timestamp = resolved.cancelled.timestamp.toISOString();
251
-
252
- resolved.trial.expires.timestampUNIX = resolved.trial.expires.timestamp.unix();
253
- resolved.trial.expires.timestamp = resolved.trial.expires.timestamp.toISOString();
254
-
255
- // Fix trial days
256
- resolved.trial.daysLeft = resolved.trial.daysLeft < 0 ? 0 : resolved.trial.daysLeft;
257
-
258
- // Set last payment
259
- resolved.lastPayment.date.timestampUNIX = moment(resolved.lastPayment.date.timestamp).unix();
260
- resolved.lastPayment.date.timestamp = resolved.lastPayment.date.timestamp.toISOString();
261
-
262
- // Log if needed
263
- if (options.log) {
264
- console.log('resolved', resolved);
265
- }
266
-
267
- if (!options.message) {
268
- resolved.details.message = '[REDACTED]';
269
- }
270
-
271
- self.resolved = resolved;
272
- // console.log('----expires 6', resolved.resource.id, resolved.status, resolved.frequency, resolved.trial.active, resolved.expires.timestamp.toISOString ? resolved.expires.timestamp.toISOString() : resolved.expires.timestamp);
273
-
274
- return resolved;
275
- };
276
-
277
- SubscriptionResolver.prototype.resolve_paypal = function (profile, resource, resolved, options) {
278
- const self = this;
279
-
280
- // Set status
281
- /*
282
- subscription: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_get
283
- APPROVAL_PENDING. The subscription is created but not yet approved by the buyer.
284
- APPROVED. The buyer has approved the subscription.
285
- ACTIVE. The subscription is active.
286
- SUSPENDED. The subscription is suspended.
287
- CANCELLED. The subscription is cancelled.
288
- EXPIRED. The subscription is expired.
289
-
290
- order: https://developer.paypal.com/docs/api/orders/v2/#orders_get
291
- CREATED The order was created with the specified context.
292
- SAVED The order was saved and persisted. The order status continues to be in progress until a capture is made with final_capture = true for all purchase units within the order.
293
- APPROVED The customer approved the payment through the PayPal wallet or another form of guest or unbranded payment. For example, a card, bank account, or so on.
294
- VOIDED All purchase units in the order are voided.
295
- COMPLETED The payment was authorized or the authorized payment was captured for the order.
296
- PAYER_ACTION_REQUIRED The order requires an action from the payer (e.g. 3DS authentication). Redirect the payer to the "rel":"payer-action" HATEOAS link returned as part of the response prior to authorizing or capturing the order.
297
- */
298
- if (['ACTIVE'].includes(resource.status)) {
299
- resolved.status = 'active';
300
-
301
- // Check for failed payments
302
- /*
303
- Special condition for PayPal
304
- Because I set the payment_failure_threshold to 0, it will not automatically set the status to suspended.
305
- We must check for failed payments and set the status to suspended if there are any.
306
- */
307
- if ((resource?.billing_info?.failed_payments_count ?? 0) > 0) {
308
- resolved.status = 'suspended';
309
- }
310
- } else if (['SUSPENDED'].includes(resource.status)) {
311
- resolved.status = 'suspended';
312
- } else {
313
- resolved.status = 'cancelled';
314
- }
315
-
316
- // Setup preliminary variables
317
- const order = resource?.purchase_units?.[0]?.payments?.captures?.[0];
318
- const subscription = resource?.billing_info?.last_payment;
319
- const isOrder = !!order;
320
-
321
- // Set resource ID
322
- resolved.resource.id = resource.id;
323
-
324
- // Set start
325
- resolved.start.timestamp = moment(
326
- (
327
- isOrder
328
- // Order
329
- ? (resource?.create_time ?? 0)
330
-
331
- // Subscription
332
- : (resource?.start_time ?? 0)
333
- )
334
- )
335
-
336
- // Set expiration
337
- resolved.expires.timestamp = moment(
338
- (
339
- isOrder
340
- // Order
341
- ? (resource?.create_time ?? 0)
342
-
343
- // Subscription
344
- : (resource?.billing_info?.last_payment?.time ?? 0)
345
- )
346
- )
347
-
348
- // Set cancelled
349
- if (resolved.status === 'cancelled') {
350
- resolved.cancelled.timestamp = moment(
351
- (
352
- isOrder
353
- // Order
354
- ? (resource?.create_time ?? 0)
355
-
356
- // Subscription
357
- : (resource?.status_update_time ?? 0)
358
- )
359
- )
360
- }
361
-
362
- // Set last payment
363
- if (order) {
364
- resolved.lastPayment.amount = parseFloat(
365
- order?.amount?.value ?? '0.00'
366
- );
367
- resolved.lastPayment.date.timestamp = moment(
368
- order.create_time || 0
369
- );
370
- } else if (subscription) {
371
- resolved.lastPayment.amount = parseFloat(subscription.amount.value);
372
- resolved.lastPayment.date.timestamp = moment(subscription.time);
373
- }
374
-
375
- // Get trial
376
- const trialTenure = (resource?.plan?.billing_cycles ?? []).find((cycle) => cycle.tenure_type === 'TRIAL');
377
- const regularTenure = (resource?.plan?.billing_cycles ?? []).find((cycle) => cycle.tenure_type === 'REGULAR');
378
- const trialClaimed = !!trialTenure && parseFloat(trialTenure?.pricing_scheme?.fixed_price?.value ?? '0.00') === 0;
379
-
380
- // Resolve trial
381
- /*
382
- Special condition for PayPal
383
- Because you cannot remove trial on a sub-level, you have to charge a prorated amount for the "trial".
384
- Even if charged, it is still considered a trial period by paypal.
385
- Thus, we must remove the trial indicator if the user has been charged.
386
- */
387
- if (
388
- resolved.status === 'active'
389
- && (trialTenure && regularTenure && regularTenure.total_cycles === 0)
390
- && resolved.lastPayment.amount === 0
391
- ) {
392
- resolved.trial.active = true;
393
-
394
- // Set expiration
395
- resolved.expires.timestamp = moment(
396
- resource?.billing_info?.next_billing_time ?? 0
397
- )
398
-
399
- /*
400
- Special condition for PayPal #2
401
- I want to put the subscription in a suspended state if it's even one day past due
402
- */
403
- const trialLength = trialTenure?.frequency?.interval_count ?? 0;
404
- const daysSinceStart = Math.abs(moment(options.today).diff(moment(resolved.start.timestamp), 'days'));
405
- if (daysSinceStart > trialLength) {
406
- resolved.status = 'suspended';
407
- resolved.trial.active = false;
408
- }
409
- // console.log('----resolved.resource.id', resolved.resource.id);
410
- // console.log('----resolved.start.timestamp', resolved.start.timestamp);
411
- // console.log('----options.today', options.today);
412
- // console.log('======daysSinceStart', daysSinceStart);
413
- // console.log('======trialLength', trialLength);
414
- }
415
- resolved.trial.claimed = trialClaimed;
416
-
417
- // Resolve frequency
418
- const unit = regularTenure?.frequency?.interval_unit;
419
- if (unit === 'YEAR') {
420
- resolved.frequency = 'annually';
421
- } else if (unit === 'MONTH') {
422
- resolved.frequency = 'monthly';
423
- } else if (unit === 'WEEK') {
424
- resolved.frequency = 'weekly';
425
- } else if (unit === 'DAY') {
426
- resolved.frequency = 'daily';
427
- }
428
-
429
- // Set completed
430
- if (resource.plan) {
431
- resolved.payment.completed = !['APPROVAL_PENDING', 'APPROVED'].includes(resource.status);
432
- } else {
433
- resolved.payment.completed = !['CREATED', 'SAVED', 'APPROVED', 'VOIDED', 'PAYER_ACTION_REQUIRED'].includes(resource.status);
434
- }
435
-
436
- // Check if refunded
437
- if (resource.plan) {
438
- const transactions = arrayify(resource?.transactions ?? []);
439
-
440
- resolved.payment.refunded = transactions.some(t => t.status === 'REFUNDED');
441
- // ALSO PARTIALLY_REFUNDED?
442
- } else {
443
- // resolved.payment.refunded = false; // @@@ TODO: check if this is correct
444
- }
445
-
446
- return resolved;
447
- }
448
-
449
- SubscriptionResolver.prototype.resolve_chargebee = function (profile, resource, resolved, options) {
450
- const self = this;
451
-
452
- // Set status
453
- // subscription: https://apidocs.chargebee.com/docs/api/subscriptions?prod_cat_ver=2#subscription_status
454
- // future The subscription is scheduled to start at a future date.
455
- // in_trial The subscription is in trial.
456
- // active The subscription is active and will be charged for automatically based on the items in it.
457
- // non_renewing The subscription will be canceled at the end of the current term.
458
- // paused The subscription is paused. The subscription will not renew while in this state.
459
- // cancelled The subscription has been canceled and is no longer in service.
460
-
461
- // order: https://apidocs.chargebee.com/docs/api/invoices?prod_cat_ver=2#invoice_status
462
- // paid: Indicates a paid invoice.
463
- // posted: Indicates the payment is not yet collected and will be in this state till the due date to indicate the due period.
464
- // payment_due: Indicates the payment is not yet collected and is being retried as per retry settings.
465
- // not_paid: Indicates the payment is not made and all attempts to collect is failed.
466
- // voided: Indicates a voided invoice.
467
- // pending: The invoice is yet to be closed (sent for payment collection). An invoice is generated with this status when it has line items that belong to items that are metered or when the subscription.create_pending_invoicesattribute is set to true.
468
-
469
- if (['in_trial', 'active'].includes(resource.status)) {
470
- resolved.status = 'active';
471
-
472
- // If there's a due invoice, it's suspended
473
- if (resource.total_dues > 0) {
474
- resolved.status = 'suspended';
475
- }
476
- } else if (['paused'].includes(resource.status)) {
477
- resolved.status = 'suspended';
478
- } else {
479
- resolved.status = 'cancelled';
480
- }
481
-
482
- // Setup preliminary variables
483
- const isOrder = profile.type === 'order';
484
-
485
- // Set resource ID
486
- resolved.resource.id = resource.id;
487
-
488
- // Set start
489
- resolved.start.timestamp = moment(
490
- (
491
- isOrder
492
- // Order
493
- ? (resource?.date ?? 0)
494
-
495
- // Subscription
496
- : (resource?.created_at ?? 0)
497
- ) * 1000
498
- )
499
-
500
- // Set expiration
501
- resolved.expires.timestamp = moment(
502
- (
503
- isOrder
504
- // Order
505
- ? (resource?.date ?? 0)
506
-
507
- // Subscription
508
- : (resource?.current_term_start ?? 0)
509
- ) * 1000
510
- )
511
- // console.log('---resolved.expires 1', resolved.expires);
512
- // if (resource.total_dues > 0) {
513
- // resolved.expires.timestamp = moment(0);
514
- // } else {
515
- // resolved.expires.timestamp = moment(
516
- // (
517
- // get(resource, 'current_term_start', 0)
518
- // ) * 1000
519
- // )
520
- // }
521
-
522
- // Set cancelled
523
- if (resolved.status === 'cancelled') {
524
- resolved.cancelled.timestamp = moment(
525
- (
526
- isOrder
527
- // Order
528
- ? (resource?.date ?? 0)
529
-
530
- // Subscription
531
- : (resource?.cancelled_at ?? 0)
532
- ) * 1000
533
- )
534
- }
535
-
536
- // Set last payment
537
- if (
538
- // Order
539
- resource.amount_due > 0
540
-
541
- // Subscription
542
- || resource.total_dues > 0
543
- ) {
544
- resolved.lastPayment.amount = 0;
545
- resolved.lastPayment.date.timestamp = moment(
546
- (
547
- isOrder
548
- // Order
549
- ? (resource.date || 0)
550
-
551
- // Subscription
552
- : (resource.due_since || 0)
553
- ) * 1000
554
- );
555
- } else {
556
- resolved.lastPayment.amount = (
557
- (
558
- isOrder
559
- // Order
560
- ? (resource.amount_paid)
561
-
562
- // Subscription
563
- : (resource.plan_amount)
564
- ) / 100
565
- )
566
- resolved.lastPayment.date.timestamp = moment(
567
- (
568
- isOrder
569
- // Order
570
- ? (resource.date || 0)
571
-
572
- // Subscription
573
- : (resource.current_term_start || 0)
574
- ) * 1000
575
- );
576
- }
577
-
578
- // Get trial
579
- if (resource.status === 'in_trial') {
580
- resolved.trial.active = true;
581
-
582
- // Set expiration
583
- resolved.expires.timestamp = moment(
584
- (
585
- resource?.trial_end ?? 0
586
- ) * 1000
587
- )
588
- }
589
-
590
- // Resolve frequency
591
- const unit = resource?.billing_period_unit;
592
- if (unit === 'year') {
593
- resolved.frequency = 'annually';
594
- } else if (unit === 'month') {
595
- resolved.frequency = 'monthly';
596
- } else if (unit === 'week') {
597
- resolved.frequency = 'weekly';
598
- } else if (unit === 'day') {
599
- resolved.frequency = 'daily';
600
- }
601
-
602
- // Set completed
603
- if (isOrder) {
604
- resolved.payment.completed = !['posted', 'payment_due', 'not_paid', 'voided', 'pending'].includes(resource.status);
605
- } else {
606
- resolved.payment.completed = !['future'].includes(resource.status);
607
- }
608
-
609
- // Check if refunded
610
- if (isOrder) {
611
- resolved.payment.refunded = false; // @@@ TODO: check if this is correct
612
- } else {
613
- const invoices = resource?.invoices ?? [];
614
-
615
- resolved.payment.refunded = invoices.some(invoice => {
616
- const creditNotes = invoice?.invoice?.issued_credit_notes ?? [];
617
- return creditNotes.some(creditNote => {
618
- return creditNote.cn_status === 'refunded'
619
- })
620
- })
621
- }
622
-
623
- // Special chargebee reset lastPayment
624
- // If trial is active OR if it was cancelled after the trial has ended
625
- const trialStart = (resource?.trial_start ?? 0) * 1000;
626
- const trialEnd = (resource?.trial_end ?? 0) * 1000;
627
- const cancelledAt = (resource?.cancelled_at ?? 0) * 1000;
628
- const trialDaysDifference = Math.abs(moment(trialEnd).diff(moment(trialStart), 'days'));
629
- const trialClaimed = !!trialStart && !!trialEnd && trialDaysDifference > 1;
630
- if (
631
- resolved.trial.active
632
- || (trialEnd > 0 && cancelledAt > 0 && cancelledAt === trialEnd)
633
- ) {
634
- resolved.lastPayment.amount = 0;
635
- resolved.lastPayment.date.timestamp = moment(0);
636
- }
637
- resolved.trial.claimed = trialClaimed;
638
-
639
- return resolved;
640
- }
641
-
642
- SubscriptionResolver.prototype.resolve_stripe = function (profile, resource, resolved, options) {
643
- const self = this;
644
-
645
- // Subscription: https://stripe.com/docs/api/subscriptions/object#subscription_object-status
646
- // incomplete
647
- // incomplete_expired
648
- // trialing
649
- // active
650
- // past_due
651
- // canceled
652
- // unpaid
653
-
654
- // Charge: https://stripe.com/docs/api/payment_intents/object#payment_intent_object-status
655
- // requires_payment_method
656
- // requires_confirmation
657
- // requires_action
658
- // processing
659
- // requires_capture
660
- // canceled
661
- // succeeded
662
- // Set status
663
- if (['trialing', 'active'].includes(resource.status)) {
664
- resolved.status = 'active';
665
- } else if (['past_due', 'unpaid'].includes(resource.status)) {
666
- resolved.status = 'suspended';
667
- } else {
668
- resolved.status = 'cancelled';
669
- }
670
-
671
- // Setup preliminary variables
672
- const order = resource.object === 'charge' ? resource : null;
673
- const subscription = resource?.latest_invoice;
674
- const isOrder = !!order;
675
-
676
- // Set resource ID
677
- resolved.resource.id = resource.id;
678
-
679
- // Set start
680
- resolved.start.timestamp = moment(
681
- (
682
- isOrder
683
- // Order
684
- ? (resource?.created ?? 0)
685
-
686
- // Subscription
687
- : (resource?.start_date ?? 0)
688
- ) * 1000
689
- );
690
-
691
- // Set expiration
692
- resolved.expires.timestamp = moment(
693
- (
694
- isOrder
695
- // Order
696
- ? (resource?.created ?? 0)
697
-
698
- // Subscription
699
- : (resource?.current_period_start ?? 0)
700
- ) * 1000
701
- );
702
-
703
- // Set cancelled
704
- if (resolved.status === 'cancelled') {
705
- resolved.cancelled.timestamp = moment(
706
- (
707
- isOrder
708
- // Order
709
- ? (resource?.created ?? 0)
710
-
711
- // Subscription
712
- : (resource?.canceled_at ?? 0)
713
- ) * 1000
714
- )
715
- }
716
-
717
- // Set last payment
718
- // TODO: check if suspended payments are handled correctly when using resource.latest_invoice.amount_paid
719
- if (order) {
720
- resolved.lastPayment.amount = order.amount_captured / 100;
721
- resolved.lastPayment.date.timestamp = moment(
722
- (order.created || 0) * 1000
723
- );
724
- } else if (subscription) {
725
- resolved.lastPayment.amount = subscription.amount_paid / 100;
726
- resolved.lastPayment.date.timestamp = moment(
727
- (subscription.created || 0) * 1000
728
- );
729
- }
730
-
731
- // Get trial
732
- const trialStart = (resource?.trial_start ?? 0) * 1000;
733
- const trialEnd = (resource?.trial_end ?? 0) * 1000;
734
- const trialDaysDifference = Math.abs(moment(trialEnd).diff(moment(trialStart), 'days'));
735
- const trialClaimed = !!trialStart && !!trialEnd && trialDaysDifference > 1;
736
- if (resource.status === 'trialing') {
737
- resolved.trial.active = true;
738
-
739
- // Set expiration
740
- resolved.expires.timestamp = moment(
741
- (
742
- trialEnd
743
- )
744
- )
745
- }
746
- resolved.trial.claimed = trialClaimed;
747
-
748
- // Resolve frequency
749
- const unit = resource?.plan?.interval;
750
- if (unit === 'year') {
751
- resolved.frequency = 'annually';
752
- } else if (unit === 'month') {
753
- resolved.frequency = 'monthly';
754
- } else if (unit === 'week') {
755
- resolved.frequency = 'weekly';
756
- } else if (unit === 'day') {
757
- resolved.frequency = 'daily';
758
- }
759
-
760
- // Set completed
761
- if (resource.object === 'charge') {
762
- resolved.payment.completed = !['requires_payment_method', 'requires_confirmation', 'requires_action', 'processing', 'requires_capture', 'canceled'].includes(resource.status);
763
- } else {
764
- resolved.payment.completed = !['incomplete', 'incomplete_expired'].includes(resource.status);
765
- }
766
-
767
- // Check if refunded
768
- if (resource.object === 'charge') {
769
- resolved.payment.refunded = resource.refunded;
770
- } else {
771
- resolved.payment.refunded = resource?.latest_invoice?.charge?.refunded ?? false;
772
- }
773
-
774
- return resolved;
775
- }
776
-
777
- SubscriptionResolver.prototype.resolve_coinbase = function (profile, resource, resolved, options) {
778
- const self = this;
779
-
780
- // Setup preliminary variables
781
- const isOrder = profile.type === 'order';
782
-
783
- // Set status
784
- resolved.status = 'cancelled';
785
-
786
- // Set resource ID
787
- resolved.resource.id = resource.id;
788
-
789
- // Set start
790
- resolved.start.timestamp = moment(
791
- resource?.created_at ?? 0
792
- );
793
-
794
- // Set expiration
795
- resolved.expires.timestamp = moment(
796
- resource?.created_at ?? 0
797
- );
798
-
799
- // Set cancelled
800
- resolved.cancelled.timestamp = moment(
801
- resource?.created_at ?? 0
802
- )
803
-
804
- // Retrieve last payment
805
- // Coinbase at some point changed status from being UPPER to lower case!!! FUCK YOU!
806
- const lastPayment = resource.payments.find(p => p.status.toLowerCase() === 'confirmed');
807
-
808
- // Set last payment
809
- if (lastPayment) {
810
- resolved.lastPayment.amount = parseFloat(lastPayment.value.local.amount);
811
- resolved.lastPayment.date.timestamp = moment(lastPayment.detected_at);
812
- }
813
-
814
- // Get trial
815
- if (true) {
816
- resolved.trial.active = false;
817
- }
818
- resolved.trial.claimed = false;
819
-
820
- // Resolve frequency
821
- const unit = profile.details.planFrequency;
822
- if (unit) {
823
- resolved.frequency = unit;
824
- } else {
825
- resolved.frequency = 'single';
826
- }
827
-
828
- // Set completed
829
- if (true) {
830
- resolved.payment.completed = !!lastPayment;
831
- }
832
-
833
- // Check if refunded
834
- if (true) {
835
- resolved.payment.refunded = false;
836
- }
837
-
838
- return resolved;
839
- }
840
-
841
- module.exports = SubscriptionResolver;