@dineway-ai/plugin-payment 0.2.6 → 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.6",
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",
@@ -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,18 @@ 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
44
  const [webhookSecret, setWebhookSecret] = React.useState("");
38
- const [detail, setDetail] = React.useState<{
39
- subscription: MemberSubscription;
40
- member: MemberBilling;
41
- } | null>(null);
45
+ const [detail, setDetail] = React.useState<
46
+ (SubscriptionSnapshot & { member: MemberBilling }) | null
47
+ >(null);
42
48
  const [busy, setBusy] = React.useState(false);
43
49
  const [error, setError] = React.useState<string | null>(null);
44
50
  const [notice, setNotice] = React.useState<string | null>(null);
@@ -46,15 +52,18 @@ export function PaymentSubscriptionsPage() {
46
52
  const config = await postPaymentRoute<Configuration>("subscriptions/configuration/get");
47
53
  setConfiguration(config);
48
54
  if (config.ready) {
49
- const [catalog, subscriptions] = await Promise.all([
55
+ const [catalog, subscriptions, purchasePage] = await Promise.all([
50
56
  postPaymentRoute<{ items: SubscriptionPlan[] }>("subscriptions/plans/list"),
51
- postPaymentRoute<{ items: MemberSubscription[]; nextCursor?: string }>(
52
- "subscriptions/list",
57
+ postPaymentRoute<{ items: AdminSubscription[]; nextCursor?: string }>("subscriptions/list"),
58
+ postPaymentRoute<{ items: AdminPurchase[]; nextCursor?: string }>(
59
+ "subscriptions/purchases/list",
53
60
  ),
54
61
  ]);
55
62
  setPlans(catalog.items);
56
63
  setMembers(subscriptions.items);
57
64
  setCursor(subscriptions.nextCursor);
65
+ setPurchases(purchasePage.items);
66
+ setPurchaseCursor(purchasePage.nextCursor);
58
67
  }
59
68
  }, []);
60
69
  React.useEffect(() => {
@@ -391,7 +400,100 @@ export function PaymentSubscriptionsPage() {
391
400
  </form>
392
401
  </section>
393
402
  <section className="space-y-4">
394
- <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>
395
497
  <p className="text-sm text-kumo-subtle">
396
498
  Failed renewals keep access until the end of the paid period. Cancellation takes
397
499
  effect at period end.
@@ -410,8 +512,18 @@ export function PaymentSubscriptionsPage() {
410
512
  <tbody>
411
513
  {members.map((member) => (
412
514
  <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>
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>
415
527
  <td className="p-2">{member.status}</td>
416
528
  <td className="p-2">
417
529
  {new Date(member.verifiedAt).toLocaleString()}
@@ -441,7 +553,7 @@ export function PaymentSubscriptionsPage() {
441
553
  onClick={() =>
442
554
  void run(async () => {
443
555
  const page = await postPaymentRoute<{
444
- items: MemberSubscription[];
556
+ items: AdminSubscription[];
445
557
  nextCursor?: string;
446
558
  }>("subscriptions/list", { cursor });
447
559
  setMembers([...members, ...page.items]);
@@ -457,11 +569,31 @@ export function PaymentSubscriptionsPage() {
457
569
  <h3 className="font-semibold">{detail.subscription.id}</h3>
458
570
  <dl className="grid gap-2 text-sm">
459
571
  <dt>Member</dt>
460
- <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>
461
579
  <dt>Paid access until</dt>
462
580
  <dd>
463
- {detail.member.paidThrough
464
- ? 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()
465
597
  : "No current paid period"}
466
598
  </dd>
467
599
  <dt>Cancellation</dt>
@@ -469,7 +601,9 @@ export function PaymentSubscriptionsPage() {
469
601
  {detail.subscription.cancelAtPeriodEnd
470
602
  ? "At period end: " +
471
603
  new Date(detail.subscription.periodEnd * 1000).toLocaleString()
472
- : "Not scheduled"}
604
+ : detail.subscription.status === "canceled"
605
+ ? "Canceled"
606
+ : "Not scheduled"}
473
607
  </dd>
474
608
  <dt>Last synchronization</dt>
475
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";
@@ -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
+ }
@@ -0,0 +1,168 @@
1
+ import type { Kysely } from "kysely";
2
+
3
+ import { schemaExists } from "./adoption.js";
4
+
5
+ const tables = [
6
+ "payment_subscription_plans",
7
+ "payment_subscription_prices",
8
+ "payment_subscription_customers",
9
+ "payment_member_subscriptions",
10
+ "payment_subscription_periods",
11
+ "payment_subscription_events",
12
+ "payment_subscription_attempts",
13
+ "payment_subscription_locks",
14
+ "payment_subscription_portals",
15
+ ] as const;
16
+
17
+ export async function up(db: Kysely<unknown>): Promise<void> {
18
+ if (
19
+ await schemaExists(db, {
20
+ payment_subscription_plans: ["scope", "id", "name", "enabled", "entitlements", "version"],
21
+ payment_subscription_prices: [
22
+ "scope",
23
+ "id",
24
+ "plan_code",
25
+ "product_id",
26
+ "currency",
27
+ "amount",
28
+ "interval",
29
+ "enabled",
30
+ ],
31
+ payment_subscription_customers: ["scope", "id", "customer_id"],
32
+ payment_member_subscriptions: [
33
+ "scope",
34
+ "id",
35
+ "user_id",
36
+ "customer_id",
37
+ "plan_code",
38
+ "price_id",
39
+ "status",
40
+ "period_end",
41
+ "cancel_at_period_end",
42
+ "verified_at",
43
+ "last_error",
44
+ ],
45
+ payment_subscription_periods: [
46
+ "scope",
47
+ "id",
48
+ "subscription_id",
49
+ "invoice_id",
50
+ "plan_code",
51
+ "starts_at",
52
+ "ends_at",
53
+ ],
54
+ payment_subscription_events: ["scope", "id", "received_at"],
55
+ payment_subscription_attempts: [
56
+ "scope",
57
+ "id",
58
+ "fingerprint",
59
+ "request_id",
60
+ "session_id",
61
+ "created_at",
62
+ ],
63
+ payment_subscription_locks: ["scope", "id", "token", "expires_at"],
64
+ payment_subscription_portals: ["scope", "id", "configuration_id"],
65
+ })
66
+ )
67
+ return;
68
+ const base = (table: (typeof tables)[number]) =>
69
+ db.schema
70
+ .createTable(table)
71
+ .addColumn("scope", "text", (col) => col.notNull())
72
+ .addColumn("id", "text", (col) => col.notNull())
73
+ .addPrimaryKeyConstraint("pk_" + table, ["scope", "id"]);
74
+ await base("payment_subscription_plans")
75
+ .addColumn("name", "text", (col) => col.notNull())
76
+ .addColumn("enabled", "integer", (col) => col.notNull().defaultTo(0))
77
+ .addColumn("entitlements", "text", (col) => col.notNull())
78
+ .addColumn("version", "integer", (col) => col.notNull().defaultTo(1))
79
+ .execute();
80
+ await base("payment_subscription_prices")
81
+ .addColumn("plan_code", "text", (col) => col.notNull())
82
+ .addColumn("product_id", "text", (col) => col.notNull())
83
+ .addColumn("currency", "text", (col) => col.notNull())
84
+ .addColumn("amount", "integer", (col) => col.notNull())
85
+ .addColumn("interval", "text", (col) => col.notNull())
86
+ .addColumn("enabled", "integer", (col) => col.notNull().defaultTo(1))
87
+ .addForeignKeyConstraint(
88
+ "fk_payment_price_plan",
89
+ ["scope", "plan_code"],
90
+ "payment_subscription_plans",
91
+ ["scope", "id"],
92
+ )
93
+ .execute();
94
+ await base("payment_subscription_customers")
95
+ .addColumn("customer_id", "text", (col) => col.notNull())
96
+ .addUniqueConstraint("uq_payment_subscription_customer", ["scope", "customer_id"])
97
+ .execute();
98
+ await base("payment_member_subscriptions")
99
+ .addColumn("user_id", "text", (col) => col.notNull())
100
+ .addColumn("customer_id", "text", (col) => col.notNull())
101
+ .addColumn("plan_code", "text", (col) => col.notNull())
102
+ .addColumn("price_id", "text", (col) => col.notNull())
103
+ .addColumn("status", "text", (col) => col.notNull())
104
+ .addColumn("period_end", "integer", (col) => col.notNull())
105
+ .addColumn("cancel_at_period_end", "integer", (col) => col.notNull())
106
+ .addColumn("verified_at", "integer", (col) => col.notNull())
107
+ .addColumn("last_error", "text")
108
+ .addForeignKeyConstraint(
109
+ "fk_payment_subscription_user",
110
+ ["scope", "user_id"],
111
+ "payment_subscription_customers",
112
+ ["scope", "id"],
113
+ )
114
+ .addForeignKeyConstraint(
115
+ "fk_payment_subscription_price",
116
+ ["scope", "price_id"],
117
+ "payment_subscription_prices",
118
+ ["scope", "id"],
119
+ )
120
+ .execute();
121
+ await base("payment_subscription_periods")
122
+ .addColumn("subscription_id", "text", (col) => col.notNull())
123
+ .addColumn("invoice_id", "text", (col) => col.notNull())
124
+ .addColumn("plan_code", "text", (col) => col.notNull())
125
+ .addColumn("starts_at", "integer", (col) => col.notNull())
126
+ .addColumn("ends_at", "integer", (col) => col.notNull())
127
+ .addForeignKeyConstraint(
128
+ "fk_payment_period_subscription",
129
+ ["scope", "subscription_id"],
130
+ "payment_member_subscriptions",
131
+ ["scope", "id"],
132
+ )
133
+ .execute();
134
+ await base("payment_subscription_events")
135
+ .addColumn("received_at", "integer", (col) => col.notNull())
136
+ .execute();
137
+ await base("payment_subscription_attempts")
138
+ .addColumn("fingerprint", "text", (col) => col.notNull())
139
+ .addColumn("request_id", "text", (col) => col.notNull())
140
+ .addColumn("session_id", "text")
141
+ .addColumn("created_at", "integer", (col) => col.notNull())
142
+ .execute();
143
+ await base("payment_subscription_locks")
144
+ .addColumn("token", "text", (col) => col.notNull())
145
+ .addColumn("expires_at", "integer", (col) => col.notNull())
146
+ .execute();
147
+ for (const [table, column] of [
148
+ // Composite indexes keep reads within the configured account and application.
149
+ ["payment_subscription_prices", "plan_code"],
150
+ ["payment_member_subscriptions", "user_id"],
151
+ ["payment_member_subscriptions", "price_id"],
152
+ ["payment_subscription_periods", "subscription_id"],
153
+ ["payment_subscription_periods", "invoice_id"],
154
+ ] as const) {
155
+ await db.schema
156
+ .createIndex("idx_" + table + "_" + column)
157
+ .on(table)
158
+ .columns(["scope", column])
159
+ .execute();
160
+ }
161
+ await base("payment_subscription_portals")
162
+ .addColumn("configuration_id", "text", (col) => col.notNull())
163
+ .execute();
164
+ }
165
+
166
+ export async function down(db: Kysely<unknown>): Promise<void> {
167
+ for (const table of tables.toReversed()) await db.schema.dropTable(table).execute();
168
+ }