Skip to content

Commit

Permalink
feat(payment-stripe): support stripe invoice
Browse files Browse the repository at this point in the history
  • Loading branch information
silenaker committed Nov 19, 2024
1 parent 21997c7 commit 142fdd8
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 70 deletions.
244 changes: 174 additions & 70 deletions packages/modules/providers/payment-stripe/src/core/stripe-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,87 +73,177 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
async initiatePayment(
data: CreatePaymentProviderSession
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
const intentRequestData = this.getPaymentIntentOptions()
const { currency_code, amount, token, context } = data
const {
billing_address,
shipping_address,
email,
customer,
payment_description,
invoice,
...metadata
} = data.context
const { currency_code, amount, token } = data

const description = (payment_description ??
this.options_?.paymentDescription) as string

const intentRequest: Stripe.PaymentIntentCreateParams = {
description,
amount: getSmallestUnit(amount, currency_code),
currency: currency_code,
payment_method: token,
confirm: !!token,
shipping:
shipping_address || customer
? {
address: {
city: shipping_address?.city ?? undefined,
country: shipping_address?.country_code ?? undefined,
line1: shipping_address?.address_1 ?? undefined,
line2: shipping_address?.address_2 ?? undefined,
postal_code: shipping_address?.postal_code ?? undefined,
state: shipping_address?.province ?? undefined,
},
name: `${customer?.first_name ?? ""} ${
customer?.last_name ?? ""
}`.trim(),
phone: shipping_address?.phone ?? undefined,
} = context

const customerName = `${customer?.first_name ?? ""} ${
customer?.last_name ?? ""
}`.trim()
const customerPhone = customer?.phone ?? ""
const intentOptions = this.getPaymentIntentOptions()

let stripeCustomerId: string | undefined
let shipping:
| {
address: Stripe.AddressParam
name: string
phone?: string
}
| undefined

if (customer?.metadata?.stripe_id) {
stripeCustomerId = customer.metadata.stripe_id as string
} else if (this.options_.createCustomer || invoice) {
try {
if (customer?.email) {
const list = await this.stripe_.customers.list({
email: customer.email,
limit: 100,
})
for (let i = 0; i < list.data.length; i++) {
const stripeCustomer = list.data[i]
if (
customer.id &&
customer.id === stripeCustomer.metadata.medusa_id
) {
stripeCustomerId = stripeCustomer.id
if (
(stripeCustomer.name ?? "") !== customerName ||
(stripeCustomer.phone ?? "") !== customerPhone
) {
await this.stripe_.customers.update(stripeCustomerId, {
name: customerName,
phone: customerPhone,
})
}
break
}
: undefined,
metadata: metadata as Stripe.MetadataParam,
capture_method: this.options_.capture ? "automatic" : "manual",
expand: ["latest_charge", "payment_method"],
...intentRequestData,
}
}

if (!stripeCustomerId) {
const stripeCustomer = await this.stripe_.customers.create({
email: customer?.email,
name: customerName,
phone: customerPhone,
metadata: customer?.id && { medusa_id: customer.id },
})
stripeCustomerId = stripeCustomer.id
}
} catch (e) {
return this.buildError(
"An error occurred in initiatePayment when creating a Stripe customer",
e
)
}
}

const automaticPaymentMethods = this.options_?.automaticPaymentMethods
if (automaticPaymentMethods) {
intentRequest.automatic_payment_methods =
typeof automaticPaymentMethods === "boolean"
? { enabled: true }
: automaticPaymentMethods
if (shipping_address || customer) {
shipping = {
address: {
city: shipping_address?.city ?? undefined,
country: shipping_address?.country_code ?? undefined,
line1: shipping_address?.address_1 ?? undefined,
line2: shipping_address?.address_2 ?? undefined,
postal_code: shipping_address?.postal_code ?? undefined,
state: shipping_address?.province ?? undefined,
},
name: customerName,
phone: shipping_address?.phone || customerPhone,
}
}

if (customer?.metadata?.stripe_id) {
intentRequest.customer = customer.metadata.stripe_id as string
} else if (this.options_.createCustomer) {
let stripeCustomer
if (invoice) {
try {
stripeCustomer = await this.stripe_.customers.create({
email,
let stripeInvoice = await this.stripe_.invoices.create({
...(typeof invoice === "boolean" ? {} : invoice),
auto_advance: false,
currency: currency_code,
customer: stripeCustomerId,
shipping_details: shipping,
payment_settings: {
payment_method_types:
intentOptions.payment_method_types as Stripe.InvoiceCreateParams.PaymentSettings.PaymentMethodType[],
},
})
await this.stripe_.invoiceItems.create({
customer: stripeCustomerId!,
invoice: stripeInvoice.id,
currency: currency_code,
amount: getSmallestUnit(amount, currency_code),
description: payment_description ?? this.options_.paymentDescription,
})
stripeInvoice = await this.stripe_.invoices.finalizeInvoice(
stripeInvoice.id,
{ auto_advance: false }
)
const intent = await this.stripe_.paymentIntents.update(
stripeInvoice.payment_intent as string,
{
shipping,
metadata: metadata as Stripe.MetadataParam,
description:
payment_description ?? this.options_.paymentDescription,
setup_future_usage: intentOptions.setup_future_usage,
expand: ["latest_charge", "payment_method", "invoice"],
}
)
return {
...(await this.buildResponse(intent)),
data: intent as unknown as Record<string, unknown>,
context: intent.metadata,
}
} catch (e) {
return this.buildError(
"An error occurred in initiatePayment when creating a Stripe customer",
"An error occurred in initiatePayment during the creation of the stripe invoice",
e
)
}
} else {
const createParams: Stripe.PaymentIntentCreateParams = {
...intentOptions,
amount: getSmallestUnit(amount, currency_code),
currency: currency_code,
payment_method: token,
confirm: !!token,
customer: stripeCustomerId,
shipping,
metadata: metadata as Stripe.MetadataParam,
description: payment_description ?? this.options_.paymentDescription,
expand: ["latest_charge", "payment_method"],
capture_method:
typeof this.options_.capture === "boolean"
? this.options_.capture
? "automatic"
: "manual"
: intentOptions.capture_method ?? "automatic",
automatic_payment_methods:
this.options_.automaticPaymentMethods === true
? { enabled: true }
: { enabled: false, ...this.options_.automaticPaymentMethods },
}

intentRequest.customer = stripeCustomer.id
}

try {
const intent = await this.stripe_.paymentIntents.create(intentRequest)
return {
...(await this.buildResponse(intent)),
data: intent as unknown as Record<string, unknown>,
context: intent.metadata,
try {
const intent = await this.stripe_.paymentIntents.create(createParams)
return {
...(await this.buildResponse(intent)),
data: intent as unknown as Record<string, unknown>,
context: intent.metadata,
}
} catch (e) {
return this.buildError(
"An error occurred in initiatePayment during the creation of the stripe payment intent",
e
)
}
} catch (e) {
return this.buildError(
"An error occurred in initiatePayment during the creation of the stripe payment intent",
e
)
}
}

Expand All @@ -164,7 +254,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
try {
const intent = await this.stripe_.paymentIntents.confirm(id, {
payment_method: data.token,
expand: ["latest_charge", "payment_method"],
expand: ["latest_charge", "payment_method", "invoice"],
})
return {
...(await this.buildResponse(intent)),
Expand All @@ -182,11 +272,24 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
async cancelPayment(
paymentSessionData: PaymentProviderSessionResponse["data"]
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
const { id } = paymentSessionData as unknown as Stripe.PaymentIntent
const { id, invoice } =
paymentSessionData as unknown as Stripe.PaymentIntent
try {
const intent = await this.stripe_.paymentIntents.cancel(id, {
expand: ["latest_charge", "payment_method"],
})
let intent: Stripe.PaymentIntent

if (invoice) {
await this.stripe_.invoices.voidInvoice(
typeof invoice === "string" ? invoice : invoice.id
)
intent = await this.stripe_.paymentIntents.retrieve(id, {
expand: ["latest_charge", "payment_method", "invoice"],
})
} else {
intent = await this.stripe_.paymentIntents.cancel(id, {
expand: ["latest_charge", "payment_method"],
})
}

return {
...(await this.buildResponse(intent)),
data: intent as unknown as Record<string, unknown>,
Expand Down Expand Up @@ -262,7 +365,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
: undefined,
})
const intent = await this.stripe_.paymentIntents.retrieve(id, {
expand: ["latest_charge", "payment_method"],
expand: ["latest_charge", "payment_method", "invoice"],
})
return {
...(await this.buildResponse(intent)),
Expand All @@ -280,7 +383,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
const { id } = paymentSessionData as unknown as Stripe.PaymentIntent
try {
const intent = await this.stripe_.paymentIntents.retrieve(id, {
expand: ["latest_charge", "payment_method"],
expand: ["latest_charge", "payment_method", "invoice"],
})
return {
...(await this.buildResponse(intent)),
Expand All @@ -307,6 +410,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
email,
customer,
payment_description,
invoice,
...metadata
} = context
if (payment_description) {
Expand Down Expand Up @@ -351,7 +455,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
}
const intent = await this.stripe_.paymentIntents.update(id, {
...updateParams,
expand: ["latest_charge", "payment_method"],
expand: ["latest_charge", "payment_method", "invoice"],
})
return {
...(await this.buildResponse(intent)),
Expand All @@ -372,7 +476,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
if (event.data.object.object === "payment_intent") {
intent = await this.stripe_.paymentIntents.retrieve(
event.data.object.id,
{ expand: ["latest_charge", "payment_method"] }
{ expand: ["latest_charge", "payment_method", "invoice"] }
)
} else if (event.data.object.object === "charge") {
if (!event.data.object.payment_intent) {
Expand All @@ -382,7 +486,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
}
intent = await this.stripe_.paymentIntents.retrieve(
event.data.object.payment_intent as string,
{ expand: ["latest_charge", "payment_method"] }
{ expand: ["latest_charge", "payment_method", "invoice"] }
)
} else if (event.data.object.object === "invoice") {
// TODO
Expand Down
2 changes: 2 additions & 0 deletions packages/modules/providers/payment-stripe/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
StripeIdealService,
StripeProviderService,
StripePrzelewy24Service,
StripeInvoiceService,
} from "./services"

const services = [
Expand All @@ -15,6 +16,7 @@ const services = [
StripeIdealService,
StripeProviderService,
StripePrzelewy24Service,
StripeInvoiceService,
]

const providerExport: ModuleProviderExports = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as StripeGiropayService } from "./stripe-giropay"
export { default as StripeIdealService } from "./stripe-ideal"
export { default as StripeProviderService } from "./stripe-provider"
export { default as StripePrzelewy24Service } from "./stripe-przelewy24"
export { default as StripeInvoiceService } from "./stripe-invoice"
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import StripeBase from "../core/stripe-base"
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"

class InvoiceProviderService extends StripeBase {
static PROVIDER = PaymentProviderKeys.INVOICE

constructor(_, options) {
super(_, options)
}

get paymentIntentOptions(): PaymentIntentOptions {
return {
payment_method_types: ["card"],
}
}
}

export default InvoiceProviderService
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ export const PaymentProviderKeys = {
GIROPAY: "stripe-giropay",
IDEAL: "stripe-ideal",
PRZELEWY_24: "stripe-przelewy24",
INVOICE: "stripe-invoice",
}

0 comments on commit 142fdd8

Please sign in to comment.