import {
Payment,
PaymentStatus,
PaymentProcessingState,
PaymentBatchApprove,
} from '../types';
import { tracer } from '@capchase/tracer';
import { create } from './create';
import { requestPayment } from './request';
import { getLastPayment } from './getters';
import { callLog } from '../call_log';
import { PaymentRequestResponse } from '@capchase/messaging-system';
const acceptedStatuses = [
PaymentStatus.NEEDS_APPROVAL,
PaymentStatus.REQUEST_FAILED,
];
const internalBatchApprovePayments = async (
paymentKeys: string[],
actionBy: string
): Promise<PaymentBatchApprove> => {
await callLog({
callName: 'approvePayment',
actionBy,
args: [paymentKeys, actionBy],
});
const span = tracer?.scope()?.active();
span?.addTags({
payment_keys: paymentKeys,
action_by: actionBy,
});
const response: PaymentBatchApprove = {
approved: [],
errors: [],
};
for (const key of paymentKeys) {
try {
const approvedPayment = await internalApprovePayment(key, actionBy);
response.approved.push(approvedPayment);
} catch (err) {
response.errors.push({
key,
error: err.toString(),
});
}
}
return response;
};
/**
* Given a list of payment keys it tries to approve them in a sync way.
* If one fails it logs the
*
* @param {string[]} paymentKeys Payment keys
* @param {string} actionBy Action executor
* @return {Promise<PaymentBatchApprove>} Batch approval response
*
* @async
* @function
* @memberOf module:payments/actions
* @access public
*/
const batchApprovePayments =
tracer?.wrap('payments.batchApprovePayments', internalBatchApprovePayments) ||
internalBatchApprovePayments;
const internalApprovePayment = async (
paymentKey: string,
actionBy: string
): Promise<Payment> => {
await callLog({
callName: 'approvePayment',
actionBy,
args: [paymentKey, actionBy],
});
const span = tracer?.scope()?.active();
span?.addTags({
payment_key: paymentKey,
action_by: actionBy,
});
// TODO(SHI-1454): Validate actual user permissions
if (actionBy.length === 0) {
throw new Error('Action by should be set');
}
const payment = await getLastPayment(paymentKey, actionBy);
// Compare with a reduce as the the `acceptedStatuses` is an array of enum types
// we can not use the `include` method of the array
if (
!acceptedStatuses.reduce(
(flag, status) => (status === payment.status ? true : flag),
false
)
) {
throw new Error(
`Last payment for key ${paymentKey} is not ${acceptedStatuses} but ${payment.status}`
);
}
const approvedPayment = await create({
...payment,
actionBy: actionBy,
status: PaymentStatus.APPROVED,
processingState: PaymentProcessingState.UNPROCESSED,
});
let response: PaymentRequestResponse;
try {
response = await requestPayment(approvedPayment, actionBy);
} catch (err) {
// In case it fails, we create a new payment with request failed state
await create({
...payment,
actionBy: actionBy,
status: PaymentStatus.REQUEST_FAILED,
processingState: PaymentProcessingState.UNPROCESSED,
metadata: {
...(payment.metadata || {}),
errors: [
...(payment.metadata?.errors || []),
{
ts: new Date().toISOString(),
error: `${err}`,
},
],
},
});
// Still we propagate and throw the wrapped error
throw new Error(`Payment request failed: ${err}`);
}
span?.addTags({
request_success: response.ok,
});
if (!response.ok) {
span?.addTags({
error: response.error,
});
await create({
...payment,
actionBy: actionBy,
status: PaymentStatus.REQUEST_FAILED,
processingState: PaymentProcessingState.UNPROCESSED,
metadata: {
...(payment.metadata || {}),
errors: [
...(payment.metadata?.errors || []),
{
ts: new Date(),
error: response.error,
},
],
},
});
throw new Error(
`Payment request failed with error: ${response.error || 'unknown'}`
);
}
return create({
...payment,
actionBy: actionBy,
status: PaymentStatus.REQUESTED,
processingState: PaymentProcessingState.UNPROCESSED,
// Propagate the external service data
externalKey: response?.data?.key,
externalStatus: response?.data?.status,
externalService: response?.data?.service,
eventData: {
id: '',
url: response?.data?.external_url || '',
metadata: {},
createdAt: new Date().toISOString(),
},
});
};
/**
* Given a `paymentKey` it does the following:
*
* 1. Gets the last payment for the key and checks tha its pending
* 2. Add a new row with `approved` state with the same info
* 3. Request payment to external payment system/s
* 4. Add a new roww with `requested` state
*
* If any of the steps fails it throws an error.
*
* @param {string} paymentKey Payment key
* @param {string} actionBy Action executor
* @return {Promise<Payment>} The approved payment
*
* @async
* @function
* @memberOf module:payments/actions
* @access public
*/
const approvePayment =
tracer?.wrap('payments.approvePayment', internalApprovePayment) ||
internalApprovePayment;
export { approvePayment, batchApprovePayments };