UNPKG

@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
"use strict"; 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