@mirahi/vendure-adyen-dropin-plugin
Version:
A Vendure plugin to integrate the Adyen payment provider to your server. This plugin only handles the flow for a drop-in integration on your storefront.
281 lines • 16.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AdyenService = void 0;
const tslib_1 = require("tslib");
const common_1 = require("@nestjs/common");
const core_1 = require("@vendure/core");
const api_library_1 = require("@adyen/api-library");
const constant_1 = require("./constant");
const loggerCtx = "AdyenService";
let AdyenService = class AdyenService {
constructor(paymentMethodService, activeOrderService, orderService, channelService, entityHydrator,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
options) {
this.paymentMethodService = paymentMethodService;
this.activeOrderService = activeOrderService;
this.orderService = orderService;
this.channelService = channelService;
this.entityHydrator = entityHydrator;
this.options = options;
core_1.Logger.verbose("Service instantiated", loggerCtx);
}
createPaymentIntent(ctx) {
var _a, _b, _c, _d, _e, _f;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const paymentMethodCode = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.paymentMethodCode) !== null && _b !== void 0 ? _b : "payment-adyen";
const [order, paymentMethod] = yield Promise.all([
this.activeOrderService.getActiveOrder(ctx, undefined),
this.getPaymentMethod(ctx, paymentMethodCode),
]);
if (!paymentMethod) {
core_1.Logger.debug(`No paymentMethod found!`, loggerCtx);
return { message: `No paymentMethod found!` };
}
/* Getting arguments from payment method (set in Admin UI) and options (set in `vendure-config.ts`) */
const apiKey = this.getPaymentMethodArg(paymentMethod, "apiKey");
const redirectUrl = this.trimEnd(this.getPaymentMethodArg(paymentMethod, "redirectUrl"), "/");
const environment = (_d = (_c = this.options) === null || _c === void 0 ? void 0 : _c.environment) !== null && _d !== void 0 ? _d : "TEST";
// #region Error handling & Logging
if (!order || !order.code || !order.active) {
core_1.Logger.debug("No active order for this session!", loggerCtx);
return { message: "No active order for this session!" };
}
if (!order.total || order.total <= 0) {
core_1.Logger.debug("The total for the order caused an error!", loggerCtx);
return { message: "The total for the order caused an error!" };
}
if (!apiKey || !redirectUrl) {
core_1.Logger.warn(`CreatePaymentIntent failed, because no apiKey or redirect is configured for ${paymentMethod.code}`, loggerCtx);
return {
message: `Paymentmethod ${paymentMethod.code} has no apiKey or redirectUrl configured`,
};
}
core_1.Logger.info(`Payment intent is valid for order ${order.code} (${order.total / 100} ${order.currencyCode}). Now creating session...`, loggerCtx);
// #endregion
/* Storing this to create a payment later */
const adyenPluginPaymentMethodCode = paymentMethodCode;
yield this.orderService.updateCustomFields(ctx, order.id, {
adyenPluginPaymentMethodCode,
});
/* Adding some relevant information from context to the order object */
yield this.entityHydrator.hydrate(ctx, order, {
relations: ["customer", "surcharges", "lines.productVariant", "shippingLines.shippingMethod"],
});
if (!order.customer) {
core_1.Logger.debug("The order doesn't have a customer!", loggerCtx);
return { message: "The order doesn't have a customer!" };
}
const { firstName, lastName, emailAddress, phoneNumber } = order.customer;
if (!firstName || !lastName || !emailAddress) {
core_1.Logger.debug(`Some required customer data is missing. firstName: ${firstName}, lastName: ${lastName}, email: ${emailAddress}`, loggerCtx);
return {
message: `Some required customer data is missing. firstName: ${firstName}, lastName: ${lastName}, email: ${emailAddress}`,
};
}
/** !! Minimum length: 3 characters !! Your reference to uniquely identify this shopper, for example user ID or account ID. */
const shopperReference = ((_f = (_e = ctx.session) === null || _e === void 0 ? void 0 : _e.user) === null || _f === void 0 ? void 0 : _f.id)
? `vendure${String(ctx.session.user.id)}`
: undefined;
/* Creating Adyen client */
const client = new api_library_1.Client({ apiKey, environment });
const checkout = new api_library_1.CheckoutAPI(client);
/* Getting session */
const checkoutSession = yield checkout
.sessions({
// The channel token on Vendure needs to match the merchantAccount name on Adyen (or the other way around)
merchantAccount: ctx.channel.token,
amount: { currency: order.currencyCode, value: order.total },
reference: order.code /* The value you pass here will be later called `merchantReference`. This must be unique. */,
returnUrl: `${redirectUrl}?orderCode=${order.code}`,
lineItems: this.toAdyenOrderLines(order === null || order === void 0 ? void 0 : order.lines),
shopperEmail: emailAddress,
shopperName: { firstName, lastName },
telephoneNumber: phoneNumber,
billingAddress: this.toAdyenBillingAddress(order.billingAddress),
shopperReference,
/* This can't be `true` if there's no `shopperReference` */
storePaymentMethod: !!shopperReference,
// store: "", /* (Opt.) The ecommerce or point-of-sale store that is processing the payment. */
// countryCode: order.currencyCode /* The two-character ISO-3166-1 alpha-2 country code. You might want to use a third-party lib to parse this one (https://www.npmjs.com/package/iso-3166-1-alpha-2) */,
// allowedPaymentMethods: ["bcmc","visa"], /* You can restrict payment methods here. */
// dateOfBirth: "2000-12-12", /* (Opt.) The shopper's date of birth. Format [ISO-8601](https://www.w3.org/TR/NOTE-datetime): YYYY-MM-DD */
// metadata: { "key": "value" }, /* (Opt.) Metadata consists of entries, each of which includes a key and a value. Limits: * Maximum 20 key-value pairs per request. * Maximum 20 characters per key. * Maximum 80 characters per value. */
// shopperIP: "123.12.23.34" /* (Opt.) Recommended for risk checks */,
})
.catch((err) => {
core_1.Logger.error(`Failed to create Adyen session!`, loggerCtx, err === null || err === void 0 ? void 0 : err.message);
return { error: err === null || err === void 0 ? void 0 : err.message };
});
if (!("error" in checkoutSession))
core_1.Logger.info(`Sending sessionData to client.`, loggerCtx);
else {
core_1.Logger.warn(`No sessionData to send to the client!`, loggerCtx);
return { message: "The total for the order caused an error!" };
}
return {
sessionData: checkoutSession === null || checkoutSession === void 0 ? void 0 : checkoutSession.sessionData,
transactionId: checkoutSession === null || checkoutSession === void 0 ? void 0 : checkoutSession.id,
};
});
}
/**
* You get the outcome of each payment asynchronously, in a webhook event with eventCode: AUTHORISATION.
* For a successful payment, the event contains `success`: `"true"`.
* For an unsuccessful payment, you get `success`: `"false"`, and the `reason` field has details about why the payment was unsuccessful.
*/
handleAdyenStatusUpdate(notificationRequestItem) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const { eventCode, merchantAccountCode, merchantReference: orderCode, success, } = notificationRequestItem;
const ctx = yield this.createContext(merchantAccountCode);
const order = yield this.orderService.findOneByCode(ctx, orderCode);
if (!order) {
core_1.Logger.warn(`No Vendure order matches Adyen's 'merchantReference' ${orderCode}`, loggerCtx);
return;
}
core_1.Logger.info(`Received status update for channel ${merchantAccountCode} for order ${order === null || order === void 0 ? void 0 : order.code} (Vendure code)`, loggerCtx);
/** This `switch` statement is where you can handle all situations based on the provided `eventCode`
* With a simple architecture using only card payments, e.g., you only receive `Authorisation` webhooks.
*/
core_1.Logger.debug(`Webhook eventCode is ${eventCode} and success is ${success}`, loggerCtx);
switch (eventCode) {
case constant_1.EventCode.Authorisation: {
yield this.addPayment(ctx, order, notificationRequestItem);
return;
}
/* Examples */
/* By default, capture happens automatically, so this eventCode won't be sent a lot.
* https://docs.adyen.com/online-payments/classic-integrations/modify-payments/capture#automatic-capture
* */
// case EventCode.Capture: {
// await this.settleExistingPayment(ctx, order, notificationRequestItem.pspReference);
// return;
// }
// case EventCode.AuthorisationAdjustment: { return }
/* ... */
}
// No other status is handled
throw Error(`Unhandled incoming Adyen eventCode '${eventCode}' for order ${order.code}; pspReference ${notificationRequestItem.pspReference}; success=${success}`);
});
}
/**
* Add payment to order. Can be settled or authorized depending on the payment method.
*/
addPayment(ctx, order, notificationRequestItem) {
var _a, _b;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
if (!order.customFields.adyenPluginPaymentMethodCode) {
throw Error(`Order ${order.code} doesn't have an 'adyenPluginPaymentMethodCode'`);
}
if (order.state !== "AddingItems" && order.state !== "ArrangingPayment") {
core_1.Logger.info(`Order ${order.code} has status "${order.state}". Skipping...`, loggerCtx);
return;
}
yield this.transitionOrderState(ctx, order, "ArrangingPayment");
const updatedOrder = yield this.orderService.addPaymentToOrder(ctx, order.id, {
method: order.customFields.adyenPluginPaymentMethodCode,
metadata: notificationRequestItem,
});
if (!(updatedOrder instanceof core_1.Order)) {
throw Error(`Error when processing payment for order ${order.code}: ${updatedOrder.message}`);
}
const paymentMethodCode = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.paymentMethodCode) !== null && _b !== void 0 ? _b : "payment-adyen";
const paymentMethod = yield this.getPaymentMethod(ctx, paymentMethodCode);
if (!paymentMethod) {
core_1.Logger.verbose(`No paymentMethod found!`, loggerCtx);
return;
}
if (updatedOrder.state === "PaymentAuthorized") {
this.settleExistingPayment(ctx, updatedOrder, notificationRequestItem.pspReference);
}
});
}
settleExistingPayment(ctx, order, pspReference) {
var _a;
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const payment = (_a = order.payments) === null || _a === void 0 ? void 0 : _a.find((p) => p.transactionId === pspReference);
if (!payment) {
throw Error(`Cannot find payment with transactionId ${pspReference} for ${order.code}. Unable to settle this payment!`);
}
const settlePaymentResult = yield this.orderService.settlePayment(ctx, payment.id);
if (settlePaymentResult.message) {
throw Error(`Error settling payment ${payment.id} for order ${order.code}: ${settlePaymentResult.errorCode} - ${settlePaymentResult.message}`);
}
core_1.Logger.info(`Settled payment with transactionId ${pspReference} for ${order.code}.`, loggerCtx);
});
}
trimEnd(str, unwantedEnding) {
if (!str)
return undefined;
return str.endsWith(unwantedEnding) ? str.slice(0, -unwantedEnding.length) : str;
}
transitionOrderState(ctx, order, newState) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
if (order.state !== newState) {
const transitionToStateResult = yield this.orderService.transitionToState(ctx, order.id, newState);
if (transitionToStateResult instanceof core_1.OrderStateTransitionError) {
throw Error(`Error transitioning order ${order.code} from ${transitionToStateResult.fromState} to ${transitionToStateResult.toState}: ${transitionToStateResult.message}`);
}
return transitionToStateResult;
}
else {
core_1.Logger.debug(`Order ${order.code} is already in state '${newState}'. Skipping...`, loggerCtx);
}
});
}
toAdyenOrderLines(orderLines) {
return orderLines === null || orderLines === void 0 ? void 0 : orderLines.map(({ id, unitPriceWithTax, quantity }) => ({
id: id.toString(),
amountIncludingTax: unitPriceWithTax,
quantity,
}));
}
toAdyenBillingAddress(billingAddress) {
const { streetLine1, streetLine2, city, postalCode, province, countryCode } = billingAddress;
if (!streetLine1 || !city || !postalCode || !countryCode)
return undefined;
const trim = (str) => (str && str.length > 3000 ? str.slice(0, 2999) : str);
return {
/* All address fields are strings with max length 3000. */
country: countryCode /* The two-character ISO-3166-1 alpha-2 country code. */,
city: trim(city),
street: trim(streetLine1),
houseNumberOrName: trim(streetLine2) || "",
postalCode: trim(postalCode),
stateOrProvince: trim(province),
};
}
getPaymentMethod(ctx, paymentMethodCode) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const paymentMethods = yield this.paymentMethodService.findAll(ctx);
return paymentMethods.items.find((pm) => pm.code === paymentMethodCode);
});
}
getPaymentMethodArg(paymentMethod, desiredArg) {
var _a;
return (_a = paymentMethod === null || paymentMethod === void 0 ? void 0 : paymentMethod.handler.args.find((arg) => arg.name === desiredArg)) === null || _a === void 0 ? void 0 : _a.value;
}
createContext(channelToken) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const channel = yield this.channelService.getChannelFromToken(channelToken);
return new core_1.RequestContext({
apiType: "admin",
isAuthorized: true,
authorizedAsOwnerOnly: false,
channel,
languageCode: core_1.LanguageCode.en,
});
});
}
};
AdyenService = tslib_1.__decorate([
common_1.Injectable(),
tslib_1.__param(5, common_1.Inject(constant_1.ADYEN_PLUGIN_INIT_OPTIONS)),
tslib_1.__metadata("design:paramtypes", [core_1.PaymentMethodService,
core_1.ActiveOrderService,
core_1.OrderService,
core_1.ChannelService,
core_1.EntityHydrator, Object])
], AdyenService);
exports.AdyenService = AdyenService;
//# sourceMappingURL=adyen.service.js.map