UNPKG

@unchainedshop/plugins

Version:

Official plugin collection for the Unchained Engine with payment, delivery, and pricing adapters

193 lines (192 loc) 9.37 kB
import { createLogger } from '@unchainedshop/logger'; import { OrderPricingSheet, PaymentAdapter, PaymentDirector, PaymentError, } from '@unchainedshop/core'; import { confirmDeferredTransaction, createTransaction, getIframeJavascriptUrl, getLightboxJavascriptUrl, getPaymentPageUrl, getToken, getTransaction, refundTransaction, voidTransaction, } from "./api.js"; import { orderIsPaid } from "./utils.js"; import { CompletionModes, IntegrationModes } from "./types.js"; import { LineItemType, TokenizationMode, TransactionCompletionBehavior, TransactionState, CreationEntityState, } from "./api-types.js"; const { PFCHECKOUT_SPACE_ID, PFCHECKOUT_USER_ID, PFCHECKOUT_SECRET, PFCHECKOUT_SUCCESS_URL, PFCHECKOUT_FAILED_URL, } = process.env; const logger = createLogger('unchained:postfinance-checkout'); const newError = ({ code, message }) => { const error = new Error(message); error.name = `POSTFINANCE_${code}`; return error; }; const PostfinanceCheckout = { ...PaymentAdapter, key: 'shop.unchained.payment.postfinance-checkout', label: 'Postfinance Checkout', version: '1.0.0', typeSupported(type) { return type === 'GENERIC'; }, actions: (config, context) => { const adapter = { ...PaymentAdapter.actions(config, context), getCompletionMode() { return (config.find((item) => item.key === 'completionMode')?.value || CompletionModes.Deferred); }, configurationError() { if (!PFCHECKOUT_SPACE_ID || !PFCHECKOUT_USER_ID || !PFCHECKOUT_SECRET || !PFCHECKOUT_SUCCESS_URL || !PFCHECKOUT_FAILED_URL) { return PaymentError.INCOMPLETE_CONFIGURATION; } return null; }, isActive() { if (adapter.configurationError() === null) return true; return false; }, isPayLaterAllowed() { return false; }, async validate(credentials) { if (!credentials.meta) return false; const { linkedSpaceId } = credentials.meta; const tokenData = await getToken(linkedSpaceId, credentials.token); return tokenData.state === CreationEntityState.ACTIVE; }, sign: async (transactionContext = {}) => { const { orderPayment, order } = context; if (!order) throw new Error('No order in context, can only sign with order context'); if (!orderPayment) throw new Error('No order payment in context, can only sign with order payment context'); const { integrationMode = IntegrationModes.PaymentPage } = transactionContext; const completionMode = adapter.getCompletionMode(); const pricing = OrderPricingSheet({ calculation: order.calculation, currencyCode: order.currencyCode, }); const totalAmount = pricing?.total({ useNetPrice: false }).amount; const transaction = { currency: order.currencyCode, metaData: { orderPaymentId: orderPayment._id, }, successUrl: `${transactionContext?.successUrl || PFCHECKOUT_SUCCESS_URL}?order_id=${orderPayment.orderId}`, failedUrl: `${transactionContext?.failedUrl || PFCHECKOUT_FAILED_URL}?order_id=${orderPayment.orderId}`, customerId: order?.userId || context?.userId, tokenizationMode: TokenizationMode.ALLOW_ONE_CLICK_PAYMENT, }; if (completionMode === CompletionModes.Immediate) { transaction.completionBehavior = TransactionCompletionBehavior.COMPLETE_IMMEDIATELY; } else if (completionMode === CompletionModes.Deferred) { transaction.completionBehavior = TransactionCompletionBehavior.COMPLETE_DEFERRED; } if (totalAmount) { const lineItemSum = { name: `Bestellung ${orderPayment.orderId}`, type: LineItemType.FEE, quantity: 1, uniqueId: orderPayment.orderId, amountIncludingTax: totalAmount / 100, }; transaction.lineItems = [lineItemSum]; } const transactionId = await createTransaction(transaction); if (!transactionId) throw new Error('Could not create transaction'); let location = null; if (integrationMode === IntegrationModes.PaymentPage) { location = await getPaymentPageUrl(transactionId); } else if (integrationMode === IntegrationModes.Lightbox) { location = await getLightboxJavascriptUrl(transactionId); } else if (integrationMode === IntegrationModes.iFrame) { location = await getIframeJavascriptUrl(transactionId); } const res = { transactionId, location, }; return JSON.stringify(res); }, charge: async ({ transactionId, paymentCredentials, }) => { if (paymentCredentials && !transactionId) { throw new Error('Not implemented yet'); } if (!transactionId) return false; const transaction = await getTransaction(transactionId); if (!transaction) { logger.error(`Transaction #${transactionId}: Transaction not found`); throw newError({ code: `TRANSACTION_NOT_FOUND`, message: 'Payment declined', }); } const { order, orderPayment } = context; if (!orderPayment) throw new Error('No order payment in context, cannot charge payment'); if (!order) throw new Error('No order in context, can only sign with order context'); const isPaid = await orderIsPaid(order, transaction); if (!isPaid) { logger.error(`Transaction #${transactionId}: Invalid state / Amount incorrect`); throw newError({ code: `STATE_${transaction.state?.toUpperCase()}`, message: 'Invalid state / Amount incorrect', }); } if (transaction.metaData?.orderPaymentId !== orderPayment._id) { logger.error(`Transaction #${transactionId}: Invalid state / Amount incorrect`); throw newError({ code: `TRANSACTION_ALREADY_USED`, message: 'Transaction already used in previous checkout', }); } const { id, ...tokenMeta } = transaction.token || {}; return { transaction, transactionId, credentials: id && { ...tokenMeta, token: id.toString(), }, }; }, cancel: async () => { const { orderPayment, order } = context; if (!order) throw new Error('No order in context, can only cancel with order context'); if (!orderPayment?.transactionId) { return false; } const { transactionId } = orderPayment; const transaction = await getTransaction(transactionId); const refund = transaction.state === TransactionState.FULFILL; const pricing = OrderPricingSheet({ calculation: order.calculation, currencyCode: order.currencyCode, }); const totalAmount = pricing?.total({ useNetPrice: false }).amount; return ((refund && refundTransaction(transactionId, order._id, totalAmount / 100)) || voidTransaction(transactionId)); }, confirm: async () => { const { orderPayment } = context; if (!orderPayment?.transactionId) { return false; } const { transactionId } = orderPayment; const transaction = await getTransaction(transactionId); if (transaction.state === TransactionState.AUTHORIZED) { return confirmDeferredTransaction(transactionId); } return false; }, }; return adapter; }, }; PaymentDirector.registerAdapter(PostfinanceCheckout); export * from "./types.js"; export * from "./api-types.js";