payment-kit 1.13.22 → 1.13.24

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 (36) hide show
  1. package/api/src/integrations/stripe/handlers/invoice.ts +129 -101
  2. package/api/src/jobs/event.ts +27 -13
  3. package/api/src/jobs/invoice.ts +8 -8
  4. package/api/src/jobs/payment.ts +1 -1
  5. package/api/src/jobs/subscription.ts +1 -1
  6. package/api/src/jobs/webhook.ts +26 -19
  7. package/api/src/libs/audit.ts +3 -3
  8. package/api/src/libs/event.ts +3 -0
  9. package/api/src/libs/util.ts +5 -0
  10. package/api/src/routes/connect/pay.ts +1 -1
  11. package/api/src/routes/subscriptions.ts +15 -0
  12. package/api/src/store/models/types.ts +1 -0
  13. package/blocklet.yml +2 -2
  14. package/package.json +4 -3
  15. package/src/components/actions.tsx +4 -10
  16. package/src/components/blockchain/tx.tsx +38 -9
  17. package/src/components/click-boundary.tsx +7 -0
  18. package/src/components/confirm.tsx +2 -18
  19. package/src/components/customer/actions.tsx +3 -2
  20. package/src/components/invoice/action.tsx +3 -2
  21. package/src/components/payment-intent/actions.tsx +4 -3
  22. package/src/components/payment-intent/list.tsx +2 -2
  23. package/src/components/payment-link/actions.tsx +22 -10
  24. package/src/components/payment-link/item.tsx +18 -15
  25. package/src/components/payment-method/stripe.tsx +7 -4
  26. package/src/components/price/actions.tsx +9 -6
  27. package/src/components/price/form.tsx +4 -1
  28. package/src/components/product/actions.tsx +3 -2
  29. package/src/components/subscription/actions/index.tsx +3 -2
  30. package/src/components/subscription/items/actions.tsx +17 -14
  31. package/src/libs/util.ts +21 -5
  32. package/src/locales/en.tsx +6 -0
  33. package/src/pages/admin/billing/subscriptions/detail.tsx +1 -1
  34. package/src/pages/admin/payments/intents/detail.tsx +1 -6
  35. package/src/pages/admin/products/products/create.tsx +8 -4
  36. package/src/pages/admin/settings/payment-methods/create.tsx +3 -0
@@ -9,6 +9,7 @@ import {
9
9
  Customer,
10
10
  Invoice,
11
11
  InvoiceItem,
12
+ PaymentMethod,
12
13
  Subscription,
13
14
  SubscriptionItem,
14
15
  TEventExpanded,
@@ -29,6 +30,109 @@ export async function handleStripeInvoicePaid(invoice: Invoice, event: TEventExp
29
30
  });
30
31
  }
31
32
 
33
+ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subscription, client: Stripe) {
34
+ const customer = await Customer.findByPk(subscription.customer_id);
35
+ const checkoutSession = await CheckoutSession.findOne({ where: { subscription_id: subscription.id } });
36
+
37
+ let invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoice.id } });
38
+ if (invoice) {
39
+ return invoice;
40
+ }
41
+
42
+ // @ts-ignore
43
+ invoice = await Invoice.create({
44
+ // @ts-ignore
45
+ number: await customer.getInvoiceNumber(),
46
+ ...pick(stripeInvoice, [
47
+ 'amount_due',
48
+ 'amount_paid',
49
+ 'amount_remaining',
50
+ 'amount_shipping',
51
+ 'attempt_count',
52
+ 'attempted',
53
+ 'auto_advance',
54
+ 'billing_reason',
55
+ 'collection_method',
56
+ 'custom_fields',
57
+ 'customer_address',
58
+ 'customer_email',
59
+ 'customer_name',
60
+ 'customer_phone',
61
+ 'description',
62
+ 'discounts',
63
+ 'due_date',
64
+ 'effective_at',
65
+ 'ending_balance',
66
+ 'livemode',
67
+ 'paid_out_of_band',
68
+ 'paid',
69
+ 'period_end',
70
+ 'period_start',
71
+ 'starting_balance',
72
+ 'status_transitions',
73
+ 'status',
74
+ 'subtotal_excluding_tax',
75
+ 'subtotal',
76
+ 'tax',
77
+ 'total_discount_amounts',
78
+ 'total',
79
+ ]),
80
+
81
+ currency_id: subscription.currency_id,
82
+ customer_id: subscription.customer_id,
83
+ default_payment_method_id: subscription.default_payment_method_id as string,
84
+ payment_intent_id: '',
85
+ subscription_id: subscription.id,
86
+ checkout_session_id: checkoutSession?.id,
87
+ statement_descriptor: stripeInvoice.statement_descriptor || '',
88
+
89
+ payment_settings: subscription.payment_settings,
90
+ metadata: {
91
+ stripe_id: stripeInvoice.id,
92
+ },
93
+ });
94
+ await client.invoices.update(stripeInvoice.id, { metadata: { appPid: env.appPid, id: invoice.id } });
95
+ logger.info('stripe invoice mirrored', { local: invoice.id, remote: stripeInvoice.id });
96
+
97
+ await Promise.all(
98
+ stripeInvoice.lines.data.map(async (line: any) => {
99
+ const subscriptionItem = line.subscription_item
100
+ ? await SubscriptionItem.findOne({ where: { 'metadata.stripe_id': line.subscription_item } })
101
+ : null;
102
+
103
+ // @ts-ignore
104
+ const item = await InvoiceItem.create({
105
+ currency_id: subscription.currency_id,
106
+ customer_id: subscription.customer_id,
107
+ price_id: line.price?.metadata.id as string,
108
+ invoice_id: invoice?.id as string,
109
+ subscription_id: subscription.id,
110
+ subscription_item_id: subscriptionItem?.id,
111
+ ...pick(line, [
112
+ 'livemode',
113
+ 'amount',
114
+ 'quantity',
115
+ 'description',
116
+ 'period',
117
+ 'discountable',
118
+ 'discount_amounts',
119
+ 'discounts',
120
+ 'proration',
121
+ 'proration_details',
122
+ ]),
123
+ metadata: {
124
+ stripe_id: line.id,
125
+ },
126
+ });
127
+
128
+ logger.info('stripe invoice items mirrored', { local: item.id, remote: line.id });
129
+ return item;
130
+ })
131
+ );
132
+
133
+ return invoice;
134
+ }
135
+
32
136
  export async function handleStripeInvoiceCreated(event: TEventExpanded, client: Stripe) {
33
137
  if (['invoice.created', 'payment_intent.created'].includes(event.type) === false) {
34
138
  logger.warn('abort because event type not expected', { id: event.id, type: event.type });
@@ -74,98 +178,7 @@ export async function handleStripeInvoiceCreated(event: TEventExpanded, client:
74
178
  const checkoutSession = await CheckoutSession.findOne({ where: { subscription_id: subscription.id } });
75
179
 
76
180
  // create stripe invoice
77
- let invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoice.id } });
78
- if (!invoice) {
79
- invoice = await Invoice.create({
80
- // @ts-ignore
81
- number: await customer.getInvoiceNumber(),
82
- ...pick(stripeInvoice, [
83
- 'amount_due',
84
- 'amount_paid',
85
- 'amount_remaining',
86
- 'amount_shipping',
87
- 'attempt_count',
88
- 'attempted',
89
- 'auto_advance',
90
- 'billing_reason',
91
- 'collection_method',
92
- 'custom_fields',
93
- 'customer_address',
94
- 'customer_email',
95
- 'customer_name',
96
- 'customer_phone',
97
- 'description',
98
- 'discounts',
99
- 'due_date',
100
- 'effective_at',
101
- 'ending_balance',
102
- 'livemode',
103
- 'paid_out_of_band',
104
- 'paid',
105
- 'period_end',
106
- 'period_start',
107
- 'starting_balance',
108
- 'status_transitions',
109
- 'status',
110
- 'subtotal_excluding_tax',
111
- 'subtotal',
112
- 'tax',
113
- 'total_discount_amounts',
114
- 'total',
115
- ]),
116
-
117
- currency_id: subscription.currency_id,
118
- customer_id: subscription.customer_id,
119
- default_payment_method_id: subscription.default_payment_method_id as string,
120
- payment_intent_id: '',
121
- subscription_id: subscription.id,
122
- checkout_session_id: checkoutSession?.id,
123
- statement_descriptor: stripeInvoice.statement_descriptor || '',
124
-
125
- payment_settings: subscription.payment_settings,
126
- metadata: {
127
- stripe_id: stripeInvoice.id,
128
- },
129
- });
130
- await client.invoices.update(stripeInvoice.id, { metadata: { appPid: env.appPid, id: invoice.id } });
131
- logger.info('stripe invoice mirrored', { local: invoice.id, remote: stripeInvoice.id });
132
-
133
- await Promise.all(
134
- stripeInvoice.lines.data.map(async (line) => {
135
- const subscriptionItem = line.subscription_item
136
- ? await SubscriptionItem.findOne({ where: { 'metadata.stripe_id': line.subscription_item } })
137
- : null;
138
-
139
- // @ts-ignore
140
- const item = await InvoiceItem.create({
141
- currency_id: subscription.currency_id,
142
- customer_id: subscription.customer_id,
143
- price_id: line.price?.metadata.id as string,
144
- invoice_id: invoice?.id as string,
145
- subscription_id: subscription.id,
146
- subscription_item_id: subscriptionItem?.id,
147
- ...pick(line, [
148
- 'livemode',
149
- 'amount',
150
- 'quantity',
151
- 'description',
152
- 'period',
153
- 'discountable',
154
- 'discount_amounts',
155
- 'discounts',
156
- 'proration',
157
- 'proration_details',
158
- ]),
159
- metadata: {
160
- stripe_id: line.id,
161
- },
162
- });
163
-
164
- logger.info('stripe invoice items mirrored', { local: item.id, remote: line.id });
165
- return item;
166
- })
167
- );
168
- }
181
+ const invoice = await ensureStripeInvoice(stripeInvoice, subscription, client);
169
182
 
170
183
  return { subscription, invoice, customer, checkoutSession };
171
184
  }
@@ -186,16 +199,32 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
186
199
  return;
187
200
  }
188
201
 
189
- const localInvoiceId = event.data.object.metadata?.id;
202
+ let localInvoiceId = event.data.object.metadata?.id;
203
+
204
+ // in case we missed some of the events
205
+ const subscriptionId = event.data.object.subscription_details?.metadata?.id;
206
+ const appPid = event.data.object.subscription_details?.metadata?.appPid;
190
207
  if (!localInvoiceId) {
191
- try {
192
- await waitForStripeInvoiceMirrored(event.data.object.id);
193
- } catch (err) {
194
- logger.error('wait for stripe invoice mirror error', { localInvoiceId, error: err });
195
- }
208
+ if (subscriptionId && appPid && appPid === env.appPid) {
209
+ logger.warn('try mirror invoice from stripe', { invoiceId: event.data.object.id });
210
+ const subscription = await Subscription.findByPk(subscriptionId);
211
+ if (subscription) {
212
+ const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
213
+ if (method && method.type === 'stripe') {
214
+ const tmp = await ensureStripeInvoice(event.data.object, subscription, method.getStripe());
215
+ localInvoiceId = tmp.id;
216
+ }
217
+ }
218
+ } else {
219
+ try {
220
+ await waitForStripeInvoiceMirrored(event.data.object.id);
221
+ } catch (err) {
222
+ logger.error('wait for stripe invoice mirror error', { localInvoiceId, error: err });
223
+ }
196
224
 
197
- logger.warn('local invoice id not found in strip event', { localInvoiceId });
198
- return;
225
+ logger.warn('local invoice id not found in strip event', { localInvoiceId });
226
+ return;
227
+ }
199
228
  }
200
229
 
201
230
  const invoice = await Invoice.findByPk(localInvoiceId);
@@ -232,7 +261,6 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
232
261
  return;
233
262
  }
234
263
 
235
- // TODO: handle upcoming invoices?
236
264
  if (event.type === 'invoice.upcoming') {
237
265
  logger.info('received invoice upcoming event', event);
238
266
  }
@@ -1,44 +1,54 @@
1
1
  import { Op } from 'sequelize';
2
2
 
3
+ import { events } from '../libs/event';
3
4
  import logger from '../libs/logger';
4
5
  import createQueue from '../libs/queue';
6
+ import { getWebhookJobId } from '../libs/util';
5
7
  import { Event } from '../store/models/event';
8
+ import { WebhookAttempt } from '../store/models/webhook-attempt';
6
9
  import { WebhookEndpoint } from '../store/models/webhook-endpoint';
7
- import { getJobId, webhookQueue } from './webhook';
10
+ import { webhookQueue } from './webhook';
8
11
 
9
12
  type EventJob = {
10
13
  eventId: string;
11
14
  };
12
15
 
13
16
  export const handleEvent = async (job: EventJob) => {
14
- logger.info('handleEvent', job);
17
+ logger.info('handle event', job);
15
18
 
16
19
  const event = await Event.findByPk(job.eventId);
17
20
  if (!event) {
18
- logger.warn(`Event not found: ${job.eventId}`);
21
+ logger.warn('event not found', job);
19
22
  return;
20
23
  }
21
24
 
22
25
  if (!event.pending_webhooks) {
23
- logger.warn(`Event already processed: ${job.eventId}`);
26
+ logger.warn('event already processed', job);
24
27
  return;
25
28
  }
26
29
 
27
30
  const webhooks = await WebhookEndpoint.findAll({ where: { status: 'enabled', livemode: event.livemode } });
28
31
  const eventWebhooks = webhooks.filter((webhook) => webhook.enabled_events.includes(event.type));
29
32
  if (eventWebhooks.length === 0) {
30
- logger.info(`No webhook endpoint for event: ${job.eventId}`);
33
+ logger.info('no webhook endpoint for event', job);
31
34
  await event.update({ pending_webhooks: 0 });
32
35
  return;
33
36
  }
34
37
 
35
38
  await event.update({ pending_webhooks: eventWebhooks.length });
36
- eventWebhooks.forEach((webhook) => {
37
- logger.info(`Schedule webhook ${webhook.id} attempt for event: ${job.eventId}`);
38
- webhookQueue.push({
39
- id: getJobId(event.id, webhook.id),
40
- job: { eventId: event.id, webhookId: webhook.id },
39
+ eventWebhooks.forEach(async (webhook) => {
40
+ const attempted = await WebhookAttempt.findOne({
41
+ where: { event_id: event.id, webhook_endpoint_id: webhook.id },
41
42
  });
43
+
44
+ // we should only push webhook if it's not attempted before
45
+ if (!attempted) {
46
+ logger.info('schedule initial attempt for event', job);
47
+ webhookQueue.push({
48
+ id: getWebhookJobId(event.id, webhook.id),
49
+ job: { eventId: event.id, webhookId: webhook.id },
50
+ });
51
+ }
42
52
  });
43
53
  };
44
54
 
@@ -52,14 +62,14 @@ export const eventQueue = createQueue<EventJob>({
52
62
  });
53
63
 
54
64
  export const startEventQueue = async () => {
55
- const events = await Event.findAll({
65
+ const docs = await Event.findAll({
56
66
  where: {
57
67
  pending_webhooks: { [Op.gt]: 0 },
58
68
  },
59
69
  attributes: ['id'],
60
70
  });
61
71
 
62
- events.forEach(async (x) => {
72
+ docs.forEach(async (x) => {
63
73
  const exist = await eventQueue.get(x.id);
64
74
  if (!exist) {
65
75
  eventQueue.push({ id: x.id, job: { eventId: x.id } });
@@ -68,5 +78,9 @@ export const startEventQueue = async () => {
68
78
  };
69
79
 
70
80
  eventQueue.on('failed', ({ id, job, error }) => {
71
- logger.error('Event job failed', { id, job, error });
81
+ logger.error('event job failed', { id, job, error });
82
+ });
83
+
84
+ events.on('event.created', (event) => {
85
+ eventQueue.push({ id: event.id, job: { eventId: event.id } });
72
86
  });
@@ -18,31 +18,31 @@ type InvoiceJob = {
18
18
  // handle invoice payment
19
19
  // TODO: send invoice to user with email
20
20
  export const handleInvoice = async (job: InvoiceJob) => {
21
- logger.info('handleInvoice', job);
21
+ logger.info('handle invoice', job);
22
22
 
23
23
  const invoice = await Invoice.findByPk(job.invoiceId);
24
24
  if (!invoice) {
25
- logger.warn(`Invoice not found: ${job.invoiceId}`);
25
+ logger.warn(`invoice not found: ${job.invoiceId}`);
26
26
  return;
27
27
  }
28
28
  if (invoice.status !== 'open') {
29
- logger.warn(`Invoice not open: ${job.invoiceId}`);
29
+ logger.warn(`invoice not open: ${job.invoiceId}`);
30
30
  return;
31
31
  }
32
32
  if (invoice.auto_advance === false) {
33
- logger.warn(`Invoice not configured to auto advance: ${job.invoiceId}`);
33
+ logger.warn(`invoice not configured to auto advance: ${job.invoiceId}`);
34
34
  return;
35
35
  }
36
36
 
37
37
  const supportAutoCharge = await PaymentMethod.supportAutoCharge(invoice.default_payment_method_id);
38
38
  if (supportAutoCharge === false) {
39
- logger.warn(`Invoice does not support auto charge: ${job.invoiceId}`);
39
+ logger.warn(`invoice does not support auto charge: ${job.invoiceId}`);
40
40
  return;
41
41
  }
42
42
 
43
43
  // no payment required
44
44
  if (invoice.total === '0') {
45
- logger.warn(`Invoice does not require payment: ${job.invoiceId}`);
45
+ logger.warn(`invoice does not require payment: ${job.invoiceId}`);
46
46
 
47
47
  await invoice.update({
48
48
  paid: true,
@@ -59,7 +59,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
59
59
  const subscription = await Subscription.findByPk(invoice.subscription_id);
60
60
  if (subscription && subscription.status === 'incomplete') {
61
61
  await subscription.update({ status: subscription.trail_end ? 'trialing' : 'active' });
62
- logger.info('Invoice subscription updated', subscription.id);
62
+ logger.info('invoice subscription updated', subscription.id);
63
63
  }
64
64
  }
65
65
 
@@ -67,7 +67,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
67
67
  const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
68
68
  if (checkoutSession && checkoutSession.status === 'open') {
69
69
  await checkoutSession.update({ status: 'complete', payment_status: 'no_payment_required' });
70
- logger.info('Invoice checkout session updated', checkoutSession.id);
70
+ logger.info('invoice checkout session updated', checkoutSession.id);
71
71
  }
72
72
  }
73
73
 
@@ -19,7 +19,7 @@ type PaymentJob = {
19
19
  };
20
20
 
21
21
  export const handlePayment = async (job: PaymentJob) => {
22
- logger.info('handlePayment', job);
22
+ logger.info('handle payment', job);
23
23
 
24
24
  const paymentIntent = await PaymentIntent.findByPk(job.paymentIntentId);
25
25
  if (!paymentIntent) {
@@ -21,7 +21,7 @@ type SubscriptionJob = {
21
21
 
22
22
  // generate invoice for subscription periodically
23
23
  export const handleSubscription = async (job: SubscriptionJob) => {
24
- logger.info('handleSubscription', job);
24
+ logger.info('handle subscription', job);
25
25
 
26
26
  const subscription = await Subscription.findByPk(job.subscriptionId);
27
27
  if (!subscription) {
@@ -4,8 +4,10 @@ import axios, { AxiosError } from 'axios';
4
4
  import { wallet } from '../libs/auth';
5
5
  import logger from '../libs/logger';
6
6
  import createQueue from '../libs/queue';
7
- import { MAX_RETRY_COUNT, getNextRetry, md5 } from '../libs/util';
7
+ import { MAX_RETRY_COUNT, getNextRetry, getWebhookJobId } from '../libs/util';
8
+ import { Customer } from '../store/models/customer';
8
9
  import { Event } from '../store/models/event';
10
+ import { PaymentCurrency } from '../store/models/payment-currency';
9
11
  import { WebhookAttempt } from '../store/models/webhook-attempt';
10
12
  import { WebhookEndpoint } from '../store/models/webhook-endpoint';
11
13
 
@@ -14,49 +16,54 @@ type WebhookJob = {
14
16
  webhookId: string;
15
17
  };
16
18
 
17
- export const getJobId = (eventId: string, webhookId: string) => {
18
- return md5([eventId, webhookId].join('-'));
19
- };
20
-
21
19
  // https://stripe.com/docs/webhooks
22
20
  export const handleWebhook = async (job: WebhookJob) => {
23
- logger.info('handleWebhook', job);
21
+ logger.info('handle webhook', job);
24
22
 
25
23
  const event = await Event.findByPk(job.eventId);
26
24
  if (!event) {
27
- logger.warn(`Event not found when attempt webhook: ${job.eventId}`);
25
+ logger.warn('event not found when attempt webhook', job);
28
26
  return;
29
27
  }
30
28
 
31
29
  const webhook = await WebhookEndpoint.findByPk(job.webhookId);
32
30
  if (!webhook) {
33
- logger.warn(`Webhook not found on attempt: ${job.webhookId}`);
31
+ logger.warn('webhook not found on attempt', job);
34
32
  return;
35
33
  }
36
34
  if (webhook.status !== 'enabled') {
37
- logger.warn(`Webhook disabled on attempt: ${job.webhookId}`);
35
+ logger.warn('webhook disabled on attempt', job);
38
36
  return;
39
37
  }
40
38
 
41
- const lastAttempt = await WebhookAttempt.findOne({
39
+ const lastRetryCount = await WebhookAttempt.max('retry_count', {
42
40
  where: { event_id: event.id, webhook_endpoint_id: webhook.id },
43
- order: [['retry_count', 'DESC']],
44
- attributes: ['retry_count'],
45
41
  });
46
42
 
47
- const retryCount = lastAttempt ? lastAttempt.retry_count + 1 : 0;
43
+ const retryCount = lastRetryCount ? +lastRetryCount + 1 : 1;
48
44
 
49
45
  try {
46
+ const json = event.toJSON();
47
+
48
+ // expand basic fields
49
+ const { object } = json.data;
50
+ if (object.customer_id && !object.customer) {
51
+ object.customer = await Customer.findByPk(object.customer_id);
52
+ }
53
+ if (object.currency_id && !object.currency) {
54
+ object.currency = await PaymentCurrency.findByPk(object.currency_id);
55
+ }
56
+
50
57
  // verify similar to component call, but supports external urls
51
58
  const result = await axios({
52
59
  url: webhook.url,
53
60
  method: 'POST',
54
61
  timeout: 60 * 1000,
55
- data: event.toJSON(),
62
+ data: json,
56
63
  headers: {
57
64
  'x-app-id': wallet.address,
58
65
  'x-app-pk': wallet.publicKey,
59
- 'x-component-sig': sign(event.toJSON()),
66
+ 'x-component-sig': sign(json),
60
67
  'x-component-did': process.env.BLOCKLET_COMPONENT_DID as string,
61
68
  },
62
69
  });
@@ -72,9 +79,9 @@ export const handleWebhook = async (job: WebhookJob) => {
72
79
  });
73
80
 
74
81
  await event.decrement('pending_webhooks');
75
- logger.info(`Webhook attempt success: ${job.webhookId}`);
82
+ logger.info('webhook attempt success', { ...job, retryCount });
76
83
  } catch (err: any) {
77
- logger.error(`Webhook attempt error: ${job.webhookId}`, { message: err.message });
84
+ logger.warn('webhook attempt error', { ...job, retryCount, message: err.message });
78
85
  await WebhookAttempt.create({
79
86
  livemode: event.livemode,
80
87
  event_id: event.id,
@@ -89,9 +96,9 @@ export const handleWebhook = async (job: WebhookJob) => {
89
96
  if (retryCount < MAX_RETRY_COUNT) {
90
97
  process.nextTick(() => {
91
98
  webhookQueue.push({
92
- id: getJobId(event.id, webhook.id),
99
+ id: getWebhookJobId(event.id, webhook.id),
93
100
  job: { eventId: event.id, webhookId: webhook.id },
94
- runAt: getNextRetry(retryCount + 1),
101
+ runAt: getNextRetry(retryCount),
95
102
  });
96
103
  });
97
104
  }
@@ -1,7 +1,7 @@
1
1
  import pick from 'lodash/pick';
2
2
 
3
- import { eventQueue } from '../jobs/event';
4
3
  import { Event } from '../store/models/event';
4
+ import { events } from './event';
5
5
 
6
6
  export async function createEvent(scope: string, type: string, model: any, options: any) {
7
7
  // console.log('createEvent', scope, type, model, options);
@@ -28,7 +28,7 @@ export async function createEvent(scope: string, type: string, model: any, optio
28
28
  pending_webhooks: 99, // force all events goto the event queue
29
29
  });
30
30
 
31
- eventQueue.push({ id: event.id, job: { eventId: event.id } });
31
+ events.emit('event.created', { id: event.id });
32
32
  }
33
33
 
34
34
  export async function createStatusEvent(
@@ -69,5 +69,5 @@ export async function createStatusEvent(
69
69
  pending_webhooks: 99, // force all events goto the event queue
70
70
  });
71
71
 
72
- eventQueue.push({ id: event.id, job: { eventId: event.id } });
72
+ events.emit('event.created', { id: event.id });
73
73
  }
@@ -0,0 +1,3 @@
1
+ import EventEmitter from 'events';
2
+
3
+ export const events = new EventEmitter();
@@ -66,6 +66,7 @@ export function createCodeGenerator(prefix: string, size: number = 24) {
66
66
  return prefix ? () => `${prefix}_${generator()}` : generator;
67
67
  }
68
68
 
69
+ // FIXME: merge with old metadata
69
70
  export function formatMetadata(metadata?: Record<string, any>): Record<string, any> {
70
71
  if (!metadata) {
71
72
  return {};
@@ -137,3 +138,7 @@ export const getNextRetry = (retryCount: number) => {
137
138
  const now = dayjs().unix();
138
139
  return now + delay;
139
140
  };
141
+
142
+ export const getWebhookJobId = (eventId: string, webhookId: string) => {
143
+ return md5([eventId, webhookId].join('-'));
144
+ };
@@ -8,7 +8,7 @@ import dayjs from '../../libs/dayjs';
8
8
  import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
9
9
 
10
10
  export default {
11
- action: 'pay',
11
+ action: 'payment',
12
12
  authPrincipal: false,
13
13
  claims: {
14
14
  authPrincipal: async ({ extraParams }: CallbackArgs) => {
@@ -8,6 +8,7 @@ import dayjs from '../libs/dayjs';
8
8
  import logger from '../libs/logger';
9
9
  import { authenticate } from '../libs/security';
10
10
  import { expandLineItems } from '../libs/session';
11
+ import { formatMetadata } from '../libs/util';
11
12
  import { Customer } from '../store/models/customer';
12
13
  import { PaymentCurrency } from '../store/models/payment-currency';
13
14
  import { PaymentMethod } from '../store/models/payment-method';
@@ -302,4 +303,18 @@ router.put('/:id/resume', auth, async (req, res) => {
302
303
  return res.json(doc);
303
304
  });
304
305
 
306
+ router.put('/:id', auth, async (req, res) => {
307
+ const doc = await Subscription.findByPk(req.params.id);
308
+
309
+ if (!doc) {
310
+ return res.status(404).json({ error: 'Subscription not found' });
311
+ }
312
+
313
+ if (req.body.metadata) {
314
+ await doc.update({ metadata: formatMetadata(req.body.metadata) });
315
+ }
316
+
317
+ return res.json(doc);
318
+ });
319
+
305
320
  export default router;
@@ -210,6 +210,7 @@ export type PaymentMethodOptions = {
210
210
 
211
211
  export type PaymentMethodSettings = {
212
212
  stripe?: {
213
+ dashboard: string;
213
214
  publishable_key: string;
214
215
  secret_key: string;
215
216
  webhook_signing_secret: string;
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.22
17
+ version: 1.13.24
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
@@ -40,7 +40,7 @@ payment:
40
40
  timeout:
41
41
  start: 60
42
42
  requirements:
43
- server: '>=1.16.15'
43
+ server: '>=1.16.10'
44
44
  os: '*'
45
45
  cpu: '*'
46
46
  scripts: