@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,268 @@
1
+ import { SqlDatabaseLike } from "../meter/d1_usage_event_store";
2
+ import {
3
+ mergeDocumentData,
4
+ StripeDocumentData,
5
+ StripePaymentDocument,
6
+ StripePurchaseDocument,
7
+ StripePurchaseStore,
8
+ StripeUserDocument,
9
+ } from "./interface";
10
+
11
+ /**
12
+ * Table name options for [D1StripePurchaseStore].
13
+ *
14
+ * Expected default schema (see the package README for the full DDL):
15
+ *
16
+ * - `stripe_users(user_id PK, customer_id, account_id, data, created_at, updated_at)`
17
+ * - `stripe_payments(user_id, payment_id, data, created_at, updated_at, PK(user_id, payment_id))`
18
+ * - `stripe_purchases(order_id PK, user_id, purchase_id, subscription_id, data, created_at, updated_at)`
19
+ *
20
+ * [D1StripePurchaseStore]のテーブル名オプション。
21
+ *
22
+ * 想定するデフォルトスキーマ(完全なDDLはパッケージREADMEを参照):
23
+ *
24
+ * - `stripe_users(user_id PK, customer_id, account_id, data, created_at, updated_at)`
25
+ * - `stripe_payments(user_id, payment_id, data, created_at, updated_at, PK(user_id, payment_id))`
26
+ * - `stripe_purchases(order_id PK, user_id, purchase_id, subscription_id, data, created_at, updated_at)`
27
+ */
28
+ export interface D1StripePurchaseStoreOptions {
29
+ /**
30
+ * Users table name. Defaults to `stripe_users`.
31
+ *
32
+ * ユーザーテーブル名。デフォルトは`stripe_users`。
33
+ */
34
+ usersTable?: string | undefined;
35
+
36
+ /**
37
+ * Payments table name. Defaults to `stripe_payments`.
38
+ *
39
+ * 支払い方法テーブル名。デフォルトは`stripe_payments`。
40
+ */
41
+ paymentsTable?: string | undefined;
42
+
43
+ /**
44
+ * Purchases table name. Defaults to `stripe_purchases`.
45
+ *
46
+ * 購入テーブル名。デフォルトは`stripe_purchases`。
47
+ */
48
+ purchasesTable?: string | undefined;
49
+ }
50
+
51
+ interface DocumentRow {
52
+ data: string | null;
53
+ user_id?: string | null;
54
+ }
55
+
56
+ /**
57
+ * [StripePurchaseStore] backed by Cloudflare D1 (or any compatible SQL database).
58
+ *
59
+ * Documents are stored as JSON in the `data` column while lookup keys
60
+ * (`customer_id` / `account_id` / `purchase_id` / `subscription_id`) are kept in
61
+ * sync as indexed columns.
62
+ *
63
+ * Cloudflare D1(または互換SQLデータベース)を用いた[StripePurchaseStore]。
64
+ *
65
+ * ドキュメントは`data`カラムへJSONとして保存し、検索キー
66
+ * (`customer_id` / `account_id` / `purchase_id` / `subscription_id`)は
67
+ * インデックス付きカラムとして同期します。
68
+ */
69
+ export class D1StripePurchaseStore implements StripePurchaseStore {
70
+ /**
71
+ * [StripePurchaseStore] backed by Cloudflare D1 (or any compatible SQL database).
72
+ *
73
+ * Cloudflare D1(または互換SQLデータベース)を用いた[StripePurchaseStore]。
74
+ */
75
+ constructor(db: SqlDatabaseLike, options: D1StripePurchaseStoreOptions = {}) {
76
+ this._db = db;
77
+ this._users = options.usersTable ?? "stripe_users";
78
+ this._payments = options.paymentsTable ?? "stripe_payments";
79
+ this._purchases = options.purchasesTable ?? "stripe_purchases";
80
+ }
81
+
82
+ private readonly _db: SqlDatabaseLike;
83
+ private readonly _users: string;
84
+ private readonly _payments: string;
85
+ private readonly _purchases: string;
86
+
87
+ private static _parse(row: DocumentRow | undefined): StripeDocumentData | null {
88
+ if (!row) {
89
+ return null;
90
+ }
91
+ try {
92
+ return JSON.parse(row.data ?? "{}") as StripeDocumentData;
93
+ } catch {
94
+ return {};
95
+ }
96
+ }
97
+
98
+ async getUser(userId: string): Promise<StripeUserDocument | null> {
99
+ const res = await this._db
100
+ .prepare(`SELECT data FROM ${this._users} WHERE user_id = ?`)
101
+ .bind(userId)
102
+ .all<DocumentRow>();
103
+ const data = D1StripePurchaseStore._parse(res.results?.[0]);
104
+ return data === null ? null : { userId, data };
105
+ }
106
+
107
+ async findUserByCustomerId(customerId: string): Promise<StripeUserDocument | null> {
108
+ const res = await this._db
109
+ .prepare(`SELECT user_id, data FROM ${this._users} WHERE customer_id = ? LIMIT 1`)
110
+ .bind(customerId)
111
+ .all<DocumentRow & { user_id: string }>();
112
+ const row = res.results?.[0];
113
+ const data = D1StripePurchaseStore._parse(row);
114
+ return row && data !== null ? { userId: row.user_id, data } : null;
115
+ }
116
+
117
+ async findUserByAccountId(accountId: string): Promise<StripeUserDocument | null> {
118
+ const res = await this._db
119
+ .prepare(`SELECT user_id, data FROM ${this._users} WHERE account_id = ? LIMIT 1`)
120
+ .bind(accountId)
121
+ .all<DocumentRow & { user_id: string }>();
122
+ const row = res.results?.[0];
123
+ const data = D1StripePurchaseStore._parse(row);
124
+ return row && data !== null ? { userId: row.user_id, data } : null;
125
+ }
126
+
127
+ async saveUser(userId: string, update: StripeDocumentData): Promise<void> {
128
+ const now = Date.now();
129
+ const existing = await this.getUser(userId);
130
+ const merged = mergeDocumentData(existing?.data ?? {}, update);
131
+ const customerId = typeof merged["customer"] === "string" ? merged["customer"] : null;
132
+ const accountId = typeof merged["account"] === "string" ? merged["account"] : null;
133
+ if (existing) {
134
+ await this._db
135
+ .prepare(
136
+ `UPDATE ${this._users}
137
+ SET customer_id = ?, account_id = ?, data = ?, updated_at = ?
138
+ WHERE user_id = ?`,
139
+ )
140
+ .bind(customerId, accountId, JSON.stringify(merged), now, userId)
141
+ .run();
142
+ } else {
143
+ await this._db
144
+ .prepare(
145
+ `INSERT INTO ${this._users}
146
+ (user_id, customer_id, account_id, data, created_at, updated_at)
147
+ VALUES (?, ?, ?, ?, ?, ?)`,
148
+ )
149
+ .bind(userId, customerId, accountId, JSON.stringify(merged), now, now)
150
+ .run();
151
+ }
152
+ }
153
+
154
+ async listPayments(userId: string): Promise<StripePaymentDocument[]> {
155
+ const res = await this._db
156
+ .prepare(`SELECT payment_id, data FROM ${this._payments} WHERE user_id = ?`)
157
+ .bind(userId)
158
+ .all<{ payment_id: string, data: string | null }>();
159
+ return (res.results ?? []).map((row) => ({
160
+ userId,
161
+ paymentId: row.payment_id,
162
+ data: D1StripePurchaseStore._parse(row) ?? {},
163
+ }));
164
+ }
165
+
166
+ async savePayment(userId: string, paymentId: string, update: StripeDocumentData): Promise<void> {
167
+ const now = Date.now();
168
+ const res = await this._db
169
+ .prepare(`SELECT data FROM ${this._payments} WHERE user_id = ? AND payment_id = ?`)
170
+ .bind(userId, paymentId)
171
+ .all<DocumentRow>();
172
+ const existing = D1StripePurchaseStore._parse(res.results?.[0]);
173
+ const merged = mergeDocumentData(existing ?? {}, update);
174
+ if (existing !== null) {
175
+ await this._db
176
+ .prepare(
177
+ `UPDATE ${this._payments} SET data = ?, updated_at = ?
178
+ WHERE user_id = ? AND payment_id = ?`,
179
+ )
180
+ .bind(JSON.stringify(merged), now, userId, paymentId)
181
+ .run();
182
+ } else {
183
+ await this._db
184
+ .prepare(
185
+ `INSERT INTO ${this._payments}
186
+ (user_id, payment_id, data, created_at, updated_at)
187
+ VALUES (?, ?, ?, ?, ?)`,
188
+ )
189
+ .bind(userId, paymentId, JSON.stringify(merged), now, now)
190
+ .run();
191
+ }
192
+ }
193
+
194
+ async deletePayment(userId: string, paymentId: string): Promise<void> {
195
+ await this._db
196
+ .prepare(`DELETE FROM ${this._payments} WHERE user_id = ? AND payment_id = ?`)
197
+ .bind(userId, paymentId)
198
+ .run();
199
+ }
200
+
201
+ async getPurchase(orderId: string, userId?: string | undefined): Promise<StripePurchaseDocument | null> {
202
+ const res = userId
203
+ ? await this._db
204
+ .prepare(`SELECT user_id, data FROM ${this._purchases} WHERE order_id = ? AND user_id = ?`)
205
+ .bind(orderId, userId)
206
+ .all<DocumentRow>()
207
+ : await this._db
208
+ .prepare(`SELECT user_id, data FROM ${this._purchases} WHERE order_id = ?`)
209
+ .bind(orderId)
210
+ .all<DocumentRow>();
211
+ const row = res.results?.[0];
212
+ const data = D1StripePurchaseStore._parse(row);
213
+ return row && data !== null ? { orderId, userId: row.user_id ?? null, data } : null;
214
+ }
215
+
216
+ async findPurchaseByPurchaseId(purchaseId: string): Promise<StripePurchaseDocument | null> {
217
+ const res = await this._db
218
+ .prepare(`SELECT order_id, user_id, data FROM ${this._purchases} WHERE purchase_id = ? LIMIT 1`)
219
+ .bind(purchaseId)
220
+ .all<DocumentRow & { order_id: string }>();
221
+ const row = res.results?.[0];
222
+ const data = D1StripePurchaseStore._parse(row);
223
+ return row && data !== null ? { orderId: row.order_id, userId: row.user_id ?? null, data } : null;
224
+ }
225
+
226
+ async findPurchaseBySubscriptionId(subscriptionId: string): Promise<StripePurchaseDocument | null> {
227
+ const res = await this._db
228
+ .prepare(`SELECT order_id, user_id, data FROM ${this._purchases} WHERE subscription_id = ? LIMIT 1`)
229
+ .bind(subscriptionId)
230
+ .all<DocumentRow & { order_id: string }>();
231
+ const row = res.results?.[0];
232
+ const data = D1StripePurchaseStore._parse(row);
233
+ return row && data !== null ? { orderId: row.order_id, userId: row.user_id ?? null, data } : null;
234
+ }
235
+
236
+ async savePurchase(
237
+ orderId: string,
238
+ update: StripeDocumentData,
239
+ options: { userId?: string | undefined } = {},
240
+ ): Promise<void> {
241
+ const now = Date.now();
242
+ const existing = await this.getPurchase(orderId);
243
+ const merged = mergeDocumentData(existing?.data ?? {}, update);
244
+ const userId = options.userId
245
+ ?? (typeof merged["user"] === "string" ? merged["user"] : existing?.userId ?? null);
246
+ const purchaseId = typeof merged["purchaseId"] === "string" ? merged["purchaseId"] : null;
247
+ const subscriptionId = typeof merged["subscription"] === "string" ? merged["subscription"] : null;
248
+ if (existing) {
249
+ await this._db
250
+ .prepare(
251
+ `UPDATE ${this._purchases}
252
+ SET user_id = ?, purchase_id = ?, subscription_id = ?, data = ?, updated_at = ?
253
+ WHERE order_id = ?`,
254
+ )
255
+ .bind(userId, purchaseId, subscriptionId, JSON.stringify(merged), now, orderId)
256
+ .run();
257
+ } else {
258
+ await this._db
259
+ .prepare(
260
+ `INSERT INTO ${this._purchases}
261
+ (order_id, user_id, purchase_id, subscription_id, data, created_at, updated_at)
262
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
263
+ )
264
+ .bind(orderId, userId, purchaseId, subscriptionId, JSON.stringify(merged), now, now)
265
+ .run();
266
+ }
267
+ }
268
+ }
@@ -0,0 +1,77 @@
1
+ import Stripe from "stripe";
2
+ import { Context } from "hono";
3
+ import { HttpError } from "@mathrunet/masamune_cloudflare/dist/lib/src/http_error";
4
+ import { StripePurchaseWorkersOptions } from "../options";
5
+ import { StripePurchaseStore, StripeUserDocument } from "./interface";
6
+
7
+ /**
8
+ * Resolve the default payment method of a user, following the same fallback
9
+ * chain as the Firebase implementation: stored `defaultPayment` -> customer's
10
+ * `invoice_settings.default_payment_method` -> first stored payment method.
11
+ * The resolved value is saved back to the user document.
12
+ *
13
+ * Firebase実装と同じフォールバック連鎖でユーザーのデフォルト支払い方法を
14
+ * 解決します: 保存済み`defaultPayment` → カスタマーの
15
+ * `invoice_settings.default_payment_method` → 保存済み支払い方法の先頭。
16
+ * 解決した値はユーザードキュメントへ書き戻します。
17
+ */
18
+ export async function resolveDefaultPayment(options: {
19
+ stripeClient: Stripe,
20
+ store: StripePurchaseStore,
21
+ user: StripeUserDocument,
22
+ }): Promise<string> {
23
+ const { stripeClient, store, user } = options;
24
+ let defaultPayment = user.data["defaultPayment"] as string | undefined;
25
+ if (defaultPayment) {
26
+ return defaultPayment;
27
+ }
28
+ const customer = await stripeClient.customers.retrieve(
29
+ user.data["customer"],
30
+ ) as Stripe.Customer;
31
+ defaultPayment = (customer.invoice_settings.default_payment_method ?? undefined) as string | undefined;
32
+ if (!defaultPayment) {
33
+ const payments = await store.listPayments(user.userId);
34
+ if (payments.length <= 0) {
35
+ throw new HttpError(404, "The payment method is not found.");
36
+ }
37
+ defaultPayment = payments[0]?.data["id"] as string | undefined;
38
+ if (!defaultPayment) {
39
+ throw new HttpError(404, "The payment method is not found.");
40
+ }
41
+ }
42
+ await store.saveUser(user.userId, { defaultPayment });
43
+ return defaultPayment;
44
+ }
45
+
46
+ /**
47
+ * Resolve the email address of a user (parity with `admin.auth().getUser()` in
48
+ * the Firebase implementation): `options.resolveUserEmail` -> `email` field of
49
+ * the user document -> billing details of the default payment method.
50
+ *
51
+ * ユーザーのメールアドレスを解決します(Firebase実装の
52
+ * `admin.auth().getUser()`相当): `options.resolveUserEmail` → ユーザー
53
+ * ドキュメントの`email`フィールド → デフォルト支払い方法のbilling details。
54
+ */
55
+ export async function resolveUserEmail(options: {
56
+ context: Context,
57
+ workersOptions: StripePurchaseWorkersOptions,
58
+ stripeClient: Stripe,
59
+ user: StripeUserDocument,
60
+ defaultPayment?: string | undefined,
61
+ }): Promise<string> {
62
+ const { context, workersOptions, stripeClient, user, defaultPayment } = options;
63
+ let email = workersOptions.resolveUserEmail
64
+ ? await workersOptions.resolveUserEmail(context, user.userId)
65
+ : undefined;
66
+ if (!email && typeof user.data["email"] === "string") {
67
+ email = user.data["email"];
68
+ }
69
+ if (!email && defaultPayment) {
70
+ const paymentMethod = await stripeClient.paymentMethods.retrieve(defaultPayment);
71
+ email = paymentMethod?.billing_details?.email ?? undefined;
72
+ }
73
+ if (!email) {
74
+ throw new HttpError(404, "The user's email is not found.");
75
+ }
76
+ return email;
77
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Free-form document data stored for Stripe users / payments / purchases.
3
+ *
4
+ * Mirrors the Firestore document model of
5
+ * `@mathrunet/masamune_firebase_purchase_stripe`. Setting a key to `null` in an
6
+ * update deletes the key (equivalent to `FieldValue.delete()`).
7
+ *
8
+ * Stripeのユーザー・支払い方法・購入情報として保存する自由形式のドキュメントデータ。
9
+ *
10
+ * `@mathrunet/masamune_firebase_purchase_stripe`のFirestoreドキュメントモデルを
11
+ * 踏襲します。更新時にキーへ`null`を設定するとそのキーを削除します
12
+ * (`FieldValue.delete()`相当)。
13
+ */
14
+ export type StripeDocumentData = { [key: string]: any };
15
+
16
+ /**
17
+ * Stored Stripe user (customer / connect account) document.
18
+ *
19
+ * 保存されたStripeユーザー(カスタマー / Connectアカウント)ドキュメント。
20
+ */
21
+ export interface StripeUserDocument {
22
+ /**
23
+ * Application user ID.
24
+ *
25
+ * アプリケーションのユーザーID。
26
+ */
27
+ userId: string;
28
+
29
+ /**
30
+ * Document data (`customer`, `account`, `defaultPayment`, etc.).
31
+ *
32
+ * ドキュメントデータ(`customer`、`account`、`defaultPayment`など)。
33
+ */
34
+ data: StripeDocumentData;
35
+ }
36
+
37
+ /**
38
+ * Stored Stripe payment method document.
39
+ *
40
+ * 保存されたStripe支払い方法ドキュメント。
41
+ */
42
+ export interface StripePaymentDocument {
43
+ /**
44
+ * Application user ID.
45
+ *
46
+ * アプリケーションのユーザーID。
47
+ */
48
+ userId: string;
49
+
50
+ /**
51
+ * Stripe payment method ID.
52
+ *
53
+ * Stripeの支払い方法ID。
54
+ */
55
+ paymentId: string;
56
+
57
+ /**
58
+ * Document data (`type`, `brand`, `numberLast`, `default`, etc.).
59
+ *
60
+ * ドキュメントデータ(`type`、`brand`、`numberLast`、`default`など)。
61
+ */
62
+ data: StripeDocumentData;
63
+ }
64
+
65
+ /**
66
+ * Stored Stripe purchase / subscription document.
67
+ *
68
+ * 保存されたStripe購入・サブスクリプションドキュメント。
69
+ */
70
+ export interface StripePurchaseDocument {
71
+ /**
72
+ * Order ID (database key).
73
+ *
74
+ * 注文ID(データベースのキー)。
75
+ */
76
+ orderId: string;
77
+
78
+ /**
79
+ * Application user ID. May be `null` for subscription records created from
80
+ * webhooks before the user is known.
81
+ *
82
+ * アプリケーションのユーザーID。ユーザーが未確定のWebhook起点の
83
+ * サブスクリプションレコードでは`null`になることがあります。
84
+ */
85
+ userId: string | null;
86
+
87
+ /**
88
+ * Document data (`purchaseId`, `subscription`, `confirm`, `success`, etc.).
89
+ *
90
+ * ドキュメントデータ(`purchaseId`、`subscription`、`confirm`、`success`など)。
91
+ */
92
+ data: StripeDocumentData;
93
+ }
94
+
95
+ /**
96
+ * Persistence boundary for Stripe purchase data.
97
+ *
98
+ * Replaces the Firestore paths (`PURCHASE_STRIPE_USERPATH` /
99
+ * `PURCHASE_STRIPE_PAYMENTPATH` / `PURCHASE_STRIPE_PURCHASEPATH`) of the
100
+ * Firebase implementation. All `save*` methods merge the update into the
101
+ * existing document; keys set to `null` are removed.
102
+ *
103
+ * Stripe購入データの永続化境界。
104
+ *
105
+ * Firebase実装のFirestoreパス(`PURCHASE_STRIPE_USERPATH` /
106
+ * `PURCHASE_STRIPE_PAYMENTPATH` / `PURCHASE_STRIPE_PURCHASEPATH`)を置き換えます。
107
+ * すべての`save*`メソッドは既存ドキュメントへ更新をマージし、`null`が設定された
108
+ * キーは削除されます。
109
+ */
110
+ export interface StripePurchaseStore {
111
+ /**
112
+ * Get a user document by application user ID.
113
+ *
114
+ * アプリケーションのユーザーIDでユーザードキュメントを取得します。
115
+ */
116
+ getUser(userId: string): Promise<StripeUserDocument | null>;
117
+
118
+ /**
119
+ * Find a user document by Stripe customer ID.
120
+ *
121
+ * StripeのカスタマーIDでユーザードキュメントを検索します。
122
+ */
123
+ findUserByCustomerId(customerId: string): Promise<StripeUserDocument | null>;
124
+
125
+ /**
126
+ * Find a user document by Stripe connect account ID.
127
+ *
128
+ * StripeのConnectアカウントIDでユーザードキュメントを検索します。
129
+ */
130
+ findUserByAccountId(accountId: string): Promise<StripeUserDocument | null>;
131
+
132
+ /**
133
+ * Merge-save a user document.
134
+ *
135
+ * ユーザードキュメントをマージ保存します。
136
+ */
137
+ saveUser(userId: string, update: StripeDocumentData): Promise<void>;
138
+
139
+ /**
140
+ * List payment method documents of a user.
141
+ *
142
+ * ユーザーの支払い方法ドキュメントを一覧します。
143
+ */
144
+ listPayments(userId: string): Promise<StripePaymentDocument[]>;
145
+
146
+ /**
147
+ * Merge-save a payment method document.
148
+ *
149
+ * 支払い方法ドキュメントをマージ保存します。
150
+ */
151
+ savePayment(userId: string, paymentId: string, update: StripeDocumentData): Promise<void>;
152
+
153
+ /**
154
+ * Delete a payment method document.
155
+ *
156
+ * 支払い方法ドキュメントを削除します。
157
+ */
158
+ deletePayment(userId: string, paymentId: string): Promise<void>;
159
+
160
+ /**
161
+ * Get a purchase document by order ID. When `userId` is given the document
162
+ * must belong to that user.
163
+ *
164
+ * 注文IDで購入ドキュメントを取得します。`userId`を指定した場合はその
165
+ * ユーザーの所有ドキュメントに限定します。
166
+ */
167
+ getPurchase(orderId: string, userId?: string | undefined): Promise<StripePurchaseDocument | null>;
168
+
169
+ /**
170
+ * Find a purchase document by Stripe payment intent ID.
171
+ *
172
+ * StripeのPaymentIntent IDで購入ドキュメントを検索します。
173
+ */
174
+ findPurchaseByPurchaseId(purchaseId: string): Promise<StripePurchaseDocument | null>;
175
+
176
+ /**
177
+ * Find a purchase document by Stripe subscription ID.
178
+ *
179
+ * StripeのサブスクリプションIDで購入ドキュメントを検索します。
180
+ */
181
+ findPurchaseBySubscriptionId(subscriptionId: string): Promise<StripePurchaseDocument | null>;
182
+
183
+ /**
184
+ * Merge-save a purchase document.
185
+ *
186
+ * 購入ドキュメントをマージ保存します。
187
+ */
188
+ savePurchase(orderId: string, update: StripeDocumentData, options?: { userId?: string | undefined }): Promise<void>;
189
+ }
190
+
191
+ /**
192
+ * Apply a merge update to document data. Keys set to `null` are removed
193
+ * (equivalent to `FieldValue.delete()`), `undefined` keys are ignored.
194
+ *
195
+ * ドキュメントデータへマージ更新を適用します。`null`が設定されたキーは削除され
196
+ * (`FieldValue.delete()`相当)、`undefined`のキーは無視されます。
197
+ */
198
+ export function mergeDocumentData(
199
+ current: StripeDocumentData,
200
+ update: StripeDocumentData,
201
+ ): StripeDocumentData {
202
+ const merged: StripeDocumentData = { ...current };
203
+ for (const key of Object.keys(update)) {
204
+ const value = update[key];
205
+ if (value === undefined) {
206
+ continue;
207
+ }
208
+ if (value === null) {
209
+ delete merged[key];
210
+ } else {
211
+ merged[key] = value;
212
+ }
213
+ }
214
+ return merged;
215
+ }
@@ -0,0 +1,95 @@
1
+ import Stripe from "stripe";
2
+ import { StripePurchaseStore } from "./interface";
3
+
4
+ /**
5
+ * Synchronize the payment methods of a Stripe customer into the store
6
+ * (parity with `syncStripePayment` in the Firebase implementation).
7
+ *
8
+ * Stripeカスタマーの支払い方法をストアへ同期します
9
+ * (Firebase実装の`syncStripePayment`相当)。
10
+ */
11
+ export async function syncStripePayment(options: {
12
+ stripeClient: Stripe,
13
+ store: StripePurchaseStore,
14
+ userId: string,
15
+ customerId: string,
16
+ }): Promise<void> {
17
+ const { stripeClient, store, userId, customerId } = options;
18
+ const customer = await stripeClient.customers.retrieve(
19
+ customerId,
20
+ ) as Stripe.Customer;
21
+ let defaultSource = customer.invoice_settings.default_payment_method as string | null;
22
+ const paymentMethods = await stripeClient.customers.listPaymentMethods(
23
+ customerId,
24
+ {
25
+ type: "card",
26
+ },
27
+ );
28
+
29
+ if (!defaultSource && paymentMethods.data.length > 0) {
30
+ defaultSource = paymentMethods.data[0]?.id ?? null;
31
+ if (defaultSource) {
32
+ await stripeClient.customers.update(
33
+ customerId,
34
+ {
35
+ invoice_settings: {
36
+ default_payment_method: defaultSource,
37
+ },
38
+ },
39
+ );
40
+ }
41
+ }
42
+
43
+ const payments = await store.listPayments(userId);
44
+
45
+ for (const payment of payments) {
46
+ const method = paymentMethods.data.find((m) => m.id == payment.data["id"]);
47
+ if (!method) {
48
+ await store.deletePayment(userId, payment.paymentId);
49
+ continue;
50
+ }
51
+ const card = method.card;
52
+ if (!card) {
53
+ continue;
54
+ }
55
+ const isDefault = method.id == defaultSource;
56
+ if (method.type === payment.data["type"] && card.exp_month === payment.data["expMonth"] && card.exp_year === payment.data["expYear"] && card.brand === payment.data["brand"] && card.last4 === payment.data["numberLast"] && isDefault === payment.data["default"]) {
57
+ continue;
58
+ }
59
+ await store.savePayment(userId, payment.paymentId, {
60
+ type: method.type,
61
+ expMonth: card.exp_month,
62
+ expYear: card.exp_year,
63
+ brand: card.brand,
64
+ numberLast: card.last4,
65
+ default: isDefault,
66
+ });
67
+ }
68
+ for (const method of paymentMethods.data) {
69
+ const card = method.card;
70
+ if (!card) {
71
+ continue;
72
+ }
73
+ const existing = payments.find((item) => item.data["id"] == method.id);
74
+ if (existing) {
75
+ continue;
76
+ }
77
+ const uid = method.id;
78
+ const isDefault = method.id == defaultSource;
79
+ await store.savePayment(userId, uid, {
80
+ "@uid": uid,
81
+ "@time": new Date(),
82
+ id: uid,
83
+ type: method.type,
84
+ expMonth: card.exp_month,
85
+ expYear: card.exp_year,
86
+ brand: card.brand,
87
+ numberLast: card.last4,
88
+ default: isDefault,
89
+ });
90
+ }
91
+
92
+ await store.saveUser(userId, {
93
+ defaultPayment: defaultSource ?? null,
94
+ });
95
+ }