Untitled
unknown
plain_text
a year ago
11 kB
3
Indexable
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)
Editor is loading...
Leave a Comment