mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-11-26 00:24:54 +00:00
Merge pull request #36 from HamzaSha1/ZOD-309-child-transaction-history-parent-→-child-transfers
ZOD-309-child-transaction-history-parent-→-child-transfers
This commit is contained in:
16
src/card/dtos/responses/child-transfer-item.response.dto.ts
Normal file
16
src/card/dtos/responses/child-transfer-item.response.dto.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ChildTransferItemDto {
|
||||
@ApiProperty({ example: '2025-10-14T09:53:40.000Z' })
|
||||
date!: Date;
|
||||
|
||||
@ApiProperty({ example: 50.0 })
|
||||
amount!: number;
|
||||
|
||||
@ApiProperty({ example: 'SAR' })
|
||||
currency!: string;
|
||||
|
||||
@ApiProperty({ example: 'You received {{amount}} {{currency}} from your parent.' })
|
||||
message!: string;
|
||||
}
|
||||
|
||||
@ -4,3 +4,9 @@ export * from './child-card.response.dto';
|
||||
export * from './transaction-item.response.dto';
|
||||
export * from './guardian-home.response.dto';
|
||||
export * from './paged-transactions.response.dto';
|
||||
export * from './parent-transfer-item.response.dto';
|
||||
export * from './parent-home.response.dto';
|
||||
export * from './paged-parent-transfers.response.dto';
|
||||
export * from './child-transfer-item.response.dto';
|
||||
export * from './junior-home.response.dto';
|
||||
export * from './paged-child-transfers.response.dto';
|
||||
|
||||
16
src/card/dtos/responses/junior-home.response.dto.ts
Normal file
16
src/card/dtos/responses/junior-home.response.dto.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ChildTransferItemDto } from './child-transfer-item.response.dto';
|
||||
|
||||
export class JuniorHomeResponseDto {
|
||||
@ApiProperty({ example: 500.0 })
|
||||
availableBalance!: number;
|
||||
|
||||
@ApiProperty({ type: [ChildTransferItemDto] })
|
||||
recentTransfers!: ChildTransferItemDto[];
|
||||
|
||||
constructor(availableBalance: number, recentTransfers: ChildTransferItemDto[]) {
|
||||
this.availableBalance = availableBalance;
|
||||
this.recentTransfers = recentTransfers;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ChildTransferItemDto } from './child-transfer-item.response.dto';
|
||||
|
||||
export class PagedChildTransfersResponseDto {
|
||||
@ApiProperty({ type: [ChildTransferItemDto] })
|
||||
items!: ChildTransferItemDto[];
|
||||
|
||||
@ApiProperty({ example: 1 })
|
||||
page!: number;
|
||||
|
||||
@ApiProperty({ example: 10 })
|
||||
size!: number;
|
||||
|
||||
@ApiProperty({ example: 20 })
|
||||
total!: number;
|
||||
|
||||
@ApiProperty({ example: true })
|
||||
hasMore!: boolean;
|
||||
|
||||
constructor(
|
||||
items: ChildTransferItemDto[],
|
||||
page: number,
|
||||
size: number,
|
||||
total: number,
|
||||
) {
|
||||
this.items = items;
|
||||
this.page = page;
|
||||
this.size = size;
|
||||
this.total = total;
|
||||
this.hasMore = page * size < total;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ParentTransferItemDto } from './parent-transfer-item.response.dto';
|
||||
|
||||
export class PagedParentTransfersResponseDto {
|
||||
@ApiProperty({ type: [ParentTransferItemDto] })
|
||||
items!: ParentTransferItemDto[];
|
||||
|
||||
@ApiProperty({ example: 1 })
|
||||
page!: number;
|
||||
|
||||
@ApiProperty({ example: 10 })
|
||||
size!: number;
|
||||
|
||||
@ApiProperty({ example: 45 })
|
||||
total!: number;
|
||||
|
||||
@ApiProperty({ example: true })
|
||||
hasMore!: boolean;
|
||||
|
||||
constructor(
|
||||
items: ParentTransferItemDto[],
|
||||
page: number,
|
||||
size: number,
|
||||
total: number,
|
||||
) {
|
||||
this.items = items;
|
||||
this.page = page;
|
||||
this.size = size;
|
||||
this.total = total;
|
||||
this.hasMore = page * size < total;
|
||||
}
|
||||
}
|
||||
|
||||
16
src/card/dtos/responses/parent-home.response.dto.ts
Normal file
16
src/card/dtos/responses/parent-home.response.dto.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ParentTransferItemDto } from './parent-transfer-item.response.dto';
|
||||
|
||||
export class ParentHomeResponseDto {
|
||||
@ApiProperty({ example: 2000.0 })
|
||||
availableBalance!: number;
|
||||
|
||||
@ApiProperty({ type: [ParentTransferItemDto] })
|
||||
recentTransfers!: ParentTransferItemDto[];
|
||||
|
||||
constructor(availableBalance: number, recentTransfers: ParentTransferItemDto[]) {
|
||||
this.availableBalance = availableBalance;
|
||||
this.recentTransfers = recentTransfers;
|
||||
}
|
||||
}
|
||||
|
||||
16
src/card/dtos/responses/parent-transfer-item.response.dto.ts
Normal file
16
src/card/dtos/responses/parent-transfer-item.response.dto.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ParentTransferItemDto {
|
||||
@ApiProperty({ example: '2025-10-14T09:53:40.000Z' })
|
||||
date!: Date;
|
||||
|
||||
@ApiProperty({ example: 50.0 })
|
||||
amount!: number;
|
||||
|
||||
@ApiProperty({ example: 'SAR' })
|
||||
currency!: string;
|
||||
|
||||
@ApiProperty({ example: 'Ahmed Ali' })
|
||||
childName!: string;
|
||||
}
|
||||
|
||||
@ -144,4 +144,28 @@ export class TransactionRepository {
|
||||
.andWhere('parentCards.customerId = :guardianCustomerId', { guardianCustomerId })
|
||||
.getCount();
|
||||
}
|
||||
|
||||
findTransfersToJunior(juniorId: string, skip: number, take: number): Promise<Transaction[]> {
|
||||
return this.transactionRepository
|
||||
.createQueryBuilder('tx')
|
||||
.innerJoinAndSelect('tx.card', 'card')
|
||||
.innerJoinAndSelect('card.account', 'account')
|
||||
.where('card.customerId = :juniorId', { juniorId })
|
||||
.andWhere('tx.transactionScope = :scope', { scope: TransactionScope.CARD })
|
||||
.andWhere('tx.transactionType = :type', { type: TransactionType.INTERNAL })
|
||||
.orderBy('tx.transactionDate', 'DESC')
|
||||
.skip(skip)
|
||||
.take(take)
|
||||
.getMany();
|
||||
}
|
||||
|
||||
countTransfersToJunior(juniorId: string): Promise<number> {
|
||||
return this.transactionRepository
|
||||
.createQueryBuilder('tx')
|
||||
.innerJoin('tx.card', 'card')
|
||||
.where('card.customerId = :juniorId', { juniorId })
|
||||
.andWhere('tx.transactionScope = :scope', { scope: TransactionScope.CARD })
|
||||
.andWhere('tx.transactionType = :type', { type: TransactionType.INTERNAL })
|
||||
.getCount();
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,14 @@ import { CustomerType, TransactionType } from '../enums';
|
||||
import { TransactionRepository } from '../repositories/transaction.repository';
|
||||
import { AccountService } from './account.service';
|
||||
import { CardService } from './card.service';
|
||||
import { TransactionItemResponseDto, PagedTransactionsResponseDto } from '../dtos/responses';
|
||||
import {
|
||||
TransactionItemResponseDto,
|
||||
PagedTransactionsResponseDto,
|
||||
ParentTransferItemDto,
|
||||
PagedParentTransfersResponseDto,
|
||||
ChildTransferItemDto,
|
||||
PagedChildTransfersResponseDto,
|
||||
} from '../dtos/responses';
|
||||
import { ParentTransactionType } from '../enums';
|
||||
|
||||
@Injectable()
|
||||
@ -182,6 +189,68 @@ export class TransactionService {
|
||||
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.`,
|
||||
};
|
||||
}
|
||||
|
||||
private mapParentItem(t: Transaction): TransactionItemResponseDto {
|
||||
const dto = new TransactionItemResponseDto();
|
||||
dto.date = t.transactionDate;
|
||||
|
||||
@ -6,8 +6,7 @@ import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
||||
import { AccessTokenGuard, RolesGuard } from '~/common/guards';
|
||||
import { ApiDataResponse, ApiLangRequestHeader } from '~/core/decorators';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { GuardianHomeResponseDto, PagedTransactionsResponseDto } from '~/card/dtos/responses';
|
||||
import { ParentTransactionType } from '~/card/enums';
|
||||
import { ParentHomeResponseDto, PagedParentTransfersResponseDto } from '~/card/dtos/responses';
|
||||
import { GuardianTransactionsService } from '../services';
|
||||
|
||||
|
||||
@ -22,7 +21,7 @@ export class GuardianTransactionsController {
|
||||
|
||||
@Get('home')
|
||||
@ApiQuery({ name: 'size', required: false, type: Number, example: 5 })
|
||||
@ApiDataResponse(GuardianHomeResponseDto)
|
||||
@ApiDataResponse(ParentHomeResponseDto)
|
||||
async getHome(
|
||||
@AuthenticatedUser() user: IJwtPayload,
|
||||
@Query('size') size?: number,
|
||||
@ -32,20 +31,18 @@ export class GuardianTransactionsController {
|
||||
return ResponseFactory.data(res);
|
||||
}
|
||||
|
||||
@Get('transactions')
|
||||
@Get('transfers')
|
||||
@ApiQuery({ name: 'page', required: false, type: Number, example: 1 })
|
||||
@ApiQuery({ name: 'size', required: false, type: Number, example: 10 })
|
||||
@ApiQuery({ name: 'type', required: false, enum: ParentTransactionType })
|
||||
@ApiDataResponse(PagedTransactionsResponseDto)
|
||||
async getTransactions(
|
||||
@ApiDataResponse(PagedParentTransfersResponseDto)
|
||||
async getTransfers(
|
||||
@AuthenticatedUser() user: IJwtPayload,
|
||||
@Query('page') page?: number,
|
||||
@Query('size') size?: number,
|
||||
@Query('type') type?: ParentTransactionType,
|
||||
) {
|
||||
const pageNum = Math.max(1, Number(page) || 1);
|
||||
const pageSize = Math.max(1, Math.min(Number(size) || 10, 50));
|
||||
const res = await this.guardianTxService.getTransactions(user.sub, pageNum, pageSize, type);
|
||||
const res = await this.guardianTxService.getTransfers(user.sub, pageNum, pageSize);
|
||||
return ResponseFactory.data(res);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CustomerService } from '~/customer/services';
|
||||
import { GuardianHomeResponseDto, PagedTransactionsResponseDto } from '~/card/dtos/responses';
|
||||
import { TransactionItemResponseDto } from '~/card/dtos/responses';
|
||||
import { ParentTransactionType } from '~/card/enums';
|
||||
import { ParentHomeResponseDto, PagedParentTransfersResponseDto } from '~/card/dtos/responses';
|
||||
import { TransactionService } from '~/card/services/transaction.service';
|
||||
|
||||
@Injectable()
|
||||
@ -12,7 +10,7 @@ export class GuardianTransactionsService {
|
||||
private readonly transactionService: TransactionService,
|
||||
) {}
|
||||
|
||||
async getHome(guardianId: string, size: number): Promise<GuardianHomeResponseDto> {
|
||||
async getHome(guardianId: string, size: number): Promise<ParentHomeResponseDto> {
|
||||
const parent = await this.customerService.findCustomerById(guardianId);
|
||||
const primaryCard = parent.cards?.[0];
|
||||
|
||||
@ -27,18 +25,17 @@ export class GuardianTransactionsService {
|
||||
}
|
||||
}
|
||||
|
||||
const items: TransactionItemResponseDto[] = await this.transactionService.getParentConsolidated(guardianId, 1, size);
|
||||
const recentTransfers = await this.transactionService.getParentTransfersOnly(guardianId, 1, size);
|
||||
|
||||
return new GuardianHomeResponseDto(availableBalance, items);
|
||||
return new ParentHomeResponseDto(availableBalance, recentTransfers);
|
||||
}
|
||||
|
||||
async getTransactions(
|
||||
async getTransfers(
|
||||
guardianId: string,
|
||||
page: number,
|
||||
size: number,
|
||||
type?: ParentTransactionType,
|
||||
): Promise<PagedTransactionsResponseDto> {
|
||||
return this.transactionService.getParentTransactionsPaginated(guardianId, page, size, type);
|
||||
): Promise<PagedParentTransfersResponseDto> {
|
||||
return this.transactionService.getParentTransfersPaginated(guardianId, page, size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiBearerAuth, ApiTags, ApiQuery } from '@nestjs/swagger';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { AllowedRoles, AuthenticatedUser, Public } from '~/common/decorators';
|
||||
@ -33,6 +33,7 @@ import {
|
||||
TransferToJuniorResponseDto,
|
||||
} from '../dtos/response';
|
||||
import { WeeklySummaryResponseDto } from '../dtos/response/weekly-summary.response.dto';
|
||||
import { JuniorHomeResponseDto, PagedChildTransfersResponseDto } from '~/card/dtos/responses';
|
||||
import { JuniorService } from '../services';
|
||||
|
||||
@Controller('juniors')
|
||||
@ -157,4 +158,37 @@ export class JuniorController {
|
||||
const summary = await this.juniorService.getWeeklySummary(juniorId, user.sub);
|
||||
return ResponseFactory.data(summary);
|
||||
}
|
||||
|
||||
@Get(':juniorId/home')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.JUNIOR, Roles.GUARDIAN)
|
||||
@ApiQuery({ name: 'size', required: false, type: Number, example: 5 })
|
||||
@ApiDataResponse(JuniorHomeResponseDto)
|
||||
async getJuniorHome(
|
||||
@Param('juniorId', CustomParseUUIDPipe) juniorId: string,
|
||||
@AuthenticatedUser() user: IJwtPayload,
|
||||
@Query('size') size?: number,
|
||||
) {
|
||||
const limit = Math.max(1, Math.min(Number(size) || 5, 20));
|
||||
const res = await this.juniorService.getJuniorHome(juniorId, user.sub, limit);
|
||||
return ResponseFactory.data(res);
|
||||
}
|
||||
|
||||
@Get(':juniorId/transfers')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.JUNIOR, Roles.GUARDIAN)
|
||||
@ApiQuery({ name: 'page', required: false, type: Number, example: 1 })
|
||||
@ApiQuery({ name: 'size', required: false, type: Number, example: 10 })
|
||||
@ApiDataResponse(PagedChildTransfersResponseDto)
|
||||
async getJuniorTransfers(
|
||||
@Param('juniorId', CustomParseUUIDPipe) juniorId: string,
|
||||
@AuthenticatedUser() user: IJwtPayload,
|
||||
@Query('page') page?: number,
|
||||
@Query('size') size?: number,
|
||||
) {
|
||||
const pageNum = Math.max(1, Number(page) || 1);
|
||||
const pageSize = Math.max(1, Math.min(Number(size) || 10, 50));
|
||||
const res = await this.juniorService.getJuniorTransfers(juniorId, user.sub, pageNum, pageSize);
|
||||
return ResponseFactory.data(res);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { IsNull, Not } from 'typeorm';
|
||||
import { Transactional } from 'typeorm-transactional';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { CardService } from '~/card/services';
|
||||
import { CardService, TransactionService } from '~/card/services';
|
||||
import { NeoLeapService } from '~/common/modules/neoleap/services';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { setIf } from '~/core/utils';
|
||||
@ -20,6 +20,7 @@ import {
|
||||
import { Junior } from '../entities';
|
||||
import { JuniorRepository } from '../repositories';
|
||||
import { QrcodeService } from './qrcode.service';
|
||||
import { JuniorHomeResponseDto, PagedChildTransfersResponseDto } from '~/card/dtos/responses';
|
||||
|
||||
@Injectable()
|
||||
export class JuniorService {
|
||||
@ -34,6 +35,7 @@ export class JuniorService {
|
||||
private readonly qrCodeService: QrcodeService,
|
||||
private readonly neoleapService: NeoLeapService,
|
||||
private readonly cardService: CardService,
|
||||
private readonly transactionService: TransactionService,
|
||||
) {}
|
||||
|
||||
@Transactional()
|
||||
@ -221,6 +223,56 @@ export class JuniorService {
|
||||
return this.cardService.getWeeklySummary(juniorId);
|
||||
}
|
||||
|
||||
async getJuniorHome(juniorId: string, userId: string, size: number): Promise<JuniorHomeResponseDto> {
|
||||
this.logger.log(`Getting home for junior ${juniorId}`);
|
||||
|
||||
// Check if user is the junior themselves or their guardian
|
||||
let junior: Junior | null;
|
||||
if (juniorId === userId) {
|
||||
// User is the junior accessing their own home
|
||||
junior = await this.findJuniorById(juniorId, false);
|
||||
} else {
|
||||
// User might be the guardian accessing junior's home
|
||||
junior = await this.findJuniorById(juniorId, false, userId);
|
||||
}
|
||||
|
||||
if (!junior) {
|
||||
throw new BadRequestException('JUNIOR.NOT_FOUND');
|
||||
}
|
||||
|
||||
const card = junior.customer?.cards?.[0];
|
||||
const availableBalance = card ? Math.min(card.limit, card.account.balance) : 0;
|
||||
|
||||
const recentTransfers = await this.transactionService.getChildTransfers(juniorId, 1, size);
|
||||
|
||||
return new JuniorHomeResponseDto(availableBalance, recentTransfers);
|
||||
}
|
||||
|
||||
async getJuniorTransfers(
|
||||
juniorId: string,
|
||||
userId: string,
|
||||
page: number,
|
||||
size: number,
|
||||
): Promise<PagedChildTransfersResponseDto> {
|
||||
this.logger.log(`Getting transfers for junior ${juniorId}`);
|
||||
|
||||
// Check if user is the junior themselves or their guardian
|
||||
let junior: Junior | null;
|
||||
if (juniorId === userId) {
|
||||
// User is the junior accessing their own transfers
|
||||
junior = await this.findJuniorById(juniorId, false);
|
||||
} else {
|
||||
// User might be the guardian accessing junior's transfers
|
||||
junior = await this.findJuniorById(juniorId, false, userId);
|
||||
}
|
||||
|
||||
if (!junior) {
|
||||
throw new BadRequestException('JUNIOR.NOT_FOUND');
|
||||
}
|
||||
|
||||
return this.transactionService.getChildTransfersPaginated(juniorId, page, size);
|
||||
}
|
||||
|
||||
private async prepareJuniorImages(juniors: Junior[]) {
|
||||
this.logger.log(`Preparing junior images`);
|
||||
await Promise.all(
|
||||
|
||||
Reference in New Issue
Block a user