backend-manager 3.0.13 → 3.0.15

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