@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dineway-ai/plugin-payment",
3
- "version": "0.2.6",
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.29"
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<MemberSubscription[]>([]);
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 [webhookSecret, setWebhookSecret] = React.useState("");
38
- const [detail, setDetail] = React.useState<{
39
- subscription: MemberSubscription;
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: MemberSubscription[]; nextCursor?: string }>(
52
- "subscriptions/list",
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 href="/_dineway/admin/payments" className="mt-2 inline-block text-sm underline">
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">Subscription webhook</h2>
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
- Create a Stripe webhook for this endpoint using the subscription events in the Payment
183
- documentation. Its signing secret is separate from other payment webhooks.
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 save this webhook secret to manage subscription plans.
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">Member subscriptions</h2>
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">{member.userId}</td>
414
- <td className="p-2">{member.planCode}</td>
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: MemberSubscription[];
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>{detail.subscription.userId}</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.member.paidThrough
464
- ? new Date(detail.member.paidThrough * 1000).toLocaleString()
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
- : "Not scheduled"}
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 to send domain events to the webhook shown by each installed
215
- transactional plugin. Venue and Restaurant validate exact request bytes with this shared
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<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
@@ -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
+ }