mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-11-26 00:24:54 +00:00
303 lines
11 KiB
TypeScript
303 lines
11 KiB
TypeScript
import { forwardRef, Inject, Injectable, UnprocessableEntityException } from '@nestjs/common';
|
|
import Decimal from 'decimal.js';
|
|
import moment from 'moment';
|
|
import { Transactional } from 'typeorm-transactional';
|
|
import {
|
|
AccountTransactionWebhookRequest,
|
|
CardTransactionWebhookRequest,
|
|
} from '~/common/modules/neoleap/dtos/requests';
|
|
import { Transaction } from '../entities/transaction.entity';
|
|
import { CustomerType, TransactionType } from '../enums';
|
|
import { TransactionRepository } from '../repositories/transaction.repository';
|
|
import { AccountService } from './account.service';
|
|
import { CardService } from './card.service';
|
|
import {
|
|
TransactionItemResponseDto,
|
|
PagedTransactionsResponseDto,
|
|
ParentTransferItemDto,
|
|
PagedParentTransfersResponseDto,
|
|
ChildTransferItemDto,
|
|
PagedChildTransfersResponseDto,
|
|
} from '../dtos/responses';
|
|
import { ParentTransactionType } from '../enums';
|
|
|
|
@Injectable()
|
|
export class TransactionService {
|
|
constructor(
|
|
private readonly transactionRepository: TransactionRepository,
|
|
private readonly accountService: AccountService,
|
|
@Inject(forwardRef(() => CardService)) private readonly cardService: CardService,
|
|
) {}
|
|
|
|
@Transactional()
|
|
async createCardTransaction(body: CardTransactionWebhookRequest) {
|
|
const card = await this.cardService.getCardByVpan(body.cardId);
|
|
const existingTransaction = await this.findExistingTransaction(body.transactionId, card.account.accountReference);
|
|
|
|
if (existingTransaction) {
|
|
throw new UnprocessableEntityException('TRANSACTION.ALREADY_EXISTS');
|
|
}
|
|
|
|
const transaction = await this.transactionRepository.createCardTransaction(card, body);
|
|
const total = new Decimal(body.transactionAmount).plus(body.billingAmount).plus(body.fees).plus(body.vatOnFees);
|
|
|
|
if (card.customerType === CustomerType.CHILD) {
|
|
await Promise.all([
|
|
this.accountService.decreaseAccountBalance(card.account.accountReference, total.toNumber()),
|
|
this.accountService.decrementReservedBalance(card.account, total.toNumber()),
|
|
]);
|
|
} else {
|
|
await this.accountService.decreaseAccountBalance(card.account.accountReference, total.toNumber());
|
|
}
|
|
|
|
return transaction;
|
|
}
|
|
|
|
@Transactional()
|
|
async createAccountTransaction(body: AccountTransactionWebhookRequest) {
|
|
const account = await this.accountService.getAccountByAccountNumber(body.accountId);
|
|
|
|
const existingTransaction = await this.findExistingTransaction(body.transactionId, account.accountReference);
|
|
|
|
if (existingTransaction) {
|
|
throw new UnprocessableEntityException('TRANSACTION.ALREADY_EXISTS');
|
|
}
|
|
|
|
const transaction = await this.transactionRepository.createAccountTransaction(account, body);
|
|
await this.accountService.creditAccountBalance(account.accountReference, body.amount);
|
|
|
|
return transaction;
|
|
}
|
|
|
|
async createInternalChildTransaction(cardId: string, amount: number) {
|
|
const card = await this.cardService.getCardById(cardId);
|
|
const transaction = await this.transactionRepository.createInternalChildTransaction(card, amount);
|
|
return transaction;
|
|
}
|
|
|
|
private async findExistingTransaction(transactionId: string, accountReference: string): Promise<Transaction | null> {
|
|
const existingTransaction = await this.transactionRepository.findTransactionByReference(
|
|
transactionId,
|
|
accountReference,
|
|
);
|
|
|
|
return existingTransaction;
|
|
}
|
|
|
|
async getWeeklySummary(juniorId: string) {
|
|
const startOfWeek = moment().startOf('week').toDate();
|
|
const endOfWeek = moment().endOf('week').toDate();
|
|
|
|
const transactions = await this.transactionRepository.getTransactionsForCardWithinDateRange(
|
|
juniorId,
|
|
startOfWeek,
|
|
endOfWeek,
|
|
);
|
|
const summary = {
|
|
startOfWeek: startOfWeek,
|
|
endOfWeek: endOfWeek,
|
|
total: 0,
|
|
monday: 0,
|
|
tuesday: 0,
|
|
wednesday: 0,
|
|
thursday: 0,
|
|
friday: 0,
|
|
saturday: 0,
|
|
sunday: 0,
|
|
};
|
|
|
|
transactions.forEach((transaction) => {
|
|
const day = moment(transaction.transactionDate).format('dddd').toLowerCase() as
|
|
| 'monday'
|
|
| 'tuesday'
|
|
| 'wednesday'
|
|
| 'thursday'
|
|
| 'friday'
|
|
| 'saturday'
|
|
| 'sunday';
|
|
summary[day] += transaction.transactionAmount;
|
|
});
|
|
|
|
summary.total = transactions.reduce((acc, curr) => acc + curr.transactionAmount, 0);
|
|
|
|
return summary;
|
|
}
|
|
|
|
async getParentConsolidated(
|
|
guardianCustomerId: string,
|
|
page: number,
|
|
size: number,
|
|
): Promise<TransactionItemResponseDto[]> {
|
|
const skip = (page - 1) * size;
|
|
|
|
const [transfers, topups] = await Promise.all([
|
|
this.transactionRepository.findParentTransfers(guardianCustomerId, skip, size),
|
|
this.transactionRepository.findParentTopups(guardianCustomerId, skip, size),
|
|
]);
|
|
|
|
const merged = [...transfers, ...topups].sort(
|
|
(a, b) => new Date(b.transactionDate).getTime() - new Date(a.transactionDate).getTime(),
|
|
);
|
|
|
|
const trimmed = merged.slice(0, size);
|
|
|
|
return trimmed.map((t) => this.mapParentItem(t));
|
|
}
|
|
|
|
async getParentTransactionsPaginated(
|
|
guardianCustomerId: string,
|
|
page: number,
|
|
size: number,
|
|
type?: ParentTransactionType,
|
|
): Promise<PagedTransactionsResponseDto> {
|
|
const skip = (page - 1) * size;
|
|
|
|
let transfers: Transaction[] = [];
|
|
let topups: Transaction[] = [];
|
|
let transferCount = 0;
|
|
let topupCount = 0;
|
|
|
|
if (!type || type === ParentTransactionType.PARENT_TRANSFER) {
|
|
[transfers, transferCount] = await Promise.all([
|
|
this.transactionRepository.findParentTransfers(guardianCustomerId, skip, size),
|
|
this.transactionRepository.countParentTransfers(guardianCustomerId),
|
|
]);
|
|
}
|
|
|
|
if (!type || type === ParentTransactionType.PARENT_TOPUP) {
|
|
[topups, topupCount] = await Promise.all([
|
|
this.transactionRepository.findParentTopups(guardianCustomerId, skip, size),
|
|
this.transactionRepository.countParentTopups(guardianCustomerId),
|
|
]);
|
|
}
|
|
|
|
const total = transferCount + topupCount;
|
|
|
|
if (type) {
|
|
const items = type === ParentTransactionType.PARENT_TRANSFER ? transfers : topups;
|
|
const mapped = items.map((t) => this.mapParentItem(t));
|
|
return new PagedTransactionsResponseDto(mapped, page, size, total);
|
|
}
|
|
|
|
const merged = [...transfers, ...topups].sort(
|
|
(a, b) => new Date(b.transactionDate).getTime() - new Date(a.transactionDate).getTime(),
|
|
);
|
|
|
|
const paginated = merged.slice(0, size);
|
|
const mapped = paginated.map((t) => this.mapParentItem(t));
|
|
|
|
return new PagedTransactionsResponseDto(mapped, page, size, total);
|
|
}
|
|
|
|
async getParentTransfersOnly(guardianCustomerId: string, page: number, size: number): Promise<ParentTransferItemDto[]> {
|
|
const skip = (page - 1) * size;
|
|
const transfers = await this.transactionRepository.findParentTransfers(guardianCustomerId, skip, size);
|
|
return transfers.map((t) => this.mapToParentTransferItem(t));
|
|
}
|
|
|
|
async getParentTransfersPaginated(
|
|
guardianCustomerId: string,
|
|
page: number,
|
|
size: number,
|
|
): Promise<PagedParentTransfersResponseDto> {
|
|
const skip = (page - 1) * size;
|
|
const [transfers, total] = await Promise.all([
|
|
this.transactionRepository.findParentTransfers(guardianCustomerId, skip, size),
|
|
this.transactionRepository.countParentTransfers(guardianCustomerId),
|
|
]);
|
|
const items = transfers.map((t) => this.mapToParentTransferItem(t));
|
|
return new PagedParentTransfersResponseDto(items, page, size, total);
|
|
}
|
|
|
|
async getChildTransfers(juniorId: string, page: number, size: number): Promise<ChildTransferItemDto[]> {
|
|
const skip = (page - 1) * size;
|
|
const transfers = await this.transactionRepository.findTransfersToJunior(juniorId, skip, size);
|
|
return transfers.map((t) => this.mapToChildTransferItem(t));
|
|
}
|
|
|
|
async getChildTransfersPaginated(
|
|
juniorId: string,
|
|
page: number,
|
|
size: number,
|
|
): Promise<PagedChildTransfersResponseDto> {
|
|
const skip = (page - 1) * size;
|
|
const [transfers, total] = await Promise.all([
|
|
this.transactionRepository.findTransfersToJunior(juniorId, skip, size),
|
|
this.transactionRepository.countTransfersToJunior(juniorId),
|
|
]);
|
|
const items = transfers.map((t) => this.mapToChildTransferItem(t));
|
|
return new PagedChildTransfersResponseDto(items, page, size, total);
|
|
}
|
|
|
|
private mapToParentTransferItem(t: Transaction): ParentTransferItemDto {
|
|
const child = t.card?.customer;
|
|
const currency = t.transactionCurrency === '682' ? 'SAR' : t.transactionCurrency;
|
|
return {
|
|
date: t.transactionDate,
|
|
amount: Math.abs(t.transactionAmount),
|
|
currency,
|
|
childName: child ? `${child.firstName} ${child.lastName}` : 'Child',
|
|
};
|
|
}
|
|
|
|
private mapToChildTransferItem(t: Transaction): ChildTransferItemDto {
|
|
const amount = Math.abs(t.transactionAmount);
|
|
const currency = t.transactionCurrency === '682' ? 'SAR' : t.transactionCurrency;
|
|
return {
|
|
date: t.transactionDate,
|
|
amount,
|
|
currency,
|
|
message: `You received {{amount}} {{currency}} from your parent.`,
|
|
};
|
|
}
|
|
|
|
async getChildSpendingHistory(juniorId: string, startUtc: Date, endUtc: Date) {
|
|
const transactions = await this.transactionRepository.getTransactionsForCardWithinDateRange(
|
|
juniorId,
|
|
startUtc,
|
|
endUtc,
|
|
);
|
|
|
|
const { SpendingHistoryItemDto, SpendingHistoryResponseDto } = await import('../dtos/responses');
|
|
const items = transactions.map((t) => new SpendingHistoryItemDto(t));
|
|
return new SpendingHistoryResponseDto(items);
|
|
}
|
|
|
|
async getTransactionDetail(transactionId: string, juniorId: string) {
|
|
const transaction = await this.transactionRepository.findTransactionById(transactionId, juniorId);
|
|
|
|
if (!transaction) {
|
|
throw new UnprocessableEntityException('TRANSACTION.NOT_FOUND');
|
|
}
|
|
|
|
const { TransactionDetailResponseDto } = await import('../dtos/responses');
|
|
return new TransactionDetailResponseDto(transaction);
|
|
}
|
|
|
|
private mapParentItem(t: Transaction): TransactionItemResponseDto {
|
|
const dto = new TransactionItemResponseDto();
|
|
dto.date = t.transactionDate;
|
|
|
|
if (t.transactionType === TransactionType.INTERNAL) {
|
|
dto.type = ParentTransactionType.PARENT_TRANSFER;
|
|
dto.amountSigned = -Math.abs(t.transactionAmount);
|
|
const child = t.card?.customer;
|
|
dto.counterpartyName = child ? `${child.firstName} ${child.lastName}` : 'Child';
|
|
dto.childName = dto.counterpartyName;
|
|
dto.counterpartyAccountMasked = t.card?.account?.accountReference
|
|
? `****${t.card.account.accountReference.slice(-4)}`
|
|
: null;
|
|
return dto;
|
|
}
|
|
|
|
dto.type = ParentTransactionType.PARENT_TOPUP;
|
|
const settlement = Number(t.settlementAmount ?? 0);
|
|
const txn = Number(t.transactionAmount ?? 0);
|
|
const creditAmount = settlement > 0 ? settlement : txn;
|
|
dto.amountSigned = Math.abs(Number.isFinite(creditAmount) ? creditAmount : 0);
|
|
dto.counterpartyName = 'Top-up';
|
|
dto.counterpartyAccountMasked = t.accountReference ? `****${t.accountReference.slice(-4)}` : null;
|
|
return dto;
|
|
}
|
|
}
|