@stacksjs/payments 0.75.357 → 0.75.361

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.
@@ -1,9 +1,12 @@
1
1
  import { asAccount, connectedCustomer, directCheckout, directCheckoutPaymentData, directCheckoutSubscriptionData, directPaymentIntentParams, directSubscriptionParams, manageDirectSubscription } from './direct';
2
2
  import { checkoutLineItemsTotal, destinationCheckoutPaymentData, destinationCheckoutSubscriptionData, destinationFeeCents, destinationFeePercent, destinationPaymentIntentParams, destinationSubscriptionParams, splitFee } from './destination';
3
+ import { collectDirectInvoice } from './invoice-collection';
4
+ import { completeDirectInvoicePurchase, directInvoicePurchaseMetadata } from './invoice-purchase';
3
5
  import { connectRefund, connectRefundParams, isDestinationCharge } from './refund';
4
6
  import { creditDirectInvoiceRefund } from './invoice-credit';
5
7
  import { directChargeFinancials } from './financials';
6
8
  import { directInvoiceTaxFinancials } from './invoice-tax';
9
+ import { ensureDirectInvoicePrice } from './invoice-price';
7
10
  import { manageConnectedAccount } from './account';
8
11
  import { manageConnectWebhook } from './webhook';
9
12
  import { manageDestinationCharge } from './charge';
@@ -35,6 +38,10 @@ export declare const connect: {
35
38
  directCheckout: typeof directCheckout;
36
39
  directChargeFinancials: typeof directChargeFinancials;
37
40
  prepareDirectInvoiceFee: typeof prepareDirectInvoiceFee;
41
+ collectDirectInvoice: typeof collectDirectInvoice;
42
+ ensureDirectInvoicePrice: typeof ensureDirectInvoicePrice;
43
+ directInvoicePurchaseMetadata: typeof directInvoicePurchaseMetadata;
44
+ completeDirectInvoicePurchase: typeof completeDirectInvoicePurchase;
38
45
  creditDirectInvoiceRefund: typeof creditDirectInvoiceRefund;
39
46
  directInvoiceTaxFinancials: typeof directInvoiceTaxFinancials;
40
47
  directPaymentIntentParams: typeof directPaymentIntentParams;
@@ -84,6 +91,9 @@ export * from './destination';
84
91
  export * from './direct';
85
92
  export * from './financials';
86
93
  export * from './invoice-fee';
94
+ export * from './invoice-collection';
95
+ export * from './invoice-price';
96
+ export * from './invoice-purchase';
87
97
  export * from './invoice-credit';
88
98
  export * from './tax';
89
99
  export * from './invoice-tax';
@@ -1 +1 @@
1
- export*from"./account";export*from"./charge";export*from"./destination";export*from"./direct";export*from"./financials";export*from"./invoice-fee";export*from"./invoice-credit";export*from"./tax";export*from"./invoice-tax";export*from"./payout";export*from"./refund";export*from"./subscription";export*from"./transfer";export*from"./webhook";import{manageConnectedAccount}from"./account";import{manageDestinationCharge}from"./charge";import{asAccount,connectedCustomer,directCheckout,directCheckoutPaymentData,directCheckoutSubscriptionData,directPaymentIntentParams,directSubscriptionParams,manageDirectSubscription}from"./direct";import{checkoutLineItemsTotal,destinationCheckoutPaymentData,destinationCheckoutSubscriptionData,destinationFeeCents,destinationFeePercent,destinationPaymentIntentParams,destinationSubscriptionParams,splitFee}from"./destination";import{managePayout}from"./payout";import{directChargeFinancials}from"./financials";import{prepareDirectInvoiceFee}from"./invoice-fee";import{creditDirectInvoiceRefund}from"./invoice-credit";import{directInvoiceTaxFinancials}from"./invoice-tax";import{connectRefund,connectRefundParams,isDestinationCharge}from"./refund";import{manageDestinationSubscription}from"./subscription";import{manageTransfer}from"./transfer";import{manageConnectWebhook}from"./webhook";export const connect={createAccount:manageConnectedAccount.createConnectedAccount,createTestAccount:manageConnectedAccount.createTestConnectedAccount,onboardingLink:manageConnectedAccount.accountOnboardingLink,accountStatus:manageConnectedAccount.accountStatus,retrieveAccount:manageConnectedAccount.retrieveConnectedAccount,destinationCharge:manageDestinationCharge.destinationCharge,applicationFeeFor:manageDestinationCharge.applicationFeeFor,feeCents:destinationFeeCents,feePercent:destinationFeePercent,paymentIntentParams:destinationPaymentIntentParams,checkoutPaymentData:destinationCheckoutPaymentData,checkoutSubscriptionData:destinationCheckoutSubscriptionData,subscriptionParams:destinationSubscriptionParams,lineItemsTotal:checkoutLineItemsTotal,splitFee,asAccount,customer:connectedCustomer,directCheckout,directChargeFinancials,prepareDirectInvoiceFee,creditDirectInvoiceRefund,directInvoiceTaxFinancials,directPaymentIntentParams,directCheckoutPaymentData,directCheckoutSubscriptionData,directSubscriptionParams,directSubscriptionCheckout:manageDirectSubscription.checkout,refund:connectRefund,refundParams:connectRefundParams,isDestinationCharge,subscriptionCheckout:manageDestinationSubscription.checkout,subscriptionFeePercent:manageDestinationSubscription.feePercentFor,subscriptionDestination:manageDestinationSubscription.destinationOf,createTransfer:manageTransfer.createTransfer,reverseTransfer:manageTransfer.reverseTransfer,listTransfers:manageTransfer.listTransfers,retrieveTransfer:manageTransfer.retrieveTransfer,listPayouts:managePayout.listPayouts,retrievePayout:managePayout.retrievePayout,accountBalance:managePayout.accountBalance,onAccount:manageConnectWebhook.onAccount,onPayout:manageConnectWebhook.onPayout,getAccount:manageConnectWebhook.getAccount,getPayout:manageConnectWebhook.getPayout,getConnectedAccountId:manageConnectWebhook.getConnectedAccountId,account:manageConnectedAccount,charge:manageDestinationCharge,subscription:manageDestinationSubscription,directSubscription:manageDirectSubscription,transfer:manageTransfer,payout:managePayout};
1
+ export*from"./account";export*from"./charge";export*from"./destination";export*from"./direct";export*from"./financials";export*from"./invoice-fee";export*from"./invoice-collection";export*from"./invoice-price";export*from"./invoice-purchase";export*from"./invoice-credit";export*from"./tax";export*from"./invoice-tax";export*from"./payout";export*from"./refund";export*from"./subscription";export*from"./transfer";export*from"./webhook";import{manageConnectedAccount}from"./account";import{manageDestinationCharge}from"./charge";import{asAccount,connectedCustomer,directCheckout,directCheckoutPaymentData,directCheckoutSubscriptionData,directPaymentIntentParams,directSubscriptionParams,manageDirectSubscription}from"./direct";import{checkoutLineItemsTotal,destinationCheckoutPaymentData,destinationCheckoutSubscriptionData,destinationFeeCents,destinationFeePercent,destinationPaymentIntentParams,destinationSubscriptionParams,splitFee}from"./destination";import{managePayout}from"./payout";import{directChargeFinancials}from"./financials";import{prepareDirectInvoiceFee}from"./invoice-fee";import{collectDirectInvoice}from"./invoice-collection";import{ensureDirectInvoicePrice}from"./invoice-price";import{completeDirectInvoicePurchase,directInvoicePurchaseMetadata}from"./invoice-purchase";import{creditDirectInvoiceRefund}from"./invoice-credit";import{directInvoiceTaxFinancials}from"./invoice-tax";import{connectRefund,connectRefundParams,isDestinationCharge}from"./refund";import{manageDestinationSubscription}from"./subscription";import{manageTransfer}from"./transfer";import{manageConnectWebhook}from"./webhook";export const connect={createAccount:manageConnectedAccount.createConnectedAccount,createTestAccount:manageConnectedAccount.createTestConnectedAccount,onboardingLink:manageConnectedAccount.accountOnboardingLink,accountStatus:manageConnectedAccount.accountStatus,retrieveAccount:manageConnectedAccount.retrieveConnectedAccount,destinationCharge:manageDestinationCharge.destinationCharge,applicationFeeFor:manageDestinationCharge.applicationFeeFor,feeCents:destinationFeeCents,feePercent:destinationFeePercent,paymentIntentParams:destinationPaymentIntentParams,checkoutPaymentData:destinationCheckoutPaymentData,checkoutSubscriptionData:destinationCheckoutSubscriptionData,subscriptionParams:destinationSubscriptionParams,lineItemsTotal:checkoutLineItemsTotal,splitFee,asAccount,customer:connectedCustomer,directCheckout,directChargeFinancials,prepareDirectInvoiceFee,collectDirectInvoice,ensureDirectInvoicePrice,directInvoicePurchaseMetadata,completeDirectInvoicePurchase,creditDirectInvoiceRefund,directInvoiceTaxFinancials,directPaymentIntentParams,directCheckoutPaymentData,directCheckoutSubscriptionData,directSubscriptionParams,directSubscriptionCheckout:manageDirectSubscription.checkout,refund:connectRefund,refundParams:connectRefundParams,isDestinationCharge,subscriptionCheckout:manageDestinationSubscription.checkout,subscriptionFeePercent:manageDestinationSubscription.feePercentFor,subscriptionDestination:manageDestinationSubscription.destinationOf,createTransfer:manageTransfer.createTransfer,reverseTransfer:manageTransfer.reverseTransfer,listTransfers:manageTransfer.listTransfers,retrieveTransfer:manageTransfer.retrieveTransfer,listPayouts:managePayout.listPayouts,retrievePayout:managePayout.retrievePayout,accountBalance:managePayout.accountBalance,onAccount:manageConnectWebhook.onAccount,onPayout:manageConnectWebhook.onPayout,getAccount:manageConnectWebhook.getAccount,getPayout:manageConnectWebhook.getPayout,getConnectedAccountId:manageConnectWebhook.getConnectedAccountId,account:manageConnectedAccount,charge:manageDestinationCharge,subscription:manageDestinationSubscription,directSubscription:manageDirectSubscription,transfer:manageTransfer,payout:managePayout};
@@ -0,0 +1,25 @@
1
+ import type { DirectInvoiceFeeOptions } from './invoice-fee';
2
+ import type Stripe from 'stripe';
3
+ /** Collect an original merchant invoice only after validating the approved
4
+ * total, tax and exact after-tax application fee. Drafts remain held while
5
+ * preparing/finalizing. Open invoices can be recovered without recreating a
6
+ * purchase; paid invoices are read-only replays. A card requiring customer
7
+ * action returns Stripe's hosted invoice instead of granting access. Network
8
+ * failures propagate for the caller's durable retry. No proportional tax math.
9
+ */
10
+ export declare function collectDirectInvoice(invoiceId: string, account: string, options: DirectInvoiceCollectionOptions, client?: Pick<Stripe, 'invoices' | 'invoicePayments' | 'paymentMethods' | 'paymentIntents'>): Promise<DirectInvoiceCollection>;
11
+ export declare interface DirectInvoiceCollectionOptions extends DirectInvoiceFeeOptions {
12
+ approvedTotal: number
13
+ paymentMethod: string
14
+ }
15
+ export declare interface DirectInvoiceCollection {
16
+ invoice: Stripe.Invoice
17
+ status: 'paid' | 'requires_payment' | 'processing'
18
+ paymentIntent?: Stripe.PaymentIntent
19
+ paymentUrl?: string
20
+ currency: string
21
+ gross: number
22
+ net: number
23
+ tax: number
24
+ fee: number
25
+ }
@@ -0,0 +1 @@
1
+ import{stripe}from"../drivers/stripe";import{stacksIdempotencyKey}from"../idempotency";import{asAccount}from"./direct";import{splitFee}from"./destination";import{prepareDirectInvoiceFee}from"./invoice-fee";import{allPages}from"./pagination";import{manageDestinationSubscription}from"./subscription";import{invoiceTaxAmounts}from"./tax";function id(value){return typeof value==="string"?value:value?.id}export async function collectDirectInvoice(invoiceId,account,options,client=stripe){const request=asAccount(account);if(!/^in_[a-z0-9_]+$/i.test(invoiceId)||!/^cus_[a-z0-9_]+$/i.test(options.customer)||options.subscription!=null&&!/^sub_[a-z0-9_]+$/i.test(options.subscription)||!/^[a-z]{3}$/i.test(options.currency)||!/^pm_[a-z0-9_]+$/i.test(options.paymentMethod)||!Number.isSafeInteger(options.approvedTotal)||options.approvedTotal<0)throw Error("[payments/connect] Expected original invoice, customer, subscription, currency, saved method and approved total");if(options.platformFeePercent==null)throw Error("[payments/connect] An explicit invoice fee percentage is required");manageDestinationSubscription.feePercentFor({platformFeePercent:options.platformFeePercent});const verify=(value)=>{if(value.id!==invoiceId||!["draft","open","paid"].includes(value.status||"")||id(value.customer)!==options.customer||id(value.parent?.subscription_details?.subscription)!==options.subscription||value.currency.toUpperCase()!==options.currency.toUpperCase()||value.amount_due!==options.approvedTotal)throw Error("[payments/connect] Collection requires the original collectible invoice and approved amount");const totals=invoiceTaxAmounts(value);if(totals.gross!==options.approvedTotal)throw Error("[payments/connect] Invoice total differs from the buyer-approved amount");return totals};let invoice=await client.invoices.retrieve(invoiceId,{},request);const basis=verify(invoice),bps=Math.round(options.platformFeePercent*100),fee=splitFee(basis.net,{platform:bps,merchant:1e4-bps},"merchant").platform,stable=(value)=>{const totals=verify(value);if(totals.net!==basis.net||totals.tax!==basis.tax)throw Error("[payments/connect] Invoice tax changed during collection; payment was not authorized")};if(invoice.status!=="paid"&&invoice.auto_advance!==!1){invoice=await client.invoices.update(invoiceId,{auto_advance:!1},{...request,idempotencyKey:stacksIdempotencyKey("connect.invoice.hold",account,invoiceId)});stable(invoice)}if(invoice.status!=="paid"&&basis.gross>0){const method=await client.paymentMethods.retrieve(options.paymentMethod,{},request);if(method.id!==options.paymentMethod||id(method.customer)!==options.customer)throw Error("[payments/connect] Saved payment method belongs to another customer")}if(invoice.status==="draft"){const prepared=await prepareDirectInvoiceFee(invoiceId,account,options,client);stable(prepared.invoice);invoice=await client.invoices.finalizeInvoice(invoiceId,{auto_advance:!1},{...request,idempotencyKey:stacksIdempotencyKey("connect.invoice.finalize",account,invoiceId,basis.gross,basis.tax,fee)});stable(invoice)}const result=(status,intent)=>({invoice,status,...intent?{paymentIntent:intent}:{},...status==="requires_payment"&&invoice.hosted_invoice_url?{paymentUrl:invoice.hosted_invoice_url}:{},currency:invoice.currency.toUpperCase(),...basis,fee});if(basis.gross===0){if(invoice.status!=="paid")throw Error("[payments/connect] Zero invoice has not been marked paid by the provider");return result("paid")}const candidates=(await allPages((after)=>client.invoicePayments.list({invoice:invoiceId,limit:100,...after?{starting_after:after}:{}},request))).filter((payment)=>payment.status!=="canceled"&&payment.payment.type==="payment_intent");if(candidates.length!==1||id(candidates[0].invoice)!==invoiceId||!id(candidates[0].payment.payment_intent))throw Error("[payments/connect] Expected one canonical invoice payment intent");const intentId=id(candidates[0].payment.payment_intent),readIntent=async()=>{const intent=await client.paymentIntents.retrieve(intentId,{},request);if(intent.id!==intentId||id(intent.customer)!==options.customer||intent.currency.toUpperCase()!==options.currency.toUpperCase()||intent.amount!==basis.gross||(intent.application_fee_amount??0)!==fee||intent.status==="canceled")throw Error("[payments/connect] Invoice payment does not match its original customer, currency, amount or after-tax fee");return intent};let intent=await readIntent();if(invoice.status==="paid"){if(intent.status!=="succeeded")throw Error("[payments/connect] Paid invoice has no successful original payment");return result("paid",intent)}if(intent.status==="processing"||intent.status==="succeeded")return result("processing",intent);if(intent.status==="requires_action")return result("requires_payment",intent);if(!["requires_confirmation","requires_payment_method"].includes(intent.status))throw Error("[payments/connect] Invoice payment is not ready for automatic collection");try{invoice=await client.invoices.pay(invoiceId,{payment_method:options.paymentMethod,off_session:!0},{...request,idempotencyKey:stacksIdempotencyKey("connect.invoice.pay",account,invoiceId,intentId,options.paymentMethod,basis.gross,basis.tax,fee)});stable(invoice);intent=await readIntent();if(invoice.status==="paid"&&intent.status!=="succeeded")throw Error("[payments/connect] Paid invoice has no successful original payment");return result(invoice.status==="paid"?"paid":["processing","succeeded"].includes(intent.status)?"processing":"requires_payment",intent)}catch(error){if(error?.type!=="StripeCardError")throw error;invoice=await client.invoices.retrieve(invoiceId,{},request);stable(invoice);intent=await readIntent();if(invoice.status==="paid"&&intent.status!=="succeeded")throw Error("[payments/connect] Paid invoice has no successful original payment");return result(invoice.status==="paid"?"paid":"requires_payment",intent)}}
@@ -0,0 +1,17 @@
1
+ import type Stripe from 'stripe';
2
+ /** Find or create an inclusive catalog price on the original merchant. Stripe's
3
+ * unique lookup key survives the idempotency window and lost create responses.
4
+ * Different commercial terms receive a different immutable price. Existing
5
+ * prices are verified, never silently edited/reactivated or transferred from
6
+ * another lookup key. The merchant supplies its actual tax classification.
7
+ */
8
+ export declare function ensureDirectInvoicePrice(account: string, options: DirectInvoicePriceOptions, client?: Pick<Stripe, 'prices'>): Promise<Stripe.Price>;
9
+ export declare interface DirectInvoicePriceOptions {
10
+ reference: string
11
+ name: string
12
+ amount: number
13
+ currency: string
14
+ taxCode: string
15
+ interval?: 'day' | 'week' | 'month' | 'year'
16
+ intervalCount?: number
17
+ }
@@ -0,0 +1 @@
1
+ import{stripe}from"../drivers/stripe";import{idempotencyFingerprint,stacksIdempotencyKey}from"../idempotency";import{asAccount}from"./direct";export async function ensureDirectInvoicePrice(account,options,client=stripe){const request=asAccount(account);if(typeof options.reference!=="string"||!/^[a-z0-9:._-]{1,160}$/i.test(options.reference)||typeof options.name!=="string"||!options.name.trim()||options.name.length>250||!Number.isSafeInteger(options.amount)||options.amount<=0||!/^[a-z]{3}$/i.test(options.currency)||!/^txcd_\d{8}$/.test(options.taxCode)||options.interval!==void 0&&!["day","week","month","year"].includes(options.interval)||options.intervalCount!==void 0&&(!options.interval||!Number.isSafeInteger(options.intervalCount)||options.intervalCount<1))throw Error("[payments/connect] Expected stable offer identity, name, positive minor-unit price, currency and merchant tax code");const terms={reference:options.reference,name:options.name.trim(),amount:options.amount,currency:options.currency.toLowerCase(),taxCode:options.taxCode,interval:options.interval??null,intervalCount:options.interval?options.intervalCount??1:null},fingerprint=idempotencyFingerprint(terms),lookup=`stacks.invoice-price.${fingerprint}`,listed=await client.prices.list({lookup_keys:[lookup],limit:100},request);if(listed.has_more||listed.data.length>1)throw Error("[payments/connect] Merchant invoice price lookup is ambiguous");const price=listed.data[0]??await client.prices.create({currency:terms.currency,unit_amount:terms.amount,tax_behavior:"inclusive",lookup_key:lookup,metadata:{stacks_invoice_price:fingerprint,stacks_offer_reference:terms.reference},product_data:{name:terms.name,tax_code:terms.taxCode,metadata:{stacks_invoice_price:fingerprint,stacks_offer_reference:terms.reference}},...options.interval?{recurring:{interval:options.interval,interval_count:options.intervalCount??1}}:{}},{...request,idempotencyKey:stacksIdempotencyKey("connect.invoice.price",account,fingerprint)});if(!/^price_[a-z0-9_]+$/i.test(price.id))throw Error("[payments/connect] Provider returned no catalog price identity");const current=await client.prices.retrieve(price.id,{expand:["product"]},request),{product,recurring}=current;if(current.id!==price.id||!current.active||current.lookup_key!==lookup||current.metadata?.stacks_invoice_price!==fingerprint||current.currency!==terms.currency||current.unit_amount!==terms.amount||current.tax_behavior!=="inclusive"||current.type!==(terms.interval?"recurring":"one_time")||(terms.interval?!recurring||recurring.interval!==terms.interval||recurring.interval_count!==terms.intervalCount||recurring.usage_type!=="licensed":recurring!=null)||typeof product==="string"||product.deleted||!product.active||product.name!==terms.name||(typeof product.tax_code==="string"?product.tax_code:product.tax_code?.id)!==terms.taxCode)throw Error("[payments/connect] Existing merchant price differs from the frozen inclusive offer or is inactive");return current}
@@ -0,0 +1,22 @@
1
+ import type { DirectInvoiceCollection } from './invoice-collection';
2
+ import type { DirectInvoicePriceOptions } from './invoice-price';
3
+ import type Stripe from 'stripe';
4
+ /** Copy onto Setup Checkout's setup_intent_data.metadata. Completion verifies
5
+ * this binding before creating any purchase. The app owns explicit consent and
6
+ * durable locking of the reference; metadata is not a substitute for either. */
7
+ export declare function directInvoicePurchaseMetadata(account: string, options: DirectInvoicePurchaseOptions): Record<string, string>;
8
+ /** Complete only a successful setup bound to the original frozen quote. The
9
+ * caller serializes the durable purchase reference. Provider lists recover the
10
+ * original invoice/schedule beyond idempotency retention; no search-index lag
11
+ * or process-local cache decides whether to create another purchase. */
12
+ export declare function completeDirectInvoicePurchase(setupIntentId: string, account: string, options: DirectInvoicePurchaseOptions, client?: PurchaseClient): Promise<DirectInvoiceCollection & { subscription?: Stripe.Subscription }>;
13
+ export declare interface DirectInvoicePurchaseOptions extends Pick<DirectInvoicePriceOptions, 'amount' | 'currency' | 'taxCode' | 'interval' | 'intervalCount'> {
14
+ reference: string
15
+ customer: string
16
+ price: string
17
+ expiresAt: number
18
+ platformFeePercent: number
19
+ trialDays?: number
20
+ metadata?: Record<string, string>
21
+ }
22
+ declare type PurchaseClient = Pick<Stripe, 'setupIntents' | 'prices' | 'paymentMethods' | 'invoices' | 'invoiceItems' | 'subscriptionSchedules' | 'subscriptions' | 'invoicePayments' | 'paymentIntents'>;
@@ -0,0 +1 @@
1
+ import{stripe}from"../drivers/stripe";import{idempotencyFingerprint,stacksIdempotencyKey}from"../idempotency";import{asAccount}from"./direct";import{collectDirectInvoice}from"./invoice-collection";import{allPages}from"./pagination";import{manageDestinationSubscription}from"./subscription";export function directInvoicePurchaseMetadata(account,options){asAccount(account);if(typeof options.reference!=="string"||!/^[a-z0-9:._-]{1,160}$/i.test(options.reference)||!/^cus_[a-z0-9_]+$/i.test(options.customer)||!/^price_[a-z0-9_]+$/i.test(options.price)||!Number.isSafeInteger(options.expiresAt)||options.expiresAt<=0||!Number.isSafeInteger(options.amount)||options.amount<=0||!/^[a-z]{3}$/i.test(options.currency)||!/^txcd_\d{8}$/.test(options.taxCode)||options.interval!==void 0&&!["day","week","month","year"].includes(options.interval)||options.intervalCount!==void 0&&(!options.interval||!Number.isSafeInteger(options.intervalCount)||options.intervalCount<1)||options.trialDays!==void 0&&(!Number.isSafeInteger(options.trialDays)||options.trialDays<0||options.trialDays>730||!options.interval&&options.trialDays>0))throw Error("[payments/connect] Expected original customer, price and frozen invoice purchase terms");if(options.platformFeePercent==null)throw Error("[payments/connect] An explicit invoice fee percentage is required");manageDestinationSubscription.feePercentFor({platformFeePercent:options.platformFeePercent});const metadata=options.metadata??{};if(typeof metadata!=="object"||Array.isArray(metadata)||Object.keys(metadata).length>47||Object.entries(metadata).some(([key,value])=>!key||key.length>40||/[\[\]]/.test(key)||key.startsWith("stacks_purchase_")||typeof value!=="string"||value.length>500))throw Error("[payments/connect] Invalid purchase metadata or reserved binding fields");const terms={account,reference:options.reference,customer:options.customer,price:options.price,amount:options.amount,currency:options.currency.toLowerCase(),taxCode:options.taxCode,expiresAt:options.expiresAt,interval:options.interval??null,intervalCount:options.interval?options.intervalCount??1:null,trialDays:options.trialDays??0,platformFeePercent:options.platformFeePercent,metadata};return{...metadata,stacks_purchase_reference:options.reference,stacks_purchase_quote:idempotencyFingerprint(terms)}}function id(value){return typeof value==="string"?value:value?.id}export async function completeDirectInvoicePurchase(setupIntentId,account,options,client=stripe){const quote=directInvoicePurchaseMetadata(account,options),request=asAccount(account);if(!/^seti_[a-z0-9_]+$/i.test(setupIntentId))throw Error("[payments/connect] Expected original successful payment setup");const setup=await client.setupIntents.retrieve(setupIntentId,{},request),matches=(metadata,expected)=>Object.entries(expected).every(([key,value])=>metadata?.[key]===value);if(setup.id!==setupIntentId||setup.status!=="succeeded"||setup.usage!=="off_session"||!Number.isSafeInteger(setup.created)||setup.created<=0||id(setup.customer)!==options.customer||!/^pm_[a-z0-9_]+$/i.test(id(setup.payment_method)||"")||!matches(setup.metadata,quote))throw Error("[payments/connect] Payment setup does not match the original customer and frozen quote");const paymentMethod=id(setup.payment_method),metadata={...quote,stacks_purchase_setup:setupIntentId},key=(operation)=>stacksIdempotencyKey(`connect.invoice.purchase.${operation}`,account,options.reference,quote.stacks_purchase_quote),canCreate=()=>{if(Date.now()/1000>=Math.min(options.expiresAt,setup.created+86400))throw Error("[payments/connect] Expired purchase can only recover its original provider invoice or schedule")},existing=(rows)=>{const own=rows.filter((row)=>row.metadata?.stacks_purchase_reference===options.reference);if(own.length>1||own.some((row)=>!matches(row.metadata,metadata)))throw Error("[payments/connect] Purchase reference has ambiguous or different provider bindings");return own[0]},verifyPrice=async()=>{const price=await client.prices.retrieve(options.price,{expand:["product"]},request),product=price.product;if(price.id!==options.price||!price.active||price.currency!==options.currency.toLowerCase()||price.unit_amount!==options.amount||price.tax_behavior!=="inclusive"||price.type!==(options.interval?"recurring":"one_time")||(options.interval?price.recurring?.interval!==options.interval||price.recurring?.interval_count!==(options.intervalCount??1)||price.recurring?.usage_type!=="licensed":price.recurring!=null)||typeof product==="string"||product.deleted||!product.active||id(product.tax_code)!==options.taxCode)throw Error("[payments/connect] Catalog price does not match the approved inclusive purchase");const method=await client.paymentMethods.retrieve(paymentMethod,{},request);if(method.id!==paymentMethod||id(method.customer)!==options.customer)throw Error("[payments/connect] Purchase payment method belongs to another customer")};let invoice,subscription,reloadSubscription;if(options.interval){let schedule=existing(await allPages((after)=>client.subscriptionSchedules.list({customer:options.customer,limit:100,...after?{starting_after:after}:{}},request)));if(!schedule){canCreate();await verifyPrice();const phase={items:[{price:options.price,quantity:1}],automatic_tax:{enabled:!0},metadata};schedule=await client.subscriptionSchedules.create({customer:options.customer,start_date:"now",end_behavior:"release",default_settings:{default_payment_method:paymentMethod},phases:options.trialDays?[{...phase,trial:!0,duration:{interval:"day",interval_count:options.trialDays}},phase]:[phase],metadata},{...request,idempotencyKey:key("schedule")})}const subscriptionId=id(schedule.subscription)??id(schedule.released_subscription);if(!/^sub_sched_[a-z0-9_]+$/i.test(schedule.id)||id(schedule.customer)!==options.customer||!matches(schedule.metadata,metadata)||!/^sub_[a-z0-9_]+$/i.test(subscriptionId||""))throw Error("[payments/connect] Purchase schedule has no original customer and subscription");const readSubscription=async()=>{const value=await client.subscriptions.retrieve(subscriptionId,{},request);if(value.id!==subscriptionId||id(value.customer)!==options.customer||!matches(value.metadata,metadata)||value.items.has_more||value.items.data.length!==1||id(value.items.data[0].price)!==options.price||value.items.data[0].quantity!==1)throw Error("[payments/connect] Subscription does not match the original frozen purchase");return value};reloadSubscription=readSubscription;subscription=await readSubscription();const initial=(await allPages((after)=>client.invoices.list({customer:options.customer,subscription:subscriptionId,limit:100,...after?{starting_after:after}:{}},request))).filter((value)=>value.billing_reason==="subscription_create"&&id(value.parent?.subscription_details?.subscription)===subscriptionId);if(initial.length!==1)throw Error("[payments/connect] Purchase has no unique original subscription invoice");invoice=await client.invoices.retrieve(initial[0].id,{},request);if(invoice.id!==initial[0].id||id(invoice.customer)!==options.customer||id(invoice.parent?.subscription_details?.subscription)!==subscriptionId||!matches(invoice.parent?.subscription_details?.metadata,metadata))throw Error("[payments/connect] First invoice does not match the original subscription quote");if(invoice.status!=="paid"&&["canceled","incomplete_expired","unpaid"].includes(subscription.status))throw Error("[payments/connect] Subscription no longer permits its initial collection");if(invoice.status==="draft"&&invoice.auto_advance!==!1){invoice=await client.invoices.update(invoice.id,{auto_advance:!1},{...request,idempotencyKey:key("hold")});if(invoice.id!==initial[0].id||id(invoice.customer)!==options.customer||id(invoice.parent?.subscription_details?.subscription)!==subscriptionId||!matches(invoice.parent?.subscription_details?.metadata,metadata))throw Error("[payments/connect] Held invoice no longer matches the original subscription")}if(schedule.status==="active"){if(id(subscription.schedule)!==schedule.id)throw Error("[payments/connect] Subscription belongs to another schedule");const trialEnd=subscription.trial_end,released=await client.subscriptionSchedules.release(schedule.id,{},{...request,idempotencyKey:key("release")});if(released.id!==schedule.id||released.status!=="released"||id(released.released_subscription)!==subscriptionId)throw Error("[payments/connect] Schedule release did not preserve its original subscription");subscription=await readSubscription();if(subscription.schedule!=null||subscription.trial_end!==trialEnd)throw Error("[payments/connect] Released purchase changed its schedule or native trial");if(invoice.status!=="paid"&&["canceled","incomplete_expired","unpaid"].includes(subscription.status))throw Error("[payments/connect] Subscription no longer permits its initial collection")}else if(!["released","completed","canceled"].includes(schedule.status))throw Error("[payments/connect] Purchase schedule has not started")}else{const previous=existing(await allPages((after)=>client.invoices.list({customer:options.customer,limit:100,...after?{starting_after:after}:{}},request)));if(previous)invoice=await client.invoices.retrieve(previous.id,{},request);else{canCreate();await verifyPrice();invoice=await client.invoices.create({customer:options.customer,currency:options.currency.toLowerCase(),auto_advance:!1,automatic_tax:{enabled:!0},default_payment_method:paymentMethod,pending_invoice_items_behavior:"exclude",metadata},{...request,idempotencyKey:key("create")})}if(!/^in_[a-z0-9_]+$/i.test(invoice.id)||previous&&invoice.id!==previous.id||id(invoice.customer)!==options.customer||!matches(invoice.metadata,metadata))throw Error("[payments/connect] Purchase invoice belongs to another customer or quote");let lines=await allPages((after)=>client.invoices.listLineItems(invoice.id,{limit:100,...after?{starting_after:after}:{}},request));if(lines.length===0&&invoice.status==="draft"){if(previous)await verifyPrice();await client.invoiceItems.create({customer:options.customer,invoice:invoice.id,pricing:{price:options.price},quantity:1},{...request,idempotencyKey:key("line")});lines=await allPages((after)=>client.invoices.listLineItems(invoice.id,{limit:100,...after?{starting_after:after}:{}},request))}if(lines.length!==1||lines[0].quantity!==1||id(lines[0].pricing?.price_details?.price)!==options.price)throw Error("[payments/connect] Purchase invoice contains different catalog lines")}if(!invoice.automatic_tax.enabled)throw Error("[payments/connect] Purchase invoice no longer uses its original automatic tax policy");return{...await collectDirectInvoice(invoice.id,account,{customer:options.customer,...subscription?{subscription:subscription.id}:{},currency:options.currency,platformFeePercent:options.platformFeePercent,approvedTotal:options.trialDays?0:options.amount,paymentMethod},client),...reloadSubscription?{subscription:await reloadSubscription()}:{}}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/payments",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.357",
5
+ "version": "0.75.361",
6
6
  "description": "The Stacks payments package. Currently supporting Stripe.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -60,12 +60,12 @@
60
60
  "prepublishOnly": "bun run build"
61
61
  },
62
62
  "dependencies": {
63
- "@stacksjs/config": "0.75.357",
64
- "@stacksjs/database": "0.75.357",
65
- "@stacksjs/error-handling": "0.75.357",
66
- "@stacksjs/logging": "0.75.357",
67
- "@stacksjs/orm": "0.75.357",
68
- "@stacksjs/types": "0.75.357"
63
+ "@stacksjs/config": "0.75.361",
64
+ "@stacksjs/database": "0.75.361",
65
+ "@stacksjs/error-handling": "0.75.361",
66
+ "@stacksjs/logging": "0.75.361",
67
+ "@stacksjs/orm": "0.75.361",
68
+ "@stacksjs/types": "0.75.361"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "@stripe/stripe-js": "^10.0.0",
@@ -80,7 +80,7 @@
80
80
  }
81
81
  },
82
82
  "devDependencies": {
83
- "@stacksjs/utils": "0.75.357",
83
+ "@stacksjs/utils": "0.75.361",
84
84
  "@stripe/stripe-js": "^10.0.0",
85
85
  "better-dx": "^0.2.24",
86
86
  "stripe": "23.0.0"