payment-kit 1.13.18 → 1.13.20

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 (114) hide show
  1. package/README.md +14 -0
  2. package/api/src/index.ts +17 -6
  3. package/api/src/integrations/stripe/handlers/index.ts +53 -0
  4. package/api/src/integrations/stripe/handlers/invoice.ts +252 -0
  5. package/api/src/integrations/stripe/handlers/payment-intent.ts +172 -0
  6. package/api/src/integrations/stripe/handlers/setup-intent.ts +42 -0
  7. package/api/src/integrations/stripe/handlers/subscription.ts +61 -0
  8. package/api/src/integrations/stripe/resource.ts +317 -0
  9. package/api/src/integrations/stripe/setup.ts +50 -0
  10. package/api/src/jobs/invoice.ts +11 -0
  11. package/api/src/jobs/payment.ts +15 -7
  12. package/api/src/jobs/subscription.ts +18 -2
  13. package/api/src/libs/session.ts +104 -8
  14. package/api/src/libs/util.ts +47 -1
  15. package/api/src/routes/checkout-sessions.ts +134 -27
  16. package/api/src/routes/connect/collect.ts +12 -4
  17. package/api/src/routes/connect/pay.ts +30 -20
  18. package/api/src/routes/connect/setup.ts +12 -4
  19. package/api/src/routes/connect/shared.ts +28 -4
  20. package/api/src/routes/connect/subscribe.ts +12 -5
  21. package/api/src/routes/customers.ts +37 -5
  22. package/api/src/routes/events.ts +9 -6
  23. package/api/src/routes/index.ts +2 -0
  24. package/api/src/routes/integrations/stripe.ts +64 -0
  25. package/api/src/routes/invoices.ts +19 -9
  26. package/api/src/routes/payment-intents.ts +19 -9
  27. package/api/src/routes/payment-links.ts +57 -15
  28. package/api/src/routes/payment-methods.ts +98 -1
  29. package/api/src/routes/prices.ts +71 -14
  30. package/api/src/routes/products.ts +79 -22
  31. package/api/src/routes/settings.ts +10 -11
  32. package/api/src/routes/subscription-items.ts +5 -5
  33. package/api/src/routes/subscriptions.ts +61 -10
  34. package/api/src/routes/usage-records.ts +52 -18
  35. package/api/src/routes/webhook-attempts.ts +5 -5
  36. package/api/src/routes/webhook-endpoints.ts +5 -5
  37. package/api/src/store/migrations/20230905-genesis.ts +2 -2
  38. package/api/src/store/migrations/20230911-seeding.ts +4 -3
  39. package/api/src/store/models/checkout-session.ts +15 -7
  40. package/api/src/store/models/index.ts +31 -7
  41. package/api/src/store/models/invoice.ts +1 -1
  42. package/api/src/store/models/payment-intent.ts +2 -5
  43. package/api/src/store/models/payment-link.ts +1 -1
  44. package/api/src/store/models/payment-method.ts +54 -33
  45. package/api/src/store/models/price.ts +52 -17
  46. package/api/src/store/models/product.ts +0 -3
  47. package/api/src/store/models/subscription.ts +3 -5
  48. package/api/src/store/models/types.ts +56 -2
  49. package/api/third.d.ts +2 -0
  50. package/blocklet.yml +1 -1
  51. package/package.json +13 -6
  52. package/public/currencies/dai.png +0 -0
  53. package/public/currencies/dollar.png +0 -0
  54. package/public/currencies/usdc.png +0 -0
  55. package/public/currencies/usdt.png +0 -0
  56. package/public/methods/arcblock.png +0 -0
  57. package/public/methods/binance.png +0 -0
  58. package/public/methods/coinbase.png +0 -0
  59. package/public/methods/ethereum.jpg +0 -0
  60. package/public/methods/stripe.png +0 -0
  61. package/src/components/checkout/form/address.tsx +84 -10
  62. package/src/components/checkout/form/index.tsx +169 -83
  63. package/src/components/checkout/form/phone.tsx +102 -0
  64. package/src/components/checkout/form/stripe.tsx +195 -0
  65. package/src/components/checkout/pay.tsx +115 -34
  66. package/src/components/checkout/product-item.tsx +4 -3
  67. package/src/components/checkout/summary.tsx +5 -4
  68. package/src/components/customer/edit.tsx +73 -0
  69. package/src/components/customer/form.tsx +104 -0
  70. package/src/components/drawer-form.tsx +4 -4
  71. package/src/components/input.tsx +22 -4
  72. package/src/components/invoice/table.tsx +8 -3
  73. package/src/components/metadata/editor.tsx +2 -3
  74. package/src/components/payment-link/after-pay.tsx +1 -1
  75. package/src/components/payment-link/before-pay.tsx +11 -6
  76. package/src/components/payment-link/chrome.tsx +13 -0
  77. package/src/components/payment-link/preview.tsx +31 -0
  78. package/src/components/payment-link/product-select.tsx +8 -3
  79. package/src/components/payment-link/rename.tsx +2 -2
  80. package/src/components/payment-method/arcblock.tsx +53 -0
  81. package/src/components/payment-method/bitcoin.tsx +53 -0
  82. package/src/components/payment-method/ethereum.tsx +53 -0
  83. package/src/components/payment-method/form.tsx +54 -0
  84. package/src/components/payment-method/stripe.tsx +45 -0
  85. package/src/components/portal/invoice/list.tsx +1 -1
  86. package/src/components/portal/subscription/list.tsx +1 -1
  87. package/src/components/price/currency-select.tsx +53 -0
  88. package/src/components/price/form.tsx +118 -24
  89. package/src/components/product/add-price.tsx +1 -1
  90. package/src/components/product/edit-price.tsx +6 -2
  91. package/src/components/subscription/items/index.tsx +7 -6
  92. package/src/components/subscription/items/usage-records.tsx +98 -0
  93. package/src/components/subscription/list.tsx +3 -2
  94. package/src/components/subscription/status.tsx +68 -0
  95. package/src/contexts/settings.tsx +2 -2
  96. package/src/env.d.ts +2 -0
  97. package/src/libs/util.ts +116 -21
  98. package/src/locales/en.tsx +72 -3
  99. package/src/pages/admin/billing/invoices/detail.tsx +5 -2
  100. package/src/pages/admin/billing/subscriptions/detail.tsx +6 -6
  101. package/src/pages/admin/customers/customers/detail.tsx +43 -9
  102. package/src/pages/admin/payments/intents/detail.tsx +8 -3
  103. package/src/pages/admin/payments/links/create.tsx +23 -3
  104. package/src/pages/admin/payments/links/detail.tsx +13 -26
  105. package/src/pages/admin/products/prices/detail.tsx +55 -11
  106. package/src/pages/admin/products/prices/list.tsx +7 -1
  107. package/src/pages/admin/products/products/create.tsx +1 -1
  108. package/src/pages/admin/products/products/detail.tsx +14 -7
  109. package/src/pages/admin/settings/index.tsx +16 -6
  110. package/src/pages/admin/settings/payment-methods/create.tsx +81 -0
  111. package/src/pages/admin/settings/{payment-methods.tsx → payment-methods/index.tsx} +9 -6
  112. package/src/pages/checkout/pay.tsx +3 -1
  113. package/src/pages/customer/index.tsx +36 -1
  114. package/public/.gitkeep +0 -0
@@ -0,0 +1,317 @@
1
+ import env from '@blocklet/sdk/lib/env';
2
+ import merge from 'lodash/merge';
3
+
4
+ import logger from '../../libs/logger';
5
+ import { getPriceUintAmountByCurrency } from '../../libs/session';
6
+ import {
7
+ Customer,
8
+ PaymentCurrency,
9
+ PaymentIntent,
10
+ PaymentMethod,
11
+ Price,
12
+ Product,
13
+ Subscription,
14
+ SubscriptionItem,
15
+ TLineItemExpanded,
16
+ } from '../../store/models';
17
+
18
+ export async function ensureStripeProduct(internal: Product, method: PaymentMethod) {
19
+ const client = method.getStripe();
20
+ const result = await client.products.search({ query: `metadata['id']:'${internal.id}'` });
21
+ if (result.data.length > 0) {
22
+ return result.data[0] as any;
23
+ }
24
+
25
+ const attrs: any = {
26
+ name: internal.name,
27
+ description: internal.description,
28
+ metadata: {
29
+ appPid: env.appPid,
30
+ id: internal.id,
31
+ },
32
+ };
33
+ if (internal.unit_label) {
34
+ attrs.unit_label = internal.unit_label;
35
+ }
36
+ if (internal.statement_descriptor) {
37
+ attrs.statement_descriptor = internal.statement_descriptor;
38
+ }
39
+
40
+ const product = await client.products.create(attrs);
41
+ await internal.update({ metadata: merge(internal.metadata || {}, { stripe_id: product.id }) });
42
+ logger.info('product created on stripe', { local: internal.id, remote: product.id });
43
+
44
+ return product;
45
+ }
46
+
47
+ export async function ensureStripePrice(internal: Price, method: PaymentMethod, currency: PaymentCurrency) {
48
+ const client = method.getStripe();
49
+ const result = await client.prices.search({ query: `metadata['id']:'${internal.id}'` });
50
+ if (result.data.length > 0) {
51
+ return result.data[0] as any;
52
+ }
53
+
54
+ // create stripe product
55
+ const local = await Product.findByPk(internal.product_id);
56
+ const product = await ensureStripeProduct(local as Product, method);
57
+
58
+ const attrs: any = {
59
+ product: product.id,
60
+ currency: currency.symbol.toLowerCase(),
61
+ nickname: internal.nickname as string,
62
+ lookup_key: internal.lookup_key as string,
63
+ billing_scheme: internal.billing_scheme as any,
64
+ metadata: {
65
+ appPid: env.appPid,
66
+ id: internal.id,
67
+ },
68
+ };
69
+
70
+ if (internal.type === 'recurring') {
71
+ attrs.recurring = {
72
+ interval: internal.recurring?.interval,
73
+ interval_count: internal.recurring?.interval_count,
74
+ };
75
+ if (internal.recurring?.usage_type) {
76
+ attrs.recurring.usage_type = internal.recurring.usage_type;
77
+ }
78
+ if (internal.recurring?.usage_type === 'metered') {
79
+ attrs.recurring.aggregate_usage = internal.recurring.aggregate_usage;
80
+ }
81
+ }
82
+ if (internal.tiers_mode) {
83
+ attrs.tiers_mode = internal.tiers_mode;
84
+ }
85
+ if (internal.tiers_mode === 'graduated' || internal.tiers_mode === 'volume') {
86
+ attrs.tiers = internal.tiers;
87
+ } else if (internal.transform_quantity) {
88
+ attrs.transform_quantity = internal.transform_quantity;
89
+ }
90
+ if (internal.custom_unit_amount) {
91
+ attrs.custom_unit_amount = internal.custom_unit_amount;
92
+ } else {
93
+ attrs.unit_amount = Number(getPriceUintAmountByCurrency(internal, currency));
94
+ }
95
+
96
+ // create stripe price
97
+ const price = await client.prices.create(attrs);
98
+ await internal.update({ metadata: merge(internal.metadata || {}, { stripe_id: price.id }) });
99
+ logger.info('price created on stripe', { local: internal.id, remote: price.id });
100
+
101
+ // update product default price
102
+ if (!product.default_price) {
103
+ await client.products.update(product.id, { default_price: price.id });
104
+ }
105
+
106
+ return price;
107
+ }
108
+
109
+ export async function ensureStripeCustomer(internal: Customer, method: PaymentMethod) {
110
+ const client = method.getStripe();
111
+ const result = await client.customers.search({ query: `metadata['did']:'${internal.did}'` });
112
+ if (result.data.length > 0) {
113
+ return result.data[0] as any;
114
+ }
115
+
116
+ const customer = await client.customers.create({
117
+ name: internal.name,
118
+ email: internal.email,
119
+ phone: internal.phone,
120
+ invoice_prefix: internal.invoice_prefix,
121
+ metadata: {
122
+ appPid: env.appPid,
123
+ id: internal.id,
124
+ did: internal.did,
125
+ },
126
+ });
127
+
128
+ await internal.update({ metadata: merge(internal.metadata || {}, { stripe_id: customer.id }) });
129
+ logger.info('customer created on stripe', { local: internal.id, remote: customer.id });
130
+
131
+ return customer;
132
+ }
133
+
134
+ export async function ensureStripePaymentCustomer(internal: any, method: PaymentMethod) {
135
+ const client = method.getStripe();
136
+ let customer = null;
137
+ if (internal.payment_details?.stripe?.customer_id) {
138
+ customer = await client.customers.retrieve(internal.payment_details.stripe.customer_id);
139
+ } else {
140
+ const local = await Customer.findByPk(internal.customer_id);
141
+ customer = await ensureStripeCustomer(local as Customer, method);
142
+ }
143
+
144
+ return customer;
145
+ }
146
+
147
+ export async function ensureStripePaymentIntent(
148
+ internal: PaymentIntent,
149
+ method: PaymentMethod,
150
+ currency: PaymentCurrency
151
+ ) {
152
+ const client = method.getStripe();
153
+
154
+ let stripeIntent = null;
155
+ if (internal.payment_details?.stripe?.payment_intent_id) {
156
+ stripeIntent = await client.paymentIntents.retrieve(internal.payment_details.stripe.payment_intent_id);
157
+ // FIXME: update?
158
+ } else {
159
+ const customer = await ensureStripePaymentCustomer(internal, method);
160
+ stripeIntent = await client.paymentIntents.create({
161
+ amount: Number(internal.amount),
162
+ currency: currency.symbol.toLowerCase(),
163
+ customer: customer.id,
164
+ automatic_payment_methods: {
165
+ enabled: true,
166
+ allow_redirects: 'never',
167
+ },
168
+ statement_descriptor: internal.statement_descriptor,
169
+ metadata: {
170
+ appPid: env.appPid,
171
+ id: internal.id,
172
+ },
173
+ });
174
+ logger.info('stripe payment intent created', { local: internal.id, remote: stripeIntent.id });
175
+
176
+ await internal.update({
177
+ payment_details: {
178
+ stripe: {
179
+ customer_id: customer.id,
180
+ payment_intent_id: stripeIntent.id,
181
+ },
182
+ },
183
+ });
184
+ }
185
+
186
+ return stripeIntent;
187
+ }
188
+
189
+ export async function ensureStripeSubscription(
190
+ internal: Subscription,
191
+ method: PaymentMethod,
192
+ currency: PaymentCurrency,
193
+ items: TLineItemExpanded[],
194
+ trialInDays: number = 0
195
+ ) {
196
+ const client = method.getStripe();
197
+
198
+ let stripeSubscription: any = null;
199
+ if (internal.payment_details?.stripe?.subscription_id) {
200
+ stripeSubscription = await client.subscriptions.retrieve(internal.payment_details.stripe.subscription_id, {
201
+ expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
202
+ });
203
+ // FIXME: update?
204
+ } else {
205
+ const customer = await ensureStripePaymentCustomer(internal, method);
206
+ const prices = await Promise.all(
207
+ items.map(async (x: any) => {
208
+ x.stripePrice = await ensureStripePrice(x.price as any, method, currency);
209
+ return x;
210
+ })
211
+ );
212
+
213
+ const recurringItems = prices
214
+ .filter((x) => x.price.type === 'recurring')
215
+ .map((x) => {
216
+ if (x.price.recurring?.usage_type === 'metered') {
217
+ return { price: x.stripePrice.id };
218
+ }
219
+ return { price: x.stripePrice.id, quantity: x.quantity };
220
+ });
221
+
222
+ const onetimeItems = prices
223
+ .filter((x) => x.price.type !== 'recurring')
224
+ .map((x) => ({ price: x.stripePrice.id, quantity: x.quantity }));
225
+
226
+ stripeSubscription = await client.subscriptions.create({
227
+ currency: currency.symbol.toLowerCase(),
228
+ customer: customer.id,
229
+ items: recurringItems,
230
+ add_invoice_items: onetimeItems,
231
+ trial_period_days: trialInDays,
232
+ payment_behavior: 'default_incomplete',
233
+ payment_settings: { save_default_payment_method: 'on_subscription' },
234
+ metadata: {
235
+ appPid: env.appPid,
236
+ id: internal.id,
237
+ },
238
+ expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
239
+ });
240
+ logger.info('stripe subscription created', { local: internal.id, remote: stripeSubscription.id });
241
+
242
+ await Promise.all(
243
+ stripeSubscription.items.data.map(async (x: any) => {
244
+ const item = prices.find((y) => y.stripePrice.id === x.price.id);
245
+ let exist = await SubscriptionItem.findOne({
246
+ where: { price_id: item.price_id, subscription_id: internal.id },
247
+ });
248
+ if (exist) {
249
+ await exist.update({ metadata: { stripe_id: x.id, stripe_subscription_id: stripeSubscription.id } });
250
+ await client.subscriptionItems.update(x.id, { metadata: { appPid: env.appPid, id: exist.id } });
251
+ logger.info('stripe subscription items related', { local: exist.id, remote: x.id });
252
+ } else {
253
+ exist = await SubscriptionItem.create({
254
+ livemode: stripeSubscription.livemode,
255
+ price_id: item.price.id,
256
+ quantity: x.quantity,
257
+ subscription_id: internal.id,
258
+ billing_thresholds: x.billing_threshold,
259
+ metadata: {
260
+ stripe_id: x.id,
261
+ stripe_subscription_id: stripeSubscription.id,
262
+ },
263
+ });
264
+ await client.subscriptionItems.update(x.id, { metadata: { appPid: env.appPid, id: exist.id } });
265
+ logger.info('stripe subscription items mirrored', { local: exist.id, remote: x.id });
266
+ }
267
+ })
268
+ );
269
+
270
+ await internal.update({
271
+ payment_details: {
272
+ stripe: {
273
+ customer_id: customer.id,
274
+ subscription_id: stripeSubscription.id,
275
+ setup_intent_id: stripeSubscription.pending_setup_intent?.id,
276
+ },
277
+ },
278
+ });
279
+ }
280
+
281
+ return stripeSubscription;
282
+ }
283
+
284
+ export async function forwardUsageRecordToStripe(
285
+ subscriptionItem: SubscriptionItem,
286
+ updates: { quantity: number; timestamp: number; action: string }
287
+ ) {
288
+ if (!subscriptionItem.metadata?.stripe_id) {
289
+ logger.info('skip usage record forwarded to stripe because no relation', {
290
+ subscriptionItemId: subscriptionItem.id,
291
+ updates,
292
+ });
293
+ return null;
294
+ }
295
+
296
+ const subscription = await Subscription.findByPk(subscriptionItem.subscription_id);
297
+ if (!subscription) {
298
+ return null;
299
+ }
300
+
301
+ const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
302
+ if (!method) {
303
+ return null;
304
+ }
305
+
306
+ const client = method.getStripe();
307
+ const result = await client.subscriptionItems.createUsageRecord(subscriptionItem.metadata.stripe_id, {
308
+ quantity: updates.quantity,
309
+ timestamp: updates.timestamp,
310
+ // @ts-ignore
311
+ action: subscription.billing_thresholds?.amount_gte ? 'increment' : updates.action,
312
+ });
313
+
314
+ logger.info('usage record forwarded to stripe', { subscriptionItemId: subscriptionItem.id, result });
315
+
316
+ return result;
317
+ }
@@ -0,0 +1,50 @@
1
+ /* eslint-disable no-await-in-loop */
2
+
3
+ import env from '@blocklet/sdk/lib/env';
4
+
5
+ import logger from '../../libs/logger';
6
+ import { STRIPE_API_VERSION, STRIPE_ENDPOINT, STRIPE_EVENTS } from '../../libs/util';
7
+ import { PaymentMethod } from '../../store/models';
8
+
9
+ // register stripe webhooks on start/create if there are any stripe payment methods
10
+ export async function ensureWebhookRegistered() {
11
+ const stripeMethods = await PaymentMethod.findAll({ where: { type: 'stripe' } });
12
+ for (const method of stripeMethods) {
13
+ const settings = PaymentMethod.decryptSettings(method.settings);
14
+ if (!settings.stripe) {
15
+ // eslint-disable-next-line no-continue
16
+ continue;
17
+ }
18
+
19
+ const stripe = method.getStripe();
20
+ const { data } = await stripe.webhookEndpoints.list({ limit: 100 });
21
+
22
+ const exist = data.find((webhook) => webhook.metadata?.appPid === env.appPid);
23
+ if (exist) {
24
+ await stripe.webhookEndpoints.update(exist.id, {
25
+ url: STRIPE_ENDPOINT,
26
+ description: env.appName,
27
+ enabled_events: STRIPE_EVENTS,
28
+ disabled: false,
29
+ });
30
+ logger.info('stripe webhook updated');
31
+ } else {
32
+ const result = await stripe.webhookEndpoints.create({
33
+ url: STRIPE_ENDPOINT,
34
+ description: env.appName,
35
+ enabled_events: STRIPE_EVENTS,
36
+ api_version: STRIPE_API_VERSION,
37
+ metadata: {
38
+ appPid: env.appPid,
39
+ },
40
+ });
41
+ logger.info('stripe webhook created');
42
+
43
+ if (result.secret) {
44
+ settings.stripe.webhook_signing_secret = result.secret;
45
+ await method.update({ settings: PaymentMethod.encryptSettings(settings) });
46
+ logger.info('stripe webhook signing secret updated');
47
+ }
48
+ }
49
+ }
50
+ }
@@ -3,6 +3,7 @@ import { Op } from 'sequelize';
3
3
  import dayjs from '../libs/dayjs';
4
4
  import logger from '../libs/logger';
5
5
  import createQueue from '../libs/queue';
6
+ import { PaymentMethod } from '../store/models';
6
7
  import { CheckoutSession } from '../store/models/checkout-session';
7
8
  import { Invoice } from '../store/models/invoice';
8
9
  import { PaymentIntent } from '../store/models/payment-intent';
@@ -33,6 +34,12 @@ export const handleInvoice = async (job: InvoiceJob) => {
33
34
  return;
34
35
  }
35
36
 
37
+ const supportAutoCharge = await PaymentMethod.supportAutoCharge(invoice.default_payment_method_id);
38
+ if (supportAutoCharge === false) {
39
+ logger.warn(`Invoice does not support auto charge: ${job.invoiceId}`);
40
+ return;
41
+ }
42
+
36
43
  // no payment required
37
44
  if (invoice.total === '0') {
38
45
  logger.warn(`Invoice does not require payment: ${job.invoiceId}`);
@@ -136,6 +143,10 @@ export const startInvoiceQueue = async () => {
136
143
  });
137
144
 
138
145
  invoices.forEach(async (x) => {
146
+ const supportAutoCharge = await PaymentMethod.supportAutoCharge(x.default_payment_method_id);
147
+ if (supportAutoCharge === false) {
148
+ return;
149
+ }
139
150
  const exist = await invoiceQueue.get(x.id);
140
151
  if (!exist) {
141
152
  invoiceQueue.push({ id: x.id, job: { invoiceId: x.id, retryOnError: true } });
@@ -38,9 +38,9 @@ export const handlePayment = async (job: PaymentJob) => {
38
38
  return;
39
39
  }
40
40
 
41
- // FIXME: more methods supported here
42
- if (paymentMethod.type !== 'arcblock') {
43
- logger.warn(`Unexpected payment method type: ${paymentMethod.type}`);
41
+ const supportAutoCharge = await PaymentMethod.supportAutoCharge(paymentIntent.payment_method_id);
42
+ if (supportAutoCharge === false) {
43
+ logger.warn(`PaymentMethod does not support auto charge: ${paymentIntent.payment_method_id}`);
44
44
  return;
45
45
  }
46
46
 
@@ -88,8 +88,10 @@ export const handlePayment = async (job: PaymentJob) => {
88
88
  status: 'succeeded',
89
89
  amount_received: paymentIntent.amount,
90
90
  payment_details: {
91
- tx_hash: txHash,
92
- payer: paymentSettings?.payment_method_options.arcblock?.payer,
91
+ arcblock: {
92
+ tx_hash: txHash,
93
+ payer: paymentSettings?.payment_method_options.arcblock?.payer as string,
94
+ },
93
95
  },
94
96
  });
95
97
 
@@ -126,8 +128,10 @@ export const handlePayment = async (job: PaymentJob) => {
126
128
  status: 'complete',
127
129
  payment_status: 'paid',
128
130
  payment_details: {
129
- tx_hash: txHash,
130
- payer: paymentSettings?.payment_method_options.arcblock?.payer,
131
+ arcblock: {
132
+ tx_hash: txHash,
133
+ payer: paymentSettings?.payment_method_options.arcblock?.payer as string,
134
+ },
131
135
  },
132
136
  });
133
137
  logger.info(`CheckoutSession ${checkoutSession.id} updated on payment done ${invoice.id}`);
@@ -196,6 +200,10 @@ export const startPaymentQueue = async () => {
196
200
  });
197
201
 
198
202
  payments.forEach(async (x) => {
203
+ const supportAutoCharge = await PaymentMethod.supportAutoCharge(x.payment_method_id);
204
+ if (supportAutoCharge === false) {
205
+ return;
206
+ }
199
207
  const exist = await paymentQueue.get(x.id);
200
208
  if (!exist) {
201
209
  paymentQueue.push({ id: x.id, job: { paymentIntentId: x.id } });
@@ -5,7 +5,7 @@ import dayjs from '../libs/dayjs';
5
5
  import logger from '../libs/logger';
6
6
  import createQueue from '../libs/queue';
7
7
  import { getStatementDescriptor, getSubscriptionCycleAmount, getSubscriptionCycleSetup } from '../libs/session';
8
- import { UsageRecord } from '../store/models';
8
+ import { PaymentCurrency, PaymentMethod, UsageRecord } from '../store/models';
9
9
  import { Customer } from '../store/models/customer';
10
10
  import { Invoice } from '../store/models/invoice';
11
11
  import { InvoiceItem } from '../store/models/invoice-item';
@@ -32,6 +32,11 @@ export const handleSubscription = async (job: SubscriptionJob) => {
32
32
  logger.warn(`Subscription status not expected: ${job.subscriptionId}`);
33
33
  return;
34
34
  }
35
+ const supportAutoCharge = await PaymentMethod.supportAutoCharge(subscription.default_payment_method_id);
36
+ if (supportAutoCharge === false) {
37
+ logger.warn(`Subscription does not support auto charge: ${job.subscriptionId}`);
38
+ return;
39
+ }
35
40
 
36
41
  const now = dayjs().unix();
37
42
 
@@ -86,6 +91,13 @@ export const handleSubscription = async (job: SubscriptionJob) => {
86
91
  return;
87
92
  }
88
93
 
94
+ // Do we still have the currency
95
+ const currency = await PaymentCurrency.findByPk(subscription.currency_id);
96
+ if (!currency) {
97
+ logger.warn(`Currency ${subscription.currency_id} not found for subscription: ${subscription.id}`);
98
+ return;
99
+ }
100
+
89
101
  // get setup for next subscription period
90
102
  const previousPeriodEnd =
91
103
  subscription.status === 'trialing' ? subscription.trail_end : subscription.current_period_end;
@@ -154,7 +166,7 @@ export const handleSubscription = async (job: SubscriptionJob) => {
154
166
  })
155
167
  );
156
168
 
157
- const amount = getSubscriptionCycleAmount(expandedItems);
169
+ const amount = getSubscriptionCycleAmount(expandedItems, currency);
158
170
 
159
171
  // create invoice
160
172
  const invoice = await Invoice.create({
@@ -285,6 +297,10 @@ export const startSubscriptionQueue = async () => {
285
297
  });
286
298
 
287
299
  subscriptions.forEach(async (x) => {
300
+ const supportAutoCharge = await PaymentMethod.supportAutoCharge(x.default_payment_method_id);
301
+ if (supportAutoCharge === false) {
302
+ return;
303
+ }
288
304
  const exist = await subscriptionQueue.get(x.id);
289
305
  if (!exist) {
290
306
  subscriptionQueue.push({
@@ -1,9 +1,12 @@
1
1
  import { env } from '@blocklet/sdk/lib/config';
2
2
  import { BN } from '@ocap/util';
3
+ import cloneDeep from 'lodash/cloneDeep';
4
+ import isEqual from 'lodash/isEqual';
3
5
 
6
+ import type { TPaymentCurrency, TPaymentMethodExpanded } from '../store/models';
4
7
  import type { Price, TPrice } from '../store/models/price';
5
8
  import type { Product } from '../store/models/product';
6
- import type { LineItem, PriceRecurring } from '../store/models/types';
9
+ import type { LineItem, PriceCurrency, PriceRecurring } from '../store/models/types';
7
10
  import dayjs from './dayjs';
8
11
 
9
12
  export type TLineItemExpanded = LineItem & { price: TPrice };
@@ -35,8 +38,26 @@ export function getCheckoutMode(items: TLineItemExpanded[] = []) {
35
38
  return 'payment';
36
39
  }
37
40
 
41
+ export function getPriceUintAmountByCurrency(price: TPrice, currency: TPaymentCurrency) {
42
+ const options = getPriceCurrencyOptions(price);
43
+ const option = options.find((x) => x.currency_id === currency.id);
44
+ if (option) {
45
+ return option.unit_amount;
46
+ }
47
+
48
+ return price.unit_amount;
49
+ }
50
+
51
+ export function getPriceCurrencyOptions(price: TPrice): PriceCurrency[] {
52
+ if (Array.isArray(price.currency_options)) {
53
+ return price.currency_options;
54
+ }
55
+
56
+ return [{ currency_id: price.currency_id, unit_amount: price.unit_amount, tiers: null, custom_unit_amount: null }];
57
+ }
58
+
38
59
  // FIXME: apply coupon for discounts
39
- export function getCheckoutAmount(items: TLineItemExpanded[] = [], includeFreeTrial = false) {
60
+ export function getCheckoutAmount(items: TLineItemExpanded[], currency: TPaymentCurrency, includeFreeTrial = false) {
40
61
  const subtotal = items
41
62
  .reduce((acc, x) => {
42
63
  if (x.price.type === 'recurring') {
@@ -47,7 +68,7 @@ export function getCheckoutAmount(items: TLineItemExpanded[] = [], includeFreeTr
47
68
  return acc;
48
69
  }
49
70
  }
50
- return acc.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
71
+ return acc.add(new BN(getPriceUintAmountByCurrency(x.price, currency)).mul(new BN(x.quantity)));
51
72
  }, new BN(0))
52
73
  .toString();
53
74
 
@@ -61,7 +82,7 @@ export function getCheckoutAmount(items: TLineItemExpanded[] = [], includeFreeTr
61
82
  return acc;
62
83
  }
63
84
  }
64
- return acc.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
85
+ return acc.add(new BN(getPriceUintAmountByCurrency(x.price, currency)).mul(new BN(x.quantity)));
65
86
  }, new BN(0))
66
87
  .toString();
67
88
 
@@ -89,7 +110,7 @@ export function getRecurringPeriod(recurring: PriceRecurring) {
89
110
  }
90
111
  }
91
112
 
92
- export function getSubscriptionCreateSetup(items: TLineItemExpanded[], trialInDays = 0) {
113
+ export function getSubscriptionCreateSetup(items: TLineItemExpanded[], currency: TPaymentCurrency, trialInDays = 0) {
93
114
  let setup = new BN(0);
94
115
  let subscription = new BN(0);
95
116
 
@@ -97,7 +118,7 @@ export function getSubscriptionCreateSetup(items: TLineItemExpanded[], trialInDa
97
118
  setup = setup.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
98
119
  if (x.price.type === 'recurring') {
99
120
  if (trialInDays === 0) {
100
- subscription = setup.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
121
+ subscription = setup.add(new BN(getPriceUintAmountByCurrency(x.price, currency)).mul(new BN(x.quantity)));
101
122
  }
102
123
  }
103
124
  });
@@ -142,11 +163,11 @@ export function getSubscriptionCycleSetup(recurring: PriceRecurring, previousPer
142
163
  };
143
164
  }
144
165
 
145
- export function getSubscriptionCycleAmount(items: TLineItemExpanded[]) {
166
+ export function getSubscriptionCycleAmount(items: TLineItemExpanded[], currency: TPaymentCurrency) {
146
167
  let amount = new BN(0);
147
168
 
148
169
  items.forEach((x) => {
149
- amount = amount.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
170
+ amount = amount.add(new BN(getPriceUintAmountByCurrency(x.price, currency)).mul(new BN(x.quantity)));
150
171
  });
151
172
 
152
173
  return {
@@ -162,3 +183,78 @@ export function expandLineItems(items: any[], products: Product[], prices: Price
162
183
 
163
184
  return items;
164
185
  }
186
+
187
+ export function filterCurrencies(method: TPaymentMethodExpanded, hasSelected: (currency: TPaymentCurrency) => boolean) {
188
+ method.payment_currencies = method.payment_currencies.filter((x) => hasSelected(x));
189
+ return method;
190
+ }
191
+
192
+ export function getSupportedPaymentMethods(
193
+ methods: TPaymentMethodExpanded[],
194
+ hasSelected: (currency: TPaymentCurrency) => boolean
195
+ ) {
196
+ const filtered = cloneDeep(methods).map((x) => filterCurrencies(x, hasSelected));
197
+ return filtered.filter((x) => x.payment_currencies.length);
198
+ }
199
+
200
+ export function getSupportedPaymentCurrencies(items: TLineItemExpanded[]) {
201
+ const currencies = items.reduce((acc: string[], x: any) => {
202
+ return acc.concat(getPriceCurrencyOptions(x.price).map((c: any) => c.currency_id));
203
+ }, []);
204
+ return Array.from(new Set(currencies));
205
+ }
206
+
207
+ export function isLineItemCurrencyAligned(list: TLineItemExpanded[], index: number) {
208
+ const prices = list.map((x) => x.price);
209
+
210
+ const current = getPriceCurrencyOptions(prices[index] as TPrice)
211
+ .map((x) => x.currency_id)
212
+ .sort();
213
+
214
+ for (let i = 0; i < index; i++) {
215
+ const previous = getPriceCurrencyOptions(prices[i] as TPrice)
216
+ .map((x) => x.currency_id)
217
+ .sort();
218
+ if (isEqual(current, previous) === false) {
219
+ return false;
220
+ }
221
+ }
222
+
223
+ return true;
224
+ }
225
+
226
+ export function isLineItemRecurringAligned(list: TLineItemExpanded[], index: number) {
227
+ const prices = list.map((x) => x.price);
228
+
229
+ if (prices[index]?.type !== 'recurring') {
230
+ return true;
231
+ }
232
+
233
+ const recurring = prices.slice(0, index).find((x) => x?.type === 'recurring')?.recurring;
234
+ if (!recurring) {
235
+ return true;
236
+ }
237
+
238
+ return prices.slice(0, index + 1).every((x: any) => {
239
+ if (x.type !== 'recurring') {
240
+ return true;
241
+ }
242
+
243
+ // If the interval and interval_count are different, the recurring is not aligned
244
+ if (recurring?.interval !== x.recurring?.interval || recurring?.interval_count !== x.recurring?.interval_count) {
245
+ return false;
246
+ }
247
+
248
+ return true;
249
+ });
250
+ }
251
+
252
+ export function isLineItemAligned(list: TLineItemExpanded[], index: number) {
253
+ const currency = isLineItemCurrencyAligned(list, index);
254
+ const recurring = isLineItemRecurringAligned(list, index);
255
+ return {
256
+ currency,
257
+ recurring,
258
+ aligned: currency && recurring,
259
+ };
260
+ }