@dineway-ai/plugin-payment 0.2.8 → 0.2.9

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.8",
3
+ "version": "0.2.9",
4
4
  "description": "Provider-neutral payment infrastructure and Stripe integration for Dineway plugins",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/src/admin/form.ts CHANGED
@@ -2,10 +2,11 @@ import type {
2
2
  PaymentStripeConfiguration,
3
3
  UpdatePaymentStripeConfigurationInput,
4
4
  } from "../configuration/service.js";
5
- import type { StripeMode } from "../types.js";
5
+ import type { StripeCollectionMode, StripeMode } from "../types.js";
6
6
 
7
7
  export interface PaymentSettingsForm {
8
8
  mode: StripeMode;
9
+ collectionMode: StripeCollectionMode;
9
10
  publishableKey: string;
10
11
  secretKey: string;
11
12
  webhookSecret: string;
@@ -16,6 +17,7 @@ export function paymentSettingsForm(
16
17
  ): PaymentSettingsForm {
17
18
  return {
18
19
  mode: configuration.settings.mode,
20
+ collectionMode: configuration.settings.collectionMode,
19
21
  publishableKey: configuration.settings.publishableKey ?? "",
20
22
  secretKey: "",
21
23
  webhookSecret: "",
@@ -45,6 +47,7 @@ export function buildPaymentSettingsUpdate(
45
47
  return {
46
48
  expectedVersion: configuration.settings.version,
47
49
  mode: form.mode,
50
+ collectionMode: form.collectionMode,
48
51
  publishableKey: publishableKey || null,
49
52
  ...(secretKey ? { secretKey } : {}),
50
53
  ...(webhookSecret ? { webhookSecret } : {}),
@@ -183,7 +183,10 @@ export function PaymentSubscriptionsPage() {
183
183
  <h2 className="text-lg font-semibold">Payment webhook</h2>
184
184
  <p className="text-sm">
185
185
  Application: {configuration.namespace} · {configuration.mode} ·{" "}
186
- {configuration.ready ? "Ready" : "Configuration required"}
186
+ {configuration.collectionMode === "managed"
187
+ ? "Managed Payments"
188
+ : "Standard Stripe Billing"}{" "}
189
+ · {configuration.ready ? "Ready" : "Configuration required"}
187
190
  </p>
188
191
  <label className="block text-sm">
189
192
  Endpoint
@@ -408,6 +411,10 @@ export function PaymentSubscriptionsPage() {
408
411
  {purchase.reference}
409
412
  <br />
410
413
  {purchase.contract.pricing.kind}
414
+ <br />
415
+ {purchase.collectionMode === "managed"
416
+ ? "Managed Payments"
417
+ : "Standard"}
411
418
  </td>
412
419
  <td className="p-2">
413
420
  {formatSubscriptionPrice(
package/src/admin.tsx CHANGED
@@ -103,6 +103,7 @@ export function PaymentSettingsPage() {
103
103
  configuration.settings.publishableKey &&
104
104
  configuration.secretKey &&
105
105
  form.mode === configuration.settings.mode &&
106
+ form.collectionMode === configuration.settings.collectionMode &&
106
107
  form.publishableKey.trim() === configuration.settings.publishableKey &&
107
108
  !form.secretKey.trim(),
108
109
  );
@@ -150,6 +151,31 @@ export function PaymentSettingsPage() {
150
151
  ))}
151
152
  </div>
152
153
  </fieldset>
154
+ <label className="grid gap-1 text-sm font-medium">
155
+ Collection mode
156
+ <select
157
+ className="rounded-lg border border-kumo-line bg-kumo-base px-3 py-2 font-normal"
158
+ value={form.collectionMode}
159
+ disabled={pending}
160
+ onChange={(event) =>
161
+ setForm({
162
+ ...form,
163
+ collectionMode: event.target.value === "managed" ? "managed" : "standard",
164
+ })
165
+ }
166
+ >
167
+ <option value="standard">Standard Stripe Billing (default)</option>
168
+ <option value="managed">Stripe Managed Payments</option>
169
+ </select>
170
+ </label>
171
+ <p className="text-sm text-kumo-subtle">
172
+ {form.collectionMode === "managed"
173
+ ? "Stripe acts as merchant of record for eligible digital products. Enable Managed Payments in your Stripe account and assign an eligible tax code to each product. Stripe calculates tax and chooses supported payment methods."
174
+ : "Your business is the merchant of record and manages its tax obligations. Subscriptions use standard Stripe Billing."}{" "}
175
+ This setting applies to new subscription Checkouts. Existing subscriptions and pending
176
+ purchases keep their original mode. Restaurant orders and venue payments continue to use
177
+ standard payments.
178
+ </p>
153
179
  <Field
154
180
  label="Publishable key"
155
181
  value={form.publishableKey}
@@ -38,6 +38,7 @@ export class PaymentStripeConfigurationRepository {
38
38
  ): Promise<PaymentStripeSettings> {
39
39
  const update: Updateable<PaymentDatabase["payment_stripe_settings"]> = {
40
40
  ...(input.mode === undefined ? {} : { mode: input.mode }),
41
+ ...(input.collectionMode === undefined ? {} : { collection_mode: input.collectionMode }),
41
42
  ...(input.publishableKey === undefined ? {} : { publishable_key: input.publishableKey }),
42
43
  ...(input.configurationStatus === undefined
43
44
  ? {}
@@ -75,6 +76,7 @@ function mapSettings(row: SettingsRow): PaymentStripeSettings {
75
76
  return {
76
77
  id: row.id,
77
78
  mode: row.mode,
79
+ collectionMode: row.collection_mode,
78
80
  publishableKey: row.publishable_key,
79
81
  configurationStatus: row.configuration_status,
80
82
  stripeAccountId: row.stripe_account_id,
@@ -1,7 +1,7 @@
1
1
  import type { Database, PluginSecretStatus, PluginSecretStoreOptions } from "dineway";
2
2
  import type { Kysely, Transaction } from "kysely";
3
3
 
4
- import type { StripeAccountProbe, StripeMode } from "../types.js";
4
+ import type { StripeAccountProbe, StripeCollectionMode, StripeMode } from "../types.js";
5
5
  import { PaymentStripeConfigurationRepository } from "./repository.js";
6
6
  import {
7
7
  PAYMENT_STRIPE_SECRET_KEY,
@@ -19,6 +19,7 @@ export interface PaymentStripeConfiguration {
19
19
  export interface UpdatePaymentStripeConfigurationInput {
20
20
  expectedVersion: number;
21
21
  mode?: StripeMode;
22
+ collectionMode?: StripeCollectionMode;
22
23
  publishableKey?: string | null;
23
24
  secretKey?: string | null;
24
25
  webhookSecret?: string | null;
@@ -89,6 +90,7 @@ export async function updatePaymentStripeConfiguration(
89
90
  const credentialsComplete = Boolean(publishableKey && nextSecret);
90
91
  await repository.updateSettings(before.version, {
91
92
  mode,
93
+ collectionMode: input.collectionMode,
92
94
  publishableKey,
93
95
  ...(credentialsChanged || !credentialsComplete
94
96
  ? {
@@ -212,6 +214,12 @@ function normalizeOptionalSecret(value: string | null): string | null {
212
214
  }
213
215
 
214
216
  function validateUpdate(input: UpdatePaymentStripeConfigurationInput): void {
217
+ if (
218
+ input.collectionMode !== undefined &&
219
+ !["standard", "managed"].includes(input.collectionMode)
220
+ ) {
221
+ throw new PaymentConfigurationError("VALIDATION_ERROR", "Choose Standard or Managed Payments");
222
+ }
215
223
  if (!Number.isSafeInteger(input.expectedVersion) || input.expectedVersion < 1) {
216
224
  throw new PaymentConfigurationError("VALIDATION_ERROR", "Expected version is invalid");
217
225
  }
@@ -1,4 +1,4 @@
1
- import type { StripeMode, StripeTaxStatus } from "../types.js";
1
+ import type { StripeCollectionMode, StripeMode, StripeTaxStatus } from "../types.js";
2
2
 
3
3
  export type PaymentStripeConfigurationStatus = "unconfigured" | "unverified" | "valid" | "invalid";
4
4
  export type PaymentStripeConnectStatus = "not_configured" | "restricted" | "ready";
@@ -6,6 +6,7 @@ export type PaymentStripeConnectStatus = "not_configured" | "restricted" | "read
6
6
  export interface PaymentStripeSettings {
7
7
  id: string;
8
8
  mode: StripeMode;
9
+ collectionMode: StripeCollectionMode;
9
10
  publishableKey: string | null;
10
11
  configurationStatus: PaymentStripeConfigurationStatus;
11
12
  stripeAccountId: string | null;
@@ -22,6 +23,7 @@ export interface PaymentStripeSettings {
22
23
 
23
24
  export interface UpdatePaymentStripeSettings {
24
25
  mode?: StripeMode;
26
+ collectionMode?: StripeCollectionMode;
25
27
  publishableKey?: string | null;
26
28
  configurationStatus?: PaymentStripeConfigurationStatus;
27
29
  stripeAccountId?: string | null;
@@ -3,10 +3,12 @@ import type { PluginMigration } from "dineway";
3
3
  import * as configuration from "./migrations/001_stripe_configuration.js";
4
4
  import * as subscriptions from "./migrations/002_subscriptions.js";
5
5
  import * as purchases from "./migrations/003_purchases.js";
6
+ import * as collectionMode from "./migrations/004_collection_mode.js";
6
7
 
7
8
  export const paymentMigrations = {
8
9
  "001_stripe_configuration": configuration,
9
10
  "002_subscriptions": subscriptions,
10
11
  "003_purchases": purchases,
12
+ "004_collection_mode": collectionMode,
11
13
  } satisfies Record<string, PluginMigration>;
12
14
  export type { PaymentDatabase, PaymentTables } from "./types.js";
@@ -0,0 +1,26 @@
1
+ import { sql, type Kysely } from "kysely";
2
+
3
+ export async function up(db: Kysely<unknown>): Promise<void> {
4
+ for (const table of ["payment_stripe_settings", "payment_subscription_purchases"]) {
5
+ await db.schema
6
+ .alterTable(table)
7
+ .addColumn("collection_mode", "text", (column) =>
8
+ column
9
+ .notNull()
10
+ .defaultTo("standard")
11
+ .check(sql`collection_mode IN ('standard', 'managed')`),
12
+ )
13
+ .execute();
14
+ }
15
+ }
16
+
17
+ export async function down(db: Kysely<unknown>): Promise<void> {
18
+ const purchases = await sql`SELECT 1 FROM payment_subscription_purchases LIMIT 1`.execute(db);
19
+ if (purchases.rows.length)
20
+ throw new Error(
21
+ "Restore a database snapshot to roll back payment purchases without losing their collection mode.",
22
+ );
23
+ for (const table of ["payment_subscription_purchases", "payment_stripe_settings"]) {
24
+ await db.schema.alterTable(table).dropColumn("collection_mode").execute();
25
+ }
26
+ }
@@ -1,6 +1,8 @@
1
1
  import type { Database } from "dineway";
2
2
  import type { Generated } from "kysely";
3
3
 
4
+ import type { StripeCollectionMode } from "../types.js";
5
+
4
6
  export interface PaymentSubscriptionKey {
5
7
  scope: string;
6
8
  id: string;
@@ -86,6 +88,7 @@ export interface PaymentSubscriptionPortalTable extends PaymentSubscriptionKey {
86
88
  }
87
89
 
88
90
  export interface PaymentSubscriptionPurchaseTable extends PaymentSubscriptionKey {
91
+ collection_mode: Generated<StripeCollectionMode>;
89
92
  user_id: string;
90
93
  group_key: string;
91
94
  contract: string;
@@ -117,6 +120,7 @@ export interface PaymentStripeSettingsTable {
117
120
  id: string;
118
121
  default_key: Generated<"default">;
119
122
  mode: Generated<PaymentStripeMode>;
123
+ collection_mode: Generated<StripeCollectionMode>;
120
124
  publishable_key: string | null;
121
125
  configuration_status: Generated<PaymentStripeConfigurationStatus>;
122
126
  stripe_account_id: string | null;
@@ -22,7 +22,7 @@ import {
22
22
  } from "./schemas.js";
23
23
 
24
24
  const AUTHORIZATION = {
25
- permission: "content:edit_any" as const,
25
+ permission: "payments:manage" as const,
26
26
  anyOfTokenScopes: ["admin"],
27
27
  };
28
28
 
@@ -6,6 +6,7 @@ export const updatePaymentStripeConfigurationSchema = z
6
6
  .object({
7
7
  expectedVersion: z.number().int().positive(),
8
8
  mode: z.enum(["test", "live"]).optional(),
9
+ collectionMode: z.enum(["standard", "managed"]).optional(),
9
10
  publishableKey: z.string().max(512).nullable().optional(),
10
11
  secretKey: z.string().max(512).nullable().optional(),
11
12
  webhookSecret: z.string().max(512).nullable().optional(),
@@ -1,5 +1,6 @@
1
1
  import type Stripe from "stripe";
2
2
 
3
+ import type { StripeCollectionMode } from "../types.js";
3
4
  import { relationId } from "./purchases.js";
4
5
  import {
5
6
  SubscriptionError,
@@ -92,13 +93,31 @@ export async function invoiceFunding(
92
93
  invoice: Stripe.Invoice,
93
94
  contract: PurchaseContract | null,
94
95
  now: number,
96
+ collectionMode: StripeCollectionMode = "standard",
95
97
  ): Promise<InvoiceFundingEvidence> {
96
98
  const item = subscription.items.data[0]!;
97
99
  const amount = contract?.amount ?? item.price.unit_amount ?? 0;
98
100
  const currency = contract?.currency ?? item.price.currency;
99
101
  const credit =
100
102
  invoice.billing_reason === "subscription_create" ? (contract?.firstPeriodCredit ?? 0) : 0;
101
- const cash = amount - credit;
103
+ const taxes = invoice.total_taxes ?? [];
104
+ const invalidTax = taxes.some(
105
+ (tax) =>
106
+ !Number.isSafeInteger(tax.amount) ||
107
+ tax.amount < 0 ||
108
+ (tax.amount !== 0 &&
109
+ (collectionMode !== "managed" || !["exclusive", "inclusive"].includes(tax.tax_behavior))),
110
+ );
111
+ const exclusiveTax = taxes.reduce(
112
+ (sum, tax) => sum + (tax.tax_behavior === "exclusive" ? tax.amount : 0),
113
+ 0,
114
+ );
115
+ // Stripe line subtotals exclude inclusive tax; invoice subtotals retain it.
116
+ const inclusiveTax = taxes.reduce(
117
+ (sum, tax) => sum + (tax.tax_behavior === "inclusive" ? tax.amount : 0),
118
+ 0,
119
+ );
120
+ const cash = amount - credit + exclusiveTax;
102
121
  const evidence: InvoiceFundingEvidence = {
103
122
  verifiedAt: now,
104
123
  amount,
@@ -132,7 +151,7 @@ export async function invoiceFunding(
132
151
  line.quantity !== 1 ||
133
152
  line.currency !== currency ||
134
153
  line.amount !== amount ||
135
- line.subtotal !== amount ||
154
+ line.subtotal !== amount - inclusiveTax ||
136
155
  invoice.subtotal !== amount ||
137
156
  invoice.total !== cash ||
138
157
  invoice.amount_due !== cash ||
@@ -142,7 +161,11 @@ export async function invoiceFunding(
142
161
  invoice.ending_balance !== 0 ||
143
162
  invoice.pre_payment_credit_notes_amount !== 0 ||
144
163
  invoice.post_payment_credit_notes_amount !== 0 ||
145
- invoice.total_taxes?.some((tax) => tax.amount !== 0) ||
164
+ invalidTax ||
165
+ !Number.isSafeInteger(inclusiveTax) ||
166
+ inclusiveTax > amount - credit ||
167
+ !Number.isSafeInteger(cash) ||
168
+ cash < 0 ||
146
169
  !Number.isSafeInteger(line.period.start) ||
147
170
  !Number.isSafeInteger(line.period.end) ||
148
171
  line.period.start >= line.period.end
@@ -245,7 +268,7 @@ export async function invoiceFunding(
245
268
  const resolved =
246
269
  typeof method === "string" ? await stripe.paymentMethods.retrieve(method) : method;
247
270
  if (
248
- resolved.type !== "card" ||
271
+ (collectionMode === "standard" && resolved.type !== "card") ||
249
272
  resolved.livemode !== invoice.livemode ||
250
273
  relationId(resolved.customer) !== customer
251
274
  )
@@ -94,6 +94,7 @@ export function subscriptionProjection(
94
94
  const { contract } = purchase;
95
95
  if (
96
96
  purchase.userId !== userId ||
97
+ Boolean(subscription.managed_payments?.enabled) !== (purchase.collectionMode === "managed") ||
97
98
  subscription.metadata.purchase_reference !== purchase.reference ||
98
99
  (purchase.subscriptionId && purchase.subscriptionId !== subscription.id) ||
99
100
  item.price.livemode !== (scope.mode === "live") ||
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
 
3
3
  import type Stripe from "stripe";
4
4
 
5
+ import type { StripeCollectionMode } from "../types.js";
5
6
  import { recurringPrice } from "./projection.js";
6
7
  import type { SubscriptionRepository } from "./repository.js";
7
8
  import { purchaseSchema } from "./schemas.js";
@@ -24,6 +25,7 @@ export interface PurchaseContext {
24
25
  scope: SubscriptionScope;
25
26
  now: () => number;
26
27
  origin: string;
28
+ collectionMode: StripeCollectionMode;
27
29
  }
28
30
  const idempotency = (context: PurchaseContext, ...values: unknown[]) =>
29
31
  "dineway:" + digest([context.repository.scope, ...values]);
@@ -108,6 +110,8 @@ export async function createPurchase(
108
110
  input: SubscriptionPurchaseInput,
109
111
  ) {
110
112
  const parsed = purchaseSchema.parse(input);
113
+ if (!["standard", "managed"].includes(context.collectionMode))
114
+ throw new SubscriptionError("INVALID_COLLECTION_MODE", "Choose Standard or Managed Payments");
111
115
  return context.repository.withLock("member:" + userId, async (store) => {
112
116
  const previous = await store.purchase(parsed.reference);
113
117
  if (previous) {
@@ -128,6 +132,15 @@ export async function createPurchase(
128
132
  "Resolve the existing purchase before starting another",
129
133
  409,
130
134
  );
135
+ if (
136
+ context.collectionMode === "managed" &&
137
+ parsed.pricing.kind === "dynamic" &&
138
+ !parsed.pricing.taxCode
139
+ )
140
+ throw new SubscriptionError(
141
+ "MANAGED_PAYMENTS_TAX_CODE_REQUIRED",
142
+ "Managed Payments requires an eligible Stripe product tax code",
143
+ );
131
144
  let contract: PurchaseContract;
132
145
  if (parsed.pricing.kind === "catalogue") {
133
146
  const pricing = parsed.pricing;
@@ -149,6 +162,23 @@ export async function createPurchase(
149
162
  verified.interval !== price.interval
150
163
  )
151
164
  throw new SubscriptionError("PRICE_CHANGED", "Refresh the plan price before checkout", 409);
165
+ if (context.collectionMode === "managed") {
166
+ const product = await context.stripe.products.retrieve(price.productId);
167
+ if (
168
+ product.deleted ||
169
+ !product.active ||
170
+ product.livemode !== (context.scope.mode === "live")
171
+ )
172
+ throw new SubscriptionError(
173
+ "INVALID_PRODUCT",
174
+ "Choose an active product from this Stripe environment",
175
+ );
176
+ if (!product.tax_code)
177
+ throw new SubscriptionError(
178
+ "MANAGED_PAYMENTS_TAX_CODE_REQUIRED",
179
+ "Set an eligible tax code on the Stripe product before using Managed Payments",
180
+ );
181
+ }
152
182
  contract = {
153
183
  ...parsed,
154
184
  amount: price.amount,
@@ -185,6 +215,7 @@ export async function createPurchase(
185
215
  contract,
186
216
  digest(parsed),
187
217
  Math.floor(context.now() / 1000) + 3600,
218
+ context.collectionMode,
188
219
  );
189
220
  return resumePurchase(context, store, (await store.purchase(parsed.reference))!);
190
221
  });
@@ -274,9 +305,12 @@ export async function resumePurchase(
274
305
  price_data: {
275
306
  currency: contract.currency,
276
307
  unit_amount: contract.amount,
308
+ tax_behavior:
309
+ contract.pricing.kind === "dynamic" ? contract.pricing.taxBehavior : undefined,
277
310
  recurring: { interval: contract.interval, interval_count: contract.intervalCount },
278
311
  product_data: {
279
312
  name: contract.pricing.kind === "dynamic" ? contract.pricing.name : "Subscription",
313
+ tax_code: contract.pricing.kind === "dynamic" ? contract.pricing.taxCode : undefined,
280
314
  metadata,
281
315
  },
282
316
  },
@@ -287,7 +321,10 @@ export async function resumePurchase(
287
321
  mode: "subscription",
288
322
  customer: customerId,
289
323
  line_items: [line],
290
- payment_method_types: ["card"],
324
+ managed_payments: { enabled: purchase.collectionMode === "managed" },
325
+ ...(purchase.collectionMode === "standard"
326
+ ? { payment_method_types: ["card" as const] }
327
+ : {}),
291
328
  payment_method_collection: "always",
292
329
  client_reference_id: purchase.reference,
293
330
  metadata,
@@ -305,6 +342,7 @@ export async function resumePurchase(
305
342
  if (
306
343
  relationId(session.customer) !== customerId ||
307
344
  session.mode !== "subscription" ||
345
+ Boolean(session.managed_payments?.enabled) !== (purchase.collectionMode === "managed") ||
308
346
  session.livemode !== (context.scope.mode === "live") ||
309
347
  session.metadata?.dineway_app !== context.scope.namespace ||
310
348
  session.metadata.user_id !== purchase.userId ||
@@ -478,6 +478,7 @@ export class SubscriptionRepository {
478
478
  if (!row) return null;
479
479
  return {
480
480
  reference: row.id,
481
+ collectionMode: row.collection_mode,
481
482
  userId: row.user_id,
482
483
  contract: JSON.parse(row.contract),
483
484
  createdAt: row.created_at,
@@ -523,6 +524,7 @@ export class SubscriptionRepository {
523
524
  contract: PurchaseContract,
524
525
  digest: string,
525
526
  expiresAt: number,
527
+ collectionMode: SubscriptionPurchase["collectionMode"],
526
528
  ) {
527
529
  await this.db.transaction().execute(async (trx) => {
528
530
  await this.assertLease(trx);
@@ -534,6 +536,7 @@ export class SubscriptionRepository {
534
536
  user_id: userId,
535
537
  group_key: contract.groupKey,
536
538
  contract: JSON.stringify(contract),
539
+ collection_mode: collectionMode,
537
540
  fingerprint: digest,
538
541
  created_at: this.now(),
539
542
  expires_at: expiresAt,
@@ -41,6 +41,7 @@ export async function getPaymentSubscriptionService(
41
41
  siteUrl: options.siteUrl,
42
42
  secretKey,
43
43
  webhookSecret,
44
+ collectionMode: config.settings.collectionMode,
44
45
  });
45
46
  }
46
47
 
@@ -53,6 +54,7 @@ export async function getSubscriptionConfiguration(options: SubscriptionOptions)
53
54
  return {
54
55
  namespace: options.namespace,
55
56
  mode: configuration.settings.mode,
57
+ collectionMode: configuration.settings.collectionMode,
56
58
  accountId: configuration.settings.stripeAccountId,
57
59
  ready:
58
60
  configuration.settings.configurationStatus === "valid" &&
@@ -127,6 +127,11 @@ export const purchaseSchema = z
127
127
  currency: z.string().regex(/^[a-z]{3}$/),
128
128
  interval: z.enum(["day", "week", "month", "year"]),
129
129
  intervalCount: z.number().int().min(1).max(365),
130
+ taxCode: z
131
+ .string()
132
+ .regex(/^txcd_[0-9]+$/)
133
+ .optional(),
134
+ taxBehavior: z.enum(["exclusive", "inclusive"]).optional(),
130
135
  })
131
136
  .strict(),
132
137
  ]),
@@ -5,7 +5,7 @@ import type { Kysely } from "kysely";
5
5
  import Stripe from "stripe";
6
6
 
7
7
  import { StripeNodeWebhookVerifier } from "../stripe/webhook-verifier.js";
8
- import type { StripeWebhookEvent } from "../types.js";
8
+ import type { StripeCollectionMode, StripeWebhookEvent } from "../types.js";
9
9
  import { collectBounded, invoiceFunding, inspectCharge } from "./funding.js";
10
10
  import {
11
11
  recurringPrice,
@@ -45,6 +45,7 @@ import {
45
45
  } from "./types.js";
46
46
 
47
47
  export interface SubscriptionServiceOptions {
48
+ collectionMode?: StripeCollectionMode;
48
49
  db: Kysely<Database>;
49
50
  scope: SubscriptionScope;
50
51
  siteUrl: string;
@@ -237,6 +238,7 @@ export class PaymentSubscriptionService {
237
238
  scope: this.options.scope,
238
239
  now: this.now,
239
240
  origin: this.origin,
241
+ collectionMode: this.options.collectionMode ?? "standard",
240
242
  };
241
243
  }
242
244
  private async resolveSubject(subject: BillingSubject | string) {
@@ -567,6 +569,7 @@ export class PaymentSubscriptionService {
567
569
  invoice,
568
570
  purchase?.contract ?? null,
569
571
  this.now(),
572
+ purchase?.collectionMode ?? "standard",
570
573
  );
571
574
  const old = previousPeriods.filter((period) => period.invoiceId === invoiceId);
572
575
  const projected = old.length
@@ -1,4 +1,4 @@
1
- import type { StripeMode } from "../types.js";
1
+ import type { StripeCollectionMode, StripeMode } from "../types.js";
2
2
 
3
3
  export type SubscriptionEntitlements = Record<string, boolean | number>;
4
4
  export type SubscriptionInterval = "month" | "year";
@@ -92,6 +92,8 @@ export type PurchasePricing =
92
92
  currency: string;
93
93
  interval: "day" | "week" | "month" | "year";
94
94
  intervalCount: number;
95
+ taxCode?: string;
96
+ taxBehavior?: "exclusive" | "inclusive";
95
97
  };
96
98
  export interface SubscriptionPurchaseInput {
97
99
  reference: string;
@@ -110,6 +112,7 @@ export interface PurchaseContract extends SubscriptionPurchaseInput {
110
112
  couponId: string | null;
111
113
  }
112
114
  export interface SubscriptionPurchase {
115
+ collectionMode: StripeCollectionMode;
113
116
  reference: string;
114
117
  userId: string;
115
118
  contract: PurchaseContract;
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type { PaymentDatabase, PaymentTables } from "./database/types.js";
2
2
  export type StripeMode = "test" | "live";
3
+ export type StripeCollectionMode = "standard" | "managed";
3
4
  export type StripeTaxStatus = "unknown" | "active" | "pending" | "unavailable";
4
5
 
5
6
  export type StripeProviderErrorCode =