@dineway-ai/plugin-payment 0.2.6 → 0.2.8
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/package.json +3 -2
- package/src/admin/subscriptions.tsx +160 -58
- package/src/admin.tsx +17 -3
- package/src/configuration/repository.ts +7 -3
- package/src/configuration/secrets.ts +1 -3
- package/src/database/index.ts +12 -0
- package/src/database/migrations/001_stripe_configuration.ts +100 -0
- package/src/database/migrations/002_subscriptions.ts +168 -0
- package/src/database/migrations/003_purchases.ts +193 -0
- package/src/database/migrations/adoption.ts +27 -0
- package/src/database/types.ts +137 -0
- package/src/index.ts +25 -0
- package/src/stripe/webhook-verifier.ts +1 -0
- package/src/subscriptions/funding.ts +256 -0
- package/src/subscriptions/index.ts +1 -0
- package/src/subscriptions/projection.ts +39 -7
- package/src/subscriptions/purchases.ts +329 -0
- package/src/subscriptions/refunds.ts +134 -0
- package/src/subscriptions/repository.ts +352 -13
- package/src/subscriptions/routes.ts +11 -40
- package/src/subscriptions/runtime.ts +6 -8
- package/src/subscriptions/schemas.ts +45 -0
- package/src/subscriptions/service.ts +486 -203
- package/src/subscriptions/types.ts +90 -0
- package/src/types.ts +2 -0
- package/src/webhooks/index.ts +2 -0
- package/src/webhooks/routes.ts +109 -0
- package/src/webhooks/types.ts +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dineway-ai/plugin-payment",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Provider-neutral payment infrastructure and Stripe integration for Dineway plugins",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
".": "./src/index.ts",
|
|
12
12
|
"./admin": "./src/admin.tsx",
|
|
13
13
|
"./configuration": "./src/configuration/index.ts",
|
|
14
|
+
"./database": "./src/database/index.ts",
|
|
14
15
|
"./routes": "./src/routes/index.ts",
|
|
15
16
|
"./stripe": "./src/stripe/index.ts",
|
|
16
17
|
"./types": "./src/types.ts",
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
"license": "MIT",
|
|
30
31
|
"peerDependencies": {
|
|
31
32
|
"react": "^18.0.0 || ^19.0.0",
|
|
32
|
-
"dineway": "^0.2.
|
|
33
|
+
"dineway": "^0.2.31"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
36
|
"@stripe/stripe-js": "9.9.0",
|
|
@@ -5,6 +5,9 @@ import type { getSubscriptionConfiguration } from "../subscriptions/runtime.js";
|
|
|
5
5
|
import type {
|
|
6
6
|
MemberBilling,
|
|
7
7
|
MemberSubscription,
|
|
8
|
+
BillingSubject,
|
|
9
|
+
SubscriptionPurchase,
|
|
10
|
+
SubscriptionSnapshot,
|
|
8
11
|
SaveSubscriptionPlan,
|
|
9
12
|
SubscriptionPlan,
|
|
10
13
|
} from "../subscriptions/types.js";
|
|
@@ -12,6 +15,8 @@ import { postPaymentRoute } from "./api.js";
|
|
|
12
15
|
import { SubscriptionPricePicker } from "./subscription-price-picker.js";
|
|
13
16
|
|
|
14
17
|
type Configuration = Awaited<ReturnType<typeof getSubscriptionConfiguration>>;
|
|
18
|
+
type AdminSubscription = MemberSubscription & { subject: BillingSubject | null };
|
|
19
|
+
type AdminPurchase = SubscriptionPurchase & { subject: BillingSubject | null };
|
|
15
20
|
type Feature = { key: string; kind: "boolean" | "number"; value: string };
|
|
16
21
|
const emptyPlan: SaveSubscriptionPlan = {
|
|
17
22
|
code: "",
|
|
@@ -28,17 +33,17 @@ const buttonClass = "rounded-lg border border-kumo-line px-3 py-2 text-sm disabl
|
|
|
28
33
|
export function PaymentSubscriptionsPage() {
|
|
29
34
|
const [configuration, setConfiguration] = React.useState<Configuration | null>(null);
|
|
30
35
|
const [plans, setPlans] = React.useState<SubscriptionPlan[]>([]);
|
|
31
|
-
const [members, setMembers] = React.useState<
|
|
36
|
+
const [members, setMembers] = React.useState<AdminSubscription[]>([]);
|
|
37
|
+
const [purchases, setPurchases] = React.useState<AdminPurchase[]>([]);
|
|
38
|
+
const [purchaseCursor, setPurchaseCursor] = React.useState<string>();
|
|
32
39
|
const [cursor, setCursor] = React.useState<string>();
|
|
33
40
|
const [form, setForm] = React.useState<SaveSubscriptionPlan>({ ...emptyPlan });
|
|
34
41
|
const [editorRevision, setEditorRevision] = React.useState(0);
|
|
35
42
|
const [boundPrices, setBoundPrices] = React.useState<SubscriptionPlan["prices"]>([]);
|
|
36
43
|
const [features, setFeatures] = React.useState<Feature[]>([]);
|
|
37
|
-
const [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
member: MemberBilling;
|
|
41
|
-
} | null>(null);
|
|
44
|
+
const [detail, setDetail] = React.useState<
|
|
45
|
+
(SubscriptionSnapshot & { member: MemberBilling }) | null
|
|
46
|
+
>(null);
|
|
42
47
|
const [busy, setBusy] = React.useState(false);
|
|
43
48
|
const [error, setError] = React.useState<string | null>(null);
|
|
44
49
|
const [notice, setNotice] = React.useState<string | null>(null);
|
|
@@ -46,15 +51,18 @@ export function PaymentSubscriptionsPage() {
|
|
|
46
51
|
const config = await postPaymentRoute<Configuration>("subscriptions/configuration/get");
|
|
47
52
|
setConfiguration(config);
|
|
48
53
|
if (config.ready) {
|
|
49
|
-
const [catalog, subscriptions] = await Promise.all([
|
|
54
|
+
const [catalog, subscriptions, purchasePage] = await Promise.all([
|
|
50
55
|
postPaymentRoute<{ items: SubscriptionPlan[] }>("subscriptions/plans/list"),
|
|
51
|
-
postPaymentRoute<{ items:
|
|
52
|
-
|
|
56
|
+
postPaymentRoute<{ items: AdminSubscription[]; nextCursor?: string }>("subscriptions/list"),
|
|
57
|
+
postPaymentRoute<{ items: AdminPurchase[]; nextCursor?: string }>(
|
|
58
|
+
"subscriptions/purchases/list",
|
|
53
59
|
),
|
|
54
60
|
]);
|
|
55
61
|
setPlans(catalog.items);
|
|
56
62
|
setMembers(subscriptions.items);
|
|
57
63
|
setCursor(subscriptions.nextCursor);
|
|
64
|
+
setPurchases(purchasePage.items);
|
|
65
|
+
setPurchaseCursor(purchasePage.nextCursor);
|
|
58
66
|
}
|
|
59
67
|
}, []);
|
|
60
68
|
React.useEffect(() => {
|
|
@@ -144,7 +152,10 @@ export function PaymentSubscriptionsPage() {
|
|
|
144
152
|
<p className="mt-2 text-sm text-kumo-subtle">
|
|
145
153
|
Bind Stripe prices, configure plan entitlements, and inspect paid membership periods.
|
|
146
154
|
</p>
|
|
147
|
-
<a
|
|
155
|
+
<a
|
|
156
|
+
href="/_dineway/admin/plugins/dineway-payment/payments"
|
|
157
|
+
className="mt-2 inline-block text-sm underline"
|
|
158
|
+
>
|
|
148
159
|
Stripe account settings
|
|
149
160
|
</a>
|
|
150
161
|
</header>
|
|
@@ -169,7 +180,7 @@ export function PaymentSubscriptionsPage() {
|
|
|
169
180
|
) : (
|
|
170
181
|
<>
|
|
171
182
|
<section className="space-y-4 rounded-xl border border-kumo-line p-5">
|
|
172
|
-
<h2 className="text-lg font-semibold">
|
|
183
|
+
<h2 className="text-lg font-semibold">Payment webhook</h2>
|
|
173
184
|
<p className="text-sm">
|
|
174
185
|
Application: {configuration.namespace} · {configuration.mode} ·{" "}
|
|
175
186
|
{configuration.ready ? "Ready" : "Configuration required"}
|
|
@@ -179,48 +190,14 @@ export function PaymentSubscriptionsPage() {
|
|
|
179
190
|
<input className={inputClass} readOnly value={configuration.webhookUrl} />
|
|
180
191
|
</label>
|
|
181
192
|
<p className="text-sm text-kumo-subtle">
|
|
182
|
-
|
|
183
|
-
|
|
193
|
+
Payment receives and verifies Stripe events for subscriptions, restaurant orders, and
|
|
194
|
+
venue payments at this shared endpoint. Configure its signing secret once in Payments
|
|
195
|
+
using the Stripe account settings link above.
|
|
184
196
|
</p>
|
|
185
|
-
<form
|
|
186
|
-
className="flex flex-wrap items-end gap-3"
|
|
187
|
-
onSubmit={(event) => {
|
|
188
|
-
event.preventDefault();
|
|
189
|
-
void run(async () => {
|
|
190
|
-
setConfiguration(
|
|
191
|
-
await postPaymentRoute<Configuration>("subscriptions/configuration/update", {
|
|
192
|
-
webhookSecret,
|
|
193
|
-
}),
|
|
194
|
-
);
|
|
195
|
-
setWebhookSecret("");
|
|
196
|
-
await refresh();
|
|
197
|
-
setNotice("Subscription webhook secret saved.");
|
|
198
|
-
});
|
|
199
|
-
}}
|
|
200
|
-
>
|
|
201
|
-
<label className="min-w-64 flex-1 text-sm">
|
|
202
|
-
Signing secret
|
|
203
|
-
<input
|
|
204
|
-
type="password"
|
|
205
|
-
autoComplete="new-password"
|
|
206
|
-
required
|
|
207
|
-
className={inputClass}
|
|
208
|
-
value={webhookSecret}
|
|
209
|
-
placeholder={
|
|
210
|
-
configuration.webhookSecret
|
|
211
|
-
? "Leave unchanged unless rotating the secret"
|
|
212
|
-
: "whsec_…"
|
|
213
|
-
}
|
|
214
|
-
onChange={(event) => setWebhookSecret(event.target.value)}
|
|
215
|
-
/>
|
|
216
|
-
</label>
|
|
217
|
-
<button className={buttonClass} disabled={busy || !webhookSecret}>
|
|
218
|
-
Save secret
|
|
219
|
-
</button>
|
|
220
|
-
</form>
|
|
221
197
|
{!configuration.ready && (
|
|
222
198
|
<p className="text-sm text-kumo-subtle">
|
|
223
|
-
Verify the Stripe account and
|
|
199
|
+
Verify the Stripe account and configure the shared webhook in Payments to manage
|
|
200
|
+
plans.
|
|
224
201
|
</p>
|
|
225
202
|
)}
|
|
226
203
|
</section>
|
|
@@ -391,7 +368,100 @@ export function PaymentSubscriptionsPage() {
|
|
|
391
368
|
</form>
|
|
392
369
|
</section>
|
|
393
370
|
<section className="space-y-4">
|
|
394
|
-
<h2 className="text-lg font-semibold">
|
|
371
|
+
<h2 className="text-lg font-semibold">Purchases</h2>
|
|
372
|
+
<button
|
|
373
|
+
className={buttonClass}
|
|
374
|
+
disabled={busy}
|
|
375
|
+
onClick={() =>
|
|
376
|
+
void run(async () => {
|
|
377
|
+
const result = await postPaymentRoute<{ processed: number; failed: number }>(
|
|
378
|
+
"subscriptions/reconcile",
|
|
379
|
+
);
|
|
380
|
+
await refresh();
|
|
381
|
+
setNotice(
|
|
382
|
+
`Processed ${result.processed}; ${result.failed} require retry or review.`,
|
|
383
|
+
);
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
>
|
|
387
|
+
Retry pending synchronization
|
|
388
|
+
</button>
|
|
389
|
+
<div className="overflow-x-auto">
|
|
390
|
+
<table className="w-full text-left text-sm">
|
|
391
|
+
<thead>
|
|
392
|
+
<tr>
|
|
393
|
+
<th className="p-2">Billing subject</th>
|
|
394
|
+
<th className="p-2">Purchase</th>
|
|
395
|
+
<th className="p-2">Terms</th>
|
|
396
|
+
<th className="p-2">Status</th>
|
|
397
|
+
</tr>
|
|
398
|
+
</thead>
|
|
399
|
+
<tbody>
|
|
400
|
+
{purchases.map((purchase) => (
|
|
401
|
+
<tr key={purchase.reference} className="border-t border-kumo-line">
|
|
402
|
+
<td className="p-2">
|
|
403
|
+
{purchase.subject
|
|
404
|
+
? `${purchase.subject.realm}: ${purchase.subject.id}`
|
|
405
|
+
: purchase.userId}
|
|
406
|
+
</td>
|
|
407
|
+
<td className="p-2">
|
|
408
|
+
{purchase.reference}
|
|
409
|
+
<br />
|
|
410
|
+
{purchase.contract.pricing.kind}
|
|
411
|
+
</td>
|
|
412
|
+
<td className="p-2">
|
|
413
|
+
{formatSubscriptionPrice(
|
|
414
|
+
purchase.contract.amount,
|
|
415
|
+
purchase.contract.currency,
|
|
416
|
+
)}{" "}
|
|
417
|
+
every {purchase.contract.intervalCount} {purchase.contract.interval}
|
|
418
|
+
<br />
|
|
419
|
+
First invoice credit:{" "}
|
|
420
|
+
{formatSubscriptionPrice(
|
|
421
|
+
purchase.contract.firstPeriodCredit ?? 0,
|
|
422
|
+
purchase.contract.currency,
|
|
423
|
+
)}
|
|
424
|
+
</td>
|
|
425
|
+
<td className="p-2">
|
|
426
|
+
{purchase.closed
|
|
427
|
+
? "Closed"
|
|
428
|
+
: purchase.subscriptionId
|
|
429
|
+
? "Subscription recorded"
|
|
430
|
+
: "Awaiting confirmation"}
|
|
431
|
+
<br />
|
|
432
|
+
{purchase.cancelMode && `Cancellation: ${purchase.cancelMode}`}
|
|
433
|
+
<br />
|
|
434
|
+
{purchase.lastError}
|
|
435
|
+
</td>
|
|
436
|
+
</tr>
|
|
437
|
+
))}
|
|
438
|
+
</tbody>
|
|
439
|
+
</table>
|
|
440
|
+
</div>
|
|
441
|
+
{!purchases.length && (
|
|
442
|
+
<p className="text-sm text-kumo-subtle">No purchases recorded.</p>
|
|
443
|
+
)}
|
|
444
|
+
{purchaseCursor && (
|
|
445
|
+
<button
|
|
446
|
+
className={buttonClass}
|
|
447
|
+
disabled={busy}
|
|
448
|
+
onClick={() =>
|
|
449
|
+
void run(async () => {
|
|
450
|
+
const page = await postPaymentRoute<{
|
|
451
|
+
items: AdminPurchase[];
|
|
452
|
+
nextCursor?: string;
|
|
453
|
+
}>("subscriptions/purchases/list", { cursor: purchaseCursor });
|
|
454
|
+
setPurchases([...purchases, ...page.items]);
|
|
455
|
+
setPurchaseCursor(page.nextCursor);
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
>
|
|
459
|
+
Load more purchases
|
|
460
|
+
</button>
|
|
461
|
+
)}
|
|
462
|
+
</section>
|
|
463
|
+
<section className="space-y-4">
|
|
464
|
+
<h2 className="text-lg font-semibold">Subscriptions and funding</h2>
|
|
395
465
|
<p className="text-sm text-kumo-subtle">
|
|
396
466
|
Failed renewals keep access until the end of the paid period. Cancellation takes
|
|
397
467
|
effect at period end.
|
|
@@ -410,8 +480,18 @@ export function PaymentSubscriptionsPage() {
|
|
|
410
480
|
<tbody>
|
|
411
481
|
{members.map((member) => (
|
|
412
482
|
<tr key={member.id} className="border-t border-kumo-line">
|
|
413
|
-
<td className="p-2">
|
|
414
|
-
|
|
483
|
+
<td className="p-2">
|
|
484
|
+
{member.subject
|
|
485
|
+
? `${member.subject.realm}: ${member.subject.id}`
|
|
486
|
+
: member.userId}
|
|
487
|
+
</td>
|
|
488
|
+
<td className="p-2">
|
|
489
|
+
{member.planCode || "Dynamic purchase"}
|
|
490
|
+
<br />
|
|
491
|
+
<span className="text-xs text-kumo-subtle">
|
|
492
|
+
{member.purchaseReference}
|
|
493
|
+
</span>
|
|
494
|
+
</td>
|
|
415
495
|
<td className="p-2">{member.status}</td>
|
|
416
496
|
<td className="p-2">
|
|
417
497
|
{new Date(member.verifiedAt).toLocaleString()}
|
|
@@ -441,7 +521,7 @@ export function PaymentSubscriptionsPage() {
|
|
|
441
521
|
onClick={() =>
|
|
442
522
|
void run(async () => {
|
|
443
523
|
const page = await postPaymentRoute<{
|
|
444
|
-
items:
|
|
524
|
+
items: AdminSubscription[];
|
|
445
525
|
nextCursor?: string;
|
|
446
526
|
}>("subscriptions/list", { cursor });
|
|
447
527
|
setMembers([...members, ...page.items]);
|
|
@@ -457,11 +537,31 @@ export function PaymentSubscriptionsPage() {
|
|
|
457
537
|
<h3 className="font-semibold">{detail.subscription.id}</h3>
|
|
458
538
|
<dl className="grid gap-2 text-sm">
|
|
459
539
|
<dt>Member</dt>
|
|
460
|
-
<dd>
|
|
540
|
+
<dd>
|
|
541
|
+
{detail.subject
|
|
542
|
+
? `${detail.subject.realm}: ${detail.subject.id} · ${detail.subject.email}`
|
|
543
|
+
: detail.subscription.userId}
|
|
544
|
+
</dd>
|
|
545
|
+
<dt>Purchase reference</dt>
|
|
546
|
+
<dd>{detail.purchase?.reference ?? "Catalogue subscription"}</dd>
|
|
461
547
|
<dt>Paid access until</dt>
|
|
462
548
|
<dd>
|
|
463
|
-
{detail.
|
|
464
|
-
|
|
549
|
+
{detail.periods.some(
|
|
550
|
+
(period) =>
|
|
551
|
+
period.eligible &&
|
|
552
|
+
period.startsAt * 1000 <= Date.now() &&
|
|
553
|
+
period.endsAt * 1000 > Date.now(),
|
|
554
|
+
)
|
|
555
|
+
? new Date(
|
|
556
|
+
Math.max(
|
|
557
|
+
...detail.periods
|
|
558
|
+
.filter(
|
|
559
|
+
(period) =>
|
|
560
|
+
period.eligible && period.startsAt * 1000 <= Date.now(),
|
|
561
|
+
)
|
|
562
|
+
.map((period) => period.endsAt),
|
|
563
|
+
) * 1000,
|
|
564
|
+
).toLocaleString()
|
|
465
565
|
: "No current paid period"}
|
|
466
566
|
</dd>
|
|
467
567
|
<dt>Cancellation</dt>
|
|
@@ -469,7 +569,9 @@ export function PaymentSubscriptionsPage() {
|
|
|
469
569
|
{detail.subscription.cancelAtPeriodEnd
|
|
470
570
|
? "At period end: " +
|
|
471
571
|
new Date(detail.subscription.periodEnd * 1000).toLocaleString()
|
|
472
|
-
:
|
|
572
|
+
: detail.subscription.status === "canceled"
|
|
573
|
+
? "Canceled"
|
|
574
|
+
: "Not scheduled"}
|
|
473
575
|
</dd>
|
|
474
576
|
<dt>Last synchronization</dt>
|
|
475
577
|
<dd>
|
package/src/admin.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "./admin/form.js";
|
|
9
9
|
import { PaymentSubscriptionsPage } from "./admin/subscriptions.js";
|
|
10
10
|
import type { PaymentStripeConfiguration } from "./configuration/service.js";
|
|
11
|
+
import { PAYMENT_WEBHOOK_PATH } from "./webhooks/types.js";
|
|
11
12
|
|
|
12
13
|
type CredentialName = "secretKey" | "webhookSecret";
|
|
13
14
|
|
|
@@ -184,6 +185,10 @@ export function PaymentSettingsPage() {
|
|
|
184
185
|
}
|
|
185
186
|
type="password"
|
|
186
187
|
/>
|
|
188
|
+
<p className="text-sm text-kumo-subtle">
|
|
189
|
+
Use the complete whsec_… signing secret from Stripe for the endpoint below. For local
|
|
190
|
+
forwarding, use the value printed by stripe listen. Do not generate your own value.
|
|
191
|
+
</p>
|
|
187
192
|
<Credential
|
|
188
193
|
label="Webhook secret"
|
|
189
194
|
present={Boolean(configuration.webhookSecret)}
|
|
@@ -210,10 +215,19 @@ export function PaymentSettingsPage() {
|
|
|
210
215
|
</form>
|
|
211
216
|
<section className="max-w-3xl rounded-xl bg-kumo-tint/40 p-5 text-sm text-kumo-subtle">
|
|
212
217
|
<h2 className="font-semibold text-kumo-default">Webhook endpoint</h2>
|
|
218
|
+
<input
|
|
219
|
+
aria-label="Payment webhook endpoint"
|
|
220
|
+
className="mt-2 w-full rounded-lg border border-kumo-line bg-kumo-base px-3 py-2"
|
|
221
|
+
readOnly
|
|
222
|
+
value={
|
|
223
|
+
typeof window === "undefined"
|
|
224
|
+
? PAYMENT_WEBHOOK_PATH
|
|
225
|
+
: new URL(PAYMENT_WEBHOOK_PATH, window.location.origin).href
|
|
226
|
+
}
|
|
227
|
+
/>
|
|
213
228
|
<p className="mt-2">
|
|
214
|
-
Configure Stripe
|
|
215
|
-
|
|
216
|
-
signing secret.
|
|
229
|
+
Configure one Stripe endpoint for Venue, Restaurant, and Subscriptions. Payment verifies
|
|
230
|
+
each event and delivers it to the enabled plugins.
|
|
217
231
|
</p>
|
|
218
232
|
</section>
|
|
219
233
|
</div>
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { Database } from "dineway";
|
|
2
2
|
import type { Kysely, Selectable, Transaction, Updateable } from "kysely";
|
|
3
3
|
|
|
4
|
+
import type { PaymentDatabase, PaymentTables } from "../database/types.js";
|
|
4
5
|
import type { PaymentStripeSettings, UpdatePaymentStripeSettings } from "./types.js";
|
|
5
6
|
|
|
6
7
|
type PaymentDb = Kysely<Database> | Transaction<Database>;
|
|
7
|
-
type SettingsRow = Selectable<
|
|
8
|
+
type SettingsRow = Selectable<PaymentDatabase["payment_stripe_settings"]>;
|
|
8
9
|
|
|
9
10
|
export class PaymentStripeConfigurationRepository {
|
|
10
|
-
|
|
11
|
+
private readonly db: Kysely<PaymentDatabase> | Transaction<PaymentDatabase>;
|
|
12
|
+
constructor(db: PaymentDb) {
|
|
13
|
+
this.db = db.withTables<PaymentTables>();
|
|
14
|
+
}
|
|
11
15
|
|
|
12
16
|
async ensureDefaults(): Promise<PaymentStripeSettings> {
|
|
13
17
|
await this.db
|
|
@@ -32,7 +36,7 @@ export class PaymentStripeConfigurationRepository {
|
|
|
32
36
|
expectedVersion: number,
|
|
33
37
|
input: UpdatePaymentStripeSettings,
|
|
34
38
|
): Promise<PaymentStripeSettings> {
|
|
35
|
-
const update: Updateable<
|
|
39
|
+
const update: Updateable<PaymentDatabase["payment_stripe_settings"]> = {
|
|
36
40
|
...(input.mode === undefined ? {} : { mode: input.mode }),
|
|
37
41
|
...(input.publishableKey === undefined ? {} : { publishable_key: input.publishableKey }),
|
|
38
42
|
...(input.configurationStatus === undefined
|
|
@@ -10,12 +10,10 @@ import { PAYMENT_PLUGIN_ID } from "../constants.js";
|
|
|
10
10
|
|
|
11
11
|
export const PAYMENT_STRIPE_SECRET_KEY = "stripe_secret_key";
|
|
12
12
|
export const PAYMENT_STRIPE_WEBHOOK_SECRET = "stripe_webhook_secret";
|
|
13
|
-
export const PAYMENT_STRIPE_SUBSCRIPTION_WEBHOOK_SECRET = "stripe_subscription_webhook_secret";
|
|
14
13
|
|
|
15
14
|
export type PaymentStripeSecretName =
|
|
16
15
|
| typeof PAYMENT_STRIPE_SECRET_KEY
|
|
17
|
-
| typeof PAYMENT_STRIPE_WEBHOOK_SECRET
|
|
18
|
-
| typeof PAYMENT_STRIPE_SUBSCRIPTION_WEBHOOK_SECRET;
|
|
16
|
+
| typeof PAYMENT_STRIPE_WEBHOOK_SECRET;
|
|
19
17
|
|
|
20
18
|
export class PaymentStripeSecretStore {
|
|
21
19
|
private readonly store: PluginSecretStore;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PluginMigration } from "dineway";
|
|
2
|
+
|
|
3
|
+
import * as configuration from "./migrations/001_stripe_configuration.js";
|
|
4
|
+
import * as subscriptions from "./migrations/002_subscriptions.js";
|
|
5
|
+
import * as purchases from "./migrations/003_purchases.js";
|
|
6
|
+
|
|
7
|
+
export const paymentMigrations = {
|
|
8
|
+
"001_stripe_configuration": configuration,
|
|
9
|
+
"002_subscriptions": subscriptions,
|
|
10
|
+
"003_purchases": purchases,
|
|
11
|
+
} satisfies Record<string, PluginMigration>;
|
|
12
|
+
export type { PaymentDatabase, PaymentTables } from "./types.js";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { currentTimestamp } from "dineway";
|
|
2
|
+
import type { Kysely } from "kysely";
|
|
3
|
+
import { sql } from "kysely";
|
|
4
|
+
|
|
5
|
+
import { schemaExists } from "./adoption.js";
|
|
6
|
+
|
|
7
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
8
|
+
if (
|
|
9
|
+
await schemaExists(db, {
|
|
10
|
+
payment_stripe_settings: [
|
|
11
|
+
"id",
|
|
12
|
+
"default_key",
|
|
13
|
+
"mode",
|
|
14
|
+
"publishable_key",
|
|
15
|
+
"configuration_status",
|
|
16
|
+
"stripe_account_id",
|
|
17
|
+
"charges_enabled",
|
|
18
|
+
"details_submitted",
|
|
19
|
+
"tax_status",
|
|
20
|
+
"validation_error_code",
|
|
21
|
+
"last_validated_at",
|
|
22
|
+
"connect_status",
|
|
23
|
+
"version",
|
|
24
|
+
"created_at",
|
|
25
|
+
"updated_at",
|
|
26
|
+
],
|
|
27
|
+
})
|
|
28
|
+
)
|
|
29
|
+
return;
|
|
30
|
+
await db.schema
|
|
31
|
+
.createTable("payment_stripe_settings")
|
|
32
|
+
.addColumn("id", "text", (col) => col.primaryKey())
|
|
33
|
+
.addColumn("default_key", "text", (col) => col.notNull().unique().defaultTo("default"))
|
|
34
|
+
.addColumn("mode", "text", (col) =>
|
|
35
|
+
col
|
|
36
|
+
.notNull()
|
|
37
|
+
.check(sql`mode in ('test', 'live')`)
|
|
38
|
+
.defaultTo("test"),
|
|
39
|
+
)
|
|
40
|
+
.addColumn("publishable_key", "text")
|
|
41
|
+
.addColumn("configuration_status", "text", (col) =>
|
|
42
|
+
col
|
|
43
|
+
.notNull()
|
|
44
|
+
.check(sql`configuration_status in ('unconfigured', 'unverified', 'valid', 'invalid')`)
|
|
45
|
+
.defaultTo("unconfigured"),
|
|
46
|
+
)
|
|
47
|
+
.addColumn("stripe_account_id", "text")
|
|
48
|
+
.addColumn("charges_enabled", "integer", (col) =>
|
|
49
|
+
col.check(sql`charges_enabled is null or charges_enabled in (0, 1)`),
|
|
50
|
+
)
|
|
51
|
+
.addColumn("details_submitted", "integer", (col) =>
|
|
52
|
+
col.check(sql`details_submitted is null or details_submitted in (0, 1)`),
|
|
53
|
+
)
|
|
54
|
+
.addColumn("tax_status", "text", (col) =>
|
|
55
|
+
col
|
|
56
|
+
.notNull()
|
|
57
|
+
.check(sql`tax_status in ('unknown', 'active', 'pending', 'unavailable')`)
|
|
58
|
+
.defaultTo("unknown"),
|
|
59
|
+
)
|
|
60
|
+
.addColumn("validation_error_code", "text")
|
|
61
|
+
.addColumn("last_validated_at", "text")
|
|
62
|
+
.addColumn("connect_status", "text", (col) =>
|
|
63
|
+
col
|
|
64
|
+
.notNull()
|
|
65
|
+
.check(sql`connect_status in ('not_configured', 'restricted', 'ready')`)
|
|
66
|
+
.defaultTo("not_configured"),
|
|
67
|
+
)
|
|
68
|
+
.addColumn("version", "integer", (col) => col.notNull().defaultTo(1))
|
|
69
|
+
.addColumn("created_at", "text", (col) => col.notNull().defaultTo(currentTimestamp(db)))
|
|
70
|
+
.addColumn("updated_at", "text", (col) => col.notNull().defaultTo(currentTimestamp(db)))
|
|
71
|
+
.addCheckConstraint("ck_payment_stripe_settings_default_key", sql`default_key = 'default'`)
|
|
72
|
+
.execute();
|
|
73
|
+
|
|
74
|
+
const tables = await db.introspection.getTables();
|
|
75
|
+
if (tables.some((table) => table.name === "restaurant_stripe_settings"))
|
|
76
|
+
await sql`
|
|
77
|
+
INSERT INTO payment_stripe_settings (
|
|
78
|
+
id, default_key, mode, publishable_key, configuration_status,
|
|
79
|
+
stripe_account_id, charges_enabled, details_submitted, tax_status,
|
|
80
|
+
validation_error_code, last_validated_at, connect_status, version,
|
|
81
|
+
created_at, updated_at
|
|
82
|
+
)
|
|
83
|
+
SELECT
|
|
84
|
+
id, default_key, mode, publishable_key, configuration_status,
|
|
85
|
+
stripe_account_id, charges_enabled, details_submitted, tax_status,
|
|
86
|
+
validation_error_code, last_validated_at, connect_status, version,
|
|
87
|
+
created_at, updated_at
|
|
88
|
+
FROM restaurant_stripe_settings
|
|
89
|
+
`.execute(db);
|
|
90
|
+
|
|
91
|
+
await db.schema
|
|
92
|
+
.createIndex("idx_payment_stripe_settings_updated_at")
|
|
93
|
+
.on("payment_stripe_settings")
|
|
94
|
+
.column("updated_at")
|
|
95
|
+
.execute();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
99
|
+
await db.schema.dropTable("payment_stripe_settings").execute();
|
|
100
|
+
}
|