Untitled

mail@pastecode.io avatar
unknown
plain_text
2 months ago
11 kB
1
Indexable
Never
import { add, dinero } from "dinero.js";
import * as Array from "fp-ts/lib/Array";
import { pipe } from "fp-ts/lib/function";
import * as Luxon from "luxon";
import { DateTime } from "luxon";

import { interestTransaction, isInterestTransaction } from "@lambdas/shared/models/transactions/InterestTransaction";
import { initiateMonthlyInterestTransaction } from "@lambdas/shared/models/transactions/MonthlyInterestTransaction";
import { isUnsupportedTransaction, unsupportedTransaction } from "@lambdas/shared/models/transactions/UnsupportedTransaction";

import { dineroFromFloat, zeroSgdAmount } from "../../dinero";
import {
  generateTransactionInterestBand,
  InterestBandId,
  interestBands,
  TransactionInterestBand,
} from "../../models/InterestBand";
import {
  TransactionCategory,
  Transaction,
  InterestTransaction,
  TransactionWithMonthlyInterest,
} from "../../models/transactions";
import { DbsNotificationType } from "../../types";
import { RawDepositTransaction } from "./MambuClient";
import { SGD } from "@dinero.js/currencies";
import { withdrawalTransaction } from "@lambdas/shared/models/transactions/WithdrawalTransaction";
import { depositTransaction } from "@lambdas/shared/models/transactions/DepositTransaction";

export type MakeDepositQuery = Readonly<{
  depositAccountId: string;
  body: {
    amount: number;
    notes?: string;
  };
}>;

export type MakeDepositWithNonSubsidisedInterest = Readonly<{
  depositAccountId: string;
  body: {
    amount: number;
    _Band_Info?: {
      band: InterestBandId;
    };
  };
}>;

export type MakeDepositFromDbsNotificationQuery = Readonly<{
  depositAccountId: string;
  body: {
    amount: number;
    externalId?: string;
    valueDate?: string;
    bookingDate?: string;
    _DBS_Transaction_Metadata: {
      notification_type: DbsNotificationType;
      customer_reference?: string;
      sender_name?: string;
      sender_bank_id?: string;
      sender_bank_name?: string;
      msg_id: string;
    };
  };
}>;

export type DepositTransaction = Readonly<{
  id: string;
  amount: number;
  bookingDate: string;
  accountBalances: {
    totalBalance: number;
  };
}>;

export const getTransactionCategory = (
  rawDepositTransaction: RawDepositTransaction
): TransactionCategory => {
  switch (rawDepositTransaction.type) {
    case "DEPOSIT":
      if (
        rawDepositTransaction._Card_Transaction_Metadata?.category ===
        "CARD_CASHBACK"
      ) {
        return "CARD_CASHBACK";
      }

      if (
        rawDepositTransaction._Card_Transaction_Metadata?.category ===
        "CARD_REFUND"
      ) {
        return "CARD_REFUND";
      }

      if (rawDepositTransaction._Band_Info?.band) {
        return "EARNED";
      }

      return "TOP_UP";
    case "INTEREST_APPLIED":
    case "INTEREST_APPLIED_ADJUSTMENT":
      return "EARNED";
    case "WITHDRAWAL":
      if (
        rawDepositTransaction._Card_Transaction_Metadata?.category ===
        "CARD_COMPLETED"
      ) {
        return "CARD_COMPLETED";
      }

      if (
        rawDepositTransaction._Card_Transaction_Metadata?.category ===
        "CARD_REVERSED"
      ) {
        return "CARD_REVERSED";
      }

      if (rawDepositTransaction._Band_Info?.band) {
        return "EARNED";
      }

      return "WITHDRAWAL";
    case "CARD_TRANSACTION_REVERSAL":
      if (
        rawDepositTransaction._Card_Transaction_Metadata?.category ===
        "CARD_REFUND"
      ) {
        return "CARD_REFUND";
      }

      return "CARD_REVERSED";
    default:
      return "UNSUPPORTED";
  }
};

const getMonthOfInterest = (timestamp: Luxon.DateTime) => {
  let month = timestamp.month - (timestamp.day === 1 ? 1 : 0);
  // Month range is 1-12 && interest month of Jan-01 is Dec
  // => 1 (Jan) - 1 = 0 = 12 (Dec)
  return month || 12;
};
const getYearOfInterest = (timestamp: Luxon.DateTime) => {
  const { year, month, day } = timestamp;
  return year - (day === 1 && month === 1 ? 1 : 0);
};
export const isSameMonthOfInterest = (
  timestamp1: Luxon.DateTime,
  timestamp2: Luxon.DateTime
) =>
  getMonthOfInterest(timestamp1) === getMonthOfInterest(timestamp2) &&
  getYearOfInterest(timestamp1) === getYearOfInterest(timestamp2);

export const isLastInterestDayOfMonth = (timestamp: Luxon.DateTime) =>
  timestamp.day === 1;

export const getTransactionInterestBands = (
  interestTransactions: InterestTransaction[]
): TransactionInterestBand[] => {
  return interestBands.map((band) => {
    return pipe(
      interestTransactions,
      Array.filter((t) => t.interestBand === band.id),
      Array.map((t) => t.amount),
      (amounts) => {
        const total = amounts.reduce(add, zeroSgdAmount);
        return generateTransactionInterestBand(
          band,
          0,
          amounts.length === 0
        )(total);
      }
    );
  });
};

export const hasUnsupportedCategory = (
  rawTransaction: RawDepositTransaction
): boolean => getTransactionCategory(rawTransaction) === "UNSUPPORTED";

// Combine daily interests into corresponding monthly records
export const consolidateDailyInterestTransactions = (
  transactions: Transaction[]
) => {
  let timestamp = DateTime.fromMillis(0);
  let monthlyInterestAmount = zeroSgdAmount;
  let lifetimeInterestAmount = zeroSgdAmount;
  let interestTransactionsInMonth: InterestTransaction[] = [];
  let lifetimeInterestTransactions: InterestTransaction[] = [];
  const result: TransactionWithMonthlyInterest[] = [];

  // Sort ASC by timestamp before consolidating
  const sortedData = transactions.sort((t1, t2) => {
    return t1.timestamp.diff(t2.timestamp).milliseconds;
  });

  const a = sortedData.map((v) => v.timestamp.toFormat('dd-MM-yyyy'))
  console.log(a)
  sortedData.map((transaction) => {
    if (isUnsupportedTransaction(transaction)) {
      // Skip unsupported transactions
      return;
    } else if (isInterestTransaction(transaction)) {
      // 1 day would have multiple (unknown) interest records
      // So we only know it's over when a transaction of new month is arrived
      if (
        interestTransactionsInMonth.length > 0 &&
        !isSameMonthOfInterest(timestamp, transaction.timestamp)
      ) {
        const postTransactionBalance =
          interestTransactionsInMonth[interestTransactionsInMonth.length - 1]
            .postTransactionBalance;
        result.push(
          initiateMonthlyInterestTransaction({
            amount: monthlyInterestAmount,
            timestamp,
            returnsDate: timestamp.minus({ day: 1 }),
            bands: getTransactionInterestBands(interestTransactionsInMonth),
            postTransactionBalance: postTransactionBalance,
          })
        );
        // Reset to Accumulate new month
        monthlyInterestAmount = zeroSgdAmount;
        interestTransactionsInMonth = [];
      }

      interestTransactionsInMonth.push(transaction);
      lifetimeInterestTransactions.push(transaction);

      // Accumulate amount from daily to monthly
      monthlyInterestAmount = add(monthlyInterestAmount, transaction.amount);

      // Accumulate amount from daily to lifetime
      lifetimeInterestAmount = add(lifetimeInterestAmount, transaction.amount);
      // The last daily.timestamp will be used as monthly.timestamp
      timestamp = transaction.timestamp;
    } else {
      result.push(transaction);
    }
  });
  // If the last interest transaction is at the last day of a month
  // or current date is a different month from the last interest transaction
  // it's should be the last interest record of the month
  const b = result.map((v) => v.timestamp.toFormat('dd-MM-yyyy'))
  console.log(b)
  const now = DateTime.now().setZone(timestamp.zone);
  const needNewMonthlyInterest =
    isLastInterestDayOfMonth(timestamp) ||
    !isSameMonthOfInterest(timestamp, now);
  if (interestTransactionsInMonth.length > 0 && needNewMonthlyInterest) {
    // We send monthly interest of a month in 1st day of the next month
    timestamp = isLastInterestDayOfMonth(timestamp)
      ? timestamp
      : timestamp.plus({ month: 1 }).set({ day: 1 }); // get 1st day of the next month
    result.push(
      initiateMonthlyInterestTransaction({
        amount: monthlyInterestAmount,
        timestamp,
        returnsDate: timestamp.minus({ day: 1 }),
        bands: getTransactionInterestBands(interestTransactionsInMonth),
        postTransactionBalance:
          interestTransactionsInMonth[interestTransactionsInMonth.length - 1]
            .postTransactionBalance,
      })
    );
    monthlyInterestAmount = zeroSgdAmount;
    interestTransactionsInMonth = [];
  }


  return {
    transactions: result,
    currentMonthInterest: {
      total: monthlyInterestAmount,
      bands: getTransactionInterestBands(interestTransactionsInMonth),
    },
    lifetimeInterest: {
      total: lifetimeInterestAmount,
      bands: getTransactionInterestBands(lifetimeInterestTransactions),
    },
  };
};


const input: Transaction[] = [
  interestTransaction({
    transactionId: "1",
    timestamp: Luxon.DateTime.fromISO("2022-10-19T01:04:43.854Z"),
    amount: dinero({ amount: 2, currency: SGD, scale: 2 }),
    interestBand: InterestBandId.Band2,
    postTransactionBalance: dineroFromFloat({
      amount: 2,
      currency: SGD,
      scale: 2,
    }),
  }),
  interestTransaction({
    transactionId: "11",
    timestamp: Luxon.DateTime.fromISO("2022-10-20T01:03:43.854Z"),
    amount: dinero({ amount: -1, currency: SGD, scale: 2 }),
    interestBand: InterestBandId.Band2,
    postTransactionBalance: dineroFromFloat({
      amount: 2,
      currency: SGD,
      scale: 2,
    }),
  }),
  depositTransaction({
    transactionId: "2",
    amount: zeroSgdAmount,
    timestamp: Luxon.DateTime.fromISO("2022-10-20T01:04:43.854Z"),
    postTransactionBalance: dineroFromFloat({
      amount: 2,
      currency: SGD,
      scale: 2,
    }),
  }),
  interestTransaction({
    transactionId: "3",
    timestamp: Luxon.DateTime.fromISO("2022-10-21T01:05:43.854Z"),
    amount: dinero({ amount: 1, currency: SGD, scale: 2 }),
    interestBand: InterestBandId.Band1,
    postTransactionBalance: dineroFromFloat({
      amount: 2,
      currency: SGD,
      scale: 2,
    }),
  }),
  withdrawalTransaction({
    transactionId: "4",
    amount: zeroSgdAmount,
    timestamp: Luxon.DateTime.fromISO("2022-11-01T01:04:43.854Z"),
    transactionNumber: "2345254124523",
    receiverBankDetails: {
      accountNumber: "12345",
      accountNickname: "account test",
      bankName: "bank abc",
    },
    postTransactionBalance: dineroFromFloat({
      amount: 2,
      currency: SGD,
      scale: 2,
    }),
  }),
  interestTransaction({
    transactionId: "5",
    timestamp: Luxon.DateTime.fromISO("2022-11-01T01:05:43.854Z"),
    amount: dinero({ amount: 1, currency: SGD, scale: 2 }),
    interestBand: InterestBandId.Band1,
    postTransactionBalance: dineroFromFloat({
      amount: 2,
      currency: SGD,
      scale: 2,
    }),
  }),
  unsupportedTransaction({
    transactionId: "6",
    timestamp: Luxon.DateTime.fromISO("2022-11-01T01:05:43.854Z"),
    amount: dinero({ amount: 1, currency: SGD, scale: 2 }),
    postTransactionBalance: dineroFromFloat({
      amount: 2,
      currency: SGD,
      scale: 2,
    }),
  }),
];

const { transactions } = consolidateDailyInterestTransactions(input)

console.log(transactions)
Leave a Comment