@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.
- package/CHANGELOG.md +3 -0
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/functions.d.ts +44 -0
- package/dist/functions.js +47 -0
- package/dist/functions.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/meter/d1_usage_event_store.d.ts +128 -0
- package/dist/lib/meter/d1_usage_event_store.js +78 -0
- package/dist/lib/meter/d1_usage_event_store.js.map +1 -0
- package/dist/lib/meter/flush_meter.d.ts +19 -0
- package/dist/lib/meter/flush_meter.js +38 -0
- package/dist/lib/meter/flush_meter.js.map +1 -0
- package/dist/lib/meter/interface.d.ts +109 -0
- package/dist/lib/meter/interface.js +3 -0
- package/dist/lib/meter/interface.js.map +1 -0
- package/dist/lib/meter/kv_usage_buffer.d.ts +56 -0
- package/dist/lib/meter/kv_usage_buffer.js +50 -0
- package/dist/lib/meter/kv_usage_buffer.js.map +1 -0
- package/dist/lib/meter/record_usage.d.ts +29 -0
- package/dist/lib/meter/record_usage.js +72 -0
- package/dist/lib/meter/record_usage.js.map +1 -0
- package/dist/lib/meter/stripe_meter_client.d.ts +76 -0
- package/dist/lib/meter/stripe_meter_client.js +74 -0
- package/dist/lib/meter/stripe_meter_client.js.map +1 -0
- package/dist/lib/options.d.ts +179 -0
- package/dist/lib/options.js +132 -0
- package/dist/lib/options.js.map +1 -0
- package/dist/lib/purchase/d1_purchase_store.d.ts +78 -0
- package/dist/lib/purchase/d1_purchase_store.js +194 -0
- package/dist/lib/purchase/d1_purchase_store.js.map +1 -0
- package/dist/lib/purchase/helpers.d.ts +36 -0
- package/dist/lib/purchase/helpers.js +64 -0
- package/dist/lib/purchase/helpers.js.map +1 -0
- package/dist/lib/purchase/interface.d.ts +182 -0
- package/dist/lib/purchase/interface.js +27 -0
- package/dist/lib/purchase/interface.js.map +1 -0
- package/dist/lib/purchase/sync_payment.d.ts +15 -0
- package/dist/lib/purchase/sync_payment.js +79 -0
- package/dist/lib/purchase/sync_payment.js.map +1 -0
- package/dist/lib/stripe_client.d.ts +55 -0
- package/dist/lib/stripe_client.js +50 -0
- package/dist/lib/stripe_client.js.map +1 -0
- package/dist/meter.d.ts +21 -0
- package/dist/meter.js +38 -0
- package/dist/meter.js.map +1 -0
- package/dist/purchase.d.ts +58 -0
- package/dist/purchase.js +48 -0
- package/dist/purchase.js.map +1 -0
- package/dist/workers/stripe.d.ts +1 -0
- package/dist/workers/stripe.js +861 -0
- package/dist/workers/stripe.js.map +1 -0
- package/dist/workers/stripe_webhook.d.ts +1 -0
- package/dist/workers/stripe_webhook.js +384 -0
- package/dist/workers/stripe_webhook.js.map +1 -0
- package/dist/workers/stripe_webhook_connect.d.ts +1 -0
- package/dist/workers/stripe_webhook_connect.js +97 -0
- package/dist/workers/stripe_webhook_connect.js.map +1 -0
- package/dist/workers/stripe_webhook_secure.d.ts +1 -0
- package/dist/workers/stripe_webhook_secure.js +82 -0
- package/dist/workers/stripe_webhook_secure.js.map +1 -0
- package/jest.config.json +19 -0
- package/package.json +56 -0
- package/src/functions.ts +48 -0
- package/src/index.ts +25 -0
- package/src/lib/meter/d1_usage_event_store.ts +176 -0
- package/src/lib/meter/flush_meter.ts +45 -0
- package/src/lib/meter/interface.ts +118 -0
- package/src/lib/meter/kv_usage_buffer.ts +79 -0
- package/src/lib/meter/record_usage.ts +87 -0
- package/src/lib/meter/stripe_meter_client.ts +117 -0
- package/src/lib/options.ts +299 -0
- package/src/lib/purchase/d1_purchase_store.ts +268 -0
- package/src/lib/purchase/helpers.ts +77 -0
- package/src/lib/purchase/interface.ts +215 -0
- package/src/lib/purchase/sync_payment.ts +95 -0
- package/src/lib/stripe_client.ts +76 -0
- package/src/meter.ts +21 -0
- package/src/purchase.ts +66 -0
- package/src/workers/stripe.ts +881 -0
- package/src/workers/stripe_webhook.ts +414 -0
- package/src/workers/stripe_webhook_connect.ts +107 -0
- package/src/workers/stripe_webhook_secure.ts +91 -0
- package/test/helpers/mem_store.ts +97 -0
- package/test/meter.test.ts +217 -0
- package/test/purchase_store.test.ts +154 -0
- package/test/stripe.test.ts +146 -0
- package/test/stripe_webhook.test.ts +174 -0
- 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
|
+
}
|