@unchainedshop/plugins
Version:
Because of a Typescript issue with upstream "postfinancecheckout", the Postfinance plugin has been disabled from transpilation, import the source ts files from src and enable node_module tsc or copy over the src/payment/postfinance-checkout to your projec
390 lines • 17.1 kB
JavaScript
import { createLogger } from '@unchainedshop/logger';
import createDatatransAPI from './api/index.js';
import parseRegistrationData from './parseRegistrationData.js';
import roundedAmountFromOrder from './roundedAmountFromOrder.js';
import { PaymentAdapter, PaymentDirector, PaymentError, PaymentPricingRowCategory, PaymentPricingSheet, OrderPricingSheet, } from '@unchainedshop/core';
const logger = createLogger('unchained:core-payment:datatrans');
// v2
const { DATATRANS_SECRET, DATATRANS_SIGN_KEY, DATATRANS_API_ENDPOINT = 'https://api.sandbox.datatrans.com', DATATRANS_MERCHANT_ID, } = process.env;
const newDatatransError = ({ code, message }) => {
const error = new Error(message);
error.name = `DATATRANS_${code}`;
return error;
};
const throwIfResponseError = (result) => {
if (result.error) {
const rawError = result.error;
throw newDatatransError(rawError);
}
};
const Datatrans = {
...PaymentAdapter,
key: 'shop.unchained.datatrans',
label: 'Datatrans',
version: '2.0.0',
initialConfiguration: [
{
key: 'merchantId',
value: null,
},
],
typeSupported(type) {
return type === 'GENERIC';
},
actions: (config, context) => {
const getMerchantId = () => {
return config.find((item) => item.key === 'merchantId')?.value || DATATRANS_MERCHANT_ID;
};
const api = () => {
if (!DATATRANS_SECRET)
throw new Error('Credentials not Set');
return createDatatransAPI(DATATRANS_API_ENDPOINT, getMerchantId(), DATATRANS_SECRET);
};
const shouldSettleInUnchained = () => {
return config.reduce((current, item) => {
if (item.key === 'settleInUnchained')
return Boolean(item.value);
return current;
}, true);
};
const getMarketplaceSplits = async () => {
const { order, orderPayment } = context;
const pricingForOrderPayment = PaymentPricingSheet({
calculation: orderPayment.calculation,
currency: order.currency,
});
const pricing = OrderPricingSheet({
calculation: order.calculation,
currency: order.currency,
});
const { amount: total } = pricing.total({ useNetPrice: false });
return Promise.all(config
.filter((item) => item.key === 'marketplaceSplit')
.map((item) => {
const [subMerchantId, staticDiscountId, sharePercentage] = item.value
.split(';')
.map((f) => f.trim());
const { amount: discountSum } = pricingForOrderPayment.total({
category: PaymentPricingRowCategory.Discount,
discountId: staticDiscountId,
});
const shareFactor = sharePercentage ? parseInt(sharePercentage, 10) / 100 : 1;
const amount = Math.round(total * shareFactor);
const commission = Math.round(discountSum * -1 * shareFactor);
return {
subMerchantId,
amount,
commission,
};
}));
};
const authorize = async ({ paymentCredentials, ...arbitraryFields }) => {
const { order, orderPayment } = context;
const refno = Buffer.from(orderPayment._id, 'hex').toString('base64');
const userId = order?.userId || context?.userId;
const refno2 = userId;
const { currency, amount } = roundedAmountFromOrder(order);
const splits = await getMarketplaceSplits();
const result = await api().authorize({
...arbitraryFields,
amount,
currency,
refno,
refno2,
autoSettle: false,
customer: {
id: userId,
},
marketplace: splits.length
? {
splits,
}
: undefined,
[paymentCredentials.meta.objectKey]: JSON.parse(paymentCredentials.token),
});
throwIfResponseError(result);
return result.transactionId;
};
const authorizeAuth = async ({ transactionId, refno, refno2, ...arbitraryFields }) => {
const { order } = context;
const { currency, amount } = roundedAmountFromOrder(order);
const result = await api().authorizeAuthenticated({
...arbitraryFields,
transactionId,
amount,
currency,
refno,
refno2,
autoSettle: false,
});
throwIfResponseError(result);
return result.acquirerAuthorizationCode;
};
const isTransactionAmountValid = (transaction) => {
const { order } = context;
const { currency, amount } = roundedAmountFromOrder(order);
if (transaction.currency !== currency ||
transaction.detail.authorize?.amount !== amount) {
logger.info(`currency: ${transaction.currency} === ${currency} => ${transaction.currency === currency}, amount: ${transaction.detail.authorize?.amount} === ${amount} => ${transaction.detail.authorize?.amount === amount}`);
return false;
}
return true;
};
const checkIfTransactionAmountValid = (transactionId, transaction) => {
if (!isTransactionAmountValid(transaction)) {
logger.error(`Transaction declined / Transaction ID ${transactionId} because of amount/currency mismatch`);
throw newDatatransError({
code: `YOU_HAVE_TO_PAY_THE_FULL_AMOUNT_DUDE`,
message: 'Amount / Currency Mismatch with Cart',
});
}
};
const settle = async ({ transactionId, refno, refno2, extensions }) => {
const { order } = context;
const { currency, amount } = roundedAmountFromOrder(order);
const splits = await getMarketplaceSplits();
const result = await api().settle({
transactionId,
amount,
refno,
refno2,
currency,
marketplace: splits.length
? {
splits,
}
: undefined,
extensions,
});
throwIfResponseError(result);
return result;
};
const cancel = async ({ transactionId, refno }) => {
const result = await api().cancel({
transactionId,
refno,
});
throwIfResponseError(result);
return result;
};
const adapterActions = {
...PaymentAdapter.actions(config, context),
configurationError() {
if (!getMerchantId() || !DATATRANS_SECRET || !DATATRANS_SIGN_KEY) {
return PaymentError.INCOMPLETE_CONFIGURATION;
}
return null;
},
isActive() {
if (adapterActions.configurationError() === null)
return true;
return false;
},
isPayLaterAllowed() {
return false;
},
async sign(transactionContext = {}) {
const { useSecureFields = false, ...arbitraryFields } = transactionContext || {};
const { orderPayment, paymentProviderId, order } = context;
const refno = Buffer.from(orderPayment ? orderPayment._id : paymentProviderId, 'hex').toString('base64');
const userId = order?.userId || context?.userId;
const refno2 = userId;
const price = order ? roundedAmountFromOrder(order) : {};
if (useSecureFields) {
const result = await api().secureFields({
...arbitraryFields,
currency: price.currency || 'CHF',
refno,
refno2,
customer: {
id: userId,
},
amount: price.amount,
});
throwIfResponseError(result);
return JSON.stringify(result);
}
const result = await api().init({
...arbitraryFields,
currency: price.currency || 'CHF',
refno,
refno2,
customer: {
id: userId,
},
amount: price.amount,
});
throwIfResponseError(result);
return JSON.stringify(result);
},
async validate(credentials) {
if (!credentials.meta)
return false;
const { objectKey, currency } = credentials.meta;
const result = await api().validate({
refno: Buffer.from(`valid-${new Date().getTime()}`, 'hex').toString('base64'),
currency,
[objectKey]: JSON.parse(credentials.token),
});
return Boolean(result?.transactionId);
},
async register(transactionResponse) {
const { transactionId } = transactionResponse;
const result = (await api().status({
transactionId,
}));
if (result.transactionId) {
return parseRegistrationData(result);
}
return null;
},
async confirm() {
if (!shouldSettleInUnchained())
return false;
const { orderPayment, transactionContext } = context;
const { transactionId } = orderPayment;
const { extensions } = transactionContext || {};
if (!transactionId) {
return false;
}
const transaction = (await api().status({
transactionId,
}));
throwIfResponseError(transaction);
const { status } = transaction;
if (status === 'authorized') {
// either settle or cancel
// if further deferred settlement is active, don't settle in unchained and hand off
// settlement to other systems
await settle({
transactionId,
refno: transaction.refno,
refno2: transaction.refno2,
extensions,
});
}
return true;
},
async cancel() {
if (!shouldSettleInUnchained())
return false;
const { orderPayment } = context;
const { transactionId } = orderPayment;
if (!transactionId) {
return false;
}
const transaction = (await api().status({
transactionId,
}));
throwIfResponseError(transaction);
const { status } = transaction;
if (status === 'authorized') {
// either settle or cancel
// if further deferred settlement is active, don't settle in unchained and hand off
// settlement to other systems
await cancel({
transactionId,
refno: transaction.refno,
});
}
return true;
},
async charge({ transactionId: rawTransactionId, paymentCredentials, authorizeAuthenticated, ...arbitraryFields }) {
if (!rawTransactionId && !paymentCredentials) {
logger.warn('Not trying to charge because of missing payment authorization response, return false to provide later');
return false;
}
const transactionId = rawTransactionId ||
(await authorize({
...arbitraryFields,
paymentCredentials,
}));
const transaction = (await api().status({
transactionId,
}));
throwIfResponseError(transaction);
if (!transaction) {
throw newDatatransError({
code: `TRANSACTION_NOT_FOUND`,
message: 'Amount / Currency Mismatch with Cart',
});
}
let { status } = transaction;
if (status === 'canceled' || status === 'failed') {
logger.error(`Payment declined or canceled with Transaction ID ${transactionId} and status ${status}`);
throw newDatatransError({
code: `STATUS_${status.toUpperCase()}`,
message: 'Payment declined or canceled',
});
}
if (status === 'authenticated') {
if (authorizeAuthenticated) {
checkIfTransactionAmountValid(transactionId, transaction);
await authorizeAuth({
...(authorizeAuthenticated || {}),
transactionId,
refno: transaction.refno,
refno2: transaction.refno2,
});
status = 'authorized';
}
else {
logger.error(`Transaction declined / Transaction ID ${transactionId} not authorized yet`);
throw newDatatransError({
code: `STATUS_${status.toUpperCase()}`,
message: 'Payment authenticated but not authorized',
});
}
}
if (status === 'authorized' || status === 'settled') {
let settledTransaction = transaction;
let credentials;
// here we try to guard us against a potential
// network hicup or registration data parsing error
// at the dumbest possible moment
try {
checkIfTransactionAmountValid(transactionId, settledTransaction);
credentials = await parseRegistrationData(settledTransaction);
const result = await api().status({
transactionId,
});
if (result?.error) {
settledTransaction = transaction;
}
else {
settledTransaction = result;
}
}
catch {
await cancel({ transactionId, refno: settledTransaction.refno });
logger.error(`Transaction declined / Transaction ID ${transactionId} authorization cancelled`);
throw newDatatransError({
code: `STATUS_${status.toUpperCase()}`,
message: 'Payment cancelled server-side because of amount missmatch',
});
}
return {
transactionId,
settledTransaction,
arbitraryFields,
credentials,
};
}
if (status === 'initialized' ||
status === 'challenge_required' ||
status === 'challenge_ongoing' ||
status === 'transmitted') {
logger.error(`Transaction ID ${transactionId} in transit with status ${status}`);
throw newDatatransError({
code: `STATUS_${status.toUpperCase()}`,
message: 'Transaction status invalid for checkout',
});
}
return false;
},
};
return adapterActions;
},
};
PaymentDirector.registerAdapter(Datatrans);
//# sourceMappingURL=index.js.map