@stacksjs/payments 0.75.357 → 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,5 +1,6 @@
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';
4
5
  import { creditDirectInvoiceRefund } from './invoice-credit';
5
6
  import { directChargeFinancials } from './financials';
@@ -35,6 +36,7 @@ export declare const connect: {
35
36
  directCheckout: typeof directCheckout;
36
37
  directChargeFinancials: typeof directChargeFinancials;
37
38
  prepareDirectInvoiceFee: typeof prepareDirectInvoiceFee;
39
+ collectDirectInvoice: typeof collectDirectInvoice;
38
40
  creditDirectInvoiceRefund: typeof creditDirectInvoiceRefund;
39
41
  directInvoiceTaxFinancials: typeof directInvoiceTaxFinancials;
40
42
  directPaymentIntentParams: typeof directPaymentIntentParams;
@@ -84,6 +86,7 @@ export * from './destination';
84
86
  export * from './direct';
85
87
  export * from './financials';
86
88
  export * from './invoice-fee';
89
+ export * from './invoice-collection';
87
90
  export * from './invoice-credit';
88
91
  export * from './tax';
89
92
  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-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)}}
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.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.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.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.357",
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"