Untitled
unknown
plain_text
2 years ago
11 kB
9
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