@stacksjs/payments 0.75.369 → 0.75.370

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -70,4 +70,6 @@ Call after acquiring the application's payment lock. Persist immutable movement
70
70
 
71
71
  Balance amounts, fees and net remain in their actual settlement currency with the provider exchange rate. Contributor allocations belong in their own presentment-currency ledger; a dispute status alone does not prove reinstatement, and a charge refund does not prove that the application fee was returned.
72
72
 
73
+ `directChargeFeeAmounts(facts)` uses the application fee's canonical `amount_refunded` in its declared currency. Individual fee refunds can settle in another currency; their amounts are never added across currencies. When the fee and charge share a currency, partial and full returns are exact. When they differ, only a fully retained or fully returned fee establishes exact presentment cents; partial returns require review without an inferred exchange rate. See Stripe's [application fee object](https://docs.stripe.com/api/application_fees/object).
74
+
73
75
  References: [direct charges](https://docs.stripe.com/connect/direct-charges), [refund pagination](https://docs.stripe.com/api/refunds/list), [disputes](https://docs.stripe.com/api/disputes/list), [application fees](https://docs.stripe.com/api/application_fees/list).
@@ -1 +1 @@
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}}
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),feeCurrency=fee.currency?.toUpperCase(),refunds=facts.applicationFeeRefunds,amounts=refunds.map((refund)=>cents(refund.amount)),recorded=refunds.every((refund)=>refund.currency?.toUpperCase()===feeCurrency)?amounts.reduce((sum,value)=>sum+value,0):null;if(id(fee.account)!==facts.account||id(fee.charge)!==facts.charge.id||!/^[A-Z]{3}$/.test(feeCurrency||"")||refunds.some((refund)=>id(refund.fee)!==fee.id||!/^[a-z]{3}$/i.test(refund.currency||"")||refund.amount<=0)||recorded!==null&&(!Number.isSafeInteger(recorded)||recorded!==returned)||returned>0!==refunds.length>0||returned>amount||chargedCents===0&&amount>0)return unknown();if(feeCurrency===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}}
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.369",
5
+ "version": "0.75.370",
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.369",
64
- "@stacksjs/database": "0.75.369",
65
- "@stacksjs/error-handling": "0.75.369",
66
- "@stacksjs/logging": "0.75.369",
67
- "@stacksjs/orm": "0.75.369",
68
- "@stacksjs/types": "0.75.369"
63
+ "@stacksjs/config": "0.75.370",
64
+ "@stacksjs/database": "0.75.370",
65
+ "@stacksjs/error-handling": "0.75.370",
66
+ "@stacksjs/logging": "0.75.370",
67
+ "@stacksjs/orm": "0.75.370",
68
+ "@stacksjs/types": "0.75.370"
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.369",
83
+ "@stacksjs/utils": "0.75.370",
84
84
  "@stripe/stripe-js": "^10.0.0",
85
85
  "better-dx": "^0.2.24",
86
86
  "stripe": "23.0.0"