@stacksjs/payments 0.75.362 → 0.75.367

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.
@@ -41,6 +41,9 @@ export declare interface CheckoutOutcome {
41
41
  * `processing` is a delayed method (SEPA Direct Debit, Bacs, ACH, a bank
42
42
  * redirect): the buyer finished, the money has not arrived, and one of the
43
43
  * async events follows in a few days. Fulfil on `paid` only.
44
+ * Only `payment` and `subscription` sessions have payment outcomes. Setup
45
+ * sessions save a payment method and report `no_payment_required` even before
46
+ * a purchase exists; they and unknown modes return null.
44
47
  *
45
48
  * Every other event type is `null`, so a handler can call this first and
46
49
  * return early. It reads only the event; verify the signature and match the
@@ -1 +1 @@
1
- const PAID=["paid","no_payment_required"];function idOf(value){if(typeof value==="string")return value||null;if(value&&typeof value==="object"&&typeof value.id==="string")return value.id||null;return null}export function checkoutOutcome(event){if(!event||typeof event.type!=="string"||!event.type.startsWith("checkout.session."))return null;const session=event.data?.object;if(!session||typeof session!=="object"||typeof session.id!=="string")return null;const paymentStatus=String(session.payment_status??"");let status=null;switch(event.type){case"checkout.session.completed":status=PAID.includes(paymentStatus)?"paid":paymentStatus==="unpaid"?"processing":null;break;case"checkout.session.async_payment_succeeded":status=PAID.includes(paymentStatus)?"paid":"processing";break;case"checkout.session.async_payment_failed":status="failed";break;case"checkout.session.expired":status="expired";break}if(!status)return null;return{status,session,sessionId:session.id,reference:session.client_reference_id??null,paymentIntentId:idOf(session.payment_intent),subscriptionId:idOf(session.subscription),customerId:idOf(session.customer),mode:session.mode??null,amountTotal:Number.isFinite(session.amount_total)?Number(session.amount_total):null,currency:session.currency?String(session.currency).toLowerCase():null,metadata:session.metadata??{}}}
1
+ const PAID=["paid","no_payment_required"];function idOf(value){if(typeof value==="string")return value||null;if(value&&typeof value==="object"&&typeof value.id==="string")return value.id||null;return null}export function checkoutOutcome(event){if(!event||typeof event.type!=="string"||!event.type.startsWith("checkout.session."))return null;const session=event.data?.object;if(!session||typeof session!=="object"||typeof session.id!=="string")return null;if(session.mode!=="payment"&&session.mode!=="subscription")return null;const paymentStatus=String(session.payment_status??"");let status=null;switch(event.type){case"checkout.session.completed":status=PAID.includes(paymentStatus)?"paid":paymentStatus==="unpaid"?"processing":null;break;case"checkout.session.async_payment_succeeded":status=PAID.includes(paymentStatus)?"paid":"processing";break;case"checkout.session.async_payment_failed":status="failed";break;case"checkout.session.expired":status="expired";break}if(!status)return null;return{status,session,sessionId:session.id,reference:session.client_reference_id??null,paymentIntentId:idOf(session.payment_intent),subscriptionId:idOf(session.subscription),customerId:idOf(session.customer),mode:session.mode??null,amountTotal:Number.isFinite(session.amount_total)?Number(session.amount_total):null,currency:session.currency?String(session.currency).toLowerCase():null,metadata:session.metadata??{}}}
@@ -43,7 +43,7 @@ export declare function connectedCustomerIdempotencyKey(accountId: string, userI
43
43
  * when the email has changed, and created with a deterministic idempotency
44
44
  * key, so nothing on the app's side has to remember it.
45
45
  */
46
- export declare function connectedCustomer(user: Pick<UserModel, 'id' | 'email' | 'name'> & { email?: string | null, name?: string | null }, accountId: string, client?: CustomerClient): Promise<Stripe.Customer>;
46
+ export declare function connectedCustomer(user: ConnectPayer, accountId: string, client?: CustomerClient): Promise<Stripe.Customer>;
47
47
  /**
48
48
  * A Checkout Session on the connected account, as `manageCheckout.create`
49
49
  * makes one on the platform: the user's customer there unless `params` names
@@ -59,7 +59,7 @@ export declare function connectedCustomer(user: Pick<UserModel, 'id' | 'email' |
59
59
  * success_url, cancel_url,
60
60
  * })
61
61
  */
62
- export declare function directCheckout(user: UserModel, accountId: string, params: Stripe.Checkout.SessionCreateParams, client?: Pick<Stripe, 'customers' | 'checkout'>, request?: { idempotencyKey?: string }): Promise<Stripe.Response<Stripe.Checkout.Session>>;
62
+ export declare function directCheckout(user: ConnectPayer, accountId: string, params: Stripe.Checkout.SessionCreateParams, client?: Pick<Stripe, 'customers' | 'checkout'>, request?: { idempotencyKey?: string }): Promise<Stripe.Response<Stripe.Checkout.Session>>;
63
63
  /** Subscriptions billed on the merchant's account, the platform paid its fee from each invoice. */
64
64
  export declare const manageDirectSubscription: ManageDirectSubscription;
65
65
  export declare interface DirectSubscriptionCheckoutOptions {
@@ -78,7 +78,7 @@ export declare interface DirectSubscriptionCheckoutOptions {
78
78
  }
79
79
  export declare interface ManageDirectSubscription {
80
80
  subscriptionData: (options?: Pick<DirectSubscriptionCheckoutOptions, 'trialDays' | 'platformFeePercent' | 'metadata'>) => Stripe.Checkout.SessionCreateParams.SubscriptionData
81
- checkout: (user: UserModel, options: DirectSubscriptionCheckoutOptions, client?: Pick<Stripe, 'customers' | 'checkout'>) => Promise<Stripe.Response<Stripe.Checkout.Session>>
81
+ checkout: (user: ConnectPayer, options: DirectSubscriptionCheckoutOptions, client?: Pick<Stripe, 'customers' | 'checkout'>) => Promise<Stripe.Response<Stripe.Checkout.Session>>
82
82
  }
83
83
  /**
84
84
  * Direct charges: the sale is made on the merchant's own connected account,
@@ -112,3 +112,5 @@ export type DirectPaymentFields = Pick<Stripe.PaymentIntentCreateParams, 'applic
112
112
  export type DirectSubscriptionFields = Pick<Stripe.SubscriptionCreateParams, 'application_fee_percent'>;
113
113
  /** The Stripe customer calls a connected customer needs. */
114
114
  declare type CustomerClient = Pick<Stripe, 'customers'>;
115
+ /** Connect needs a payer identity, independent of an application's ORM traits. */
116
+ export type ConnectPayer = Pick<UserModel, 'id'> & { email?: string | null, name?: string | null }
@@ -1,4 +1,7 @@
1
1
  import type Stripe from 'stripe';
2
+ /** Presentment fee amounts from canonical direct-charge facts. An unknown
3
+ * partial foreign-currency return must never become an inferred FX amount. */
4
+ export declare function directChargeFeeAmounts(facts: DirectChargeFinancials): DirectChargeFeeAmounts;
2
5
  /** Retrieve current facts on the ORIGINAL direct-charge account. Call after
3
6
  * acquiring the application's payment lock, not before it. Every list is fully
4
7
  * paginated; platform application fees never use the connected-account header.
@@ -23,6 +26,13 @@ export declare interface DirectChargeFinancials {
23
26
  applicationFeeRefunds: Stripe.FeeRefund[]
24
27
  movements: DirectChargeMovement[]
25
28
  }
29
+ export declare interface DirectChargeFeeAmounts {
30
+ currency: string
31
+ chargedCents: number
32
+ returnedCents: number | null
33
+ remainingCents: number | null
34
+ requiresReview: boolean
35
+ }
26
36
  /** Stripe creates application fees/balance transactions asynchronously. Retry
27
37
  * through the caller's durable queue; absence must never mean a zero fee. */
28
38
  export declare class DirectChargeFinancialsPendingError extends Error {
@@ -1 +1 @@
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
+ import{stripe}from"../drivers/stripe";import{asAccount}from"./direct";import{allPages}from"./pagination";export class DirectChargeFinancialsPendingError extends Error{}export function directChargeFeeAmounts(facts){const cents=(value)=>{if(typeof value!=="number"||!Number.isSafeInteger(value)||value<0)throw Error("Invalid direct-charge fee amount");return value},currency=facts.charge.currency.toUpperCase(),chargedCents=cents(facts.charge.application_fee_amount??0),unknown=()=>({currency,chargedCents,returnedCents:null,remainingCents:null,requiresReview:!0}),known=(returnedCents)=>({currency,chargedCents,returnedCents,remainingCents:chargedCents-returnedCents,requiresReview:!1}),fee=facts.applicationFee;if(!fee)return chargedCents===0&&!facts.applicationFeeRefunds.length?known(0):unknown();const amount=cents(fee.amount),returned=cents(fee.amount_refunded),recorded=facts.applicationFeeRefunds.reduce((sum,refund)=>sum+cents(refund.amount),0);if(id(fee.account)!==facts.account||id(fee.charge)!==facts.charge.id||facts.applicationFeeRefunds.some((refund)=>id(refund.fee)!==fee.id||refund.currency.toUpperCase()!==fee.currency.toUpperCase()||refund.amount<=0)||!Number.isSafeInteger(recorded)||recorded!==returned||returned>amount||chargedCents===0&&amount>0)return unknown();if(fee.currency.toUpperCase()===currency)return amount===chargedCents?known(returned):unknown();if(returned===0)return known(0);if(returned===amount)return known(chargedCents);return unknown()}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}}
@@ -4,7 +4,7 @@ import { collectDirectInvoice } from './invoice-collection';
4
4
  import { completeDirectInvoicePurchase, directInvoicePurchaseMetadata } from './invoice-purchase';
5
5
  import { connectRefund, connectRefundParams, isDestinationCharge } from './refund';
6
6
  import { creditDirectInvoiceRefund } from './invoice-credit';
7
- import { directChargeFinancials } from './financials';
7
+ import { directChargeFeeAmounts, directChargeFinancials } from './financials';
8
8
  import { directInvoiceTaxFinancials } from './invoice-tax';
9
9
  import { ensureDirectInvoicePrice } from './invoice-price';
10
10
  import { manageConnectedAccount } from './account';
@@ -37,6 +37,7 @@ export declare const connect: {
37
37
  customer: typeof connectedCustomer;
38
38
  directCheckout: typeof directCheckout;
39
39
  directChargeFinancials: typeof directChargeFinancials;
40
+ directChargeFeeAmounts: typeof directChargeFeeAmounts;
40
41
  prepareDirectInvoiceFee: typeof prepareDirectInvoiceFee;
41
42
  collectDirectInvoice: typeof collectDirectInvoice;
42
43
  ensureDirectInvoicePrice: typeof ensureDirectInvoicePrice;
@@ -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-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};
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{directChargeFeeAmounts,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,directChargeFeeAmounts,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};
@@ -10,7 +10,7 @@ import type Stripe from 'stripe';
10
10
  export declare function collectDirectInvoice(invoiceId: string, account: string, options: DirectInvoiceCollectionOptions, client?: Pick<Stripe, 'invoices' | 'invoicePayments' | 'paymentMethods' | 'paymentIntents'>): Promise<DirectInvoiceCollection>;
11
11
  export declare interface DirectInvoiceCollectionOptions extends DirectInvoiceFeeOptions {
12
12
  approvedTotal: number
13
- paymentMethod: string
13
+ paymentMethod?: string
14
14
  }
15
15
  export declare interface DirectInvoiceCollection {
16
16
  invoice: Stripe.Invoice
@@ -1 +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)}}
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)||options.paymentMethod!==void 0&&!/^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&&options.paymentMethod){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");if(!options.paymentMethod)return result("requires_payment",intent);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)}}
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.362",
5
+ "version": "0.75.367",
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.362",
64
- "@stacksjs/database": "0.75.362",
65
- "@stacksjs/error-handling": "0.75.362",
66
- "@stacksjs/logging": "0.75.362",
67
- "@stacksjs/orm": "0.75.362",
68
- "@stacksjs/types": "0.75.362"
63
+ "@stacksjs/config": "0.75.367",
64
+ "@stacksjs/database": "0.75.367",
65
+ "@stacksjs/error-handling": "0.75.367",
66
+ "@stacksjs/logging": "0.75.367",
67
+ "@stacksjs/orm": "0.75.367",
68
+ "@stacksjs/types": "0.75.367"
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.362",
83
+ "@stacksjs/utils": "0.75.367",
84
84
  "@stripe/stripe-js": "^10.0.0",
85
85
  "better-dx": "^0.2.24",
86
86
  "stripe": "23.0.0"