@dineway-ai/plugin-payment 0.2.5 → 0.2.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dineway-ai/plugin-payment",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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.29"
33
+ "dineway": "^0.2.30"
33
34
  },
34
35
  "dependencies": {
35
36
  "@stripe/stripe-js": "9.9.0",
@@ -0,0 +1,264 @@
1
+ import * as React from "react";
2
+
3
+ import { formatSubscriptionPrice } from "../subscriptions/format.js";
4
+ import type {
5
+ SubscriptionCatalogPage,
6
+ SubscriptionPrice,
7
+ SubscriptionProduct,
8
+ StripeSubscriptionPrice,
9
+ } from "../subscriptions/types.js";
10
+ import { postPaymentRoute } from "./api.js";
11
+
12
+ const inputClass = "w-full rounded-lg border border-kumo-line bg-kumo-base px-3 py-2 text-sm";
13
+ const buttonClass = "rounded-lg border border-kumo-line px-3 py-2 text-sm disabled:opacity-50";
14
+ type Selection = { monthlyPriceId: string | null; yearlyPriceId: string | null };
15
+
16
+ function useCatalog<T extends { id: string }>(
17
+ route: string,
18
+ productId: string | undefined,
19
+ revision: number,
20
+ ) {
21
+ const [state, setState] = React.useState<
22
+ SubscriptionCatalogPage<T> & { loading: boolean; error: string | null }
23
+ >({ items: [], loading: true, error: null });
24
+ const generation = React.useRef(0);
25
+ const request = React.useCallback(
26
+ (cursor?: string) =>
27
+ postPaymentRoute<SubscriptionCatalogPage<T>>(route, {
28
+ ...(productId ? { productId } : {}),
29
+ cursor,
30
+ }),
31
+ [route, productId],
32
+ );
33
+ React.useEffect(() => {
34
+ const current = ++generation.current;
35
+ setState({ items: [], loading: true, error: null });
36
+ void request().then(
37
+ (page) => {
38
+ if (generation.current === current) setState({ ...page, loading: false, error: null });
39
+ return undefined;
40
+ },
41
+ (cause) => {
42
+ if (generation.current === current)
43
+ setState({ items: [], loading: false, error: errorMessage(cause) });
44
+ },
45
+ );
46
+ return () => {
47
+ generation.current++;
48
+ };
49
+ }, [request, revision]);
50
+ async function loadMore() {
51
+ if (state.loading || !state.nextCursor) return;
52
+ const current = generation.current;
53
+ setState((previous) => ({ ...previous, loading: true, error: null }));
54
+ try {
55
+ const page = await request(state.nextCursor);
56
+ if (generation.current === current)
57
+ setState((previous) => ({
58
+ items: [
59
+ ...new Map([...previous.items, ...page.items].map((item) => [item.id, item])).values(),
60
+ ],
61
+ nextCursor: page.nextCursor,
62
+ loading: false,
63
+ error: null,
64
+ }));
65
+ } catch (cause) {
66
+ if (generation.current === current)
67
+ setState((previous) => ({ ...previous, loading: false, error: errorMessage(cause) }));
68
+ }
69
+ }
70
+ return { ...state, loadMore };
71
+ }
72
+ function errorMessage(cause: unknown) {
73
+ return cause instanceof Error
74
+ ? cause.message
75
+ : "Could not load the Stripe catalog. Try refreshing.";
76
+ }
77
+ function priceLabel(price: SubscriptionPrice & { nickname?: string | null }) {
78
+ return `${formatSubscriptionPrice(price.amount, price.currency)} ${price.currency.toUpperCase()} / ${price.interval}${price.nickname ? ` · ${price.nickname}` : ""} · ${price.id}`;
79
+ }
80
+
81
+ export function SubscriptionPricePicker({
82
+ value,
83
+ boundPrices,
84
+ onChange,
85
+ }: {
86
+ value: Selection;
87
+ boundPrices: SubscriptionPrice[];
88
+ onChange: (value: Selection) => void;
89
+ }) {
90
+ const boundProduct = boundPrices.find(
91
+ (price) => price.id === value.monthlyPriceId || price.id === value.yearlyPriceId,
92
+ )?.productId;
93
+ const [productId, setProductId] = React.useState(boundProduct ?? "");
94
+ const [revision, setRevision] = React.useState(0);
95
+ const products = useCatalog<SubscriptionProduct>(
96
+ "subscriptions/products/list",
97
+ undefined,
98
+ revision,
99
+ );
100
+ return (
101
+ <div className="space-y-3">
102
+ <div className="flex flex-wrap items-end gap-3">
103
+ <label className="w-full text-sm">
104
+ Stripe product
105
+ <select
106
+ className={inputClass}
107
+ value={productId}
108
+ disabled={products.loading}
109
+ onChange={(event) => {
110
+ setProductId(event.target.value);
111
+ onChange({ monthlyPriceId: null, yearlyPriceId: null });
112
+ }}
113
+ >
114
+ <option value="">
115
+ {products.loading ? "Loading Stripe products…" : "Choose a product"}
116
+ </option>
117
+ {productId && !products.items.some((product) => product.id === productId) && (
118
+ <option value={productId}>{productId} · saved selection</option>
119
+ )}
120
+ {products.items.map((product) => (
121
+ <option key={product.id} value={product.id}>
122
+ {product.name} · {product.id}
123
+ </option>
124
+ ))}
125
+ </select>
126
+ </label>
127
+ <button
128
+ type="button"
129
+ className={buttonClass}
130
+ disabled={products.loading}
131
+ onClick={() => setRevision((current) => current + 1)}
132
+ >
133
+ Refresh Stripe catalog
134
+ </button>
135
+ {products.nextCursor && (
136
+ <button
137
+ type="button"
138
+ className={buttonClass}
139
+ disabled={products.loading}
140
+ onClick={() => void products.loadMore()}
141
+ >
142
+ Load more products
143
+ </button>
144
+ )}
145
+ </div>
146
+ <p className="text-sm text-kumo-subtle">
147
+ Select prices from the connected Stripe account and environment. Amounts are read from
148
+ Stripe and verified again when you save.
149
+ </p>
150
+ {products.error && (
151
+ <p role="alert" className="text-sm text-kumo-danger">
152
+ {products.error} Refresh the Stripe catalog to retry.
153
+ </p>
154
+ )}
155
+ {!products.loading && !products.error && products.items.length === 0 && (
156
+ <p className="text-sm text-kumo-subtle">
157
+ No active products found on this page. Create a product in this Stripe environment, then
158
+ refresh.
159
+ </p>
160
+ )}
161
+ {productId && (
162
+ <ProductPrices
163
+ key={productId}
164
+ productId={productId}
165
+ revision={revision}
166
+ value={value}
167
+ boundPrices={boundPrices}
168
+ onChange={onChange}
169
+ />
170
+ )}
171
+ </div>
172
+ );
173
+ }
174
+
175
+ function ProductPrices({
176
+ productId,
177
+ revision,
178
+ value,
179
+ boundPrices,
180
+ onChange,
181
+ }: {
182
+ productId: string;
183
+ revision: number;
184
+ value: Selection;
185
+ boundPrices: SubscriptionPrice[];
186
+ onChange: (value: Selection) => void;
187
+ }) {
188
+ const prices = useCatalog<StripeSubscriptionPrice>(
189
+ "subscriptions/prices/list",
190
+ productId,
191
+ revision,
192
+ );
193
+ const knownPrices = [...prices.items, ...boundPrices];
194
+ return (
195
+ <div className="space-y-3">
196
+ <div className="grid gap-4 md:grid-cols-2">
197
+ {(["month", "year"] as const).map((interval) => {
198
+ const field = interval === "month" ? "monthlyPriceId" : "yearlyPriceId";
199
+ const otherField = interval === "month" ? "yearlyPriceId" : "monthlyPriceId";
200
+ const selected = value[field];
201
+ const other = knownPrices.find((price) => price.id === value[otherField]);
202
+ const saved = boundPrices.find((price) => price.id === selected);
203
+ return (
204
+ <label key={interval} className="text-sm">
205
+ {interval === "month" ? "Monthly Stripe price" : "Yearly Stripe price"}
206
+ <select
207
+ className={inputClass}
208
+ value={selected ?? ""}
209
+ disabled={prices.loading}
210
+ onChange={(event) => onChange({ ...value, [field]: event.target.value || null })}
211
+ >
212
+ <option value="">
213
+ {prices.loading ? "Loading Stripe prices…" : "Not offered"}
214
+ </option>
215
+ {selected && !prices.items.some((price) => price.id === selected) && (
216
+ <option value={selected} disabled>
217
+ {saved ? priceLabel(saved) : selected} · saved binding (not in loaded results)
218
+ </option>
219
+ )}
220
+ {prices.items
221
+ .filter((price) => price.interval === interval)
222
+ .map((price) => (
223
+ <option
224
+ key={price.id}
225
+ value={price.id}
226
+ disabled={Boolean(other && price.currency !== other.currency)}
227
+ >
228
+ {priceLabel(price)}
229
+ {other && price.currency !== other.currency ? " · different currency" : ""}
230
+ </option>
231
+ ))}
232
+ </select>
233
+ </label>
234
+ );
235
+ })}
236
+ </div>
237
+ <p className="text-sm text-kumo-subtle">
238
+ Only active fixed monthly or yearly prices are available. Both prices must use the same
239
+ currency. Keep an interval as “Not offered” if you do not sell it.
240
+ </p>
241
+ {prices.error && (
242
+ <p role="alert" className="text-sm text-kumo-danger">
243
+ {prices.error} Refresh the Stripe catalog to retry.
244
+ </p>
245
+ )}
246
+ {!prices.loading && !prices.error && prices.items.length === 0 && (
247
+ <p className="text-sm text-kumo-subtle">
248
+ No supported recurring prices found on this page. Load more prices, or create a fixed
249
+ monthly or yearly price in Stripe and refresh.
250
+ </p>
251
+ )}
252
+ {prices.nextCursor && (
253
+ <button
254
+ type="button"
255
+ className={buttonClass}
256
+ disabled={prices.loading}
257
+ onClick={() => void prices.loadMore()}
258
+ >
259
+ Load more prices
260
+ </button>
261
+ )}
262
+ </div>
263
+ );
264
+ }
@@ -5,12 +5,18 @@ 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";
11
14
  import { postPaymentRoute } from "./api.js";
15
+ import { SubscriptionPricePicker } from "./subscription-price-picker.js";
12
16
 
13
17
  type Configuration = Awaited<ReturnType<typeof getSubscriptionConfiguration>>;
18
+ type AdminSubscription = MemberSubscription & { subject: BillingSubject | null };
19
+ type AdminPurchase = SubscriptionPurchase & { subject: BillingSubject | null };
14
20
  type Feature = { key: string; kind: "boolean" | "number"; value: string };
15
21
  const emptyPlan: SaveSubscriptionPlan = {
16
22
  code: "",
@@ -27,15 +33,18 @@ const buttonClass = "rounded-lg border border-kumo-line px-3 py-2 text-sm disabl
27
33
  export function PaymentSubscriptionsPage() {
28
34
  const [configuration, setConfiguration] = React.useState<Configuration | null>(null);
29
35
  const [plans, setPlans] = React.useState<SubscriptionPlan[]>([]);
30
- const [members, setMembers] = React.useState<MemberSubscription[]>([]);
36
+ const [members, setMembers] = React.useState<AdminSubscription[]>([]);
37
+ const [purchases, setPurchases] = React.useState<AdminPurchase[]>([]);
38
+ const [purchaseCursor, setPurchaseCursor] = React.useState<string>();
31
39
  const [cursor, setCursor] = React.useState<string>();
32
40
  const [form, setForm] = React.useState<SaveSubscriptionPlan>({ ...emptyPlan });
41
+ const [editorRevision, setEditorRevision] = React.useState(0);
42
+ const [boundPrices, setBoundPrices] = React.useState<SubscriptionPlan["prices"]>([]);
33
43
  const [features, setFeatures] = React.useState<Feature[]>([]);
34
44
  const [webhookSecret, setWebhookSecret] = React.useState("");
35
- const [detail, setDetail] = React.useState<{
36
- subscription: MemberSubscription;
37
- member: MemberBilling;
38
- } | null>(null);
45
+ const [detail, setDetail] = React.useState<
46
+ (SubscriptionSnapshot & { member: MemberBilling }) | null
47
+ >(null);
39
48
  const [busy, setBusy] = React.useState(false);
40
49
  const [error, setError] = React.useState<string | null>(null);
41
50
  const [notice, setNotice] = React.useState<string | null>(null);
@@ -43,15 +52,18 @@ export function PaymentSubscriptionsPage() {
43
52
  const config = await postPaymentRoute<Configuration>("subscriptions/configuration/get");
44
53
  setConfiguration(config);
45
54
  if (config.ready) {
46
- const [catalog, subscriptions] = await Promise.all([
55
+ const [catalog, subscriptions, purchasePage] = await Promise.all([
47
56
  postPaymentRoute<{ items: SubscriptionPlan[] }>("subscriptions/plans/list"),
48
- postPaymentRoute<{ items: MemberSubscription[]; nextCursor?: string }>(
49
- "subscriptions/list",
57
+ postPaymentRoute<{ items: AdminSubscription[]; nextCursor?: string }>("subscriptions/list"),
58
+ postPaymentRoute<{ items: AdminPurchase[]; nextCursor?: string }>(
59
+ "subscriptions/purchases/list",
50
60
  ),
51
61
  ]);
52
62
  setPlans(catalog.items);
53
63
  setMembers(subscriptions.items);
54
64
  setCursor(subscriptions.nextCursor);
65
+ setPurchases(purchasePage.items);
66
+ setPurchaseCursor(purchasePage.nextCursor);
55
67
  }
56
68
  }, []);
57
69
  React.useEffect(() => {
@@ -70,6 +82,8 @@ export function PaymentSubscriptionsPage() {
70
82
  }
71
83
  }
72
84
  function edit(plan?: SubscriptionPlan) {
85
+ setEditorRevision((current) => current + 1);
86
+ setBoundPrices(plan?.prices ?? []);
73
87
  setForm(
74
88
  plan
75
89
  ? {
@@ -265,49 +279,35 @@ export function PaymentSubscriptionsPage() {
265
279
  <h3 className="font-semibold">
266
280
  {form.expectedVersion ? "Edit plan" : "Create plan"}
267
281
  </h3>
268
- <fieldset disabled={busy} className="grid gap-4 md:grid-cols-2">
269
- <label className="text-sm">
270
- Plan code
271
- <input
272
- required
273
- readOnly={form.expectedVersion > 0}
274
- className={inputClass}
275
- value={form.code}
276
- onChange={(event) => setForm({ ...form, code: event.target.value })}
277
- placeholder="pro"
278
- />
279
- </label>
280
- <label className="text-sm">
281
- Name
282
- <input
283
- required
284
- className={inputClass}
285
- value={form.name}
286
- onChange={(event) => setForm({ ...form, name: event.target.value })}
287
- />
288
- </label>
289
- <label className="text-sm">
290
- Monthly Stripe price
291
- <input
292
- className={inputClass}
293
- value={form.monthlyPriceId ?? ""}
294
- placeholder="price_…"
295
- onChange={(event) =>
296
- setForm({ ...form, monthlyPriceId: event.target.value || null })
297
- }
298
- />
299
- </label>
300
- <label className="text-sm">
301
- Yearly Stripe price
302
- <input
303
- className={inputClass}
304
- value={form.yearlyPriceId ?? ""}
305
- placeholder="price_…"
306
- onChange={(event) =>
307
- setForm({ ...form, yearlyPriceId: event.target.value || null })
308
- }
309
- />
310
- </label>
282
+ <fieldset disabled={busy} className="space-y-4">
283
+ <div className="grid gap-4 md:grid-cols-2">
284
+ <label className="text-sm">
285
+ Plan code
286
+ <input
287
+ required
288
+ readOnly={form.expectedVersion > 0}
289
+ className={inputClass}
290
+ value={form.code}
291
+ onChange={(event) => setForm({ ...form, code: event.target.value })}
292
+ placeholder="pro"
293
+ />
294
+ </label>
295
+ <label className="text-sm">
296
+ Name
297
+ <input
298
+ required
299
+ className={inputClass}
300
+ value={form.name}
301
+ onChange={(event) => setForm({ ...form, name: event.target.value })}
302
+ />
303
+ </label>
304
+ </div>
305
+ <SubscriptionPricePicker
306
+ key={editorRevision}
307
+ value={form}
308
+ boundPrices={boundPrices}
309
+ onChange={(prices) => setForm((current) => ({ ...current, ...prices }))}
310
+ />
311
311
  <label className="flex items-center gap-2 text-sm">
312
312
  <input
313
313
  type="checkbox"
@@ -400,7 +400,100 @@ export function PaymentSubscriptionsPage() {
400
400
  </form>
401
401
  </section>
402
402
  <section className="space-y-4">
403
- <h2 className="text-lg font-semibold">Member subscriptions</h2>
403
+ <h2 className="text-lg font-semibold">Purchases</h2>
404
+ <button
405
+ className={buttonClass}
406
+ disabled={busy}
407
+ onClick={() =>
408
+ void run(async () => {
409
+ const result = await postPaymentRoute<{ processed: number; failed: number }>(
410
+ "subscriptions/reconcile",
411
+ );
412
+ await refresh();
413
+ setNotice(
414
+ `Processed ${result.processed}; ${result.failed} require retry or review.`,
415
+ );
416
+ })
417
+ }
418
+ >
419
+ Retry pending synchronization
420
+ </button>
421
+ <div className="overflow-x-auto">
422
+ <table className="w-full text-left text-sm">
423
+ <thead>
424
+ <tr>
425
+ <th className="p-2">Billing subject</th>
426
+ <th className="p-2">Purchase</th>
427
+ <th className="p-2">Terms</th>
428
+ <th className="p-2">Status</th>
429
+ </tr>
430
+ </thead>
431
+ <tbody>
432
+ {purchases.map((purchase) => (
433
+ <tr key={purchase.reference} className="border-t border-kumo-line">
434
+ <td className="p-2">
435
+ {purchase.subject
436
+ ? `${purchase.subject.realm}: ${purchase.subject.id}`
437
+ : purchase.userId}
438
+ </td>
439
+ <td className="p-2">
440
+ {purchase.reference}
441
+ <br />
442
+ {purchase.contract.pricing.kind}
443
+ </td>
444
+ <td className="p-2">
445
+ {formatSubscriptionPrice(
446
+ purchase.contract.amount,
447
+ purchase.contract.currency,
448
+ )}{" "}
449
+ every {purchase.contract.intervalCount} {purchase.contract.interval}
450
+ <br />
451
+ First invoice credit:{" "}
452
+ {formatSubscriptionPrice(
453
+ purchase.contract.firstPeriodCredit ?? 0,
454
+ purchase.contract.currency,
455
+ )}
456
+ </td>
457
+ <td className="p-2">
458
+ {purchase.closed
459
+ ? "Closed"
460
+ : purchase.subscriptionId
461
+ ? "Subscription recorded"
462
+ : "Awaiting confirmation"}
463
+ <br />
464
+ {purchase.cancelMode && `Cancellation: ${purchase.cancelMode}`}
465
+ <br />
466
+ {purchase.lastError}
467
+ </td>
468
+ </tr>
469
+ ))}
470
+ </tbody>
471
+ </table>
472
+ </div>
473
+ {!purchases.length && (
474
+ <p className="text-sm text-kumo-subtle">No purchases recorded.</p>
475
+ )}
476
+ {purchaseCursor && (
477
+ <button
478
+ className={buttonClass}
479
+ disabled={busy}
480
+ onClick={() =>
481
+ void run(async () => {
482
+ const page = await postPaymentRoute<{
483
+ items: AdminPurchase[];
484
+ nextCursor?: string;
485
+ }>("subscriptions/purchases/list", { cursor: purchaseCursor });
486
+ setPurchases([...purchases, ...page.items]);
487
+ setPurchaseCursor(page.nextCursor);
488
+ })
489
+ }
490
+ >
491
+ Load more purchases
492
+ </button>
493
+ )}
494
+ </section>
495
+ <section className="space-y-4">
496
+ <h2 className="text-lg font-semibold">Subscriptions and funding</h2>
404
497
  <p className="text-sm text-kumo-subtle">
405
498
  Failed renewals keep access until the end of the paid period. Cancellation takes
406
499
  effect at period end.
@@ -419,8 +512,18 @@ export function PaymentSubscriptionsPage() {
419
512
  <tbody>
420
513
  {members.map((member) => (
421
514
  <tr key={member.id} className="border-t border-kumo-line">
422
- <td className="p-2">{member.userId}</td>
423
- <td className="p-2">{member.planCode}</td>
515
+ <td className="p-2">
516
+ {member.subject
517
+ ? `${member.subject.realm}: ${member.subject.id}`
518
+ : member.userId}
519
+ </td>
520
+ <td className="p-2">
521
+ {member.planCode || "Dynamic purchase"}
522
+ <br />
523
+ <span className="text-xs text-kumo-subtle">
524
+ {member.purchaseReference}
525
+ </span>
526
+ </td>
424
527
  <td className="p-2">{member.status}</td>
425
528
  <td className="p-2">
426
529
  {new Date(member.verifiedAt).toLocaleString()}
@@ -450,7 +553,7 @@ export function PaymentSubscriptionsPage() {
450
553
  onClick={() =>
451
554
  void run(async () => {
452
555
  const page = await postPaymentRoute<{
453
- items: MemberSubscription[];
556
+ items: AdminSubscription[];
454
557
  nextCursor?: string;
455
558
  }>("subscriptions/list", { cursor });
456
559
  setMembers([...members, ...page.items]);
@@ -466,11 +569,31 @@ export function PaymentSubscriptionsPage() {
466
569
  <h3 className="font-semibold">{detail.subscription.id}</h3>
467
570
  <dl className="grid gap-2 text-sm">
468
571
  <dt>Member</dt>
469
- <dd>{detail.subscription.userId}</dd>
572
+ <dd>
573
+ {detail.subject
574
+ ? `${detail.subject.realm}: ${detail.subject.id} · ${detail.subject.email}`
575
+ : detail.subscription.userId}
576
+ </dd>
577
+ <dt>Purchase reference</dt>
578
+ <dd>{detail.purchase?.reference ?? "Catalogue subscription"}</dd>
470
579
  <dt>Paid access until</dt>
471
580
  <dd>
472
- {detail.member.paidThrough
473
- ? new Date(detail.member.paidThrough * 1000).toLocaleString()
581
+ {detail.periods.some(
582
+ (period) =>
583
+ period.eligible &&
584
+ period.startsAt * 1000 <= Date.now() &&
585
+ period.endsAt * 1000 > Date.now(),
586
+ )
587
+ ? new Date(
588
+ Math.max(
589
+ ...detail.periods
590
+ .filter(
591
+ (period) =>
592
+ period.eligible && period.startsAt * 1000 <= Date.now(),
593
+ )
594
+ .map((period) => period.endsAt),
595
+ ) * 1000,
596
+ ).toLocaleString()
474
597
  : "No current paid period"}
475
598
  </dd>
476
599
  <dt>Cancellation</dt>
@@ -478,7 +601,9 @@ export function PaymentSubscriptionsPage() {
478
601
  {detail.subscription.cancelAtPeriodEnd
479
602
  ? "At period end: " +
480
603
  new Date(detail.subscription.periodEnd * 1000).toLocaleString()
481
- : "Not scheduled"}
604
+ : detail.subscription.status === "canceled"
605
+ ? "Canceled"
606
+ : "Not scheduled"}
482
607
  </dd>
483
608
  <dt>Last synchronization</dt>
484
609
  <dd>
@@ -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<Database["payment_stripe_settings"]>;
8
+ type SettingsRow = Selectable<PaymentDatabase["payment_stripe_settings"]>;
8
9
 
9
10
  export class PaymentStripeConfigurationRepository {
10
- constructor(private readonly db: PaymentDb) {}
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<Database["payment_stripe_settings"]> = {
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
@@ -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";