import {
Payment,
PaymentParams,
PaymentStatus,
PaymentProcessingState,
SUPPORTED_CURRENCIES,
} from '../types';
import { convertToPrisma, convertFromPrisma } from '../converters';
import { Prisma } from '@prisma/client';
import { prisma } from '../prisma';
import { publishPaymentAdded } from './publish';
import { v4 as uuid, validate as uuidValidate } from 'uuid';
import { tracer } from '@capchase/tracer';
import { callLog } from '../call_log';
import { incrementMetric } from '@capchase/metrics';
import { sendNotification } from '@capchase/notifications';
import { formatMoney } from '@capchase/money';
import { Currency } from 'dinero.js';
import { format } from 'date-fns';
import { getJarvisUrl } from '../utils';
import { PAYMENT_SYSTEM_EVENTS_CHANNEL } from '../consts';
const internalHandlePostCreateNotification = async (payment: Payment) => {
if (
payment.status === PaymentStatus.NEEDS_APPROVAL &&
payment.processingState === PaymentProcessingState.UNPROCESSED
) {
const formattedMoney = formatMoney(
payment.amount,
payment.currency as Currency
);
const formattedDate = format(payment.effectiveDate, 'yyyy-MM-dd');
const jarvisPath = `#/payments?paymentId=${payment.key}`;
const text = `🤑 New payment ${
payment.direction
} ${formattedMoney} at ${formattedDate} needs approval 🔗 ${getJarvisUrl(
jarvisPath
)}`;
await sendNotification(text, PAYMENT_SYSTEM_EVENTS_CHANNEL);
}
};
/**
* Send a notification once a payment has been created.
*
* @async
* @function
* @memberOf module:payments/actions
* @access private
*/
const handlePostCreateNotification =
tracer?.wrap(
'payments.handlePostCreateNotification',
internalHandlePostCreateNotification
) || internalHandlePostCreateNotification;
const internalCreatePayment = async (
payment: PaymentParams,
actionBy: string
): Promise<Payment> => {
await callLog({
callName: 'createPayment',
actionBy,
args: [payment, actionBy],
});
const span = tracer?.scope()?.active();
span?.addTags({
params: payment,
action_by: actionBy,
});
if (!payment.companyId && !payment.receivingAccount) {
throw new Error('No counterparty information given');
}
if (payment.key && !uuidValidate(payment.key)) {
throw new Error(`Payment key ${payment.key} is not a valid UUID`);
}
if (payment.amount < 0) {
throw new Error('Amount is negative');
}
if (!SUPPORTED_CURRENCIES.includes(payment.currency)) {
throw new Error(`Currency ${payment.currency} is not supported`);
}
if (payment.effectiveDate && payment.effectiveDate < new Date()) {
throw new Error('Effective date is in the past');
}
// TODO(SHI-1454): Validate actual user permissions
if (actionBy.length === 0) {
throw new Error('Action by should be set');
}
// The contract name should only contain `a-zA-Z0-9_\-` characters and max of 24
if (
payment.contractName.length === 0 ||
!payment.contractName.match('^[a-zA-Z0-9_-]{1,24}$')
) {
throw new Error('Contract name is not valid');
}
const key = payment.key || uuid();
// Check if there already exists a payment with the same
// key, so that we avoid duplicates
const count = await prisma.payment.count({ where: { key } });
if (count > 0) {
throw new Error(`Payment with key ${key} already exists in the system`);
}
return create({
...payment,
key,
actionBy,
} as Payment);
};
/**
* Creates a new payment with status `NEEDS_APPROVAL`.
*
* @param {PaymentParams} params Payment params
* @param {string} actionBy Action executor
* @return {Promise<Payment>} The created payment
*
* @async
* @function
* @memberOf module:payments/actions
* @access public
*/
const createPayment =
tracer?.wrap('payments.createPayment', internalCreatePayment) ||
internalCreatePayment;
const internalCreate = async (payment: Payment): Promise<Payment> => {
let processingKey = uuid();
// If the payment has not yet been processed, keep the
// same processing key
if (
payment.processingKey &&
payment.processingState !== PaymentProcessingState.UNPROCESSED
) {
processingKey = payment.processingKey;
}
const span = tracer?.scope()?.active();
span?.addTags({
processing_key: processingKey,
});
const data = convertToPrisma({
...payment,
id: undefined,
createdAt: undefined,
key: payment.key || uuid(),
effectiveDate: payment.effectiveDate || new Date(),
status: payment.status || PaymentStatus.NEEDS_APPROVAL,
processingState:
payment.processingState || PaymentProcessingState.UNPROCESSED,
processingKey,
});
const createdPayment = convertFromPrisma(
await prisma.payment.create({
data: {
...data,
companyId: undefined,
company: payment.companyId
? {
connect: {
id: payment.companyId,
},
}
: undefined,
} as Prisma.PaymentCreateInput,
})
);
// Broadcast that the payment has been added to the DB
// for those entity listeners that may need to handle it
await publishPaymentAdded(createdPayment, createdPayment.actionBy);
incrementMetric('payment_system.payment.create', {
status: createdPayment.status,
processing_state: createdPayment.processingState,
contract_name: createdPayment.contractName,
direction: createdPayment.direction,
currency: createdPayment.currency,
action_by: createdPayment.actionBy,
});
// Do not await this call
handlePostCreateNotification(createdPayment);
return createdPayment;
};
/**
* Internal function that creates a payment, basically a wrapper over
* the Prisma interface to append a new payment to the PS's table.
*
* @async
* @function
* @memberOf module:payments/actions
* @access private
*/
const create =
tracer?.wrap('payments.create', internalCreate) || internalCreate;
export { createPayment, create };