@mathrunet/masamune_cloudflare_purchase_stripe 3.2.0

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 (91) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/LICENSE +21 -0
  3. package/README.md +174 -0
  4. package/dist/functions.d.ts +44 -0
  5. package/dist/functions.js +47 -0
  6. package/dist/functions.js.map +1 -0
  7. package/dist/index.d.ts +25 -0
  8. package/dist/index.js +42 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/lib/meter/d1_usage_event_store.d.ts +128 -0
  11. package/dist/lib/meter/d1_usage_event_store.js +78 -0
  12. package/dist/lib/meter/d1_usage_event_store.js.map +1 -0
  13. package/dist/lib/meter/flush_meter.d.ts +19 -0
  14. package/dist/lib/meter/flush_meter.js +38 -0
  15. package/dist/lib/meter/flush_meter.js.map +1 -0
  16. package/dist/lib/meter/interface.d.ts +109 -0
  17. package/dist/lib/meter/interface.js +3 -0
  18. package/dist/lib/meter/interface.js.map +1 -0
  19. package/dist/lib/meter/kv_usage_buffer.d.ts +56 -0
  20. package/dist/lib/meter/kv_usage_buffer.js +50 -0
  21. package/dist/lib/meter/kv_usage_buffer.js.map +1 -0
  22. package/dist/lib/meter/record_usage.d.ts +29 -0
  23. package/dist/lib/meter/record_usage.js +72 -0
  24. package/dist/lib/meter/record_usage.js.map +1 -0
  25. package/dist/lib/meter/stripe_meter_client.d.ts +76 -0
  26. package/dist/lib/meter/stripe_meter_client.js +74 -0
  27. package/dist/lib/meter/stripe_meter_client.js.map +1 -0
  28. package/dist/lib/options.d.ts +179 -0
  29. package/dist/lib/options.js +132 -0
  30. package/dist/lib/options.js.map +1 -0
  31. package/dist/lib/purchase/d1_purchase_store.d.ts +78 -0
  32. package/dist/lib/purchase/d1_purchase_store.js +194 -0
  33. package/dist/lib/purchase/d1_purchase_store.js.map +1 -0
  34. package/dist/lib/purchase/helpers.d.ts +36 -0
  35. package/dist/lib/purchase/helpers.js +64 -0
  36. package/dist/lib/purchase/helpers.js.map +1 -0
  37. package/dist/lib/purchase/interface.d.ts +182 -0
  38. package/dist/lib/purchase/interface.js +27 -0
  39. package/dist/lib/purchase/interface.js.map +1 -0
  40. package/dist/lib/purchase/sync_payment.d.ts +15 -0
  41. package/dist/lib/purchase/sync_payment.js +79 -0
  42. package/dist/lib/purchase/sync_payment.js.map +1 -0
  43. package/dist/lib/stripe_client.d.ts +55 -0
  44. package/dist/lib/stripe_client.js +50 -0
  45. package/dist/lib/stripe_client.js.map +1 -0
  46. package/dist/meter.d.ts +21 -0
  47. package/dist/meter.js +38 -0
  48. package/dist/meter.js.map +1 -0
  49. package/dist/purchase.d.ts +58 -0
  50. package/dist/purchase.js +48 -0
  51. package/dist/purchase.js.map +1 -0
  52. package/dist/workers/stripe.d.ts +1 -0
  53. package/dist/workers/stripe.js +861 -0
  54. package/dist/workers/stripe.js.map +1 -0
  55. package/dist/workers/stripe_webhook.d.ts +1 -0
  56. package/dist/workers/stripe_webhook.js +384 -0
  57. package/dist/workers/stripe_webhook.js.map +1 -0
  58. package/dist/workers/stripe_webhook_connect.d.ts +1 -0
  59. package/dist/workers/stripe_webhook_connect.js +97 -0
  60. package/dist/workers/stripe_webhook_connect.js.map +1 -0
  61. package/dist/workers/stripe_webhook_secure.d.ts +1 -0
  62. package/dist/workers/stripe_webhook_secure.js +82 -0
  63. package/dist/workers/stripe_webhook_secure.js.map +1 -0
  64. package/jest.config.json +19 -0
  65. package/package.json +56 -0
  66. package/src/functions.ts +48 -0
  67. package/src/index.ts +25 -0
  68. package/src/lib/meter/d1_usage_event_store.ts +176 -0
  69. package/src/lib/meter/flush_meter.ts +45 -0
  70. package/src/lib/meter/interface.ts +118 -0
  71. package/src/lib/meter/kv_usage_buffer.ts +79 -0
  72. package/src/lib/meter/record_usage.ts +87 -0
  73. package/src/lib/meter/stripe_meter_client.ts +117 -0
  74. package/src/lib/options.ts +299 -0
  75. package/src/lib/purchase/d1_purchase_store.ts +268 -0
  76. package/src/lib/purchase/helpers.ts +77 -0
  77. package/src/lib/purchase/interface.ts +215 -0
  78. package/src/lib/purchase/sync_payment.ts +95 -0
  79. package/src/lib/stripe_client.ts +76 -0
  80. package/src/meter.ts +21 -0
  81. package/src/purchase.ts +66 -0
  82. package/src/workers/stripe.ts +881 -0
  83. package/src/workers/stripe_webhook.ts +414 -0
  84. package/src/workers/stripe_webhook_connect.ts +107 -0
  85. package/src/workers/stripe_webhook_secure.ts +91 -0
  86. package/test/helpers/mem_store.ts +97 -0
  87. package/test/meter.test.ts +217 -0
  88. package/test/purchase_store.test.ts +154 -0
  89. package/test/stripe.test.ts +146 -0
  90. package/test/stripe_webhook.test.ts +174 -0
  91. package/tsconfig.json +14 -0
@@ -0,0 +1,414 @@
1
+ import { Hono } from "hono";
2
+ import { jsonError } from "@mathrunet/masamune_cloudflare/dist/lib/src/http_error";
3
+ import {
4
+ resolveStripeClient,
5
+ resolveStripePurchaseStore,
6
+ resolveStripeSecretKey,
7
+ resolveStripeWebhookSecret,
8
+ StripePurchaseWorkersOptions,
9
+ } from "../lib/options";
10
+ import { constructStripeEvent } from "../lib/stripe_client";
11
+ import { syncStripePayment } from "../lib/purchase/sync_payment";
12
+
13
+ /**
14
+ * Receives and processes webhooks from Stripe (parity with the `stripeWebhook`
15
+ * function of `@mathrunet/masamune_firebase_purchase_stripe`).
16
+ * Please register the URL when you deploy this in your Stripe webhook settings.
17
+ *
18
+ * StripeからのWebhookを受け取り処理を行います
19
+ * (`@mathrunet/masamune_firebase_purchase_stripe`の`stripeWebhook`と同等)。
20
+ * こちらをデプロイした際のURLをStripeのWebhook設定に登録してください。
21
+ *
22
+ * @param {string} PURCHASE_STRIPE_SECRETKEY
23
+ * API key (secret key) to connect to Stripe.
24
+ * Stripeへ接続するためのAPIキー(シークレットキー)。
25
+ *
26
+ * @param {string} PURCHASE_STRIPE_WEBHOOKSECRET
27
+ * Specify the **Signature Secret** after setting it up as a webhook.
28
+ * Webhookとして設定したあとの**署名シークレット**を指定します。
29
+ */
30
+ module.exports = (
31
+ hono: Hono,
32
+ options: StripePurchaseWorkersOptions,
33
+ data: { [key: string]: any },
34
+ ) => {
35
+ hono.post("/", async (c) => {
36
+ try {
37
+ const apiKey = resolveStripeSecretKey(c, options);
38
+ const webhookSecret = resolveStripeWebhookSecret(c, options);
39
+ const store = resolveStripePurchaseStore(c, options);
40
+ const stripeClient = resolveStripeClient(c, options, apiKey);
41
+ const signature = c.req.header("stripe-signature");
42
+ if (!signature || !webhookSecret) {
43
+ return c.json({
44
+ "error": "Access denied.",
45
+ }, 403);
46
+ }
47
+ const payload = await c.req.text();
48
+ let event;
49
+ try {
50
+ event = await constructStripeEvent({
51
+ client: stripeClient,
52
+ payload,
53
+ signature,
54
+ secret: webhookSecret,
55
+ });
56
+ } catch (err) {
57
+ console.warn(err);
58
+ return c.json({
59
+ "error": "Access denied.",
60
+ }, 403);
61
+ }
62
+
63
+ switch (event.type) {
64
+ case "payment_intent.requires_action":
65
+ case "payment_intent.amount_capturable_updated": {
66
+ const payment = event.data.object as {
67
+ [key: string]: any
68
+ };
69
+ const purchaseId = payment["id"];
70
+ const customerId = payment["customer"];
71
+ const status = payment["status"];
72
+ if (!customerId) {
73
+ return c.json({
74
+ "error": "The customer id is not found.",
75
+ }, 404);
76
+ }
77
+ if (!status) {
78
+ return c.json({
79
+ "error": "The status is not found.",
80
+ }, 404);
81
+ }
82
+ const user = await store.findUserByCustomerId(customerId);
83
+ if (!user) {
84
+ return c.json({
85
+ "error": "The account data is not found.",
86
+ }, 404);
87
+ }
88
+ const purchase = await store.findPurchaseByPurchaseId(purchaseId);
89
+ if (!purchase) {
90
+ return c.json({
91
+ "error": "The purchase data is not found.",
92
+ }, 404);
93
+ }
94
+ const update: { [key: string]: any } = {};
95
+ switch (status) {
96
+ case "requires_payment_method":
97
+ case "requires_confirmation": {
98
+ update["confirm"] = false;
99
+ update["verify"] = false;
100
+ update["capture"] = false;
101
+ update["success"] = false;
102
+ break;
103
+ }
104
+ case "requires_action": {
105
+ update["confirm"] = true;
106
+ update["verify"] = false;
107
+ update["capture"] = false;
108
+ update["success"] = false;
109
+ break;
110
+ }
111
+ case "requires_capture": {
112
+ update["confirm"] = true;
113
+ update["verify"] = true;
114
+ update["capture"] = false;
115
+ update["success"] = false;
116
+ break;
117
+ }
118
+ }
119
+ update["updatedTime"] = new Date();
120
+ await store.savePurchase(purchase.orderId, update);
121
+ return c.json({
122
+ "success": true,
123
+ });
124
+ }
125
+ case "payment_intent.succeeded": {
126
+ const payment = event.data.object as {
127
+ [key: string]: any
128
+ };
129
+ const purchaseId = payment["id"];
130
+ const customerId = payment["customer"];
131
+ if (!customerId) {
132
+ return c.json({
133
+ "error": "The customer id is not found.",
134
+ }, 404);
135
+ }
136
+ const user = await store.findUserByCustomerId(customerId);
137
+ if (!user) {
138
+ return c.json({
139
+ "error": "The account data is not found.",
140
+ }, 404);
141
+ }
142
+ const purchase = await store.findPurchaseByPurchaseId(purchaseId);
143
+ if (!purchase) {
144
+ return c.json({
145
+ "error": "The purchase data is not found.",
146
+ }, 404);
147
+ }
148
+ const update: { [key: string]: any } = {};
149
+ update["confirm"] = true;
150
+ update["verify"] = true;
151
+ update["capture"] = true;
152
+ update["success"] = true;
153
+ update["error"] = null;
154
+ update["errorMessage"] = null;
155
+ update["updatedTime"] = new Date();
156
+
157
+ if (payment["charges"] && payment["charges"]["data"] && payment["charges"]["data"].length > 0 && payment["charges"]["data"][0]) {
158
+ if (payment["charges"]["data"][0]["receipt_url"]) {
159
+ update["receiptUrl"] = payment["charges"]["data"][0]["receipt_url"];
160
+ }
161
+ if (payment["charges"]["data"][0]["amount_captured"]) {
162
+ update["capturedAmount"] = payment["charges"]["data"][0]["amount_captured"];
163
+ }
164
+ }
165
+ await store.savePurchase(purchase.orderId, update);
166
+ return c.json({
167
+ "success": true,
168
+ });
169
+ }
170
+ case "payment_intent.payment_failed": {
171
+ const payment = event.data.object as {
172
+ [key: string]: any
173
+ };
174
+ const purchaseId = payment["id"];
175
+ const customerId = payment["customer"];
176
+ const status = payment["status"];
177
+ if (!customerId) {
178
+ return c.json({
179
+ "error": "The customer id is not found.",
180
+ }, 404);
181
+ }
182
+ const user = await store.findUserByCustomerId(customerId);
183
+ if (!user) {
184
+ return c.json({
185
+ "error": "The account data is not found.",
186
+ }, 404);
187
+ }
188
+ const purchase = await store.findPurchaseByPurchaseId(purchaseId);
189
+ if (!purchase) {
190
+ return c.json({
191
+ "error": "The purchase data is not found.",
192
+ }, 404);
193
+ }
194
+ const errorMessage = payment["last_payment_error"]?.["message"];
195
+ const update: { [key: string]: any } = {};
196
+ switch (status) {
197
+ case "requires_payment_method":
198
+ case "requires_confirmation": {
199
+ update["confirm"] = false;
200
+ update["verify"] = false;
201
+ update["capture"] = false;
202
+ update["success"] = false;
203
+ break;
204
+ }
205
+ case "requires_action": {
206
+ update["confirm"] = true;
207
+ update["verify"] = false;
208
+ update["capture"] = false;
209
+ update["success"] = false;
210
+ break;
211
+ }
212
+ case "requires_capture": {
213
+ update["confirm"] = true;
214
+ update["verify"] = true;
215
+ update["capture"] = false;
216
+ update["success"] = false;
217
+ break;
218
+ }
219
+ }
220
+ update["updatedTime"] = new Date();
221
+ update["error"] = true;
222
+ update["errorMessage"] = errorMessage;
223
+ await store.savePurchase(purchase.orderId, update);
224
+ return c.json({
225
+ "success": true,
226
+ });
227
+ }
228
+ case "payment_method.detached":
229
+ case "payment_method.updated": {
230
+ const payment = event.data.object as {
231
+ [key: string]: any
232
+ };
233
+ const previous = event.data.previous_attributes as {
234
+ [key: string]: any
235
+ };
236
+ let customerId = payment["customer"];
237
+ if (!customerId) {
238
+ customerId = previous?.["customer"];
239
+ }
240
+ if (!customerId) {
241
+ return c.json({
242
+ "error": "The customer id is not found.",
243
+ }, 404);
244
+ }
245
+ const user = await store.findUserByCustomerId(customerId);
246
+ if (!user) {
247
+ return c.json({
248
+ "error": "The account data is not found.",
249
+ }, 404);
250
+ }
251
+ await syncStripePayment({
252
+ stripeClient,
253
+ store,
254
+ userId: user.userId,
255
+ customerId,
256
+ });
257
+ return c.json({
258
+ "success": true,
259
+ });
260
+ }
261
+ case "customer.updated": {
262
+ const customer = event.data.object as {
263
+ [key: string]: any
264
+ };
265
+ const customerId = customer["id"];
266
+ if (!customerId) {
267
+ return c.json({
268
+ "error": "The customer id is not found.",
269
+ }, 404);
270
+ }
271
+ const user = await store.findUserByCustomerId(customerId);
272
+ if (!user) {
273
+ return c.json({
274
+ "error": "The account data is not found.",
275
+ }, 404);
276
+ }
277
+ await syncStripePayment({
278
+ stripeClient,
279
+ store,
280
+ userId: user.userId,
281
+ customerId,
282
+ });
283
+ return c.json({
284
+ "success": true,
285
+ });
286
+ }
287
+ case "checkout.session.completed": {
288
+ const session = event.data.object as {
289
+ [key: string]: any
290
+ };
291
+ const customerId = session["customer"];
292
+ if (!customerId) {
293
+ return c.json({
294
+ "error": "The customer id is not found.",
295
+ }, 404);
296
+ }
297
+ const setupIntent = session["setup_intent"];
298
+ if (!setupIntent) {
299
+ return c.json({
300
+ "error": "The setup intent is not found.",
301
+ }, 404);
302
+ }
303
+ const user = await store.findUserByCustomerId(customerId);
304
+ if (!user) {
305
+ return c.json({
306
+ "error": "The account data is not found.",
307
+ }, 404);
308
+ }
309
+ await store.saveUser(user.userId, {
310
+ setupIntent: session["setup_intent"],
311
+ });
312
+ await syncStripePayment({
313
+ stripeClient,
314
+ store,
315
+ userId: user.userId,
316
+ customerId,
317
+ });
318
+ return c.json({
319
+ "success": true,
320
+ });
321
+ }
322
+ case "customer.subscription.trial_will_end":
323
+ case "customer.subscription.updated":
324
+ case "customer.subscription.created": {
325
+ const now = new Date();
326
+ const update: { [key: string]: any } = {};
327
+ const subscription = event.data.object as {
328
+ [key: string]: any
329
+ };
330
+ const status = subscription["status"];
331
+ if (status != "active") {
332
+ return c.json({
333
+ "success": "Subscription is not active.",
334
+ });
335
+ }
336
+ const endDate = new Date(subscription["current_period_end"] as number * 1000);
337
+ const id = subscription["id"];
338
+ const userId = subscription["metadata"]?.["userId"] as string;
339
+ const orderId = (subscription["metadata"]?.["orderId"] as string) ?? id;
340
+ const plan = subscription["plan"] as {
341
+ [key: string]: any
342
+ } ?? {};
343
+
344
+ const existing = await store.findPurchaseBySubscriptionId(id);
345
+ const targetOrderId = existing?.orderId ?? orderId;
346
+ update["expired"] = now >= endDate;
347
+ if (userId) {
348
+ update["user"] = userId;
349
+ }
350
+ update["@uid"] = orderId;
351
+ update["@time"] = new Date();
352
+ update["subscription"] = id;
353
+ update["application"] = subscription["application"];
354
+ update["application_fee_percent"] = subscription["application_fee_percent"];
355
+ update["cancel_at"] = subscription["cancel_at"];
356
+ update["cancel_at_period_end"] = subscription["cancel_at_period_end"];
357
+ update["canceled_at"] = subscription["canceled_at"];
358
+ update["collection_method"] = subscription["collection_method"];
359
+ update["currency"] = subscription["currency"];
360
+ update["current_period_start"] = subscription["current_period_start"];
361
+ update["current_period_end"] = subscription["current_period_end"];
362
+ update["customer"] = subscription["customer"];
363
+ update["default_payment_method"] = subscription["default_payment_method"];
364
+ update["ended_at"] = subscription["ended_at"];
365
+ update["latest_invoice"] = subscription["latest_invoice"];
366
+ update["price_id"] = plan["id"];
367
+ update["active"] = plan["active"];
368
+ update["amount"] = plan["amount"];
369
+ update["billing_scheme"] = plan["billing_scheme"];
370
+ update["interval"] = plan["interval"];
371
+ update["interval_count"] = plan["interval_count"];
372
+ update["usage_type"] = plan["usage_type"];
373
+ update["quantity"] = subscription["quantity"];
374
+ update["start_date"] = subscription["start_date"];
375
+ console.log(`Subscription status is ${status}.`);
376
+ await store.savePurchase(targetOrderId, update, {
377
+ userId: userId || undefined,
378
+ });
379
+ return c.json({
380
+ "success": "Subscription is active.",
381
+ });
382
+ }
383
+ case "customer.subscription.deleted": {
384
+ const subscription = event.data.object as {
385
+ [key: string]: any
386
+ };
387
+ const status = subscription["status"];
388
+ const id = subscription["id"];
389
+ console.log(`Subscription status is ${status} expired.`);
390
+ const existing = id ? await store.findPurchaseBySubscriptionId(id) : null;
391
+ if (existing) {
392
+ await store.savePurchase(existing.orderId, {
393
+ expired: true,
394
+ ended_at: subscription["ended_at"],
395
+ canceled_at: subscription["canceled_at"],
396
+ "@time": new Date(),
397
+ });
398
+ }
399
+ return c.json({
400
+ "success": true,
401
+ });
402
+ }
403
+ default: {
404
+ return c.json({
405
+ "error": "Event is not found.",
406
+ }, 404);
407
+ }
408
+ }
409
+ } catch (err) {
410
+ return jsonError(c, err);
411
+ }
412
+ });
413
+ return hono;
414
+ };
@@ -0,0 +1,107 @@
1
+ import { Hono } from "hono";
2
+ import { jsonError } from "@mathrunet/masamune_cloudflare/dist/lib/src/http_error";
3
+ import {
4
+ resolveStripeClient,
5
+ resolveStripePurchaseStore,
6
+ resolveStripeSecretKey,
7
+ resolveStripeWebhookConnectSecret,
8
+ StripePurchaseWorkersOptions,
9
+ } from "../lib/options";
10
+ import { constructStripeEvent } from "../lib/stripe_client";
11
+
12
+ /**
13
+ * Receive and process webhooks for Stripe Connect (parity with the
14
+ * `stripeWebhookConnect` function of
15
+ * `@mathrunet/masamune_firebase_purchase_stripe`).
16
+ * If you do not use Stripe Connect, do not configure it as a Webhook.
17
+ *
18
+ * Stripe Connect用のWebhookを受信して処理します
19
+ * (`@mathrunet/masamune_firebase_purchase_stripe`の`stripeWebhookConnect`と同等)。
20
+ * Stripe Connectを利用しない場合はWebhookとして設定しないでください。
21
+ *
22
+ * @param {string} PURCHASE_STRIPE_SECRETKEY
23
+ * API key (secret key) to connect to Stripe.
24
+ * Stripeへ接続するためのAPIキー(シークレットキー)。
25
+ *
26
+ * @param {string} PURCHASE_STRIPE_WEBHOOKCONNECTSECRET
27
+ * Specify the **Signature Secret** after setting it up as a webhook.
28
+ * Webhookとして設定したあとの**署名シークレット**を指定します。
29
+ */
30
+ module.exports = (
31
+ hono: Hono,
32
+ options: StripePurchaseWorkersOptions,
33
+ data: { [key: string]: any },
34
+ ) => {
35
+ hono.post("/", async (c) => {
36
+ try {
37
+ const apiKey = resolveStripeSecretKey(c, options);
38
+ const webhookConnectSecret = resolveStripeWebhookConnectSecret(c, options);
39
+ const store = resolveStripePurchaseStore(c, options);
40
+ const stripeClient = resolveStripeClient(c, options, apiKey);
41
+ const signature = c.req.header("stripe-signature");
42
+ if (!signature || !webhookConnectSecret) {
43
+ return c.json({
44
+ "error": "Access denied.",
45
+ }, 403);
46
+ }
47
+ const payload = await c.req.text();
48
+ let event;
49
+ try {
50
+ event = await constructStripeEvent({
51
+ client: stripeClient,
52
+ payload,
53
+ signature,
54
+ secret: webhookConnectSecret,
55
+ });
56
+ } catch (err) {
57
+ console.warn(err);
58
+ return c.json({
59
+ "error": "Access denied.",
60
+ }, 403);
61
+ }
62
+
63
+ switch (event.type) {
64
+ case "account.updated": {
65
+ const account = event.data.object as {
66
+ [key: string]: any
67
+ };
68
+ const id = account["id"];
69
+ if (!id) {
70
+ return c.json({
71
+ "error": "The account id is not found.",
72
+ }, 404);
73
+ }
74
+ const user = await store.findUserByAccountId(id);
75
+ if (!user) {
76
+ return c.json({
77
+ "error": "The account data is not found.",
78
+ }, 404);
79
+ }
80
+ const update: { [key: string]: any } = {};
81
+ if (account["capabilities"]) {
82
+ const capability: { [key: string]: any } = {};
83
+ if (account["capabilities"]["card_payments"]) {
84
+ capability["card_payments"] = true;
85
+ }
86
+ if (account["capabilities"]["transfers"]) {
87
+ capability["transfers"] = true;
88
+ }
89
+ update["capability"] = capability;
90
+ }
91
+ await store.saveUser(user.userId, update);
92
+ return c.json({
93
+ "success": true,
94
+ });
95
+ }
96
+ default: {
97
+ return c.json({
98
+ "error": `Event ${event.type} is not found.`,
99
+ }, 404);
100
+ }
101
+ }
102
+ } catch (err) {
103
+ return jsonError(c, err);
104
+ }
105
+ });
106
+ return hono;
107
+ };
@@ -0,0 +1,91 @@
1
+ import { Hono } from "hono";
2
+ import { jsonError } from "@mathrunet/masamune_cloudflare/dist/lib/src/http_error";
3
+ import "@mathrunet/masamune";
4
+ import {
5
+ resolveStripeClient,
6
+ resolveStripePurchaseStore,
7
+ resolveStripeSecretKey,
8
+ StripePurchaseWorkersOptions,
9
+ } from "../lib/options";
10
+
11
+ /**
12
+ * Webhook for proper redirection when 3D Secure authentication is required
13
+ * (parity with the `stripeWebhookSecure` function of
14
+ * `@mathrunet/masamune_firebase_purchase_stripe`).
15
+ * Please set here for `returnUrl`.
16
+ *
17
+ * 3Dセキュア認証が必要な場合、適切なリダイレクトを行うためのWebhookです
18
+ * (`@mathrunet/masamune_firebase_purchase_stripe`の`stripeWebhookSecure`と同等)。
19
+ * `returnUrl`にこちらを設定してください。
20
+ *
21
+ * @param {string} PURCHASE_STRIPE_SECRETKEY
22
+ * API key (secret key) to connect to Stripe.
23
+ * Stripeへ接続するためのAPIキー(シークレットキー)。
24
+ */
25
+ module.exports = (
26
+ hono: Hono,
27
+ options: StripePurchaseWorkersOptions,
28
+ data: { [key: string]: any },
29
+ ) => {
30
+ hono.get("/", async (c) => {
31
+ try {
32
+ const apiKey = resolveStripeSecretKey(c, options);
33
+ const store = resolveStripePurchaseStore(c, options);
34
+ const stripeClient = resolveStripeClient(c, options, apiKey);
35
+ const token = c.req.query("token");
36
+ if (!token || typeof token !== "string") {
37
+ return c.json({
38
+ "error": "Invalid parameters",
39
+ }, 403);
40
+ }
41
+ let param: { [key: string]: any };
42
+ try {
43
+ param = JSON.parse(await token.decrypt({
44
+ key: apiKey.slice(0, 32),
45
+ ivKey: apiKey.slice(-16),
46
+ }));
47
+ } catch (err) {
48
+ return c.json({
49
+ "error": "Invalid parameters",
50
+ }, 403);
51
+ }
52
+ if (!param["userId"] || !param["orderId"] || !param["successUrl"] || !param["failureUrl"]) {
53
+ return c.json({
54
+ "error": "Invalid parameters",
55
+ }, 403);
56
+ }
57
+
58
+ const userId = param["userId"];
59
+ const orderId = param["orderId"];
60
+ const successUrl = param["successUrl"];
61
+ const failureUrl = param["failureUrl"];
62
+
63
+ const purchaseDoc = await store.getPurchase(orderId, userId);
64
+ // Firebase実装は`paymentId`を参照するが、購入作成時に保存されるキーは
65
+ // `purchaseId`のためそちらを正とし、`paymentId`はフォールバックに残す。
66
+ const purchaseId = purchaseDoc?.data["purchaseId"] ?? purchaseDoc?.data["paymentId"];
67
+ if (!purchaseDoc || !purchaseId) {
68
+ return c.json({
69
+ "error": "The purchase data is not found.",
70
+ }, 404);
71
+ }
72
+ const purchase = await stripeClient.paymentIntents.retrieve(
73
+ purchaseId,
74
+ );
75
+ if (!purchase) {
76
+ return c.json({
77
+ "error": "The purchase data is not found.",
78
+ }, 404);
79
+ }
80
+ const status = purchase.status;
81
+ if (status === "requires_capture" || status === "succeeded" || status === "processing") {
82
+ return c.redirect(successUrl);
83
+ } else {
84
+ return c.redirect(failureUrl);
85
+ }
86
+ } catch (err) {
87
+ return jsonError(c, err);
88
+ }
89
+ });
90
+ return hono;
91
+ };