@stacksjs/payments 0.75.73 → 0.75.75
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/dist/driver/adyen.d.ts +64 -0
- package/dist/driver/adyen.js +1 -0
- package/dist/driver/index.d.ts +23 -0
- package/dist/driver/index.js +1 -0
- package/dist/driver/stripe.d.ts +46 -0
- package/dist/driver/stripe.js +1 -0
- package/dist/driver/types.d.ts +188 -0
- package/dist/driver/types.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -1
- package/dist/payment.d.ts +1 -50
- package/dist/payment.js +1 -1
- package/package.json +8 -8
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ChargeOptions, CheckoutRequest, CheckoutSession, CreatePaymentOptions, Customer, Money, Payer, PaymentDriver, PaymentEvent, PaymentResult, PaymentStatus, RefundOptions, RefundResult, StoredPaymentMethod, WebhookRequest } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Adyen's `resultCode` in our terms. Only `Authorised`, `Refused`, `Error`
|
|
4
|
+
* and `Cancelled` are final; everything else waits on the payer or a webhook.
|
|
5
|
+
*/
|
|
6
|
+
export declare function adyenStatus(resultCode: string | undefined): PaymentStatus;
|
|
7
|
+
/**
|
|
8
|
+
* The string Adyen signs for a standard webhook item: eight fields joined by
|
|
9
|
+
* colons, empty ones as empty strings.
|
|
10
|
+
*/
|
|
11
|
+
export declare function adyenSigningPayload(item: NotificationItem): string;
|
|
12
|
+
/** HMAC-SHA256 of the signing payload under the hex key, base64. */
|
|
13
|
+
export declare function adyenSignature(item: NotificationItem, hmacKey: string): string;
|
|
14
|
+
export declare const ADYEN_API_VERSION: 'v72';
|
|
15
|
+
/**
|
|
16
|
+
* The Adyen driver (stacksjs/stacks#450), on Checkout API v72.
|
|
17
|
+
*
|
|
18
|
+
* Adyen is a processor like Stripe, not a merchant of record, so payments,
|
|
19
|
+
* hosted checkout, refunds, stored cards and webhooks map across. It has no
|
|
20
|
+
* product catalog, no subscriptions, no invoices and no customer object:
|
|
21
|
+
* a payer is a `shopperReference` you choose, and recurring billing is charging
|
|
22
|
+
* a stored card on your own schedule. Those operations throw
|
|
23
|
+
* {@link PaymentUnsupportedError}.
|
|
24
|
+
*
|
|
25
|
+
* Verified against Adyen's API reference and its published HMAC example; not
|
|
26
|
+
* yet run against a live merchant account.
|
|
27
|
+
*/
|
|
28
|
+
export declare interface AdyenConfig {
|
|
29
|
+
apiKey: string
|
|
30
|
+
merchantAccount: string
|
|
31
|
+
environment: 'test' | 'live'
|
|
32
|
+
liveUrlPrefix?: string
|
|
33
|
+
hmacKey?: string
|
|
34
|
+
shopperReferencePrefix?: string
|
|
35
|
+
}
|
|
36
|
+
declare interface NotificationItem {
|
|
37
|
+
pspReference?: string
|
|
38
|
+
originalReference?: string
|
|
39
|
+
merchantAccountCode?: string
|
|
40
|
+
merchantReference?: string
|
|
41
|
+
amount?: { value?: number, currency?: string }
|
|
42
|
+
eventCode?: string
|
|
43
|
+
success?: string | boolean
|
|
44
|
+
additionalData?: Record<string, string>
|
|
45
|
+
}
|
|
46
|
+
export declare class AdyenDriver implements PaymentDriver {
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly capabilities: unknown;
|
|
49
|
+
constructor(config: AdyenConfig, options?: { fetch?: typeof fetch });
|
|
50
|
+
get baseUrl(): string;
|
|
51
|
+
shopperReference(payer: Payer): string;
|
|
52
|
+
customer(payer: Payer): Promise<Customer>;
|
|
53
|
+
charge(payer: Payer, amount: Money, paymentMethod: string, options?: ChargeOptions): Promise<PaymentResult>;
|
|
54
|
+
createPayment(payer: Payer, amount: Money, options?: CreatePaymentOptions): Promise<PaymentResult>;
|
|
55
|
+
refund(paymentId: string, options?: RefundOptions): Promise<RefundResult>;
|
|
56
|
+
checkout(payer: Payer, request: CheckoutRequest): Promise<CheckoutSession>;
|
|
57
|
+
paymentMethods(payer: Payer): Promise<StoredPaymentMethod[]>;
|
|
58
|
+
removePaymentMethod(payer: Payer, paymentMethodId: string): Promise<void>;
|
|
59
|
+
subscribe(): Promise<never>;
|
|
60
|
+
cancelSubscription(): Promise<never>;
|
|
61
|
+
subscriptions(): Promise<never>;
|
|
62
|
+
verifyWebhook(request: WebhookRequest): Promise<PaymentEvent[]>;
|
|
63
|
+
acknowledgeWebhook(): Response;
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHmac,randomUUID,timingSafeEqual}from"node:crypto";import{assertMoney,PaymentProviderError,PaymentUnsupportedError,WebhookSignatureError}from"./types";export const ADYEN_API_VERSION="v72";const CAPABILITIES=new Set(["customers","charge","createPayment","refund","checkout","paymentMethods","webhooks"]);export function adyenStatus(resultCode){switch(resultCode){case"Authorised":return"succeeded";case"Refused":case"Error":return"failed";case"Cancelled":return"canceled";case"RedirectShopper":case"ChallengeShopper":case"IdentifyShopper":case"PresentToShopper":return"requires_action";default:return"processing"}}const EVENT_TYPES={AUTHORISATION:["payment.succeeded","payment.failed"],CANCELLATION:["payment.canceled","unknown"],REFUND:["refund.succeeded","refund.failed"],REFUND_FAILED:["refund.failed","refund.failed"],CANCEL_OR_REFUND:["refund.succeeded","refund.failed"]};export function adyenSigningPayload(item){return[item.pspReference??"",item.originalReference??"",item.merchantAccountCode??"",item.merchantReference??"",item.amount?.value??"",item.amount?.currency??"",item.eventCode??"",String(item.success??"")].join(":")}export function adyenSignature(item,hmacKey){return createHmac("sha256",Buffer.from(hmacKey,"hex")).update(adyenSigningPayload(item),"utf8").digest("base64")}export class AdyenDriver{name="adyen";capabilities=CAPABILITIES;config;fetch;constructor(config,options={}){if(!config.apiKey)throw Error("The Adyen payment driver needs an API key: set ADYEN_API_KEY or payment.adyen.apiKey.");if(!config.merchantAccount)throw Error("The Adyen payment driver needs a merchant account: set ADYEN_MERCHANT_ACCOUNT or payment.adyen.merchantAccount.");if(config.environment==="live"&&!config.liveUrlPrefix)throw Error("Adyen live requests go to your account's own endpoint: set ADYEN_LIVE_URL_PREFIX or payment.adyen.liveUrlPrefix.");this.config=config;this.fetch=options.fetch??fetch}get baseUrl(){return this.config.environment==="live"?`https://${this.config.liveUrlPrefix}-checkout-live.adyenpayments.com/checkout/${ADYEN_API_VERSION}`:`https://checkout-test.adyen.com/${ADYEN_API_VERSION}`}shopperReference(payer){return`${this.config.shopperReferencePrefix??"user-"}${payer.id}`}async request(method,path,options={}){const url=new URL(`${this.baseUrl}${path}`);for(const[key,value]of Object.entries(options.query??{}))url.searchParams.set(key,value);const headers={"X-API-Key":this.config.apiKey,Accept:"application/json"};if(options.body!==void 0)headers["Content-Type"]="application/json";if(options.idempotencyKey)headers["Idempotency-Key"]=options.idempotencyKey;const response=await this.fetch(url,{method,headers,body:options.body===void 0?void 0:JSON.stringify(options.body)});if(response.status===204)return;const text=await response.text(),body=text?JSON.parse(text):void 0;if(!response.ok){const error=body;throw new PaymentProviderError("adyen",response.status,error?.message??response.statusText,error?.errorCode??null)}return body}amount(money){assertMoney(money);return{value:money.amount,currency:money.currency.toUpperCase()}}async customer(payer){return{id:this.shopperReference(payer),email:payer.email??null,name:payer.name??null}}async charge(payer,amount,paymentMethod,options={}){const response=await this.request("POST","/payments",{body:{merchantAccount:this.config.merchantAccount,amount:this.amount(amount),reference:options.reference??randomUUID(),paymentMethod:{type:"scheme",storedPaymentMethodId:paymentMethod},shopperReference:this.shopperReference(payer),shopperInteraction:"ContAuth",recurringProcessingModel:"UnscheduledCardOnFile",...options.metadata?{metadata:options.metadata}:{}},idempotencyKey:options.idempotencyKey});return{id:response.pspReference,status:adyenStatus(response.resultCode),amount:{amount:amount.amount,currency:amount.currency.toLowerCase()},...response.action?.url?{redirectUrl:response.action.url}:{},...response.refusalReason?{failureReason:response.refusalReason}:{},raw:response}}async createPayment(payer,amount,options={}){if(!options.returnUrl)throw TypeError("An Adyen payment session needs a returnUrl: where the payer comes back after authenticating.");const session=await this.request("POST","/sessions",{body:{merchantAccount:this.config.merchantAccount,amount:this.amount(amount),reference:options.reference??randomUUID(),returnUrl:options.returnUrl,mode:"embedded",shopperReference:this.shopperReference(payer),...options.metadata?{metadata:options.metadata}:{}},idempotencyKey:options.idempotencyKey});return{id:session.id,status:"requires_payment_method",amount:{amount:amount.amount,currency:amount.currency.toLowerCase()},clientConfirmation:{provider:"adyen",sessionId:session.id,sessionData:session.sessionData},raw:session}}async refund(paymentId,options={}){const reference=randomUUID(),response=options.amount?await this.request("POST",`/payments/${encodeURIComponent(paymentId)}/refunds`,{body:{merchantAccount:this.config.merchantAccount,amount:this.amount(options.amount),reference},idempotencyKey:options.idempotencyKey}):await this.request("POST",`/payments/${encodeURIComponent(paymentId)}/reversals`,{body:{merchantAccount:this.config.merchantAccount,reference},idempotencyKey:options.idempotencyKey});return{id:response.pspReference,paymentId,status:"pending",amount:options.amount?{amount:options.amount.amount,currency:options.amount.currency.toLowerCase()}:null,raw:response}}async checkout(payer,request){if(request.mode==="subscription")throw new PaymentUnsupportedError("adyen","subscription checkout",'Adyen has no subscriptions; store a card with mode "setup" and charge it on your schedule');if(request.cancelUrl&&request.cancelUrl!==request.successUrl)throw new PaymentUnsupportedError("adyen","a separate cancelUrl","Adyen's hosted checkout returns to one URL, with the outcome in its query string");if(!request.currency)throw TypeError("An Adyen checkout needs a currency.");const lines=request.lines.map((line)=>{if("price"in line)throw new PaymentUnsupportedError("adyen","catalog prices","Adyen has no product catalog; price each line with name and unitAmount");return line}),total=lines.reduce((sum,line)=>sum+line.unitAmount*line.quantity,0),setup=request.mode==="setup",session=await this.request("POST","/sessions",{body:{merchantAccount:this.config.merchantAccount,amount:this.amount({amount:setup?0:total,currency:request.currency}),reference:request.reference??randomUUID(),returnUrl:request.successUrl,mode:"hosted",shopperReference:this.shopperReference(payer),...payer.email?{shopperEmail:payer.email}:{},...setup?{storePaymentMethodMode:"enabled",recurringProcessingModel:"UnscheduledCardOnFile"}:{},...setup?{}:{lineItems:lines.map((line)=>({description:line.name,quantity:line.quantity,amountIncludingTax:line.unitAmount}))},...request.metadata?{metadata:request.metadata}:{}}});if(!session.url)throw new PaymentProviderError("adyen",200,"the hosted session came back without a url");return{id:session.id,url:session.url,expiresAt:session.expiresAt?new Date(session.expiresAt):null,raw:session}}async paymentMethods(payer){return((await this.request("GET","/storedPaymentMethods",{query:{merchantAccount:this.config.merchantAccount,shopperReference:this.shopperReference(payer)}}))?.storedPaymentMethods??[]).map((method)=>({id:String(method.id),type:String(method.type??"scheme"),brand:method.brand??null,last4:method.lastFour??null,expMonth:method.expiryMonth?Number(method.expiryMonth):null,expYear:method.expiryYear?Number(String(method.expiryYear).length===2?`20${method.expiryYear}`:method.expiryYear):null,isDefault:!1,raw:method}))}async removePaymentMethod(payer,paymentMethodId){await this.request("DELETE",`/storedPaymentMethods/${encodeURIComponent(paymentMethodId)}`,{query:{merchantAccount:this.config.merchantAccount,shopperReference:this.shopperReference(payer)}})}async subscribe(){throw new PaymentUnsupportedError("adyen","subscriptions","charge a stored card on your own schedule with charge()")}async cancelSubscription(){throw new PaymentUnsupportedError("adyen","subscriptions")}async subscriptions(){throw new PaymentUnsupportedError("adyen","subscriptions")}async verifyWebhook(request){if(!this.config.hmacKey)throw Error("Adyen webhooks cannot be verified without the HMAC key: set ADYEN_HMAC_KEY or payment.adyen.hmacKey.");let body;try{body=JSON.parse(request.payload)}catch{throw new WebhookSignatureError("adyen","the body is not JSON")}const items=(body.notificationItems??[]).map((entry)=>entry.NotificationRequestItem).filter((item)=>Boolean(item));if(items.length===0)throw new WebhookSignatureError("adyen","the delivery holds no notification items");return items.map((item)=>{const given=item.additionalData?.hmacSignature;if(!given)throw new WebhookSignatureError("adyen",`item ${item.pspReference??"?"} has no hmacSignature`);const expected=Buffer.from(adyenSignature(item,this.config.hmacKey)),actual=Buffer.from(given);if(expected.length!==actual.length||!timingSafeEqual(expected,actual))throw new WebhookSignatureError("adyen",`item ${item.pspReference??"?"} is signed with another key`);const success=String(item.success)==="true",pair=EVENT_TYPES[item.eventCode??""];return{id:`${item.eventCode}:${item.pspReference}`,type:pair?pair[success?0:1]:"unknown",providerType:item.eventCode??"",reference:item.pspReference??null,merchantReference:item.merchantReference??null,amount:item.amount?.value!==void 0&&item.amount.currency?{amount:Number(item.amount.value),currency:item.amount.currency.toLowerCase()}:null,raw:item}})}acknowledgeWebhook(){return new Response("[accepted]",{status:202,headers:{"Content-Type":"text/plain"}})}}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { stripe } from '../drivers/stripe';
|
|
2
|
+
import type { AdyenConfig } from './adyen';
|
|
3
|
+
import type { PaymentDriver } from './types';
|
|
4
|
+
import type { StripeOperations } from './stripe';
|
|
5
|
+
/**
|
|
6
|
+
* Make a driver of your own available as `config.payment.driver`. A built-in
|
|
7
|
+
* name cannot be replaced, so `stripe` always means Stacks' Stripe driver.
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerPaymentDriver(name: string, factory: DriverFactory): void;
|
|
10
|
+
/** Adyen's settings: config/payment.ts, then the ADYEN_* environment. */
|
|
11
|
+
export declare function adyenConfig(settings?: Partial<AdyenConfig>, env?: Record<string, string | undefined>): AdyenConfig;
|
|
12
|
+
/**
|
|
13
|
+
* The payment driver `config.payment.driver` names - `stripe` unless it says
|
|
14
|
+
* otherwise - or the one named here. This is the first thing to read that
|
|
15
|
+
* setting: it used to be declared, typed as `'stripe'`, and ignored.
|
|
16
|
+
*/
|
|
17
|
+
export declare function paymentDriver(name?: string): PaymentDriver;
|
|
18
|
+
/** Stripe through the billable modules Stacks already had, so behaviour does not change. */
|
|
19
|
+
export declare const defaultStripeOperations: StripeOperations;
|
|
20
|
+
declare type DriverFactory = () => PaymentDriver;
|
|
21
|
+
export * from './adyen';
|
|
22
|
+
export * from './stripe';
|
|
23
|
+
export * from './types';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import process from"node:process";import{config,services}from"@stacksjs/config";import{manageCharge}from"../billable/charge";import{manageCheckout}from"../billable/checkout";import{manageCustomer}from"../billable/customer";import{managePaymentMethod}from"../billable/payment-method";import{manageSubscription}from"../billable/subscription";import{constructEvent}from"../billable/webhook";import{stripe}from"../drivers/stripe";import{freshIdempotencyKey}from"../idempotency";import{AdyenDriver}from"./adyen";import{StripeDriver}from"./stripe";export*from"./adyen";export*from"./stripe";export*from"./types";export const defaultStripeOperations={customer:(payer)=>manageCustomer.createOrGetStripeUser(payer,{}),createIntent:(payer,params,idempotencyKey)=>stripe.paymentIntents.create(params,{idempotencyKey:idempotencyKey??freshIdempotencyKey("payment_intent.create",payer.id,params.amount)}),refund:(paymentIntentId,params)=>manageCharge.refund(paymentIntentId,params),createCheckout:(payer,params)=>manageCheckout.create(payer,params),listPaymentMethods:async(customerId)=>(await stripe.paymentMethods.list({customer:customerId,limit:100})).data,deletePaymentMethod:(payer,paymentMethodId)=>managePaymentMethod.deletePaymentMethod(payer,paymentMethodId),subscribe:(payer,type,lookupKey)=>manageSubscription.create(payer,type,lookupKey,{}),cancelSubscription:(subscriptionId,atPeriodEnd)=>atPeriodEnd?stripe.subscriptions.update(subscriptionId,{cancel_at_period_end:!0}):manageSubscription.cancel(subscriptionId),listSubscriptions:async(customerId)=>(await stripe.subscriptions.list({customer:customerId,status:"all",limit:100})).data,constructEvent:(payload,signature,secret)=>constructEvent(payload,signature,secret)};const custom=new Map;export function registerPaymentDriver(name,factory){if(name==="stripe"||name==="adyen")throw Error(`"${name}" is a built-in payment driver and cannot be replaced.`);custom.set(name,factory)}export function adyenConfig(settings={},env=process.env){const environment=settings.environment??(env.ADYEN_ENVIRONMENT==="live"?"live":"test");return{apiKey:settings.apiKey||env.ADYEN_API_KEY||"",merchantAccount:settings.merchantAccount||env.ADYEN_MERCHANT_ACCOUNT||"",environment,liveUrlPrefix:settings.liveUrlPrefix||env.ADYEN_LIVE_URL_PREFIX||void 0,hmacKey:settings.hmacKey||env.ADYEN_HMAC_KEY||void 0,shopperReferencePrefix:settings.shopperReferencePrefix}}export function paymentDriver(name){const settings=config.payment??{},chosen=name??settings.driver??"stripe";if(chosen==="stripe")return new StripeDriver({operations:defaultStripeOperations,webhookSecret:settings.stripe?.webhookSecret||services?.stripe?.webhookSecret||process.env.STRIPE_WEBHOOK_SECRET||void 0});if(chosen==="adyen")return new AdyenDriver(adyenConfig(settings.adyen));const factory=custom.get(chosen);if(!factory)throw Error(`No payment driver named "${chosen}". Built in: stripe, adyen${custom.size?`; registered: ${[...custom.keys()].join(", ")}`:""}.`);return factory()}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ChargeOptions, CheckoutRequest, CheckoutSession, CreatePaymentOptions, Customer, Money, Payer, PaymentDriver, PaymentEvent, PaymentResult, PaymentStatus, RefundOptions, RefundResult, StoredPaymentMethod, SubscriptionSummary, WebhookRequest } from './types';
|
|
2
|
+
import type Stripe from 'stripe';
|
|
3
|
+
/**
|
|
4
|
+
* A PaymentIntent's status in our terms. `requires_capture` is authorised but
|
|
5
|
+
* not yet captured - no money has moved - so it is still `processing`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function stripePaymentStatus(intent: Pick<Stripe.PaymentIntent, 'status' | 'last_payment_error'>): PaymentStatus;
|
|
8
|
+
/**
|
|
9
|
+
* The Stripe calls the driver needs, separated from the mapping so the mapping
|
|
10
|
+
* is testable without Stripe. The defaults delegate to the billable modules
|
|
11
|
+
* Stacks already had, so a Stripe app keeps their idempotency keys,
|
|
12
|
+
* same-origin redirect checks and `stripe_id` bookkeeping unchanged.
|
|
13
|
+
*/
|
|
14
|
+
export declare interface StripeOperations {
|
|
15
|
+
customer: (payer: Payer) => Promise<Stripe.Customer>
|
|
16
|
+
createIntent: (payer: Payer, params: Stripe.PaymentIntentCreateParams, idempotencyKey?: string) => Promise<Stripe.PaymentIntent>
|
|
17
|
+
refund: (paymentIntentId: string, params: Stripe.RefundCreateParams & { idempotencyKey?: string }) => Promise<Stripe.Refund>
|
|
18
|
+
createCheckout: (payer: Payer, params: Stripe.Checkout.SessionCreateParams) => Promise<Stripe.Checkout.Session>
|
|
19
|
+
listPaymentMethods: (customerId: string) => Promise<Stripe.PaymentMethod[]>
|
|
20
|
+
deletePaymentMethod: (payer: Payer, paymentMethodId: string) => Promise<unknown>
|
|
21
|
+
subscribe: (payer: Payer, type: string, lookupKey: string) => Promise<Stripe.Subscription>
|
|
22
|
+
cancelSubscription: (subscriptionId: string, atPeriodEnd: boolean) => Promise<Stripe.Subscription>
|
|
23
|
+
listSubscriptions: (customerId: string) => Promise<Stripe.Subscription[]>
|
|
24
|
+
constructEvent: (payload: string, signature: string, secret: string) => Promise<Stripe.Event>
|
|
25
|
+
}
|
|
26
|
+
export declare interface StripeDriverOptions {
|
|
27
|
+
operations: StripeOperations
|
|
28
|
+
webhookSecret?: string
|
|
29
|
+
}
|
|
30
|
+
export declare class StripeDriver implements PaymentDriver {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly capabilities: unknown;
|
|
33
|
+
constructor(options: StripeDriverOptions);
|
|
34
|
+
customer(payer: Payer): Promise<Customer>;
|
|
35
|
+
charge(payer: Payer, amount: Money, paymentMethod: string, options?: ChargeOptions): Promise<PaymentResult>;
|
|
36
|
+
createPayment(payer: Payer, amount: Money, options?: CreatePaymentOptions): Promise<PaymentResult>;
|
|
37
|
+
refund(paymentId: string, options?: RefundOptions): Promise<RefundResult>;
|
|
38
|
+
checkout(payer: Payer, request: CheckoutRequest): Promise<CheckoutSession>;
|
|
39
|
+
paymentMethods(payer: Payer): Promise<StoredPaymentMethod[]>;
|
|
40
|
+
removePaymentMethod(payer: Payer, paymentMethodId: string): Promise<void>;
|
|
41
|
+
subscribe(payer: Payer, price: string, options?: { type?: string }): Promise<SubscriptionSummary>;
|
|
42
|
+
cancelSubscription(subscriptionId: string, options?: { atPeriodEnd?: boolean }): Promise<SubscriptionSummary>;
|
|
43
|
+
subscriptions(payer: Payer): Promise<SubscriptionSummary[]>;
|
|
44
|
+
verifyWebhook(request: WebhookRequest): Promise<PaymentEvent[]>;
|
|
45
|
+
acknowledgeWebhook(): Response;
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{assertMoney,headerOf,WebhookSignatureError}from"./types";const CAPABILITIES=new Set(["customers","charge","createPayment","refund","checkout","paymentMethods","subscriptions","webhooks"]);export function stripePaymentStatus(intent){switch(intent.status){case"succeeded":return"succeeded";case"canceled":return"canceled";case"requires_action":return"requires_action";case"processing":case"requires_capture":return"processing";default:return intent.last_payment_error?"failed":"requires_payment_method"}}function refundStatus(status){if(status==="succeeded"||status==="failed"||status==="canceled")return status;return"pending"}const SUBSCRIPTION_STATUSES=new Set(["active","trialing","past_due","canceled","incomplete","unpaid","paused"]);function subscriptionStatus(status){if(status==="incomplete_expired")return"canceled";return SUBSCRIPTION_STATUSES.has(status)?status:"unknown"}function summarizeSubscription(subscription){const item=subscription.items?.data?.[0],periodEnd=item?.current_period_end??subscription.current_period_end;return{id:subscription.id,status:subscriptionStatus(subscription.status),price:item?.price?.id??null,currentPeriodEnd:periodEnd?new Date(periodEnd*1000):null,cancelAtPeriodEnd:Boolean(subscription.cancel_at_period_end),raw:subscription}}const EVENT_TYPES={"payment_intent.succeeded":"payment.succeeded","payment_intent.payment_failed":"payment.failed","payment_intent.canceled":"payment.canceled","charge.refunded":"refund.succeeded","refund.failed":"refund.failed","checkout.session.completed":"checkout.completed","customer.subscription.created":"subscription.created","customer.subscription.updated":"subscription.updated","customer.subscription.deleted":"subscription.canceled","invoice.paid":"invoice.paid","invoice.payment_failed":"invoice.payment_failed"};function toPayment(intent){const status=stripePaymentStatus(intent);return{id:intent.id,status,amount:{amount:intent.amount,currency:intent.currency},...intent.client_secret&&(status==="requires_payment_method"||status==="requires_action")?{clientConfirmation:{provider:"stripe",clientSecret:intent.client_secret}}:{},...intent.next_action?.redirect_to_url?.url?{redirectUrl:intent.next_action.redirect_to_url.url}:{},...intent.last_payment_error?.message?{failureReason:intent.last_payment_error.message}:{},raw:intent}}export class StripeDriver{name="stripe";capabilities=CAPABILITIES;ops;webhookSecret;constructor(options){this.ops=options.operations;this.webhookSecret=options.webhookSecret}async customer(payer){const customer=await this.ops.customer(payer);return{id:customer.id,email:customer.email??null,name:customer.name??null}}async charge(payer,amount,paymentMethod,options={}){assertMoney(amount);const customer=await this.ops.customer(payer),intent=await this.ops.createIntent(payer,{amount:amount.amount,currency:amount.currency.toLowerCase(),customer:customer.id,payment_method:paymentMethod,confirm:!0,automatic_payment_methods:{enabled:!0,allow_redirects:"never"},off_session:!0,...options.description?{description:options.description}:{},metadata:{...options.metadata,...options.reference?{reference:options.reference}:{}}},options.idempotencyKey);return toPayment(intent)}async createPayment(payer,amount,options={}){assertMoney(amount);const customer=await this.ops.customer(payer),intent=await this.ops.createIntent(payer,{amount:amount.amount,currency:amount.currency.toLowerCase(),customer:customer.id,automatic_payment_methods:{enabled:!0},...options.description?{description:options.description}:{},metadata:{...options.metadata,...options.reference?{reference:options.reference}:{}}},options.idempotencyKey);return toPayment(intent)}async refund(paymentId,options={}){if(options.amount)assertMoney(options.amount,"refund amount");const refund=await this.ops.refund(paymentId,{...options.amount?{amount:options.amount.amount}:{},...options.reason?{reason:options.reason}:{},...options.idempotencyKey?{idempotencyKey:options.idempotencyKey}:{}});return{id:refund.id,paymentId,status:refundStatus(refund.status),amount:{amount:refund.amount,currency:refund.currency},raw:refund}}async checkout(payer,request){const customer=await this.ops.customer(payer),lineItems=request.lines.map((line)=>{if("price"in line)return{price:line.price,quantity:line.quantity};if(!request.currency)throw TypeError("A checkout line priced with unitAmount needs the checkout's currency.");return{quantity:line.quantity,price_data:{currency:request.currency.toLowerCase(),unit_amount:line.unitAmount,product_data:{name:line.name}}}}),session=await this.ops.createCheckout(payer,{customer:customer.id,mode:request.mode,success_url:request.successUrl,...request.cancelUrl?{cancel_url:request.cancelUrl}:{},...request.mode==="setup"?{currency:request.currency?.toLowerCase()}:{line_items:lineItems},...request.reference?{client_reference_id:request.reference}:{},...request.metadata?{metadata:request.metadata}:{}});if(!session.url)throw Error("Stripe returned a checkout session without a url.");return{id:session.id,url:session.url,expiresAt:session.expires_at?new Date(session.expires_at*1000):null,raw:session}}async paymentMethods(payer){const customer=await this.ops.customer(payer),defaultId=typeof customer.invoice_settings?.default_payment_method==="string"?customer.invoice_settings.default_payment_method:customer.invoice_settings?.default_payment_method?.id??null;return(await this.ops.listPaymentMethods(customer.id)).map((method)=>({id:method.id,type:method.type,brand:method.card?.brand??null,last4:method.card?.last4??null,expMonth:method.card?.exp_month??null,expYear:method.card?.exp_year??null,isDefault:method.id===defaultId,raw:method}))}async removePaymentMethod(payer,paymentMethodId){await this.ops.deletePaymentMethod(payer,paymentMethodId)}async subscribe(payer,price,options={}){return summarizeSubscription(await this.ops.subscribe(payer,options.type??"default",price))}async cancelSubscription(subscriptionId,options={}){return summarizeSubscription(await this.ops.cancelSubscription(subscriptionId,options.atPeriodEnd??!1))}async subscriptions(payer){const customer=await this.ops.customer(payer);return(await this.ops.listSubscriptions(customer.id)).map(summarizeSubscription)}async verifyWebhook(request){if(!this.webhookSecret)throw Error("Stripe webhooks cannot be verified without the signing secret: set STRIPE_WEBHOOK_SECRET or payment.stripe.webhookSecret.");const signature=headerOf(request.headers,"stripe-signature");if(!signature)throw new WebhookSignatureError("stripe","there is no Stripe-Signature header");let event;try{event=await this.ops.constructEvent(request.payload,signature,this.webhookSecret)}catch(error){throw new WebhookSignatureError("stripe",error.message)}const object=event.data.object,reference=event.type==="charge.refunded"?object.payment_intent??object.id:object.id,amount=typeof object.amount==="number"&&object.currency?{amount:object.amount,currency:String(object.currency)}:typeof object.amount_total==="number"&&object.currency?{amount:object.amount_total,currency:String(object.currency)}:null;return[{id:event.id,type:EVENT_TYPES[event.type]??"unknown",providerType:event.type,reference:typeof reference==="string"?reference:null,merchantReference:object.client_reference_id??object.metadata?.reference??null,amount,raw:event}]}acknowledgeWebhook(){return Response.json({received:!0})}}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/** Read a header from either a `Headers` or a plain object, case-insensitively. */
|
|
2
|
+
export declare function headerOf(headers: WebhookRequest['headers'], name: string): string | null;
|
|
3
|
+
/** Validate an amount before it reaches a provider: a positive whole number of minor units. */
|
|
4
|
+
export declare function assertMoney(money: Money, label?: string): void;
|
|
5
|
+
/**
|
|
6
|
+
* The provider-neutral payments contract (stacksjs/stacks#665, #450).
|
|
7
|
+
*
|
|
8
|
+
* `@stacksjs/payments` was Stripe's SDK with a facade on it: every function
|
|
9
|
+
* returned a `Stripe.*` type, and `config.payment.driver` was read by nothing.
|
|
10
|
+
* A `PaymentDriver` describes what an application needs from a payment
|
|
11
|
+
* provider in the application's terms - money in minor units, statuses of our
|
|
12
|
+
* own, events of our own - and each provider implements it.
|
|
13
|
+
*
|
|
14
|
+
* Providers are not the same kind of thing, and the interface does not
|
|
15
|
+
* pretend otherwise. A driver lists what it can do in `capabilities`; calling
|
|
16
|
+
* anything else throws a {@link PaymentUnsupportedError} naming the driver and
|
|
17
|
+
* the operation, rather than a stack trace from an SDK the app never
|
|
18
|
+
* installed or a result that only looks like success.
|
|
19
|
+
*/
|
|
20
|
+
/** An amount in the currency's minor unit (cents), never a float. */
|
|
21
|
+
export declare interface Money {
|
|
22
|
+
amount: number
|
|
23
|
+
currency: string
|
|
24
|
+
}
|
|
25
|
+
/** Who is paying, as the application knows them. Any Stacks user model fits. */
|
|
26
|
+
export declare interface Payer {
|
|
27
|
+
id: number | string
|
|
28
|
+
email?: string | null
|
|
29
|
+
name?: string | null
|
|
30
|
+
stripe_id?: string | null
|
|
31
|
+
update?: (values: Record<string, unknown>) => Promise<unknown>
|
|
32
|
+
}
|
|
33
|
+
export declare interface Customer {
|
|
34
|
+
id: string
|
|
35
|
+
email: string | null
|
|
36
|
+
name: string | null
|
|
37
|
+
}
|
|
38
|
+
export declare interface PaymentResult {
|
|
39
|
+
id: string
|
|
40
|
+
status: PaymentStatus
|
|
41
|
+
amount: Money
|
|
42
|
+
clientConfirmation?: ClientConfirmation
|
|
43
|
+
redirectUrl?: string
|
|
44
|
+
failureReason?: string
|
|
45
|
+
raw: unknown
|
|
46
|
+
}
|
|
47
|
+
export declare interface RefundResult {
|
|
48
|
+
id: string
|
|
49
|
+
paymentId: string
|
|
50
|
+
status: RefundStatus
|
|
51
|
+
amount: Money | null
|
|
52
|
+
raw: unknown
|
|
53
|
+
}
|
|
54
|
+
export declare interface CheckoutRequest {
|
|
55
|
+
mode: 'payment' | 'subscription' | 'setup'
|
|
56
|
+
lines: CheckoutLine[]
|
|
57
|
+
currency?: string
|
|
58
|
+
successUrl: string
|
|
59
|
+
cancelUrl?: string
|
|
60
|
+
reference?: string
|
|
61
|
+
metadata?: Record<string, string>
|
|
62
|
+
}
|
|
63
|
+
export declare interface CheckoutSession {
|
|
64
|
+
id: string
|
|
65
|
+
url: string
|
|
66
|
+
expiresAt: Date | null
|
|
67
|
+
raw: unknown
|
|
68
|
+
}
|
|
69
|
+
export declare interface StoredPaymentMethod {
|
|
70
|
+
id: string
|
|
71
|
+
type: string
|
|
72
|
+
brand: string | null
|
|
73
|
+
last4: string | null
|
|
74
|
+
expMonth: number | null
|
|
75
|
+
expYear: number | null
|
|
76
|
+
isDefault: boolean
|
|
77
|
+
raw: unknown
|
|
78
|
+
}
|
|
79
|
+
export declare interface SubscriptionSummary {
|
|
80
|
+
id: string
|
|
81
|
+
status: SubscriptionStatus
|
|
82
|
+
price: string | null
|
|
83
|
+
currentPeriodEnd: Date | null
|
|
84
|
+
cancelAtPeriodEnd: boolean
|
|
85
|
+
raw: unknown
|
|
86
|
+
}
|
|
87
|
+
/** A verified webhook notification, in our terms. */
|
|
88
|
+
export declare interface PaymentEvent {
|
|
89
|
+
id: string
|
|
90
|
+
type: PaymentEventType
|
|
91
|
+
providerType: string
|
|
92
|
+
reference: string | null
|
|
93
|
+
merchantReference: string | null
|
|
94
|
+
amount: Money | null
|
|
95
|
+
raw: unknown
|
|
96
|
+
}
|
|
97
|
+
export declare interface WebhookRequest {
|
|
98
|
+
payload: string
|
|
99
|
+
headers: Headers | Record<string, string | undefined>
|
|
100
|
+
}
|
|
101
|
+
export declare interface ChargeOptions {
|
|
102
|
+
reference?: string
|
|
103
|
+
description?: string
|
|
104
|
+
metadata?: Record<string, string>
|
|
105
|
+
idempotencyKey?: string
|
|
106
|
+
}
|
|
107
|
+
export declare interface CreatePaymentOptions extends ChargeOptions {
|
|
108
|
+
returnUrl?: string
|
|
109
|
+
}
|
|
110
|
+
export declare interface RefundOptions {
|
|
111
|
+
amount?: Money
|
|
112
|
+
reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer'
|
|
113
|
+
idempotencyKey?: string
|
|
114
|
+
}
|
|
115
|
+
export declare interface PaymentDriver {
|
|
116
|
+
readonly name: string
|
|
117
|
+
readonly capabilities: ReadonlySet<PaymentOperation>
|
|
118
|
+
customer: (payer: Payer) => Promise<Customer>
|
|
119
|
+
charge: (payer: Payer, amount: Money, paymentMethod: string, options?: ChargeOptions) => Promise<PaymentResult>
|
|
120
|
+
createPayment: (payer: Payer, amount: Money, options?: CreatePaymentOptions) => Promise<PaymentResult>
|
|
121
|
+
refund: (paymentId: string, options?: RefundOptions) => Promise<RefundResult>
|
|
122
|
+
checkout: (payer: Payer, request: CheckoutRequest) => Promise<CheckoutSession>
|
|
123
|
+
paymentMethods: (payer: Payer) => Promise<StoredPaymentMethod[]>
|
|
124
|
+
removePaymentMethod: (payer: Payer, paymentMethodId: string) => Promise<void>
|
|
125
|
+
subscribe: (payer: Payer, price: string, options?: { type?: string }) => Promise<SubscriptionSummary>
|
|
126
|
+
cancelSubscription: (subscriptionId: string, options?: { atPeriodEnd?: boolean }) => Promise<SubscriptionSummary>
|
|
127
|
+
subscriptions: (payer: Payer) => Promise<SubscriptionSummary[]>
|
|
128
|
+
verifyWebhook: (request: WebhookRequest) => Promise<PaymentEvent[]>
|
|
129
|
+
acknowledgeWebhook: () => Response
|
|
130
|
+
}
|
|
131
|
+
export type PaymentOperation = | 'customers'
|
|
132
|
+
| 'charge'
|
|
133
|
+
| 'createPayment'
|
|
134
|
+
| 'refund'
|
|
135
|
+
| 'checkout'
|
|
136
|
+
| 'paymentMethods'
|
|
137
|
+
| 'subscriptions'
|
|
138
|
+
| 'webhooks';
|
|
139
|
+
/**
|
|
140
|
+
* Where a payment stands.
|
|
141
|
+
*
|
|
142
|
+
* `requires_action`: the payer has to do something - authenticate, be
|
|
143
|
+
* redirected. `processing`: accepted, outcome not known yet (it arrives by
|
|
144
|
+
* webhook). Only `succeeded`, `failed` and `canceled` are final.
|
|
145
|
+
*/
|
|
146
|
+
export type PaymentStatus = 'succeeded' | 'processing' | 'requires_action' | 'requires_payment_method' | 'failed' | 'canceled';
|
|
147
|
+
/**
|
|
148
|
+
* What the browser needs to finish a payment on the provider's own client SDK.
|
|
149
|
+
* Tagged by provider, because the SDKs differ: Stripe.js confirms a client
|
|
150
|
+
* secret, Adyen's Drop-in mounts a session.
|
|
151
|
+
*/
|
|
152
|
+
export type ClientConfirmation = | { provider: 'stripe', clientSecret: string }
|
|
153
|
+
| { provider: 'adyen', sessionId: string, sessionData: string }
|
|
154
|
+
export type RefundStatus = 'pending' | 'succeeded' | 'failed' | 'canceled';
|
|
155
|
+
/** A line on a checkout: priced here, or a price the provider's catalog holds. */
|
|
156
|
+
export type CheckoutLine = | { name: string, unitAmount: number, quantity: number }
|
|
157
|
+
| { price: string, quantity: number }
|
|
158
|
+
/** `unknown`: a status the provider added after this driver was written; `raw` has it. */
|
|
159
|
+
export type SubscriptionStatus = 'active' | 'trialing' | 'past_due' | 'canceled' | 'incomplete' | 'unpaid' | 'paused' | 'unknown';
|
|
160
|
+
export type PaymentEventType = | 'payment.succeeded'
|
|
161
|
+
| 'payment.failed'
|
|
162
|
+
| 'payment.canceled'
|
|
163
|
+
| 'refund.succeeded'
|
|
164
|
+
| 'refund.failed'
|
|
165
|
+
| 'checkout.completed'
|
|
166
|
+
| 'subscription.created'
|
|
167
|
+
| 'subscription.updated'
|
|
168
|
+
| 'subscription.canceled'
|
|
169
|
+
| 'invoice.paid'
|
|
170
|
+
| 'invoice.payment_failed'
|
|
171
|
+
| 'unknown';
|
|
172
|
+
/** An operation the configured provider cannot do. */
|
|
173
|
+
export declare class PaymentUnsupportedError extends Error {
|
|
174
|
+
readonly driver: string;
|
|
175
|
+
readonly operation: string;
|
|
176
|
+
constructor(driver: string, operation: string, detail?: string);
|
|
177
|
+
}
|
|
178
|
+
/** A provider rejected a request. Carries its status and code. */
|
|
179
|
+
export declare class PaymentProviderError extends Error {
|
|
180
|
+
readonly driver: string;
|
|
181
|
+
readonly status: number;
|
|
182
|
+
readonly code: string | null;
|
|
183
|
+
constructor(driver: string, status: number, message: string, code?: string | null);
|
|
184
|
+
}
|
|
185
|
+
/** A webhook delivery whose signature does not verify. */
|
|
186
|
+
export declare class WebhookSignatureError extends Error {
|
|
187
|
+
constructor(driver: string, detail: string);
|
|
188
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class PaymentUnsupportedError extends Error{driver;operation;constructor(driver,operation,detail){super(`The ${driver} payment driver does not support ${operation}${detail?`: ${detail}`:"."}`);this.name="PaymentUnsupportedError";this.driver=driver;this.operation=operation}}export class PaymentProviderError extends Error{driver;status;code;constructor(driver,status,message,code=null){super(`${driver} rejected the request (${status}${code?` ${code}`:""}): ${message}`);this.name="PaymentProviderError";this.driver=driver;this.status=status;this.code=code}}export class WebhookSignatureError extends Error{constructor(driver,detail){super(`The ${driver} webhook signature does not verify: ${detail}`);this.name="WebhookSignatureError"}}export function headerOf(headers,name){if(typeof headers.get==="function")return headers.get(name);const wanted=name.toLowerCase();for(const[key,value]of Object.entries(headers))if(key.toLowerCase()===wanted)return value??null;return null}export function assertMoney(money,label="amount"){if(!Number.isInteger(money.amount)||money.amount<0)throw TypeError(`${label} must be a whole number of minor units (cents), got ${money.amount}`);if(!/^[a-z]{3}$/i.test(money.currency))throw TypeError(`${label} currency must be an ISO 4217 code, got "${money.currency}"`)}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export type { StripeSdk as Stripe };
|
|
|
6
6
|
// Main Payment facade
|
|
7
7
|
export * from './payment';
|
|
8
8
|
export { default as Payment } from './payment';
|
|
9
|
+
// The provider-neutral driver contract, and the Stripe and Adyen drivers
|
|
10
|
+
// (stacksjs/stacks#665, #450). `paymentDriver()` is the one config.payment.driver
|
|
11
|
+
// selects.
|
|
12
|
+
export * from './driver/index';
|
|
9
13
|
// Billable modules
|
|
10
14
|
export * from './billable/index';
|
|
11
15
|
// Stripe Connect (marketplaces)
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./payment";export{default as Payment}from"./payment";export*from"./billable";export*from"./connect";export*from"./drivers/stripe";export{freshIdempotencyKey,stacksIdempotencyKey}from"./idempotency";
|
|
1
|
+
export*from"./payment";export{default as Payment}from"./payment";export*from"./driver";export*from"./billable";export*from"./connect";export*from"./drivers/stripe";export{freshIdempotencyKey,stacksIdempotencyKey}from"./idempotency";
|
package/dist/payment.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { connect } from './connect/index';
|
|
2
|
-
import { onCharge, onCheckout, onInvoice, onPaymentIntent, onSubscription, processWebhook } from './billable/webhook';
|
|
3
|
-
import { stripe } from './drivers/stripe';
|
|
4
1
|
import type { UserModel } from '@stacksjs/orm';
|
|
5
2
|
import type Stripe from 'stripe';
|
|
6
3
|
/**
|
|
@@ -143,51 +140,5 @@ export declare function billingPortal(customer: string | UserModel, options?: {
|
|
|
143
140
|
// =============================================================================
|
|
144
141
|
// Payment Facade Object
|
|
145
142
|
// =============================================================================
|
|
146
|
-
export declare const Payment: {
|
|
147
|
-
charge: typeof charge;
|
|
148
|
-
createPayment: typeof createPayment;
|
|
149
|
-
refund: typeof refund;
|
|
150
|
-
checkout: typeof checkout;
|
|
151
|
-
subscriptionCheckout: typeof subscriptionCheckout;
|
|
152
|
-
subscribe: typeof subscribe;
|
|
153
|
-
cancelSubscription: typeof cancelSubscription;
|
|
154
|
-
hasActiveSubscription: typeof hasActiveSubscription;
|
|
155
|
-
changeSubscription: typeof changeSubscription;
|
|
156
|
-
getOrCreateCustomer: typeof getOrCreateCustomer;
|
|
157
|
-
updateCustomer: typeof updateCustomer;
|
|
158
|
-
deleteCustomer: typeof deleteCustomer;
|
|
159
|
-
addPaymentMethod: typeof addPaymentMethod;
|
|
160
|
-
setDefaultPaymentMethod: typeof setDefaultPaymentMethod;
|
|
161
|
-
removePaymentMethod: typeof removePaymentMethod;
|
|
162
|
-
createSetupIntent: typeof createSetupIntent;
|
|
163
|
-
getInvoices: typeof getInvoices;
|
|
164
|
-
createInvoice: typeof createInvoice;
|
|
165
|
-
payInvoice: typeof payInvoice;
|
|
166
|
-
createProduct: typeof createProduct;
|
|
167
|
-
getPrice: typeof getPrice;
|
|
168
|
-
listProducts: typeof listProducts;
|
|
169
|
-
billingPortal: typeof billingPortal;
|
|
170
|
-
createCoupon: typeof createCoupon;
|
|
171
|
-
createPromoCode: typeof createPromoCode;
|
|
172
|
-
validatePromoCode: typeof validatePromoCode;
|
|
173
|
-
formatAmount: typeof formatAmount;
|
|
174
|
-
toCents: typeof toCents;
|
|
175
|
-
toDollars: typeof toDollars;
|
|
176
|
-
webhook: unknown;
|
|
177
|
-
onPaymentIntent: typeof onPaymentIntent;
|
|
178
|
-
onSubscription: typeof onSubscription;
|
|
179
|
-
onInvoice: typeof onInvoice;
|
|
180
|
-
onCheckout: typeof onCheckout;
|
|
181
|
-
onCharge: typeof onCharge;
|
|
182
|
-
processWebhook: typeof processWebhook;
|
|
183
|
-
connect: typeof connect;
|
|
184
|
-
stripe: typeof stripe;
|
|
185
|
-
customer: unknown;
|
|
186
|
-
subscription: unknown;
|
|
187
|
-
invoice: unknown;
|
|
188
|
-
paymentMethod: unknown;
|
|
189
|
-
product: unknown;
|
|
190
|
-
price: unknown;
|
|
191
|
-
coupon: unknown
|
|
192
|
-
};
|
|
143
|
+
export declare const Payment: {};
|
|
193
144
|
export default Payment;
|
package/dist/payment.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{stripe}from"./drivers/stripe";import{manageCharge}from"./billable/charge";import{manageCheckout}from"./billable/checkout";import{manageCustomer}from"./billable/customer";import{manageInvoice}from"./billable/invoice";import{managePaymentMethod}from"./billable/payment-method";import{managePrice}from"./billable/price";import{manageBillingPortal}from"./billable/portal";import{manageCoupon,managePriceExtended,manageProduct}from"./billable/product";import{manageSubscription}from"./billable/subscription";import{manageSetupIntent}from"./billable/intent";import{manageWebhook,onCharge,onCheckout,onInvoice,onPaymentIntent,onSubscription,processWebhook}from"./billable/webhook";import{connect}from"./connect";export async function charge(user,amount,paymentMethod,options={}){return manageCharge.charge(user,amount,paymentMethod,options)}export async function createPayment(user,amount,options={}){return manageCharge.createPayment(user,amount,options)}export async function refund(paymentIntentId,amount,options={}){return manageCharge.refund(paymentIntentId,{amount,...options})}export async function checkout(user,lineItems,options={}){return manageCheckout.create(user,{line_items:lineItems,mode:"payment",...options})}export async function subscriptionCheckout(user,priceId,options={}){return manageCheckout.create(user,{line_items:[{price:priceId,quantity:1}],mode:"subscription",...options})}export async function subscribe(user,lookupKey,options={}){return manageSubscription.create(user,"default",lookupKey,options)}export async function cancelSubscription(subscriptionId,immediately=!1){return manageSubscription.cancel(subscriptionId,{prorate:!immediately,invoice_now:immediately})}export async function hasActiveSubscription(user,type="default"){return manageSubscription.isValid(user,type)}export async function changeSubscription(user,newLookupKey,type="default"){return manageSubscription.update(user,type,newLookupKey,{})}export async function getOrCreateCustomer(user,options={}){return manageCustomer.createOrGetStripeUser(user,options)}export async function updateCustomer(user,options){return manageCustomer.updateStripeCustomer(user,options)}export async function deleteCustomer(user){return manageCustomer.deleteStripeUser(user)}export async function addPaymentMethod(user,paymentMethodId){return managePaymentMethod.addPaymentMethod(user,paymentMethodId)}export async function setDefaultPaymentMethod(user,paymentMethodId){return managePaymentMethod.setUserDefaultPayment(user,paymentMethodId)}export async function removePaymentMethod(user,paymentMethodId){return managePaymentMethod.deletePaymentMethod(user,paymentMethodId)}export async function createSetupIntent(user,options={}){return manageSetupIntent.create(user,options)}export async function getInvoices(user){return manageInvoice.list(user)}export async function createInvoice(customerId,options={}){return stripe.invoices.create({customer:customerId,...options})}export async function payInvoice(invoiceId){return stripe.invoices.pay(invoiceId)}export async function createProduct(name,price,options={}){const{currency="usd",interval,description,metadata}=options,priceParams={unit_amount:price,currency};if(interval)priceParams.recurring={interval};return manageProduct.createWithPrice({name,description,metadata},priceParams)}export async function getPrice(lookupKey){return managePrice.retrieveByLookupKey(lookupKey)}export async function listProducts(options={}){return manageProduct.list(options)}export async function createCoupon(options){const params={duration:options.duration||"once"};if(options.percentOff)params.percent_off=options.percentOff;else if(options.amountOff){params.amount_off=options.amountOff;params.currency=options.currency||"usd"}if(options.name)params.name=options.name;if(options.durationInMonths)params.duration_in_months=options.durationInMonths;if(options.maxRedemptions)params.max_redemptions=options.maxRedemptions;return manageCoupon.create(params)}export async function createPromoCode(couponId,code,options={}){return manageCoupon.createPromotionCode({promotion:{type:"coupon",coupon:couponId},code,...options})}export async function validatePromoCode(code){return manageCoupon.retrievePromotionCode(code)}export function formatAmount(amount,currency){const cfg=globalThis.config||{},ccy=(currency||cfg.payment?.currency||cfg.billing?.currency||process.env.STRIPE_CURRENCY||"usd").toLowerCase(),locale=cfg.app?.locale||"en-US";return new Intl.NumberFormat(locale,{style:"currency",currency:ccy.toUpperCase()}).format(amount/100)}export function toCents(dollars){return Math.round(dollars*100)}export function toDollars(cents){return cents/100}export async function billingPortal(customer,options={}){const customerId=typeof customer==="string"?customer:customer.stripe_id;if(!customerId)throw Error("billingPortal needs a Stripe customer: this user has no stripe_id yet.");return manageBillingPortal.createSession(customerId,options)}export const Payment={charge,createPayment,refund,checkout,subscriptionCheckout,subscribe,cancelSubscription,hasActiveSubscription,changeSubscription,getOrCreateCustomer,updateCustomer,deleteCustomer,addPaymentMethod,setDefaultPaymentMethod,removePaymentMethod,createSetupIntent,getInvoices,createInvoice,payInvoice,createProduct,getPrice,listProducts,billingPortal,createCoupon,createPromoCode,validatePromoCode,formatAmount,toCents,toDollars,webhook:manageWebhook,onPaymentIntent,onSubscription,onInvoice,onCheckout,onCharge,processWebhook,connect,stripe,customer:manageCustomer,subscription:manageSubscription,invoice:manageInvoice,paymentMethod:managePaymentMethod,product:manageProduct,price:managePriceExtended,coupon:manageCoupon};export default Payment;
|
|
1
|
+
import{stripe}from"./drivers/stripe";import{manageCharge}from"./billable/charge";import{manageCheckout}from"./billable/checkout";import{manageCustomer}from"./billable/customer";import{manageInvoice}from"./billable/invoice";import{managePaymentMethod}from"./billable/payment-method";import{managePrice}from"./billable/price";import{manageBillingPortal}from"./billable/portal";import{manageCoupon,managePriceExtended,manageProduct}from"./billable/product";import{manageSubscription}from"./billable/subscription";import{manageSetupIntent}from"./billable/intent";import{manageWebhook,onCharge,onCheckout,onInvoice,onPaymentIntent,onSubscription,processWebhook}from"./billable/webhook";import{connect}from"./connect";import{paymentDriver}from"./driver";export async function charge(user,amount,paymentMethod,options={}){return manageCharge.charge(user,amount,paymentMethod,options)}export async function createPayment(user,amount,options={}){return manageCharge.createPayment(user,amount,options)}export async function refund(paymentIntentId,amount,options={}){return manageCharge.refund(paymentIntentId,{amount,...options})}export async function checkout(user,lineItems,options={}){return manageCheckout.create(user,{line_items:lineItems,mode:"payment",...options})}export async function subscriptionCheckout(user,priceId,options={}){return manageCheckout.create(user,{line_items:[{price:priceId,quantity:1}],mode:"subscription",...options})}export async function subscribe(user,lookupKey,options={}){return manageSubscription.create(user,"default",lookupKey,options)}export async function cancelSubscription(subscriptionId,immediately=!1){return manageSubscription.cancel(subscriptionId,{prorate:!immediately,invoice_now:immediately})}export async function hasActiveSubscription(user,type="default"){return manageSubscription.isValid(user,type)}export async function changeSubscription(user,newLookupKey,type="default"){return manageSubscription.update(user,type,newLookupKey,{})}export async function getOrCreateCustomer(user,options={}){return manageCustomer.createOrGetStripeUser(user,options)}export async function updateCustomer(user,options){return manageCustomer.updateStripeCustomer(user,options)}export async function deleteCustomer(user){return manageCustomer.deleteStripeUser(user)}export async function addPaymentMethod(user,paymentMethodId){return managePaymentMethod.addPaymentMethod(user,paymentMethodId)}export async function setDefaultPaymentMethod(user,paymentMethodId){return managePaymentMethod.setUserDefaultPayment(user,paymentMethodId)}export async function removePaymentMethod(user,paymentMethodId){return managePaymentMethod.deletePaymentMethod(user,paymentMethodId)}export async function createSetupIntent(user,options={}){return manageSetupIntent.create(user,options)}export async function getInvoices(user){return manageInvoice.list(user)}export async function createInvoice(customerId,options={}){return stripe.invoices.create({customer:customerId,...options})}export async function payInvoice(invoiceId){return stripe.invoices.pay(invoiceId)}export async function createProduct(name,price,options={}){const{currency="usd",interval,description,metadata}=options,priceParams={unit_amount:price,currency};if(interval)priceParams.recurring={interval};return manageProduct.createWithPrice({name,description,metadata},priceParams)}export async function getPrice(lookupKey){return managePrice.retrieveByLookupKey(lookupKey)}export async function listProducts(options={}){return manageProduct.list(options)}export async function createCoupon(options){const params={duration:options.duration||"once"};if(options.percentOff)params.percent_off=options.percentOff;else if(options.amountOff){params.amount_off=options.amountOff;params.currency=options.currency||"usd"}if(options.name)params.name=options.name;if(options.durationInMonths)params.duration_in_months=options.durationInMonths;if(options.maxRedemptions)params.max_redemptions=options.maxRedemptions;return manageCoupon.create(params)}export async function createPromoCode(couponId,code,options={}){return manageCoupon.createPromotionCode({promotion:{type:"coupon",coupon:couponId},code,...options})}export async function validatePromoCode(code){return manageCoupon.retrievePromotionCode(code)}export function formatAmount(amount,currency){const cfg=globalThis.config||{},ccy=(currency||cfg.payment?.currency||cfg.billing?.currency||process.env.STRIPE_CURRENCY||"usd").toLowerCase(),locale=cfg.app?.locale||"en-US";return new Intl.NumberFormat(locale,{style:"currency",currency:ccy.toUpperCase()}).format(amount/100)}export function toCents(dollars){return Math.round(dollars*100)}export function toDollars(cents){return cents/100}export async function billingPortal(customer,options={}){const customerId=typeof customer==="string"?customer:customer.stripe_id;if(!customerId)throw Error("billingPortal needs a Stripe customer: this user has no stripe_id yet.");return manageBillingPortal.createSession(customerId,options)}export const Payment={driver:paymentDriver,charge,createPayment,refund,checkout,subscriptionCheckout,subscribe,cancelSubscription,hasActiveSubscription,changeSubscription,getOrCreateCustomer,updateCustomer,deleteCustomer,addPaymentMethod,setDefaultPaymentMethod,removePaymentMethod,createSetupIntent,getInvoices,createInvoice,payInvoice,createProduct,getPrice,listProducts,billingPortal,createCoupon,createPromoCode,validatePromoCode,formatAmount,toCents,toDollars,webhook:manageWebhook,onPaymentIntent,onSubscription,onInvoice,onCheckout,onCharge,processWebhook,connect,stripe,customer:manageCustomer,subscription:manageSubscription,invoice:manageInvoice,paymentMethod:managePaymentMethod,product:manageProduct,price:managePriceExtended,coupon:manageCoupon};export default Payment;
|
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.
|
|
5
|
+
"version": "0.75.75",
|
|
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.
|
|
64
|
-
"@stacksjs/database": "0.75.
|
|
65
|
-
"@stacksjs/error-handling": "0.75.
|
|
66
|
-
"@stacksjs/logging": "0.75.
|
|
67
|
-
"@stacksjs/orm": "0.75.
|
|
68
|
-
"@stacksjs/types": "0.75.
|
|
63
|
+
"@stacksjs/config": "0.75.75",
|
|
64
|
+
"@stacksjs/database": "0.75.75",
|
|
65
|
+
"@stacksjs/error-handling": "0.75.75",
|
|
66
|
+
"@stacksjs/logging": "0.75.75",
|
|
67
|
+
"@stacksjs/orm": "0.75.75",
|
|
68
|
+
"@stacksjs/types": "0.75.75"
|
|
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.
|
|
83
|
+
"@stacksjs/utils": "0.75.75",
|
|
84
84
|
"@stripe/stripe-js": "^10.0.0",
|
|
85
85
|
"better-dx": "^0.2.24",
|
|
86
86
|
"stripe": "23.0.0"
|