@unchainedshop/plugins
Version:
Official plugin collection for the Unchained Engine with payment, delivery, and pricing adapters
193 lines (192 loc) • 9.37 kB
JavaScript
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";