payments/src/lib/actions/getters.ts

import {
  Payment,
  PaymentStatus,
  PaymentProcessingState,
  PaymentFilters,
} from '../types';
import { convertFromPrisma } from '../converters';
import { prisma } from '../prisma';
import { tracer } from '@capchase/tracer';
import { callLog } from '../call_log';

/**
 * Default page size for listing.
 *
 * @const
 * @memberOf module:payments/actions
 * @access private
 */
const DEFAULT_PAGE_SIZE = 100;

const internalListPayments = async (
  args: PaymentFilters | null,
  actionBy: string
): Promise<Payment[]> => {
  await callLog({
    callName: 'listPayments',
    actionBy,
    args: [args, actionBy],
  });

  const span = tracer?.scope()?.active();

  // TODO(SHI-1454): Validate actual user permissions
  if (actionBy.length === 0) {
    throw new Error('Action by should be set');
  }

  span?.addTags({
    action_by: actionBy,
  });

  const grouped = await prisma.payment.groupBy({
    by: ['key'],
    _max: {
      id: true,
    },
    where: {
      contractName: args?.contractName || undefined,
      direction: args?.direction || undefined,
      currency: args?.currency || undefined,
      effectiveDate: args?.effectiveDate || undefined,
      amount: args?.amount || undefined,
      actionBy: args?.actionBy && {
        contains: args?.actionBy,
      },
      // Get the unprocessed ones
      processingState: PaymentProcessingState.UNPROCESSED,
    },
  });

  const { offset, limit } = args || { offset: 0, limit: 100 };

  // If limit is not set, use the default page size
  const take = limit || DEFAULT_PAGE_SIZE;

  // Skip based on given offset or begin from start
  const skip = offset || 0;

  return (
    await prisma.payment.findMany({
      where: {
        id: { in: grouped.map(({ _max: { id } }) => id) as number[] },
        status: args?.status
          ? {
              in: args?.status as PaymentStatus[],
            }
          : undefined,
      },
      orderBy: {
        id: 'desc',
      },
      skip,
      take,
    })
  ).map(convertFromPrisma);
};

/**
 * Returns a list of all the payments with possible
 * filtering based on the `args`.
 *
 * @param {PaymentFilters | null} args Fitlering parameters
 * @param {string} actionBy Action executor
 * @return {Promise<Payment[]>}
 *
 * @async
 * @function
 * @memberOf module:payments/actions
 * @access public
 */
const listPayments =
  tracer?.wrap('payments.listPayments', internalListPayments) ||
  internalListPayments;

const internalCountPayments = async (
  args: PaymentFilters | null,
  actionBy: string
): Promise<number> => {
  await callLog({
    callName: 'countPayments',
    actionBy,
    args: [args, actionBy],
  });

  const span = tracer?.scope()?.active();

  // TODO(SHI-1454): Validate actual user permissions
  if (actionBy.length === 0) {
    throw new Error('Action by should be set');
  }

  span?.addTags({
    action_by: actionBy,
  });

  const grouped = await prisma.payment.groupBy({
    by: ['key'],
    _max: {
      id: true,
    },
    where: {
      contractName: args?.contractName || undefined,
      direction: args?.direction || undefined,
      currency: args?.currency || undefined,
      effectiveDate: args?.effectiveDate || undefined,
      amount: args?.amount || undefined,
      actionBy: args?.actionBy && {
        contains: args?.actionBy,
      },
      processingState: PaymentProcessingState.UNPROCESSED,
    },
  });

  return await prisma.payment.count({
    where: {
      id: { in: grouped.map(({ _max: { id } }) => id) as number[] },
      status: args?.status
        ? {
            in: args?.status as PaymentStatus[],
          }
        : undefined,
    },
  });
};

/**
 * Returns the total number of payments based on the `args` filtering.
 *
 * This is used for pagination purposes.
 *
 * @param {PaymentFilters} args Fitlering parameters
 * @param {string} actionBy Action executor
 * @return {Promise<number>}
 *
 * @async
 * @function
 * @memberOf module:payments/actions
 * @access public
 */
const countPayments =
  tracer?.wrap('payments.countPayments', internalCountPayments) ||
  internalCountPayments;

const internalGetPaymentHistory = async (
  paymentKey: string,
  actionBy: string
): Promise<Payment[]> => {
  await callLog({
    callName: 'getPaymentHistory',
    actionBy,
    args: [paymentKey, actionBy],
  });

  const span = tracer?.scope()?.active();

  // TODO(SHI-1454): Validate actual user permissions
  if (actionBy.length === 0) {
    throw new Error('Action by should be set');
  }

  span?.addTags({
    action_by: actionBy,
  });

  return (
    await prisma.payment.findMany({
      where: {
        key: paymentKey,
        processingState: PaymentProcessingState.UNPROCESSED,
      },
      orderBy: [
        {
          id: 'desc',
        },
      ],
    })
  ).map(convertFromPrisma);
};

/**
 * Given a `paymentKey` it returns the history of all payment associated with
 * that given key, ordered by in decreasing order by its additions.
 *
 * Note that only the unprocessed payments will be returned so that
 * a clean history is kept.
 *
 * @param {string} paymentKey Payment key
 * @param {string} actionBy Action executor
 * @return {Promise<Payment[]>} History of the payment as a list of payments
 *
 * @async
 * @function
 * @memberOf module:payments/actions
 * @access public
 */
const getPaymentHistory =
  tracer?.wrap('payments.getPaymentHistory', internalGetPaymentHistory) ||
  internalGetPaymentHistory;

const internalGetPaymentProcessingHistory = async (
  paymentKey: string,
  processingKey: string,
  actionBy: string
): Promise<Payment[]> => {
  await callLog({
    callName: 'getPaymentProcessingHistory',
    actionBy,
    args: [paymentKey, processingKey, actionBy],
  });

  const span = tracer?.scope()?.active();

  // TODO(SHI-1454): Validate actual user permissions
  if (actionBy.length === 0) {
    throw new Error('Action by should be set');
  }

  span?.addTags({
    action_by: actionBy,
  });

  return (
    await prisma.payment.findMany({
      where: {
        key: paymentKey,
        processingKey,
      },
      orderBy: [
        {
          id: 'desc',
        },
      ],
    })
  ).map(convertFromPrisma);
};

/**
 * Given a `paymentKey` and a `processingKey` it returns the processing history.
 *
 * @param {string} paymentKey Payment key
 * @param {string} processingKey Payment processing key
 * @param {string} actionBy Action executor
 * @return {Promise<Payment[]>}
 *
 * @throws {Error} If no payment for the given key are found
 *
 * @async
 * @function
 * @memberOf module:payments/actions
 * @access public
 */
const getPaymentProcessingHistory =
  tracer?.wrap(
    'payments.getPaymentProcessingHistory',
    internalGetPaymentProcessingHistory
  ) || internalGetPaymentProcessingHistory;

const internalGetLastPayment = async (
  paymentKey: string,
  actionBy: string
): Promise<Payment> => {
  await callLog({
    callName: 'getLastPayment',
    actionBy,
    args: [paymentKey, actionBy],
  });

  const span = tracer?.scope()?.active();

  // TODO(SHI-1454): Validate actual user permissions
  if (actionBy.length === 0) {
    throw new Error('Action by should be set');
  }

  span?.addTags({
    action_by: actionBy,
  });

  return convertFromPrisma(
    (
      await prisma.payment.findMany({
        where: {
          key: paymentKey,
          processingState: PaymentProcessingState.UNPROCESSED,
        },
        orderBy: [
          {
            id: 'desc',
          },
        ],
        take: 1,
      })
    )[0]
  );
};

/**
 * Given a `paymentKey` it returns the last unprocessed payment associated with
 * that given key.
 *
 * @param {string} paymentKey Payment key
 * @param {string} actionBy Action executor
 * @return {Promise<Payment>} The last payment for the given key
 *
 * @throws {Error} If no payment for the given key are found
 *
 * @async
 * @function
 * @memberOf module:payments/actions
 * @access public
 */
const getLastPayment =
  tracer?.wrap('payments.getLastPayment', internalGetLastPayment) ||
  internalGetLastPayment;

export {
  getLastPayment,
  getPaymentHistory,
  getPaymentProcessingHistory,
  listPayments,
  countPayments,
};