payment-kit 1.13.235 → 1.13.237

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.
@@ -6,7 +6,6 @@ import {
6
6
  batchHandleStripePayments,
7
7
  batchHandleStripeSubscriptions,
8
8
  } from '../integrations/stripe/resource';
9
- import dayjs from '../libs/dayjs';
10
9
  import {
11
10
  expiredSessionCleanupCronTime,
12
11
  notificationCronTime,
@@ -89,7 +88,7 @@ function init() {
89
88
  {
90
89
  name: 'payment.stat',
91
90
  time: paymentStatCronTime,
92
- fn: () => createPaymentStat(dayjs().subtract(1, 'day').toDate().toISOString()),
91
+ fn: () => createPaymentStat(),
93
92
  options: { runOnInit: false },
94
93
  },
95
94
  ],
@@ -84,29 +84,69 @@ export async function getPaymentStat(
84
84
  };
85
85
  }
86
86
 
87
- export async function createPaymentStat(date: string) {
88
- const { stats, timestamp, currencies } = await getPaymentStat(date);
87
+ export async function getDates() {
88
+ const item = await PaymentStat.findOne({
89
+ order: [['timestamp', 'DESC']],
90
+ limit: 1,
91
+ offset: 0,
92
+ attributes: ['timestamp'],
93
+ });
94
+
95
+ const dayInSeconds = 60 * 60 * 24;
96
+ const now = dayjs().unix() - dayInSeconds;
97
+
98
+ if (item) {
99
+ const dates: string[] = [];
100
+ let current = item.timestamp + dayInSeconds;
101
+ while (current < now) {
102
+ dates.push(
103
+ dayjs(current * 1000)
104
+ .toDate()
105
+ .toISOString()
106
+ );
107
+ current += dayInSeconds;
108
+ }
109
+
110
+ return dates;
111
+ }
112
+
113
+ return [
114
+ dayjs(now * 1000)
115
+ .subtract(1, 'day')
116
+ .toDate()
117
+ .toISOString(),
118
+ ];
119
+ }
120
+
121
+ export async function createPaymentStat(date?: string) {
122
+ const dates = date ? [date] : await getDates();
89
123
  await Promise.all(
90
- currencies.map(async (currency) => {
91
- const exist = await PaymentStat.findOne({ where: { timestamp, currency_id: currency.id } });
92
- if (exist) {
93
- await exist.update({
94
- amount_paid: stats.payment![currency.id] || '0',
95
- amount_payout: stats.payout![currency.id] || '0',
96
- amount_refund: stats.refund![currency.id] || '0',
97
- });
98
- logger.info('PaymentStat updated', { date, timestamp, currency: currency.symbol });
99
- } else {
100
- await PaymentStat.create({
101
- livemode: currency.livemode,
102
- timestamp,
103
- currency_id: currency.id,
104
- amount_paid: stats.payment![currency.id] || '0',
105
- amount_payout: stats.payout![currency.id] || '0',
106
- amount_refund: stats.refund![currency.id] || '0',
107
- });
108
- logger.info('PaymentStat created', { date, timestamp, currency: currency.symbol });
109
- }
124
+ // eslint-disable-next-line @typescript-eslint/no-shadow
125
+ dates.map(async (date) => {
126
+ const { stats, timestamp, currencies } = await getPaymentStat(date);
127
+ await Promise.all(
128
+ currencies.map(async (currency) => {
129
+ const exist = await PaymentStat.findOne({ where: { timestamp, currency_id: currency.id } });
130
+ if (exist) {
131
+ await exist.update({
132
+ amount_paid: stats.payment![currency.id] || '0',
133
+ amount_payout: stats.payout![currency.id] || '0',
134
+ amount_refund: stats.refund![currency.id] || '0',
135
+ });
136
+ logger.info('PaymentStat updated', { date, timestamp, currency: currency.symbol });
137
+ } else {
138
+ await PaymentStat.create({
139
+ livemode: currency.livemode,
140
+ timestamp,
141
+ currency_id: currency.id,
142
+ amount_paid: stats.payment![currency.id] || '0',
143
+ amount_payout: stats.payout![currency.id] || '0',
144
+ amount_refund: stats.refund![currency.id] || '0',
145
+ });
146
+ logger.info('PaymentStat created', { date, timestamp, currency: currency.symbol });
147
+ }
148
+ })
149
+ );
110
150
  })
111
151
  );
112
152
  }
@@ -155,7 +155,11 @@ export function isCreditSufficientForPayment(args: {
155
155
  return { sufficient: true, balance };
156
156
  }
157
157
 
158
- export function getGasPayerExtra(txBuffer: Buffer) {
158
+ export function getGasPayerExtra(txBuffer: Buffer, headers?: { [key: string]: string }) {
159
+ if (headers && headers['x-gas-payer-sig'] && headers['x-gas-payer-pk']) {
160
+ return { headers };
161
+ }
162
+
159
163
  const txHash = toTxHash(txBuffer);
160
164
  return {
161
165
  headers: {
@@ -104,7 +104,7 @@ export default {
104
104
  throw new Error(`ChangeMethod: Payment method ${paymentMethod.type} not supported`);
105
105
  },
106
106
 
107
- onAuth: async ({ userDid, userPk, claims, extraParams }: CallbackArgs) => {
107
+ onAuth: async ({ request, userDid, userPk, claims, extraParams }: CallbackArgs) => {
108
108
  const { subscriptionId } = extraParams;
109
109
  const { subscription, setupIntent, paymentCurrency, paymentMethod } = await ensureChangePaymentContext(
110
110
  subscriptionId
@@ -142,7 +142,7 @@ export default {
142
142
 
143
143
  if (paymentMethod.type === 'arcblock') {
144
144
  await prepareTxExecution();
145
- const paymentDetails = await executeOcapTransactions(userDid, userPk, claims, paymentMethod);
145
+ const paymentDetails = await executeOcapTransactions(userDid, userPk, claims, paymentMethod, request);
146
146
  await afterTxExecution(paymentDetails);
147
147
  return { hash: paymentDetails.tx_hash };
148
148
  }
@@ -105,7 +105,7 @@ export default {
105
105
  throw new Error(`ChangePlan: Payment method ${paymentMethod.type} not supported`);
106
106
  },
107
107
 
108
- onAuth: async ({ userDid, userPk, claims, extraParams }: CallbackArgs) => {
108
+ onAuth: async ({ request, userDid, userPk, claims, extraParams }: CallbackArgs) => {
109
109
  const { subscriptionId } = extraParams;
110
110
  const { invoice, paymentMethod, subscription } = await ensureSubscription(subscriptionId);
111
111
 
@@ -140,7 +140,7 @@ export default {
140
140
  if (paymentMethod.type === 'arcblock') {
141
141
  await prepareTxExecution();
142
142
 
143
- const paymentDetails = await executeOcapTransactions(userDid, userPk, claims, paymentMethod);
143
+ const paymentDetails = await executeOcapTransactions(userDid, userPk, claims, paymentMethod, request);
144
144
  await afterTxExecution(paymentDetails);
145
145
 
146
146
  return { hash: paymentDetails.tx_hash };
@@ -61,7 +61,7 @@ export default {
61
61
 
62
62
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
63
63
  },
64
- onAuth: async ({ userDid, claims, extraParams }: CallbackArgs) => {
64
+ onAuth: async ({ request, userDid, claims, extraParams }: CallbackArgs) => {
65
65
  const { subscriptionId, currencyId } = extraParams;
66
66
  const { invoices, paymentMethod } = await ensureSubscriptionForCollectBatch(subscriptionId, currencyId);
67
67
 
@@ -80,7 +80,7 @@ export default {
80
80
  const txHash = await client.sendTransferV3Tx(
81
81
  // @ts-ignore
82
82
  { tx, wallet: fromAddress(userDid) },
83
- getGasPayerExtra(buffer)
83
+ getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
84
84
  );
85
85
 
86
86
  const paymentIntents = await PaymentIntent.findAll({
@@ -96,7 +96,7 @@ export default {
96
96
 
97
97
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
98
98
  },
99
- onAuth: async ({ userDid, claims, extraParams }: CallbackArgs) => {
99
+ onAuth: async ({ request, userDid, claims, extraParams }: CallbackArgs) => {
100
100
  const { invoiceId } = extraParams;
101
101
  const { invoice, paymentIntent, paymentMethod } = await ensureInvoiceForCollect(invoiceId);
102
102
 
@@ -141,7 +141,7 @@ export default {
141
141
  const txHash = await client.sendTransferV3Tx(
142
142
  // @ts-ignore
143
143
  { tx, wallet: fromAddress(userDid) },
144
- getGasPayerExtra(buffer)
144
+ getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
145
145
  );
146
146
 
147
147
  await afterTxExecution({
@@ -84,7 +84,7 @@ export default {
84
84
  },
85
85
 
86
86
  onAuth: async (args: CallbackArgs) => {
87
- const { userDid, claims, extraParams } = args;
87
+ const { request, userDid, claims, extraParams } = args;
88
88
  const { checkoutSessionId, connectedDid } = extraParams;
89
89
  const { checkoutSession, customer, paymentIntent, paymentMethod } = await ensurePaymentIntent(
90
90
  checkoutSessionId,
@@ -113,7 +113,7 @@ export default {
113
113
  const txHash = await client.sendTransferV3Tx(
114
114
  // @ts-ignore
115
115
  { tx, wallet: fromAddress(userDid) },
116
- getGasPayerExtra(buffer)
116
+ getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
117
117
  );
118
118
 
119
119
  await paymentIntent.update({
@@ -115,7 +115,7 @@ export default {
115
115
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
116
116
  },
117
117
  onAuth: async (args: CallbackArgs) => {
118
- const { userDid, userPk, claims, extraParams } = args;
118
+ const { request, userDid, userPk, claims, extraParams } = args;
119
119
  const { checkoutSessionId, connectedDid } = extraParams;
120
120
  const { setupIntent, checkoutSession, paymentMethod, subscription, invoice } = await ensureSetupIntent(
121
121
  checkoutSessionId,
@@ -169,7 +169,7 @@ export default {
169
169
  try {
170
170
  await prepareTxExecution();
171
171
 
172
- const paymentDetails = await executeOcapTransactions(userDid, userPk, claims, paymentMethod);
172
+ const paymentDetails = await executeOcapTransactions(userDid, userPk, claims, paymentMethod, request);
173
173
  await afterTxExecution(paymentDetails);
174
174
 
175
175
  return { hash: paymentDetails.tx_hash };
@@ -6,6 +6,7 @@ import type { Transaction } from '@ocap/client';
6
6
  import { BN, fromTokenToUnit, toBase58 } from '@ocap/util';
7
7
  import { fromPublicKey } from '@ocap/wallet';
8
8
  import isEmpty from 'lodash/isEmpty';
9
+ import type { Request } from 'express';
9
10
 
10
11
  import { estimateMaxGasForTx, hasStakedForGas } from '../../integrations/arcblock/stake';
11
12
  import { encodeApproveItx } from '../../integrations/ethereum/token';
@@ -920,7 +921,8 @@ export async function executeOcapTransactions(
920
921
  userDid: string,
921
922
  userPk: string,
922
923
  claims: any[],
923
- paymentMethod: PaymentMethod
924
+ paymentMethod: PaymentMethod,
925
+ request: Request,
924
926
  ) {
925
927
  const client = paymentMethod.getOcapClient();
926
928
  const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
@@ -947,7 +949,7 @@ export async function executeOcapTransactions(
947
949
  const txHash = await client[`send${type}Tx`](
948
950
  // @ts-ignore
949
951
  { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
950
- getGasPayerExtra(buffer)
952
+ getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
951
953
  );
952
954
 
953
955
  return txHash;
@@ -117,7 +117,7 @@ export default {
117
117
  throw new Error(`subscription: Payment method ${paymentMethod.type} not supported`);
118
118
  },
119
119
  onAuth: async (args: CallbackArgs) => {
120
- const { userDid, userPk, claims, extraParams } = args;
120
+ const { request, userDid, userPk, claims, extraParams } = args;
121
121
  const { checkoutSessionId, connectedDid } = extraParams;
122
122
  const { checkoutSession, customer, paymentMethod, subscription } = await ensurePaymentIntent(
123
123
  checkoutSessionId,
@@ -156,7 +156,7 @@ export default {
156
156
  await prepareTxExecution();
157
157
  const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, subscription });
158
158
 
159
- const paymentDetails = await executeOcapTransactions(userDid, userPk, claims, paymentMethod);
159
+ const paymentDetails = await executeOcapTransactions(userDid, userPk, claims, paymentMethod, request);
160
160
  await afterTxExecution(invoice!, paymentDetails);
161
161
 
162
162
  return { hash: paymentDetails.tx_hash };
@@ -1,6 +1,7 @@
1
1
  import { Router } from 'express';
2
2
  import Joi from 'joi';
3
3
  import { Op } from 'sequelize';
4
+ import { joinURL } from 'ufo';
4
5
 
5
6
  import { getPaymentStat } from '../crons/payment-stat';
6
7
  import { getTokenSummaryByDid } from '../integrations/arcblock/stake';
@@ -8,8 +9,16 @@ import { createListParamSchema } from '../libs/api';
8
9
  import { ethWallet, wallet } from '../libs/auth';
9
10
  import dayjs from '../libs/dayjs';
10
11
  import { authenticate } from '../libs/security';
11
- import { Invoice, PaymentIntent, Payout, Refund, Subscription } from '../store/models';
12
- import { PaymentStat } from '../store/models/payment-stat';
12
+ import {
13
+ Invoice,
14
+ PaymentCurrency,
15
+ PaymentIntent,
16
+ PaymentMethod,
17
+ PaymentStat,
18
+ Payout,
19
+ Refund,
20
+ Subscription,
21
+ } from '../store/models';
13
22
 
14
23
  const router = Router();
15
24
  const auth = authenticate<PaymentStat>({ component: true, roles: ['owner', 'admin'] });
@@ -50,8 +59,8 @@ router.get('/', auth, async (req, res) => {
50
59
 
51
60
  // Append live data at the end
52
61
  const now = dayjs().unix();
53
- if (query.end && query.end > now) {
54
- const { stats, timestamp, currencies } = await getPaymentStat(dayjs().toDate().toISOString());
62
+ if (query.end && query.end >= now) {
63
+ const { stats, timestamp, currencies } = await getPaymentStat(dayjs().toDate().toString());
55
64
  list.push(
56
65
  // @ts-ignore
57
66
  ...currencies
@@ -74,14 +83,38 @@ router.get('/', auth, async (req, res) => {
74
83
  }
75
84
  });
76
85
 
86
+ async function getCurrencyLinks(livemode: boolean) {
87
+ const items = await PaymentCurrency.findAll({
88
+ where: { livemode },
89
+ include: [{ model: PaymentMethod, as: 'payment_method' }],
90
+ });
91
+
92
+ return items.reduce((acc, item: any) => {
93
+ if (item.payment_method.type === 'arcblock') {
94
+ acc[item.id] = joinURL(
95
+ item.payment_method.settings.arcblock?.explorer_host,
96
+ 'accounts',
97
+ wallet.address,
98
+ 'tokens'
99
+ );
100
+ }
101
+ if (item.payment_method.type === 'ethereum') {
102
+ acc[item.id] = joinURL(item.payment_method.settings.ethereum?.explorer_host, 'address', ethWallet.address);
103
+ }
104
+ return acc;
105
+ }, {} as any);
106
+ }
107
+
77
108
  // eslint-disable-next-line consistent-return
78
109
  router.get('/summary', auth, async (req, res) => {
79
110
  try {
80
- const [arcblock, ethereum] = await Promise.all([
111
+ const [arcblock, ethereum, links] = await Promise.all([
81
112
  getTokenSummaryByDid(wallet.address, !!req.livemode, 'arcblock'),
82
113
  getTokenSummaryByDid(ethWallet.address, !!req.livemode, 'ethereum'),
114
+ getCurrencyLinks(!!req.livemode),
83
115
  ]);
84
116
  res.json({
117
+ links,
85
118
  balances: { ...arcblock, ...ethereum },
86
119
  addresses: { arcblock: wallet.address, ethereum: ethWallet.address },
87
120
  summary: {
@@ -173,7 +173,6 @@ router.get('/search', auth, async (req, res) => {
173
173
  res.json({ count, list: docs, paging: { page, pageSize } });
174
174
  });
175
175
 
176
- // FIXME: exclude some sensitive fields from PaymentMethod
177
176
  router.get('/:id', authPortal, async (req, res) => {
178
177
  try {
179
178
  const doc = await Subscription.findOne({
@@ -830,8 +829,16 @@ router.put('/:id', authPortal, async (req, res) => {
830
829
  await subscriptionQueue.delete(subscription.id);
831
830
  await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
832
831
  } else {
833
- await subscription.update({ status: 'past_due' });
834
- logger.info('subscription past_due on invoice paid', {
832
+ await subscription.update({
833
+ status: 'past_due',
834
+ cancel_at_period_end: true,
835
+ cancelation_details: {
836
+ comment: 'subscription_update',
837
+ feedback: 'other',
838
+ reason: 'payment_failed',
839
+ },
840
+ });
841
+ logger.info('subscription past_due on invoice auto advance failed', {
835
842
  subscription: subscription.id,
836
843
  invoice: invoice.id,
837
844
  });
@@ -125,7 +125,13 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
125
125
  });
126
126
  }
127
127
 
128
- public static associate() {}
128
+ public static associate(models: any) {
129
+ this.hasOne(models.PaymentMethod, {
130
+ sourceKey: 'payment_method_id',
131
+ foreignKey: 'id',
132
+ as: 'payment_method',
133
+ });
134
+ }
129
135
 
130
136
  public static findByPkOrSymbol(id: string, options: FindOptions<PaymentCurrency> = {}) {
131
137
  return this.findOne({
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.13.235
17
+ version: 1.13.237
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.13.235",
3
+ "version": "1.13.237",
4
4
  "scripts": {
5
5
  "dev": "cross-env COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -42,18 +42,18 @@
42
42
  ]
43
43
  },
44
44
  "dependencies": {
45
- "@abtnode/cron": "1.16.25",
45
+ "@abtnode/cron": "1.16.26",
46
46
  "@arcblock/did": "^1.18.115",
47
47
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
48
- "@arcblock/did-connect": "^2.9.71",
48
+ "@arcblock/did-connect": "^2.9.75",
49
49
  "@arcblock/did-util": "^1.18.115",
50
50
  "@arcblock/jwt": "^1.18.115",
51
- "@arcblock/ux": "^2.9.71",
51
+ "@arcblock/ux": "^2.9.75",
52
52
  "@arcblock/validator": "^1.18.115",
53
- "@blocklet/logger": "1.16.25",
54
- "@blocklet/payment-react": "1.13.235",
55
- "@blocklet/sdk": "1.16.25",
56
- "@blocklet/ui-react": "^2.9.71",
53
+ "@blocklet/logger": "1.16.26",
54
+ "@blocklet/payment-react": "1.13.237",
55
+ "@blocklet/sdk": "1.16.26",
56
+ "@blocklet/ui-react": "^2.9.75",
57
57
  "@blocklet/uploader": "^0.0.78",
58
58
  "@mui/icons-material": "^5.15.15",
59
59
  "@mui/lab": "^5.0.0-alpha.170",
@@ -114,9 +114,9 @@
114
114
  "validator": "^13.11.0"
115
115
  },
116
116
  "devDependencies": {
117
- "@abtnode/types": "1.16.25",
117
+ "@abtnode/types": "1.16.26",
118
118
  "@arcblock/eslint-config-ts": "^0.3.0",
119
- "@blocklet/payment-types": "1.13.235",
119
+ "@blocklet/payment-types": "1.13.237",
120
120
  "@types/cookie-parser": "^1.4.7",
121
121
  "@types/cors": "^2.8.17",
122
122
  "@types/dotenv-flow": "^3.3.3",
@@ -155,5 +155,5 @@
155
155
  "parser": "typescript"
156
156
  }
157
157
  },
158
- "gitHead": "0ce69f4fb2a09658835deab572b1f860f305b195"
158
+ "gitHead": "895430a130d53b62707f4f78c5295b06ce55d4db"
159
159
  }
@@ -13,8 +13,19 @@ type Props = {
13
13
  simple?: boolean;
14
14
  };
15
15
 
16
- InvoiceTable.defaultProps = {
17
- simple: false,
16
+ type InvoiceDetailItem = {
17
+ id: string;
18
+ product: string;
19
+ quantity: number;
20
+ rawQuantity: number;
21
+ price: string;
22
+ amount: string;
23
+ };
24
+
25
+ type InvoiceSummaryItem = {
26
+ key: string;
27
+ value: string;
28
+ color: string;
18
29
  };
19
30
 
20
31
  export function getAppliedBalance(invoice: TInvoiceExpanded) {
@@ -38,9 +49,63 @@ export function getAppliedBalance(invoice: TInvoiceExpanded) {
38
49
  return '0';
39
50
  }
40
51
 
52
+ export function getInvoiceRows(invoice: TInvoiceExpanded) {
53
+ const detail: InvoiceDetailItem[] = invoice.lines.map((line) => ({
54
+ id: line.id,
55
+ product: `${line.description} ${
56
+ line.price.product.unit_label ? ` (per ${line.price.product.unit_label})` : ''
57
+ }`.trim(),
58
+ quantity: line.quantity,
59
+ rawQuantity: line.metadata?.quantity || 0,
60
+ price: !line.proration
61
+ ? formatAmount(getPriceUintAmountByCurrency(line.price, invoice.paymentCurrency) || line.amount, invoice.paymentCurrency.decimal) // prettier-ignore
62
+ : '',
63
+ amount: formatAmount(line.amount, invoice.paymentCurrency.decimal),
64
+ }));
65
+
66
+ const summary: InvoiceSummaryItem[] = [
67
+ {
68
+ key: 'common.subtotal',
69
+ value: formatAmount(invoice.subtotal, invoice.paymentCurrency.decimal),
70
+ color: 'text.primary',
71
+ },
72
+ {
73
+ key: 'common.total',
74
+ value: formatAmount(invoice.total, invoice.paymentCurrency.decimal),
75
+ color: 'text.primary',
76
+ },
77
+ ];
78
+ if (invoice.amount_paid !== '0') {
79
+ summary.push({
80
+ key: 'payment.customer.invoice.amountPaid',
81
+ value: formatAmount(invoice.amount_paid, invoice.paymentCurrency.decimal),
82
+ color: 'text.secondary',
83
+ });
84
+ }
85
+
86
+ const appliedBalance = getAppliedBalance(invoice);
87
+ if (appliedBalance !== '0') {
88
+ summary.push({
89
+ key: 'payment.customer.invoice.amountApplied',
90
+ value: formatAmount(appliedBalance, invoice.paymentCurrency.decimal),
91
+ color: 'text.secondary',
92
+ });
93
+ }
94
+ summary.push({
95
+ key: 'payment.customer.invoice.amountDue',
96
+ value: formatAmount(invoice.amount_remaining, invoice.paymentCurrency.decimal),
97
+ color: 'text.primary',
98
+ });
99
+
100
+ return {
101
+ detail,
102
+ summary,
103
+ };
104
+ }
105
+
41
106
  export default function InvoiceTable({ invoice, simple }: Props) {
42
107
  const { t } = useLocaleContext();
43
- const appliedBalance = getAppliedBalance(invoice);
108
+ const { detail, summary } = getInvoiceRows(invoice);
44
109
 
45
110
  return (
46
111
  <StyledTable>
@@ -73,30 +138,23 @@ export default function InvoiceTable({ invoice, simple }: Props) {
73
138
  )}
74
139
  </TableHead>
75
140
  <TableBody>
76
- {invoice.lines.map((line) => (
141
+ {detail.map((line) => (
77
142
  <TableRow key={line.id} sx={{ borderBottom: '1px solid #eee' }}>
78
- <TableCell sx={{ fontWeight: 600 }}>
79
- {line.description}
80
- {line.price.product.unit_label ? ` (per ${line.price.product.unit_label})` : ''}
81
- </TableCell>
143
+ <TableCell sx={{ fontWeight: 600 }}>{line.product}</TableCell>
82
144
  <TableCell align="right">
83
145
  <Stack direction="row" spacing={0.5} alignItems="center" justifyContent="flex-end">
84
146
  <Typography>{line.quantity}</Typography>
85
- {line.metadata && !!line.metadata.quantity && (
147
+ {!!line.rawQuantity && (
86
148
  <Tooltip
87
- title={t('payment.customer.invoice.rawQuantity', { quantity: line.metadata.quantity })}
149
+ title={t('payment.customer.invoice.rawQuantity', { quantity: line.rawQuantity })}
88
150
  placement="top">
89
151
  <InfoOutlined fontSize="small" sx={{ color: 'text.secondary', cursor: 'pointer' }} />
90
152
  </Tooltip>
91
153
  )}
92
154
  </Stack>
93
155
  </TableCell>
94
- <TableCell align="right">
95
- {!line.proration
96
- ? formatAmount(getPriceUintAmountByCurrency(line.price, invoice.paymentCurrency) || line.amount, invoice.paymentCurrency.decimal) // prettier-ignore
97
- : ''}
98
- </TableCell>
99
- <TableCell align="right">{formatAmount(line.amount, invoice.paymentCurrency.decimal)}</TableCell>
156
+ <TableCell align="right">{line.price}</TableCell>
157
+ <TableCell align="right">{line.amount}</TableCell>
100
158
  {!simple && (
101
159
  <TableCell align="right">
102
160
  <LineItemActions data={line as any} />
@@ -104,55 +162,17 @@ export default function InvoiceTable({ invoice, simple }: Props) {
104
162
  )}
105
163
  </TableRow>
106
164
  ))}
107
- <TableRow>
108
- <TableCell colSpan={3} align="right" sx={{ fontWeight: 600 }}>
109
- {t('common.subtotal')}
110
- </TableCell>
111
- <TableCell align="right" sx={{ fontWeight: 600 }}>
112
- {formatAmount(invoice.subtotal, invoice.paymentCurrency.decimal)}
113
- </TableCell>
114
- <TableCell>&nbsp;</TableCell>
115
- </TableRow>
116
- <TableRow sx={{ borderBottom: '1px solid #eee' }}>
117
- <TableCell colSpan={3} align="right" sx={{ fontWeight: 600 }}>
118
- {t('common.total')}
119
- </TableCell>
120
- <TableCell align="right" sx={{ fontWeight: 600 }}>
121
- {formatAmount(invoice.total, invoice.paymentCurrency.decimal)}
122
- </TableCell>
123
- <TableCell>&nbsp;</TableCell>
124
- </TableRow>
125
- {invoice.amount_paid !== '0' && (
126
- <TableRow>
127
- <TableCell colSpan={3} align="right" sx={{ fontWeight: 600, color: 'text.secondary' }}>
128
- {t('payment.customer.invoice.amountPaid')}
129
- </TableCell>
130
- <TableCell align="right" sx={{ fontWeight: 600 }}>
131
- {formatAmount(invoice.amount_paid, invoice.paymentCurrency.decimal)}
132
- </TableCell>
133
- <TableCell>&nbsp;</TableCell>
134
- </TableRow>
135
- )}
136
- {appliedBalance !== '0' && (
137
- <TableRow>
138
- <TableCell colSpan={3} align="right" sx={{ fontWeight: 600, color: 'text.secondary' }}>
139
- {t('payment.customer.invoice.amountApplied')}
165
+ {summary.map((line) => (
166
+ <TableRow key={line.key}>
167
+ <TableCell colSpan={3} align="right" sx={{ fontWeight: 600, color: line.color }}>
168
+ {t(line.key)}
140
169
  </TableCell>
141
170
  <TableCell align="right" sx={{ fontWeight: 600 }}>
142
- {formatAmount(appliedBalance, invoice.paymentCurrency.decimal)}
171
+ {line.value}
143
172
  </TableCell>
144
173
  <TableCell>&nbsp;</TableCell>
145
174
  </TableRow>
146
- )}
147
- <TableRow>
148
- <TableCell colSpan={3} align="right" sx={{ fontWeight: 600 }}>
149
- {t('payment.customer.invoice.amountDue')}
150
- </TableCell>
151
- <TableCell align="right" sx={{ fontWeight: 600 }}>
152
- {formatAmount(invoice.amount_remaining, invoice.paymentCurrency.decimal)}
153
- </TableCell>
154
- <TableCell>&nbsp;</TableCell>
155
- </TableRow>
175
+ ))}
156
176
  </TableBody>
157
177
  </StyledTable>
158
178
  );
@@ -163,3 +183,7 @@ const StyledTable = styled(Table)`
163
183
  padding: 8px 0;
164
184
  }
165
185
  `;
186
+
187
+ InvoiceTable.defaultProps = {
188
+ simple: false,
189
+ };
@@ -1,6 +1,6 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { formatAmount, formatTime, getPrefix, getPriceUintAmountByCurrency } from '@blocklet/payment-react';
3
- import type { TInvoiceExpanded, TInvoiceItemExpanded } from '@blocklet/payment-types';
2
+ import { formatTime, getPrefix } from '@blocklet/payment-react';
3
+ import type { TInvoiceExpanded } from '@blocklet/payment-types';
4
4
  import { Button } from '@mui/material';
5
5
  import {
6
6
  Font,
@@ -11,9 +11,9 @@ import {
11
11
  Text as PdfText,
12
12
  View as PdfView,
13
13
  } from '@react-pdf/renderer';
14
- import { useEffect, useState } from 'react';
15
14
  import { joinURL } from 'ufo';
16
15
 
16
+ import { getInvoiceRows } from '../invoice/table';
17
17
  import compose from './compose';
18
18
 
19
19
  Font.register({
@@ -65,40 +65,8 @@ export function Download({ data }: { data: TInvoiceExpanded }) {
65
65
  );
66
66
  }
67
67
 
68
- function InvoicePage({ data, t }: any) {
69
- const [subTotal, setSubTotal] = useState(0);
70
-
71
- const calculateAmount = (quantity: string, rate: string) => {
72
- const quantityNumber = parseFloat(quantity);
73
- const rateNumber = parseFloat(rate);
74
- const amount = quantityNumber && rateNumber ? quantityNumber * rateNumber : 0;
75
- return amount.toFixed(2);
76
- };
77
-
78
- useEffect(() => {
79
- let temp = 0;
80
-
81
- data.lines
82
- .map((line: TInvoiceItemExpanded) => {
83
- return {
84
- description:
85
- line.description + (line.price.product.unit_label ? ` (per ${line.price.product.unit_label})` : ''),
86
- quantity: line.quantity,
87
- rate: !line.proration
88
- ? formatAmount(getPriceUintAmountByCurrency(line.price, data.paymentCurrency) || line.amount, data.paymentCurrency.decimal) // prettier-ignore
89
- : '',
90
- };
91
- })
92
- .forEach((line: any) => {
93
- const quantityNumber = parseFloat(line.quantity);
94
- const rateNumber = parseFloat(line.rate);
95
- const amount = quantityNumber && rateNumber ? quantityNumber * rateNumber : 0;
96
-
97
- temp += amount;
98
- });
99
-
100
- setSubTotal(temp);
101
- }, [data.lines]);
68
+ function InvoicePage({ data, t }: { data: TInvoiceExpanded; t: any }) {
69
+ const { detail, summary } = getInvoiceRows(data);
102
70
 
103
71
  return (
104
72
  <Document>
@@ -167,63 +135,42 @@ function InvoicePage({ data, t }: any) {
167
135
  </View>
168
136
  </View>
169
137
 
170
- {data.lines
171
- .map((line: any) => {
172
- return {
173
- description:
174
- line.description + (line.price.product.unit_label ? ` (per ${line.price.product.unit_label})` : ''),
175
- quantity: line.quantity,
176
- rate: !line.proration
177
- ? formatAmount(getPriceUintAmountByCurrency(line.price, data.paymentCurrency) || line.amount, data.paymentCurrency.decimal) // prettier-ignore
178
- : '',
179
- };
180
- })
181
- .map((line: any) => {
182
- return (
183
- <View key={line.description} className="row flex">
184
- <View className="w-48 p-4-8 pb-10">
185
- <Text2 className="dark" value={line.description} />
186
- </View>
187
- <View className="w-17 p-4-8 pb-10">
188
- <Text2 className="dark right" value={line.quantity} />
189
- </View>
190
- <View className="w-17 p-4-8 pb-10">
191
- <Text className="dark right">
192
- {line.rate} {data.paymentCurrency.symbol}
193
- </Text>
194
- </View>
195
- <View className="w-18 p-4-8 pb-10">
196
- <Text className="dark right">
197
- {calculateAmount(line.quantity, line.rate)} {data.paymentCurrency.symbol}
198
- </Text>
199
- </View>
200
- </View>
201
- );
202
- })}
203
-
204
- <View className="flex">
205
- <View className="w-50 mt-10" />
206
- <View className="w-50 mt-20">
207
- <View className="flex">
208
- <View className="w-50 p-5">
209
- <Text2 value={t('common.subtotal')} className="bold" />
138
+ {detail.map((line) => {
139
+ return (
140
+ <View key={line.id} className="row flex">
141
+ <View className="w-48 p-4-8 pb-10">
142
+ <Text2 className="dark" value={line.product} />
210
143
  </View>
211
- <View className="w-50 p-5">
212
- <Text className="right bold dark">
213
- {subTotal?.toFixed(2)} {data.paymentCurrency.symbol}
214
- </Text>
144
+ <View className="w-17 p-4-8 pb-10">
145
+ <Text2 className="dark right" value={line.quantity} />
215
146
  </View>
216
- </View>
217
- <View className="flex">
218
- <View className="w-50 p-5">
219
- <Text2 className="bold" value={t('common.total')} />
147
+ <View className="w-17 p-4-8 pb-10">
148
+ <Text className="dark right">{line.price ? `${line.price} ${data.paymentCurrency.symbol}` : ''}</Text>
220
149
  </View>
221
- <View className="w-50 p-5">
222
- <Text className="right bold dark">
223
- {(typeof subTotal !== 'undefined' ? subTotal : 0).toFixed(2)} {data.paymentCurrency.symbol}
150
+ <View className="w-18 p-4-8 pb-10">
151
+ <Text className="dark right">
152
+ {line.amount} {data.paymentCurrency.symbol}
224
153
  </Text>
225
154
  </View>
226
155
  </View>
156
+ );
157
+ })}
158
+
159
+ <View className="flex">
160
+ <View className="w-50 mt-10" />
161
+ <View className="w-50 mt-20">
162
+ {summary.map((line) => (
163
+ <View className="flex" key={line.key}>
164
+ <View className="w-50 p-5">
165
+ <Text2 value={t(line.key)} className="bold" />
166
+ </View>
167
+ <View className="w-50 p-5">
168
+ <Text className="right bold dark">
169
+ {line.value} {data.paymentCurrency.symbol}
170
+ </Text>
171
+ </View>
172
+ </View>
173
+ ))}
227
174
  </View>
228
175
  </View>
229
176
  </Page>
package/src/libs/util.ts CHANGED
@@ -197,9 +197,7 @@ export function canChangePaymentMethod(subscription: TSubscriptionExpanded) {
197
197
  }
198
198
 
199
199
  // Steal from https://d3js.org/d3-scale-chromatic/categorical
200
- export function getCategoricalColors(
201
- specifier: string = '4e79a7f28e2ce1575976b7b259a14fedc949af7aa1ff9da79c755fbab0ab'
202
- ) {
200
+ export function getCategoricalColors(specifier: string = '4e79a7f28e2ce1575976b7b259a14fedc949af7aa1ff9da79c755f') {
203
201
  const n = (specifier.length / 6) | 0;
204
202
  const colors = new Array(n);
205
203
  let i = 0;
@@ -18,6 +18,7 @@ import { stringToColor } from '../../libs/util';
18
18
  type TSummary = {
19
19
  balances: GroupedBN;
20
20
  addresses: GroupedBN;
21
+ links: GroupedBN;
21
22
  summary: { [key: string]: { status: string; count: number }[] };
22
23
  };
23
24
 
@@ -66,9 +67,11 @@ export const groupData = (data: TPaymentStat[], currencies: { [key: string]: any
66
67
  grouped[x.timestamp] = { timestamp: formatToDate(x.timestamp * 1000, locale, 'YYYY-MM-DD') };
67
68
  }
68
69
 
69
- const { symbol } = currencies[x.currency_id];
70
- // @ts-ignore
71
- grouped[x.timestamp][symbol] = +x[key];
70
+ if (currencies[x.currency_id]) {
71
+ const { symbol } = currencies[x.currency_id];
72
+ // @ts-ignore
73
+ grouped[x.timestamp][symbol] = +x[key];
74
+ }
72
75
  });
73
76
 
74
77
  return Object.values(grouped);
@@ -77,7 +80,7 @@ export const groupData = (data: TPaymentStat[], currencies: { [key: string]: any
77
80
  export default function Overview() {
78
81
  const { t, locale } = useLocaleContext();
79
82
  const { settings } = usePaymentContext();
80
- const maxDate = dayjs().add(1, 'day').toDate();
83
+ const maxDate = dayjs().toDate();
81
84
  const [state, setState] = useSetState({
82
85
  anchorEl: null,
83
86
  startDate: dayjs().subtract(30, 'day').toDate(),
@@ -256,7 +259,13 @@ export default function Overview() {
256
259
  </Stack>
257
260
  <Stack direction="column" spacing={1} sx={{ mt: 2 }}>
258
261
  {Object.keys(summary.data.balances).map((currencyId) => (
259
- <Card key={currencyId} variant="outlined" sx={{ padding: 1 }}>
262
+ <Card
263
+ key={currencyId}
264
+ component="a"
265
+ href={summary.data?.links[currencyId] as string}
266
+ target="_blank"
267
+ variant="outlined"
268
+ sx={{ padding: 1 }}>
260
269
  <Stack direction="row" alignItems="center">
261
270
  <Avatar
262
271
  src={currencies[currencyId]?.logo}
@@ -1,7 +1,7 @@
1
1
  import DID from '@arcblock/ux/lib/DID';
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
- import { CustomerInvoiceList, formatError, getPrefix } from '@blocklet/payment-react';
4
+ import { CustomerInvoiceList, formatError, getPrefix, usePaymentContext } from '@blocklet/payment-react';
5
5
  import type { GroupedBN, TCustomerExpanded } from '@blocklet/payment-types';
6
6
  import { Edit } from '@mui/icons-material';
7
7
  import { Alert, Box, Button, CircularProgress, Grid, Stack, Tooltip } from '@mui/material';
@@ -32,6 +32,7 @@ const fetchData = (): Promise<Result> => {
32
32
  export default function CustomerHome() {
33
33
  const { t } = useLocaleContext();
34
34
  const { events } = useSessionContext();
35
+ const { livemode, setLivemode } = usePaymentContext();
35
36
  const [state, setState] = useSetState({ editing: false, loading: false });
36
37
  const navigate = useNavigate();
37
38
  const { isPending, startTransition } = useTransitionContext();
@@ -51,6 +52,12 @@ export default function CustomerHome() {
51
52
  });
52
53
  }, []);
53
54
 
55
+ useEffect(() => {
56
+ if (data && data.livemode !== livemode) {
57
+ setLivemode(data.livemode);
58
+ }
59
+ }, [data]);
60
+
54
61
  if (!data) {
55
62
  return <CircularProgress />;
56
63
  }
@@ -89,7 +89,7 @@ export default function CustomerInvoicePastDue() {
89
89
  </Stack>
90
90
  </Stack>
91
91
  <Root direction="column" spacing={3}>
92
- <Alert severity="info">{t('payment.customer.pastDue.warning')}</Alert>
92
+ <Alert severity="error">{t('payment.customer.pastDue.warning')}</Alert>
93
93
  <Box className="section">
94
94
  <SectionHeader title={t('payment.customer.pastDue.invoices')} mb={0}>
95
95
  {subscriptionId && currencyId && (