payment-kit 1.13.92 → 1.13.93

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 (37) hide show
  1. package/api/src/index.ts +2 -0
  2. package/api/src/libs/audit.ts +28 -34
  3. package/api/src/libs/payment.ts +2 -11
  4. package/api/src/libs/session.ts +1 -1
  5. package/api/src/libs/util.ts +8 -5
  6. package/api/src/routes/checkout-sessions.ts +41 -39
  7. package/api/src/routes/connect/collect.ts +12 -12
  8. package/api/src/routes/connect/setup.ts +8 -11
  9. package/api/src/routes/connect/shared.ts +81 -20
  10. package/api/src/routes/connect/subscribe.ts +8 -11
  11. package/api/src/routes/connect/update.ts +134 -0
  12. package/api/src/routes/pricing-table.ts +9 -121
  13. package/api/src/routes/subscriptions.ts +416 -141
  14. package/api/src/store/models/index.ts +3 -0
  15. package/api/src/store/models/pricing-table.ts +125 -1
  16. package/api/src/store/models/subscription.ts +4 -0
  17. package/api/src/store/models/types.ts +8 -0
  18. package/api/tests/libs/util.spec.ts +6 -6
  19. package/blocklet.yml +1 -1
  20. package/package.json +6 -6
  21. package/src/app.tsx +12 -4
  22. package/src/components/checkout/form/address.tsx +41 -34
  23. package/src/components/checkout/form/index.tsx +1 -1
  24. package/src/components/checkout/pricing-table.tsx +205 -0
  25. package/src/components/payment-link/product-select.tsx +13 -3
  26. package/src/components/portal/invoice/list.tsx +1 -1
  27. package/src/components/portal/subscription/actions.tsx +153 -0
  28. package/src/components/portal/subscription/list.tsx +21 -150
  29. package/src/components/subscription/metrics.tsx +46 -0
  30. package/src/contexts/products.tsx +2 -1
  31. package/src/libs/util.ts +43 -0
  32. package/src/locales/en.tsx +15 -1
  33. package/src/locales/zh.tsx +16 -2
  34. package/src/pages/admin/billing/subscriptions/detail.tsx +2 -34
  35. package/src/pages/checkout/pricing-table.tsx +9 -158
  36. package/src/pages/customer/subscription/{index.tsx → detail.tsx} +6 -36
  37. package/src/pages/customer/subscription/update.tsx +281 -0
@@ -0,0 +1,134 @@
1
+ import { toTypeInfo } from '@arcblock/did';
2
+ import { toDelegateAddress } from '@arcblock/did-util';
3
+ import type { Transaction } from '@ocap/client';
4
+ import { fromPublicKey } from '@ocap/wallet';
5
+
6
+ import type { CallbackArgs } from '../../libs/auth';
7
+ import { wallet } from '../../libs/auth';
8
+ import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
9
+ import { getFastCheckoutAmount } from '../../libs/session';
10
+ import { OCAP_PAYMENT_TX_TYPE, getTxMetadata } from '../../libs/util';
11
+ import { invoiceQueue } from '../../queues/invoice';
12
+ import { subscriptionQueue } from '../../queues/subscription';
13
+ import type { TLineItemExpanded } from '../../store/models';
14
+ import { ensureSubscription, getAuthPrincipalClaim, getTokenRequirements } from './shared';
15
+
16
+ export default {
17
+ action: 'update',
18
+ authPrincipal: false,
19
+ claims: {
20
+ authPrincipal: async ({ extraParams }: CallbackArgs) => {
21
+ const { paymentMethod } = await ensureSubscription(extraParams.subscriptionId);
22
+ return getAuthPrincipalClaim(paymentMethod, 'pay');
23
+ },
24
+ },
25
+ onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
26
+ const { subscriptionId } = extraParams;
27
+ const { paymentMethod, paymentCurrency, subscription } = await ensureSubscription(subscriptionId);
28
+
29
+ if (paymentMethod.type === 'arcblock') {
30
+ // @ts-ignore
31
+ const items = subscription!.items as TLineItemExpanded[];
32
+ const address = toDelegateAddress(userDid, wallet.address);
33
+ const amount = getFastCheckoutAmount(items, 'subscription', paymentCurrency.id);
34
+
35
+ const tokenLimits = await getTokenLimitsForDelegation(items, paymentMethod, paymentCurrency, address, amount);
36
+ const tokenRequirements = await getTokenRequirements({
37
+ items,
38
+ mode: 'subscription',
39
+ includeFreeTrial: false,
40
+ paymentMethod,
41
+ paymentCurrency,
42
+ });
43
+
44
+ return {
45
+ signature: {
46
+ type: 'DelegateTx',
47
+ description: `Sign the delegation to update subscription ${subscription?.id}`,
48
+ wallet: fromPublicKey(userPk, toTypeInfo(userDid)),
49
+ data: {
50
+ itx: {
51
+ address,
52
+ to: wallet.address,
53
+ ops: [{ typeUrl: OCAP_PAYMENT_TX_TYPE, limit: { tokens: tokenLimits, assets: [] } }],
54
+ data: getTxMetadata({ subscriptionId }),
55
+ },
56
+ },
57
+ nonce: subscriptionId,
58
+ requirement: {
59
+ tokens: tokenRequirements,
60
+ },
61
+ chainInfo: {
62
+ host: paymentMethod.settings?.arcblock?.api_host as string,
63
+ id: paymentMethod.settings?.arcblock?.chain_id as string,
64
+ },
65
+ },
66
+ };
67
+ }
68
+
69
+ throw new Error(`Payment method ${paymentMethod.type} not supported`);
70
+ },
71
+
72
+ onAuth: async ({ userDid, userPk, claims, extraParams }: CallbackArgs) => {
73
+ const { subscriptionId } = extraParams;
74
+ const { invoice, paymentMethod, subscription } = await ensureSubscription(subscriptionId);
75
+
76
+ if (paymentMethod.type === 'arcblock') {
77
+ await subscription?.update({
78
+ payment_settings: {
79
+ payment_method_types: ['arcblock'],
80
+ payment_method_options: {
81
+ arcblock: { payer: userDid },
82
+ },
83
+ },
84
+ });
85
+
86
+ const client = paymentMethod.getOcapClient();
87
+ const claim = claims.find((x) => x.type === 'signature');
88
+
89
+ // execute the delegate tx
90
+ const tx: Partial<Transaction> = client.decodeTx(claim.origin);
91
+ tx.signature = claim.sig;
92
+
93
+ // @ts-ignore
94
+ const { buffer } = await client.encodeDelegateTx({ tx });
95
+ const txHash = await client.sendDelegateTx(
96
+ // @ts-ignore
97
+ { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
98
+ getGasPayerExtra(buffer)
99
+ );
100
+
101
+ await subscription?.update({
102
+ payment_details: {
103
+ arcblock: {
104
+ tx_hash: txHash,
105
+ payer: userDid,
106
+ },
107
+ },
108
+ });
109
+
110
+ if (invoice) {
111
+ if (invoice.status === 'uncollectible') {
112
+ await invoice.update({ status: 'open' });
113
+ }
114
+
115
+ await invoiceQueue.pushAndWait({
116
+ id: invoice.id,
117
+ job: { invoiceId: invoice.id, retryOnError: false, waitForPayment: true },
118
+ });
119
+ }
120
+ if (subscription) {
121
+ subscriptionQueue.push({
122
+ id: subscription.id,
123
+ job: { subscriptionId: subscription.id, action: 'cycle' },
124
+ // our next invoice should be generated at the end of current period, either trailing or normal
125
+ runAt: subscription.trail_end || subscription.current_period_end,
126
+ });
127
+ }
128
+
129
+ return { hash: txHash };
130
+ }
131
+
132
+ throw new Error(`Payment method ${paymentMethod.type} not supported`);
133
+ },
134
+ };
@@ -1,6 +1,7 @@
1
1
  import { getUrl } from '@blocklet/sdk/lib/component';
2
2
  import { Router } from 'express';
3
3
  import Joi from 'joi';
4
+ import merge from 'lodash/merge';
4
5
  import pick from 'lodash/pick';
5
6
  import uniq from 'lodash/uniq';
6
7
  import type { WhereOptions } from 'sequelize';
@@ -10,7 +11,7 @@ import logger from '../libs/logger';
10
11
  import { authenticate } from '../libs/security';
11
12
  import { isLineItemCurrencyAligned } from '../libs/session';
12
13
  import { getDaysUntilDue } from '../libs/subscription';
13
- import { formatMetadata, getMetadataFromQuery } from '../libs/util';
14
+ import { getDataObjectFromQuery } from '../libs/util';
14
15
  import { CheckoutSession } from '../store/models/checkout-session';
15
16
  import { PaymentCurrency } from '../store/models/payment-currency';
16
17
  import { Price } from '../store/models/price';
@@ -21,113 +22,10 @@ import { formatCheckoutSession } from './checkout-sessions';
21
22
  const router = Router();
22
23
  const auth = authenticate<PricingTable>({ component: true, roles: ['owner', 'admin'] });
23
24
 
24
- const formatPricingTable = (payload: any) => {
25
- const raw: Partial<PricingTable> = Object.assign(
26
- {
27
- branding_settings: {
28
- background_color: '#ffffff',
29
- border_style: 'default',
30
- button_color: '#0074d4',
31
- font_family: 'default',
32
- },
33
- },
34
- pick(payload, ['name', 'items', 'metadata', 'brand_settings'])
35
- );
36
-
37
- raw.items = raw.items?.map((x) => {
38
- const item = Object.assign(
39
- {
40
- adjustable_quantity: {
41
- enabled: false,
42
- maximum: 1,
43
- minimum: 0,
44
- },
45
- after_completion: {
46
- type: 'hosted_confirmation',
47
- hosted_confirmation: {
48
- custom_message: '',
49
- },
50
- },
51
- allow_promotion_codes: false,
52
- customer_creation: 'always',
53
- consent_collection: {
54
- promotions: 'none',
55
- terms_of_service: 'none',
56
- },
57
- invoice_creation: {
58
- enabled: true,
59
- },
60
- phone_number_collection: {
61
- enabled: false,
62
- },
63
- billing_address_collection: 'auto',
64
- subscription_data: {
65
- description: '',
66
- trial_period_days: 0,
67
- },
68
- nft_mint_settings: {
69
- enabled: false,
70
- factory: '',
71
- },
72
- submit_type: 'auto',
73
- cross_sell_behavior: 'auto',
74
- },
75
- pick(x, [
76
- 'adjustable_quantity',
77
- 'after_completion',
78
- 'allow_promotion_codes',
79
- 'billing_address_collection',
80
- 'consent_collection',
81
- 'cross_sell_behavior',
82
- 'custom_fields',
83
- 'highlight_text',
84
- 'is_highlight',
85
- 'nft_mint_settings',
86
- 'phone_number_collection',
87
- 'price_id',
88
- 'product_id',
89
- 'submit_type',
90
- 'subscription_data',
91
- ])
92
- );
93
-
94
- if (item.adjustable_quantity?.enabled) {
95
- item.adjustable_quantity.minimum = Number(item.adjustable_quantity?.minimum);
96
- item.adjustable_quantity.maximum = Number(item.adjustable_quantity?.maximum);
97
- }
98
- if (item.after_completion?.type === 'hosted_confirmation') {
99
- // @ts-ignore
100
- item.after_completion.redirect = null;
101
- }
102
- if (item.after_completion?.type === 'redirect') {
103
- // @ts-ignore
104
- item.after_completion.hosted_confirmation = null;
105
- }
106
-
107
- return item;
108
- });
109
-
110
- if (payload.highlight && payload.highlight_product_id) {
111
- raw.items?.forEach((x) => {
112
- if (x.product_id === payload.highlight_product_id) {
113
- x.is_highlight = x.product_id === payload.highlight_product_id;
114
- x.highlight_text = payload.highlight_text || 'popular';
115
- } else {
116
- x.is_highlight = false;
117
- x.highlight_text = 'popular';
118
- }
119
- });
120
- }
121
-
122
- raw.metadata = formatMetadata(raw.metadata);
123
-
124
- return raw;
125
- };
126
-
127
25
  // FIXME: @wangshijun use schema validation
128
26
  // eslint-disable-next-line consistent-return
129
27
  router.post('/', auth, async (req, res) => {
130
- const raw: Partial<PricingTable> = formatPricingTable(req.body);
28
+ const raw: Partial<PricingTable> = PricingTable.format(req.body);
131
29
  raw.active = true;
132
30
  raw.locked = false;
133
31
  raw.livemode = !!req.livemode;
@@ -210,19 +108,9 @@ router.get('/:id', async (req, res) => {
210
108
  return res.status(404).json({ error: 'pricing table not found' });
211
109
  }
212
110
 
213
- const prices = await Price.findAll({ where: { id: uniq(doc.items.map((x) => x.price_id)) } });
214
- const products = await Product.findAll({ where: { id: uniq(doc.items.map((x) => x.product_id)) } });
215
-
216
- doc.items.forEach((i) => {
217
- // @ts-ignore
218
- i.price = prices.find((p) => p.id === i.price_id);
219
- // @ts-ignore
220
- i.product = products.find((p) => p.id === i.product_id);
221
- });
222
-
223
111
  const currency = await PaymentCurrency.findOne({ where: { livemode: doc.livemode, is_base_currency: true } });
224
-
225
- res.json({ ...doc.toJSON(), currency });
112
+ const expanded = await doc.expand();
113
+ res.json({ ...expanded, currency });
226
114
  });
227
115
 
228
116
  // update
@@ -240,7 +128,7 @@ router.put('/:id', auth, async (req, res) => {
240
128
  // return res.status(403).json({ error: 'pricing table locked' });
241
129
  // }
242
130
 
243
- await doc.update(formatPricingTable(Object.assign({}, doc.dataValues, req.body)));
131
+ await doc.update(PricingTable.format(Object.assign({}, doc.dataValues, req.body)));
244
132
 
245
133
  res.json(doc);
246
134
  });
@@ -293,7 +181,7 @@ router.post('/stash', auth, async (req, res) => {
293
181
 
294
182
  let doc = await PricingTable.findByPk(raw.id);
295
183
  if (doc) {
296
- await doc.update({ ...formatPricingTable(req.body), metadata: raw.metadata, livemode: raw.livemode });
184
+ await doc.update({ ...PricingTable.format(req.body), metadata: raw.metadata, livemode: raw.livemode });
297
185
  } else {
298
186
  doc = await PricingTable.create(raw as PricingTable);
299
187
  }
@@ -337,11 +225,11 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
337
225
  'submit_type',
338
226
  'cross_sell_behavior',
339
227
  'nft_mint_settings',
340
- 'subscription_data',
341
228
  ]),
229
+ subscription_data: merge(price.subscription_data || {}, getDataObjectFromQuery(req.query, 'subscription_data')),
342
230
  metadata: {
343
231
  ...doc.metadata,
344
- ...getMetadataFromQuery(req.query),
232
+ ...getDataObjectFromQuery(req.query),
345
233
  days_until_due: getDaysUntilDue(req.query),
346
234
  passport: await checkPassportForPricingTable(doc),
347
235
  pricing_table_id: doc.id,