@stacksjs/payments 0.75.349 → 0.75.360

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 +1 @@
1
- import{stripe}from"../drivers/stripe";import{asAccount}from"./direct";export class DirectChargeFinancialsPendingError extends Error{}function id(value){return typeof value==="string"?value:String(value?.id||"")}async function allPages(read){const rows=[],seen=new Set;let after;for(let page=0;page<100;page++){const result=await read(after);for(const row of result.data){if(!row.id||seen.has(row.id))throw Error("Incomplete or repeated Stripe financial page");seen.add(row.id);rows.push(row)}if(!result.has_more)return rows;if(!result.data.length)throw Error("Incomplete Stripe financial page");after=result.data.at(-1).id}throw Error("Stripe financial pagination exceeded its bound")}export async function directChargeFinancials(paymentIntentId,account,client=stripe){const options=asAccount(account);if(!/^pi_[a-z0-9_]+$/i.test(paymentIntentId))throw Error("Expected a Stripe PaymentIntent ID");const intent=await client.paymentIntents.retrieve(paymentIntentId,{},options);if(intent.id!==paymentIntentId||intent.status!=="succeeded"||!id(intent.latest_charge))throw new DirectChargeFinancialsPendingError("The original payment has no successful charge yet");const charge=await client.charges.retrieve(id(intent.latest_charge),{},options);if(id(charge.payment_intent)!==paymentIntentId||charge.id!==id(intent.latest_charge)||!charge.paid||!charge.captured)throw Error("Charge does not match the successful direct payment");const refunds=await allPages((after)=>client.refunds.list({charge:charge.id,limit:100,...after?{starting_after:after}:{}},options)),disputes=await allPages((after)=>client.disputes.list({charge:charge.id,limit:100,...after?{starting_after:after}:{}},options));for(const item of[...refunds,...disputes])if(id(item.charge)!==charge.id||item.payment_intent&&id(item.payment_intent)!==paymentIntentId)throw Error("Foreign refund or dispute in direct-charge financials");let applicationFee=null;if(id(charge.application_fee))applicationFee=await client.applicationFees.retrieve(id(charge.application_fee));else if(Number(charge.application_fee_amount)>0){const fees=await allPages((after)=>client.applicationFees.list({charge:charge.id,limit:100,...after?{starting_after:after}:{}}));if(fees.length!==1)throw new DirectChargeFinancialsPendingError("The direct-charge application fee is not available yet");applicationFee=fees[0]}if(applicationFee&&(id(applicationFee.account)!==account||id(applicationFee.charge)!==charge.id))throw Error("Application fee belongs to another direct charge or account");const applicationFeeRefunds=applicationFee?await allPages((after)=>client.applicationFees.listRefunds(applicationFee.id,{limit:100,...after?{starting_after:after}:{}})):[];if(applicationFeeRefunds.some((refund)=>id(refund.fee)!==applicationFee.id))throw Error("Foreign application-fee refund");const movements=[],seen=new Map;async function movement(reference,sourceId,kind,scope,required=!0){const transactionId=id(reference);if(!transactionId){if(required)throw new DirectChargeFinancialsPendingError("A provider balance transaction is not available yet");return}if(!/^txn_[a-z0-9_]+$/i.test(transactionId))throw Error("Invalid provider balance transaction");const transaction=await client.balanceTransactions.retrieve(transactionId,{},scope==="platform"?void 0:options);if(transaction.id!==transactionId||id(transaction.source)&&id(transaction.source)!==sourceId)throw Error("Foreign provider balance transaction");const entry={account:scope,sourceId,kind,transaction},key=`${scope}:${transactionId}`;if(seen.has(key)){if(JSON.stringify(seen.get(key))!==JSON.stringify(entry))throw Error("Conflicting provider balance transaction");return}seen.set(key,entry);movements.push(entry)}await movement(charge.balance_transaction,charge.id,"charge",account);for(const refund of refunds){await movement(refund.balance_transaction,refund.id,"refund",account,refund.status==="succeeded");await movement(refund.failure_balance_transaction,refund.id,"refund",account,!1)}for(const dispute of disputes)for(const transaction of dispute.balance_transactions)await movement(transaction,dispute.id,"dispute",account);if(applicationFee)await movement(applicationFee.balance_transaction,applicationFee.id,"application_fee","platform");for(const refund of applicationFeeRefunds)await movement(refund.balance_transaction,refund.id,"application_fee_refund","platform");return{account,charge,refunds,disputes,applicationFee,applicationFeeRefunds,movements}}
1
+ import{stripe}from"../drivers/stripe";import{asAccount}from"./direct";import{allPages}from"./pagination";export class DirectChargeFinancialsPendingError extends Error{}function id(value){return typeof value==="string"?value:String(value?.id||"")}export async function directChargeFinancials(paymentIntentId,account,client=stripe){const options=asAccount(account);if(!/^pi_[a-z0-9_]+$/i.test(paymentIntentId))throw Error("Expected a Stripe PaymentIntent ID");const intent=await client.paymentIntents.retrieve(paymentIntentId,{},options);if(intent.id!==paymentIntentId||intent.status!=="succeeded"||!id(intent.latest_charge))throw new DirectChargeFinancialsPendingError("The original payment has no successful charge yet");const charge=await client.charges.retrieve(id(intent.latest_charge),{},options);if(id(charge.payment_intent)!==paymentIntentId||charge.id!==id(intent.latest_charge)||!charge.paid||!charge.captured)throw Error("Charge does not match the successful direct payment");const refunds=await allPages((after)=>client.refunds.list({charge:charge.id,limit:100,...after?{starting_after:after}:{}},options)),disputes=await allPages((after)=>client.disputes.list({charge:charge.id,limit:100,...after?{starting_after:after}:{}},options));for(const item of[...refunds,...disputes])if(id(item.charge)!==charge.id||item.payment_intent&&id(item.payment_intent)!==paymentIntentId)throw Error("Foreign refund or dispute in direct-charge financials");let applicationFee=null;if(id(charge.application_fee))applicationFee=await client.applicationFees.retrieve(id(charge.application_fee));else if(Number(charge.application_fee_amount)>0){const fees=await allPages((after)=>client.applicationFees.list({charge:charge.id,limit:100,...after?{starting_after:after}:{}}));if(fees.length!==1)throw new DirectChargeFinancialsPendingError("The direct-charge application fee is not available yet");applicationFee=fees[0]}if(applicationFee&&(id(applicationFee.account)!==account||id(applicationFee.charge)!==charge.id))throw Error("Application fee belongs to another direct charge or account");const applicationFeeRefunds=applicationFee?await allPages((after)=>client.applicationFees.listRefunds(applicationFee.id,{limit:100,...after?{starting_after:after}:{}})):[];if(applicationFeeRefunds.some((refund)=>id(refund.fee)!==applicationFee.id))throw Error("Foreign application-fee refund");const movements=[],seen=new Map;async function movement(reference,sourceId,kind,scope,required=!0,providerSourceId=sourceId){const transactionId=id(reference);if(!transactionId){if(required)throw new DirectChargeFinancialsPendingError("A provider balance transaction is not available yet");return}if(!/^txn_[a-z0-9_]+$/i.test(transactionId))throw Error("Invalid provider balance transaction");const transaction=await client.balanceTransactions.retrieve(transactionId,{},scope==="platform"?void 0:options);if(transaction.id!==transactionId||id(transaction.source)&&id(transaction.source)!==providerSourceId)throw Error("Foreign provider balance transaction");const entry={account:scope,sourceId,kind,transaction},key=`${scope}:${transactionId}`;if(seen.has(key)){if(JSON.stringify(seen.get(key))!==JSON.stringify(entry))throw Error("Conflicting provider balance transaction");return}seen.set(key,entry);movements.push(entry)}await movement(charge.balance_transaction,charge.id,"charge",account);for(const refund of refunds){await movement(refund.balance_transaction,refund.id,"refund",account,refund.status==="succeeded");await movement(refund.failure_balance_transaction,refund.id,"refund",account,!1)}for(const dispute of disputes)for(const transaction of dispute.balance_transactions)await movement(transaction,dispute.id,"dispute",account);if(applicationFee)await movement(applicationFee.balance_transaction,applicationFee.id,"application_fee","platform");for(const refund of applicationFeeRefunds)await movement(refund.balance_transaction,refund.id,"application_fee_refund","platform",!0,applicationFee.id);return{account,charge,refunds,disputes,applicationFee,applicationFeeRefunds,movements}}
@@ -1,13 +1,17 @@
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';
3
4
  import { connectRefund, connectRefundParams, isDestinationCharge } from './refund';
5
+ import { creditDirectInvoiceRefund } from './invoice-credit';
4
6
  import { directChargeFinancials } from './financials';
7
+ import { directInvoiceTaxFinancials } from './invoice-tax';
5
8
  import { manageConnectedAccount } from './account';
6
9
  import { manageConnectWebhook } from './webhook';
7
10
  import { manageDestinationCharge } from './charge';
8
11
  import { manageDestinationSubscription } from './subscription';
9
12
  import { managePayout } from './payout';
10
13
  import { manageTransfer } from './transfer';
14
+ import { prepareDirectInvoiceFee } from './invoice-fee';
11
15
  /**
12
16
  * Grouped Connect surface, reached as `Payment.connect.*`.
13
17
  */
@@ -31,6 +35,10 @@ export declare const connect: {
31
35
  customer: typeof connectedCustomer;
32
36
  directCheckout: typeof directCheckout;
33
37
  directChargeFinancials: typeof directChargeFinancials;
38
+ prepareDirectInvoiceFee: typeof prepareDirectInvoiceFee;
39
+ collectDirectInvoice: typeof collectDirectInvoice;
40
+ creditDirectInvoiceRefund: typeof creditDirectInvoiceRefund;
41
+ directInvoiceTaxFinancials: typeof directInvoiceTaxFinancials;
34
42
  directPaymentIntentParams: typeof directPaymentIntentParams;
35
43
  directCheckoutPaymentData: typeof directCheckoutPaymentData;
36
44
  directCheckoutSubscriptionData: typeof directCheckoutSubscriptionData;
@@ -77,6 +85,11 @@ export * from './charge';
77
85
  export * from './destination';
78
86
  export * from './direct';
79
87
  export * from './financials';
88
+ export * from './invoice-fee';
89
+ export * from './invoice-collection';
90
+ export * from './invoice-credit';
91
+ export * from './tax';
92
+ export * from './invoice-tax';
80
93
  export * from './payout';
81
94
  export * from './refund';
82
95
  export * from './subscription';
@@ -1 +1 @@
1
- export*from"./account";export*from"./charge";export*from"./destination";export*from"./direct";export*from"./financials";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{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,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-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{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,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,19 @@
1
+ import type Stripe from 'stripe';
2
+ /** Link a successful direct refund to its ORIGINAL invoice lines. This creates
3
+ * a credit note with Stripe's actual tax reversal, never a second refund.
4
+ * Amount-only credit notes can omit tax entirely. All reads and the credit are
5
+ * scoped to the original merchant. An existing matching credit is returned on
6
+ * replay, including after Stripe's idempotency retention has elapsed.
7
+ */
8
+ export declare function creditDirectInvoiceRefund(invoiceId: string, account: string, refundId: string, lines: readonly InvoiceRefundLine[], client?: Pick<Stripe, 'invoices' | 'invoicePayments' | 'refunds' | 'creditNotes'>): Promise<DirectInvoiceRefundCredit>;
9
+ export declare interface InvoiceRefundLine {
10
+ invoiceLineItem: string
11
+ amount: number
12
+ }
13
+ export declare interface DirectInvoiceRefundCredit {
14
+ creditNote: Stripe.CreditNote
15
+ gross: number
16
+ net: number
17
+ tax: number
18
+ currency: string
19
+ }
@@ -0,0 +1 @@
1
+ import{stripe}from"../drivers/stripe";import{idempotencyFingerprint,stacksIdempotencyKey}from"../idempotency";import{asAccount}from"./direct";import{allPages}from"./pagination";import{invoiceTaxAmounts}from"./tax";function id(value){return typeof value==="string"?value:String(value?.id||"")}export async function creditDirectInvoiceRefund(invoiceId,account,refundId,lines,client=stripe){const request=asAccount(account);if(!/^in_[a-z0-9_]+$/i.test(invoiceId)||!/^re_[a-z0-9_]+$/i.test(refundId)||!lines.length||lines.length>100||lines.some((line)=>!/^il_[a-z0-9_]+$/i.test(line.invoiceLineItem)||!Number.isSafeInteger(line.amount)||line.amount<=0)||new Set(lines.map((line)=>line.invoiceLineItem)).size!==lines.length)throw Error("[payments/connect] Expected original invoice, refund and unique positive credit lines");const invoice=await client.invoices.retrieve(invoiceId,{},request),refund=await client.refunds.retrieve(refundId,{},request);if(invoice.id!==invoiceId||invoice.status!=="paid"||refund.id!==refundId||refund.status!=="succeeded"||refund.currency!==invoice.currency||!Number.isSafeInteger(refund.amount)||refund.amount<=0||!id(refund.payment_intent))throw Error("[payments/connect] Expected the original paid invoice and successful refund in its currency");if(!(await allPages((after)=>client.invoicePayments.list({invoice:invoiceId,limit:100,...after?{starting_after:after}:{}},request))).some((payment)=>payment.status==="paid"&&id(payment.payment.payment_intent)===id(refund.payment_intent)))throw Error("[payments/connect] Refund belongs to another invoice payment");const invoiceLines=await allPages((after)=>client.invoices.listLineItems(invoiceId,{limit:100,...after?{starting_after:after}:{}},request));if(lines.some((line)=>!invoiceLines.some((original)=>original.id===line.invoiceLineItem)))throw Error("[payments/connect] Credit line belongs to another invoice");const params={invoice:invoiceId,lines:[...lines].sort((a,b)=>a.invoiceLineItem.localeCompare(b.invoiceLineItem)).map((line)=>({type:"invoice_line_item",invoice_line_item:line.invoiceLineItem,amount:line.amount})),refunds:[{refund:refundId,amount_refunded:refund.amount}]},verify=(credit)=>{if(id(credit.invoice)!==invoiceId||id(credit.customer)!==id(invoice.customer)||credit.currency!==invoice.currency||credit.total!==refund.amount)throw Error("[payments/connect] Credit does not match the original invoice or refund amount");return{creditNote:credit,...invoiceTaxAmounts(credit),currency:invoice.currency.toUpperCase()}},validateIssued=async(credit)=>{const result=verify(credit);if(credit.status!=="issued"||credit.refunds.length!==1||id(credit.refunds[0].refund)!==refundId||credit.refunds[0].amount_refunded!==refund.amount)throw Error("[payments/connect] Credit does not bind the complete original refund");const actual=await allPages((after)=>client.creditNotes.listLineItems(credit.id,{limit:100,...after?{starting_after:after}:{}},request));if(actual.length!==lines.length||actual.some((line)=>line.type!=="invoice_line_item"||!lines.some((expected)=>expected.invoiceLineItem===line.invoice_line_item&&expected.amount===line.amount)))throw Error("[payments/connect] Refund was credited with different invoice lines");return result},previous=(await allPages((after)=>client.creditNotes.list({invoice:invoiceId,limit:100,...after?{starting_after:after}:{}},request))).filter((credit)=>credit.status==="issued"&&credit.refunds.some((row)=>id(row.refund)===refundId));if(previous.length>1)throw Error("[payments/connect] Refund is associated with several credit notes");if(previous[0])return validateIssued(previous[0]);verify(await client.creditNotes.preview(params,request));const credit=await client.creditNotes.create({...params,email_type:"none"},{...request,idempotencyKey:stacksIdempotencyKey("connect.invoice.refund-credit",account,invoiceId,refundId,idempotencyFingerprint(params))});return validateIssued(credit)}
@@ -0,0 +1,23 @@
1
+ import type Stripe from 'stripe';
2
+ /** Set an exact tax-exclusive fee on a draft invoice on its original merchant.
3
+ * Collection is held before returning; the caller owns finalization/payment and
4
+ * durable retries. Use invoice.created for renewals and a subscription schedule
5
+ * for an editable first invoice. Checkout's immediately finalized first invoice
6
+ * cannot be changed by this API. Never infer a tax rate from locale or currency.
7
+ * https://docs.stripe.com/connect/subscriptions#collect-fees
8
+ */
9
+ export declare function prepareDirectInvoiceFee(invoiceId: string, account: string, options: DirectInvoiceFeeOptions, client?: Pick<Stripe, 'invoices'>): Promise<DirectInvoiceFee>;
10
+ export declare interface DirectInvoiceFeeOptions {
11
+ customer: string
12
+ subscription?: string
13
+ currency: string
14
+ platformFeePercent: number
15
+ }
16
+ export declare interface DirectInvoiceFee {
17
+ invoice: Stripe.Invoice
18
+ currency: string
19
+ gross: number
20
+ tax: number
21
+ net: number
22
+ fee: number
23
+ }
@@ -0,0 +1 @@
1
+ import{stripe}from"../drivers/stripe";import{stacksIdempotencyKey}from"../idempotency";import{asAccount}from"./direct";import{splitFee}from"./destination";import{manageDestinationSubscription}from"./subscription";import{invoiceTaxAmounts}from"./tax";function id(value){return typeof value==="string"?value:value?.id}function amounts(invoice){const totals=invoiceTaxAmounts(invoice);if(invoice.amount_due!==totals.gross)throw Error("[payments/connect] Invoice credits or balance adjustments require a separate fee settlement");return totals}export async function prepareDirectInvoiceFee(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))throw Error("[payments/connect] Expected original invoice, customer, subscription and currency");if(options.platformFeePercent==null)throw Error("[payments/connect] An explicit invoice fee percentage is required");manageDestinationSubscription.feePercentFor({platformFeePercent:options.platformFeePercent});const invoice=await client.invoices.retrieve(invoiceId,{},request),verify=(value)=>{if(value.id!==invoiceId||value.status!=="draft"||id(value.customer)!==options.customer||id(value.parent?.subscription_details?.subscription)!==options.subscription||value.currency.toUpperCase()!==options.currency.toUpperCase())throw Error("[payments/connect] Fee requires the original draft invoice, customer, subscription and currency");return amounts(value)},basis=verify(invoice),feeBps=Math.round(options.platformFeePercent*100),fee=splitFee(basis.net,{platform:feeBps,merchant:1e4-feeBps},"merchant").platform,updated=await client.invoices.update(invoiceId,{auto_advance:!1,application_fee_amount:fee},{...request,idempotencyKey:stacksIdempotencyKey("connect.invoice.tax-fee",account,invoiceId,basis.gross,basis.tax,fee)}),current=verify(updated);if(current.gross!==basis.gross||current.tax!==basis.tax)throw Error("[payments/connect] Invoice tax or amount changed while preparing the fee; collection remains held");return{invoice:updated,currency:invoice.currency.toUpperCase(),...basis,fee}}
@@ -0,0 +1,20 @@
1
+ import type Stripe from 'stripe';
2
+ /** Read tax and actual credits on the original merchant. A cash refund does
3
+ * not establish a tax reversal; only issued, balanced provider credit notes do.
4
+ * Call under the application's payment lock alongside directChargeFinancials,
5
+ * and compare this invoice's gross/currency with the original payment before
6
+ * making contributor amounts available. This operation has no side effects.
7
+ */
8
+ export declare function directInvoiceTaxFinancials(invoiceId: string, paymentIntentId: string, account: string, client?: Pick<Stripe, 'invoices' | 'invoicePayments' | 'refunds' | 'creditNotes'>): Promise<DirectInvoiceTaxFinancials>;
9
+ export declare interface DirectInvoiceTaxFinancials {
10
+ invoice: Stripe.Invoice
11
+ creditNotes: Stripe.CreditNote[]
12
+ currency: string
13
+ gross: number
14
+ net: number
15
+ tax: number
16
+ creditedGross: number
17
+ creditedNet: number
18
+ creditedTax: number
19
+ requiresReview: boolean
20
+ }
@@ -0,0 +1 @@
1
+ import{stripe}from"../drivers/stripe";import{asAccount}from"./direct";import{allPages}from"./pagination";import{invoiceTaxAmounts}from"./tax";function id(value){return typeof value==="string"?value:String(value?.id||"")}export async function directInvoiceTaxFinancials(invoiceId,paymentIntentId,account,client=stripe){const request=asAccount(account);if(!/^in_[a-z0-9_]+$/i.test(invoiceId)||!/^pi_[a-z0-9_]+$/i.test(paymentIntentId))throw Error("[payments/connect] Expected original invoice and payment identities");const invoice=await client.invoices.retrieve(invoiceId,{},request);if(invoice.id!==invoiceId||invoice.status!=="paid")throw Error("[payments/connect] Expected the original paid invoice");const basis=invoiceTaxAmounts(invoice);if(!(await allPages((after)=>client.invoicePayments.list({invoice:invoiceId,limit:100,...after?{starting_after:after}:{}},request))).some((payment)=>payment.status==="paid"&&id(payment.payment.payment_intent)===paymentIntentId))throw Error("[payments/connect] Invoice belongs to another payment");const refunds=await allPages((after)=>client.refunds.list({payment_intent:paymentIntentId,limit:100,...after?{starting_after:after}:{}},request));if(refunds.some((refund)=>id(refund.payment_intent)!==paymentIntentId||refund.currency!==invoice.currency||!Number.isSafeInteger(refund.amount)||refund.amount<=0))throw Error("[payments/connect] Foreign or invalid invoice refund");const creditNotes=await allPages((after)=>client.creditNotes.list({invoice:invoiceId,limit:100,...after?{starting_after:after}:{}},request)),invoiceLines=await allPages((after)=>client.invoices.listLineItems(invoiceId,{limit:100,...after?{starting_after:after}:{}},request));let requiresReview=refunds.some((refund)=>!["succeeded","failed","canceled"].includes(String(refund.status))),creditedGross=0,creditedNet=0,creditedTax=0;const creditedRefunds=new Map;for(const credit of creditNotes){if(id(credit.invoice)!==invoiceId||id(credit.customer)!==id(invoice.customer)||credit.currency!==invoice.currency)throw Error("[payments/connect] Foreign invoice tax credit");if(credit.status==="void")continue;if(credit.status!=="issued")throw Error("[payments/connect] Unknown invoice credit status");const amounts=invoiceTaxAmounts(credit),lines=await allPages((after)=>client.creditNotes.listLineItems(credit.id,{limit:100,...after?{starting_after:after}:{}},request));if(!lines.length||lines.some((line)=>line.type!=="invoice_line_item"))requiresReview=!0;if(lines.some((line)=>line.type==="invoice_line_item"&&!invoiceLines.some((original)=>original.id===line.invoice_line_item)))throw Error("[payments/connect] Tax credit references another invoice line");creditedGross+=amounts.gross;creditedNet+=amounts.net;creditedTax+=amounts.tax;let linkedGross=0;for(const link of credit.refunds){const refund=refunds.find((row)=>row.id===id(link.refund));if(!refund||refund.status!=="succeeded"||!Number.isSafeInteger(link.amount_refunded)||link.amount_refunded<0){requiresReview=!0;continue}const credited=(creditedRefunds.get(refund.id)||0)+link.amount_refunded;creditedRefunds.set(refund.id,credited);if(credited>refund.amount)throw Error("[payments/connect] Tax credits exceed the original refund");linkedGross+=link.amount_refunded}if(linkedGross!==amounts.gross)requiresReview=!0}const refunded=refunds.filter((refund)=>refund.status==="succeeded").reduce((sum,refund)=>sum+refund.amount,0);if(creditedGross>basis.gross||creditedNet>basis.net||creditedTax>basis.tax||basis.tax>0&&(creditedGross!==refunded||refunds.some((refund)=>refund.status==="succeeded"&&creditedRefunds.get(refund.id)!==refund.amount)))requiresReview=!0;return{invoice,creditNotes,currency:invoice.currency.toUpperCase(),...basis,creditedGross,creditedNet,creditedTax,requiresReview}}
@@ -0,0 +1,2 @@
1
+ /** Read bounded Stripe lists completely; incomplete pages are never financial facts. */
2
+ export declare function allPages<T extends { id: string }>(read: (after?: string) => Promise<{ data: T[], has_more: boolean }>): Promise<T[]>;
@@ -0,0 +1 @@
1
+ export async function allPages(read){const rows=[],seen=new Set;let after;for(let page=0;page<100;page++){const result=await read(after);for(const row of result.data){if(!row.id||seen.has(row.id))throw Error("Incomplete or repeated Stripe financial page");seen.add(row.id);rows.push(row)}if(!result.has_more)return rows;if(!result.data.length)throw Error("Incomplete Stripe financial page");after=result.data.at(-1).id}throw Error("Stripe financial pagination exceeded its bound")}
@@ -9,6 +9,8 @@ export declare interface InlineRecurringPrice {
9
9
  intervalCount?: number
10
10
  productName: string
11
11
  productDescription?: string
12
+ taxBehavior?: 'inclusive' | 'exclusive'
13
+ taxCode?: string
12
14
  }
13
15
  export declare interface DestinationSubscriptionCheckoutOptions {
14
16
  destination: string
@@ -1 +1 @@
1
- import{config}from"@stacksjs/config";import{manageCheckout}from"../billable/checkout";import{manageCustomer}from"../billable/customer";import{requireAccountId}from"./account";const INTERVALS=["day","week","month","year"];function configuredFeePercent(){return(config||{}).payment?.connect?.platformFeePercent}export const manageDestinationSubscription=(()=>{function feePercentFor(options={}){const percent=options.platformFeePercent??configuredFeePercent();if(percent==null)return;if(!Number.isFinite(percent)||percent<0||percent>100)throw Error(`[payments/connect] platformFeePercent must be between 0 and 100, got ${percent}`);if(Math.round(percent*100)!==percent*100)throw Error(`[payments/connect] platformFeePercent takes at most two decimal places, got ${percent}`);return percent>0?percent:void 0}function subscriptionData(destination,options={}){requireAccountId(destination,"destinationSubscription");if(options.trialDays!=null&&(!Number.isInteger(options.trialDays)||options.trialDays<1||options.trialDays>730))throw Error(`[payments/connect] trialDays must be a whole number from 1 to 730, got ${options.trialDays}`);const fee=feePercentFor({platformFeePercent:options.platformFeePercent});return{transfer_data:{destination},...fee!=null?{application_fee_percent:fee}:{},...options.onBehalfOf?{on_behalf_of:destination}:{},...options.trialDays?{trial_period_days:options.trialDays}:{},...options.metadata?{metadata:options.metadata}:{}}}function lineItem(price,quantity=1){if(!Number.isInteger(quantity)||quantity<1)throw Error(`[payments/connect] quantity must be a positive whole number, got ${quantity}`);if(typeof price==="string"){if(!price.startsWith("price_"))throw Error(`[payments/connect] a catalog price id starts with "price_", got "${price}"`);return{price,quantity}}if(!Number.isInteger(price.amount)||price.amount<=0)throw Error("[payments/connect] a recurring price needs a positive amount in minor units");if(!INTERVALS.includes(price.interval))throw Error(`[payments/connect] interval must be one of ${INTERVALS.join(", ")}, got ${price.interval}`);const intervalCount=price.intervalCount??1;if(!Number.isInteger(intervalCount)||intervalCount<1)throw Error(`[payments/connect] intervalCount must be a positive whole number, got ${intervalCount}`);if(!price.productName?.trim())throw Error("[payments/connect] a recurring price needs a productName");return{quantity,price_data:{currency:price.currency.toLowerCase(),unit_amount:price.amount,recurring:{interval:price.interval,...intervalCount!==1?{interval_count:intervalCount}:{}},product_data:{name:price.productName.slice(0,250),...price.productDescription?{description:price.productDescription.slice(0,500)}:{}}}}}async function checkout(user,options){const customer=options.customer??(await manageCustomer.createOrGetStripeUser(user,{})).id;return await manageCheckout.create(user,{mode:"subscription",customer,line_items:[lineItem(options.price,options.quantity)],subscription_data:subscriptionData(options.destination,options),success_url:options.successUrl,cancel_url:options.cancelUrl,...options.clientReferenceId?{client_reference_id:options.clientReferenceId}:{},...options.metadata?{metadata:options.metadata}:{},...options.sessionParams})}function destinationOf(subscription){const destination=subscription.transfer_data?.destination;if(!destination)return null;return(typeof destination==="string"?destination:destination.id)||null}return{feePercentFor,subscriptionData,lineItem,checkout,destinationOf}})();
1
+ import{config}from"@stacksjs/config";import{manageCheckout}from"../billable/checkout";import{manageCustomer}from"../billable/customer";import{requireAccountId}from"./account";const INTERVALS=["day","week","month","year"];function configuredFeePercent(){return(config||{}).payment?.connect?.platformFeePercent}export const manageDestinationSubscription=(()=>{function feePercentFor(options={}){const percent=options.platformFeePercent??configuredFeePercent();if(percent==null)return;if(!Number.isFinite(percent)||percent<0||percent>100)throw Error(`[payments/connect] platformFeePercent must be between 0 and 100, got ${percent}`);if(Math.round(percent*100)!==percent*100)throw Error(`[payments/connect] platformFeePercent takes at most two decimal places, got ${percent}`);return percent>0?percent:void 0}function subscriptionData(destination,options={}){requireAccountId(destination,"destinationSubscription");if(options.trialDays!=null&&(!Number.isInteger(options.trialDays)||options.trialDays<1||options.trialDays>730))throw Error(`[payments/connect] trialDays must be a whole number from 1 to 730, got ${options.trialDays}`);const fee=feePercentFor({platformFeePercent:options.platformFeePercent});return{transfer_data:{destination},...fee!=null?{application_fee_percent:fee}:{},...options.onBehalfOf?{on_behalf_of:destination}:{},...options.trialDays?{trial_period_days:options.trialDays}:{},...options.metadata?{metadata:options.metadata}:{}}}function lineItem(price,quantity=1){if(!Number.isInteger(quantity)||quantity<1)throw Error(`[payments/connect] quantity must be a positive whole number, got ${quantity}`);if(typeof price==="string"){if(!price.startsWith("price_"))throw Error(`[payments/connect] a catalog price id starts with "price_", got "${price}"`);return{price,quantity}}if(!Number.isInteger(price.amount)||price.amount<=0)throw Error("[payments/connect] a recurring price needs a positive amount in minor units");if(!INTERVALS.includes(price.interval))throw Error(`[payments/connect] interval must be one of ${INTERVALS.join(", ")}, got ${price.interval}`);const intervalCount=price.intervalCount??1;if(!Number.isInteger(intervalCount)||intervalCount<1)throw Error(`[payments/connect] intervalCount must be a positive whole number, got ${intervalCount}`);if(!price.productName?.trim())throw Error("[payments/connect] a recurring price needs a productName");if(price.taxBehavior!=null&&price.taxBehavior!=="inclusive"&&price.taxBehavior!=="exclusive")throw Error("[payments/connect] taxBehavior must be inclusive or exclusive");if(price.taxCode!=null&&!/^txcd_\d{8}$/.test(price.taxCode))throw Error("[payments/connect] taxCode must be a Stripe tax classification (txcd_ followed by eight digits)");return{quantity,price_data:{currency:price.currency.toLowerCase(),unit_amount:price.amount,...price.taxBehavior?{tax_behavior:price.taxBehavior}:{},recurring:{interval:price.interval,...intervalCount!==1?{interval_count:intervalCount}:{}},product_data:{name:price.productName.slice(0,250),...price.productDescription?{description:price.productDescription.slice(0,500)}:{},...price.taxCode?{tax_code:price.taxCode}:{}}}}}async function checkout(user,options){const customer=options.customer??(await manageCustomer.createOrGetStripeUser(user,{})).id;return await manageCheckout.create(user,{mode:"subscription",customer,line_items:[lineItem(options.price,options.quantity)],subscription_data:subscriptionData(options.destination,options),success_url:options.successUrl,cancel_url:options.cancelUrl,...options.clientReferenceId?{client_reference_id:options.clientReferenceId}:{},...options.metadata?{metadata:options.metadata}:{},...options.sessionParams})}function destinationOf(subscription){const destination=subscription.transfer_data?.destination;if(!destination)return null;return(typeof destination==="string"?destination:destination.id)||null}return{feePercentFor,subscriptionData,lineItem,checkout,destinationOf}})();
@@ -0,0 +1,8 @@
1
+ /** Canonical provider totals, after discounts. A missing/incomplete calculation
2
+ * is never equivalent to zero tax. Shared by draft fees and issued credits. */
3
+ export declare function invoiceTaxAmounts(value: {
4
+ total: number
5
+ total_excluding_tax: number | null
6
+ total_taxes: Array<{ amount: number }> | null
7
+ automatic_tax?: { enabled: boolean, status: string | null }
8
+ }): { gross: number, net: number, tax: number };
@@ -0,0 +1 @@
1
+ export function invoiceTaxAmounts(value){const{total:gross,total_excluding_tax:net}=value;if(!Number.isSafeInteger(gross)||gross<0||net==null||!Number.isSafeInteger(net)||net<0||net>gross)throw Error("[payments/connect] Provider has no valid tax-exclusive amount");const tax=gross-net;if(!Array.isArray(value.total_taxes)||value.total_taxes.some((row)=>!Number.isSafeInteger(row.amount)||row.amount<0)||value.total_taxes.reduce((sum,row)=>sum+row.amount,0)!==tax)throw Error("[payments/connect] Provider tax amounts do not balance");if(value.automatic_tax?.enabled&&value.automatic_tax.status!=="complete")throw Error("[payments/connect] Provider tax calculation is not complete");return{gross,net,tax}}
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.349",
5
+ "version": "0.75.360",
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.349",
64
- "@stacksjs/database": "0.75.349",
65
- "@stacksjs/error-handling": "0.75.349",
66
- "@stacksjs/logging": "0.75.349",
67
- "@stacksjs/orm": "0.75.349",
68
- "@stacksjs/types": "0.75.349"
63
+ "@stacksjs/config": "0.75.360",
64
+ "@stacksjs/database": "0.75.360",
65
+ "@stacksjs/error-handling": "0.75.360",
66
+ "@stacksjs/logging": "0.75.360",
67
+ "@stacksjs/orm": "0.75.360",
68
+ "@stacksjs/types": "0.75.360"
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.349",
83
+ "@stacksjs/utils": "0.75.360",
84
84
  "@stripe/stripe-js": "^10.0.0",
85
85
  "better-dx": "^0.2.24",
86
86
  "stripe": "23.0.0"