@medusajs/payment-stripe
Version:
Stripe payment provider for Medusa
388 lines • 16.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const stripe_1 = __importDefault(require("stripe"));
const utils_1 = require("@medusajs/framework/utils");
const types_1 = require("../types");
const get_smallest_unit_1 = require("../utils/get-smallest-unit");
class StripeBase extends utils_1.AbstractPaymentProvider {
static validateOptions(options) {
if (!(0, utils_1.isDefined)(options.apiKey)) {
throw new Error("Required option `apiKey` is missing in Stripe plugin");
}
}
constructor(cradle, options) {
// @ts-ignore
super(...arguments);
this.container_ = cradle;
this.options_ = options;
this.stripe_ = new stripe_1.default(options.apiKey);
}
get options() {
return this.options_;
}
normalizePaymentIntentParameters(extra) {
const res = {};
res.description = (extra?.payment_description ??
this.options_?.paymentDescription);
res.capture_method =
extra?.capture_method ??
this.paymentIntentOptions.capture_method ??
(this.options_.capture ? "automatic" : "manual");
res.setup_future_usage =
extra?.setup_future_usage ??
this.paymentIntentOptions.setup_future_usage;
res.payment_method_types = this.paymentIntentOptions
.payment_method_types;
res.automatic_payment_methods =
extra?.automatic_payment_methods ??
(this.options_?.automaticPaymentMethods ? { enabled: true } : undefined);
res.off_session = extra?.off_session;
res.confirm = extra?.confirm;
res.payment_method = extra?.payment_method;
res.return_url = extra?.return_url;
return res;
}
async getPaymentStatus({ data, }) {
const id = data?.id;
if (!id) {
throw this.buildError("No payment intent ID provided while getting payment status", new Error("No payment intent ID provided"));
}
const paymentIntent = await this.stripe_.paymentIntents.retrieve(id);
const dataResponse = paymentIntent;
switch (paymentIntent.status) {
case "requires_payment_method":
if (paymentIntent.last_payment_error) {
return { status: utils_1.PaymentSessionStatus.ERROR, data: dataResponse };
}
return { status: utils_1.PaymentSessionStatus.PENDING, data: dataResponse };
case "requires_confirmation":
case "processing":
return { status: utils_1.PaymentSessionStatus.PENDING, data: dataResponse };
case "requires_action":
return {
status: utils_1.PaymentSessionStatus.REQUIRES_MORE,
data: dataResponse,
};
case "canceled":
return { status: utils_1.PaymentSessionStatus.CANCELED, data: dataResponse };
case "requires_capture":
return { status: utils_1.PaymentSessionStatus.AUTHORIZED, data: dataResponse };
case "succeeded":
return { status: utils_1.PaymentSessionStatus.CAPTURED, data: dataResponse };
default:
return { status: utils_1.PaymentSessionStatus.PENDING, data: dataResponse };
}
}
async initiatePayment({ currency_code, amount, data, context, }) {
const additionalParameters = this.normalizePaymentIntentParameters(data);
const intentRequest = {
amount: (0, get_smallest_unit_1.getSmallestUnit)(amount, currency_code),
currency: currency_code,
metadata: { session_id: data?.session_id },
...additionalParameters,
};
intentRequest.customer = context?.account_holder?.data?.id;
let sessionData;
try {
sessionData = (await this.stripe_.paymentIntents.create(intentRequest, {
idempotencyKey: context?.idempotency_key,
}));
}
catch (e) {
throw this.buildError("An error occurred in InitiatePayment during the creation of the stripe payment intent", e);
}
return {
id: sessionData.id,
data: sessionData,
};
}
async authorizePayment(input) {
const statusResponse = await this.getPaymentStatus(input);
return statusResponse;
}
async cancelPayment({ data, context, }) {
try {
const id = data?.id;
if (!id) {
return { data: data };
}
const res = await this.stripe_.paymentIntents.cancel(id, {
idempotencyKey: context?.idempotency_key,
});
return { data: res };
}
catch (error) {
if (error.payment_intent?.status === types_1.ErrorIntentStatus.CANCELED) {
return { data: error.payment_intent };
}
throw this.buildError("An error occurred in cancelPayment", error);
}
}
async capturePayment({ data, context, }) {
const id = data?.id;
try {
const intent = await this.stripe_.paymentIntents.capture(id, {
idempotencyKey: context?.idempotency_key,
});
return { data: intent };
}
catch (error) {
if (error.code === types_1.ErrorCodes.PAYMENT_INTENT_UNEXPECTED_STATE) {
if (error.payment_intent?.status === types_1.ErrorIntentStatus.SUCCEEDED) {
return { data: error.payment_intent };
}
}
throw this.buildError("An error occurred in capturePayment", error);
}
}
async deletePayment(input) {
return await this.cancelPayment(input);
}
async refundPayment({ amount, data, context, }) {
const id = data?.id;
if (!id) {
throw this.buildError("No payment intent ID provided while refunding payment", new Error("No payment intent ID provided"));
}
try {
const currencyCode = data?.currency;
await this.stripe_.refunds.create({
amount: (0, get_smallest_unit_1.getSmallestUnit)(amount, currencyCode),
payment_intent: id,
}, {
idempotencyKey: context?.idempotency_key,
});
}
catch (e) {
throw this.buildError("An error occurred in refundPayment", e);
}
return { data };
}
async retrievePayment({ data, }) {
try {
const id = data?.id;
const intent = await this.stripe_.paymentIntents.retrieve(id);
intent.amount = (0, get_smallest_unit_1.getAmountFromSmallestUnit)(intent.amount, intent.currency);
return { data: intent };
}
catch (e) {
throw this.buildError("An error occurred in retrievePayment", e);
}
}
async updatePayment({ data, currency_code, amount, context, }) {
const amountNumeric = (0, get_smallest_unit_1.getSmallestUnit)(amount, currency_code);
if ((0, utils_1.isPresent)(amount) && data?.amount === amountNumeric) {
return { data };
}
try {
const id = data?.id;
const sessionData = (await this.stripe_.paymentIntents.update(id, {
amount: amountNumeric,
}, {
idempotencyKey: context?.idempotency_key,
}));
return { data: sessionData };
}
catch (e) {
throw this.buildError("An error occurred in updatePayment", e);
}
}
async createAccountHolder({ context, }) {
const { account_holder, customer, idempotency_key } = context;
if (account_holder?.data?.id) {
return { id: account_holder.data.id };
}
if (!customer) {
throw this.buildError("No customer in context", new Error("No customer provided while creating account holder"));
}
const shipping = customer.billing_address
? {
address: {
city: customer.billing_address.city,
country: customer.billing_address.country_code,
line1: customer.billing_address.address_1,
line2: customer.billing_address.address_2,
postal_code: customer.billing_address.postal_code,
state: customer.billing_address.province,
},
}
: undefined;
try {
const stripeCustomer = await this.stripe_.customers.create({
email: customer.email,
name: customer.company_name ||
`${customer.first_name ?? ""} ${customer.last_name ?? ""}`.trim() ||
undefined,
phone: customer.phone,
...shipping,
}, {
idempotencyKey: idempotency_key,
});
return {
id: stripeCustomer.id,
data: stripeCustomer,
};
}
catch (e) {
throw this.buildError("An error occurred in createAccountHolder when creating a Stripe customer", e);
}
}
async updateAccountHolder({ context, }) {
const { account_holder, customer, idempotency_key } = context;
if (!account_holder?.data?.id) {
throw this.buildError("No account holder in context", new Error("No account holder provided while updating account holder"));
}
// If no customer context was provided, we simply don't update anything within the provider
if (!customer) {
return {};
}
const accountHolderId = account_holder.data.id;
const shipping = customer.billing_address
? {
address: {
city: customer.billing_address.city,
country: customer.billing_address.country_code,
line1: customer.billing_address.address_1,
line2: customer.billing_address.address_2,
postal_code: customer.billing_address.postal_code,
state: customer.billing_address.province,
},
}
: undefined;
try {
const stripeCustomer = await this.stripe_.customers.update(accountHolderId, {
email: customer.email,
name: customer.company_name ||
`${customer.first_name ?? ""} ${customer.last_name ?? ""}`.trim() ||
undefined,
phone: customer.phone,
...shipping,
}, {
idempotencyKey: idempotency_key,
});
return {
data: stripeCustomer,
};
}
catch (e) {
throw this.buildError("An error occurred in updateAccountHolder when updating a Stripe customer", e);
}
}
async deleteAccountHolder({ context, }) {
const { account_holder } = context;
const accountHolderId = account_holder?.data?.id;
if (!accountHolderId) {
throw this.buildError("No account holder in context", new Error("No account holder provided while deleting account holder"));
}
try {
await this.stripe_.customers.del(accountHolderId);
return {};
}
catch (e) {
throw this.buildError("An error occurred in deleteAccountHolder", e);
}
}
async listPaymentMethods({ context, }) {
const accountHolderId = context?.account_holder?.data?.id;
if (!accountHolderId) {
return [];
}
const paymentMethods = await this.stripe_.customers.listPaymentMethods(accountHolderId,
// In order to keep the interface simple, we just list the maximum payment methods, which should be enough in almost all cases.
// We can always extend the interface to allow additional filtering, if necessary.
{ limit: 100 });
return paymentMethods.data.map((method) => ({
id: method.id,
data: method,
}));
}
async savePaymentMethod({ context, data, }) {
const accountHolderId = context?.account_holder?.data?.id;
if (!accountHolderId) {
throw this.buildError("Account holder not set while saving a payment method", new Error("Missing account holder"));
}
const resp = await this.stripe_.setupIntents.create({
customer: accountHolderId,
...data,
}, {
idempotencyKey: context?.idempotency_key,
});
return { id: resp.id, data: resp };
}
async getWebhookActionAndData(webhookData) {
const event = this.constructWebhookEvent(webhookData);
const intent = event.data.object;
const { currency } = intent;
switch (event.type) {
case "payment_intent.created":
case "payment_intent.processing":
return {
action: utils_1.PaymentActions.PENDING,
data: {
session_id: intent.metadata.session_id,
amount: (0, get_smallest_unit_1.getAmountFromSmallestUnit)(intent.amount, currency),
},
};
case "payment_intent.canceled":
return {
action: utils_1.PaymentActions.CANCELED,
data: {
session_id: intent.metadata.session_id,
amount: (0, get_smallest_unit_1.getAmountFromSmallestUnit)(intent.amount, currency),
},
};
case "payment_intent.payment_failed":
return {
action: utils_1.PaymentActions.FAILED,
data: {
session_id: intent.metadata.session_id,
amount: (0, get_smallest_unit_1.getAmountFromSmallestUnit)(intent.amount, currency),
},
};
case "payment_intent.requires_action":
return {
action: utils_1.PaymentActions.REQUIRES_MORE,
data: {
session_id: intent.metadata.session_id,
amount: (0, get_smallest_unit_1.getAmountFromSmallestUnit)(intent.amount, currency),
},
};
case "payment_intent.amount_capturable_updated":
return {
action: utils_1.PaymentActions.AUTHORIZED,
data: {
session_id: intent.metadata.session_id,
amount: (0, get_smallest_unit_1.getAmountFromSmallestUnit)(intent.amount_capturable, currency),
},
};
case "payment_intent.succeeded":
return {
action: utils_1.PaymentActions.SUCCESSFUL,
data: {
session_id: intent.metadata.session_id,
amount: (0, get_smallest_unit_1.getAmountFromSmallestUnit)(intent.amount_received, currency),
},
};
default:
return { action: utils_1.PaymentActions.NOT_SUPPORTED };
}
}
/**
* Constructs Stripe Webhook event
* @param {object} data - the data of the webhook request: req.body
* ensures integrity of the webhook event
* @return {object} Stripe Webhook event
*/
constructWebhookEvent(data) {
const signature = data.headers["stripe-signature"];
return this.stripe_.webhooks.constructEvent(data.rawData, signature, this.options_.webhookSecret);
}
buildError(message, error) {
const errorDetails = "raw" in error ? error.raw : error;
return new Error(`${message}: ${error.message}. ${"detail" in errorDetails ? errorDetails.detail : ""}`.trim());
}
}
exports.default = StripeBase;
//# sourceMappingURL=stripe-base.js.map