mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-11-26 00:24:54 +00:00
Compare commits
11 Commits
596562f6dc
...
money-requ
| Author | SHA1 | Date | |
|---|---|---|---|
| 2172051093 | |||
| d6fb5f48d9 | |||
| 99af65a300 | |||
| 0c9b40132a | |||
| 3b295ea79f | |||
| 5ffe18ede3 | |||
| a3a61b4923 | |||
| 39d5fc1869 | |||
| 05a6ad2d84 | |||
| 5649d24724 | |||
| bbeece9e03 |
@ -64,9 +64,8 @@ export class AccountService {
|
||||
}
|
||||
|
||||
increaseReservedBalance(account: Account, amount: number) {
|
||||
if (account.balance < account.reservedBalance + amount) {
|
||||
throw new UnprocessableEntityException('CARD.INSUFFICIENT_BALANCE');
|
||||
}
|
||||
// Balance check is performed by the caller (e.g., transferToChild)
|
||||
// to ensure correct account (guardian vs child) is validated
|
||||
return this.accountRepository.increaseReservedBalance(account.id, amount);
|
||||
}
|
||||
|
||||
|
||||
@ -148,7 +148,18 @@ export class CardService {
|
||||
async transferToChild(juniorId: string, amount: number) {
|
||||
const card = await this.getCardByCustomerId(juniorId);
|
||||
|
||||
if (amount > card.account.balance - card.account.reservedBalance) {
|
||||
this.logger.debug(`Transfer to child - juniorId: ${juniorId}, parentId: ${card.parentId}, cardId: ${card.id}`);
|
||||
this.logger.debug(`Card account - balance: ${card.account.balance}, reserved: ${card.account.reservedBalance}`);
|
||||
|
||||
const fundingAccount = card.parentId
|
||||
? await this.accountService.getAccountByCustomerId(card.parentId)
|
||||
: card.account;
|
||||
|
||||
this.logger.debug(`Funding account - balance: ${fundingAccount.balance}, reserved: ${fundingAccount.reservedBalance}, available: ${fundingAccount.balance - fundingAccount.reservedBalance}`);
|
||||
this.logger.debug(`Amount requested: ${amount}`);
|
||||
|
||||
if (amount > fundingAccount.balance - fundingAccount.reservedBalance) {
|
||||
this.logger.error(`Insufficient balance - requested: ${amount}, available: ${fundingAccount.balance - fundingAccount.reservedBalance}`);
|
||||
throw new BadRequestException('CARD.INSUFFICIENT_BALANCE');
|
||||
}
|
||||
|
||||
@ -156,15 +167,15 @@ export class CardService {
|
||||
await Promise.all([
|
||||
this.neoleapService.updateCardControl(card.cardReference, finalAmount.toNumber()),
|
||||
this.updateCardLimit(card.id, finalAmount.toNumber()),
|
||||
this.accountService.increaseReservedBalance(card.account, amount),
|
||||
this.accountService.increaseReservedBalance(fundingAccount, amount),
|
||||
this.transactionService.createInternalChildTransaction(card.id, amount),
|
||||
]);
|
||||
|
||||
return finalAmount.toNumber();
|
||||
}
|
||||
|
||||
getWeeklySummary(juniorId: string) {
|
||||
return this.transactionService.getWeeklySummary(juniorId);
|
||||
getWeeklySummary(juniorId: string, startDate?: Date, endDate?: Date) {
|
||||
return this.transactionService.getWeeklySummary(juniorId, startDate, endDate);
|
||||
}
|
||||
|
||||
fundIban(iban: string, amount: number) {
|
||||
|
||||
@ -42,10 +42,18 @@ export class TransactionService {
|
||||
const total = new Decimal(body.transactionAmount).plus(body.billingAmount).plus(body.fees).plus(body.vatOnFees);
|
||||
|
||||
if (card.customerType === CustomerType.CHILD) {
|
||||
if (card.parentId) {
|
||||
const parentAccount = await this.accountService.getAccountByCustomerId(card.parentId);
|
||||
await Promise.all([
|
||||
this.accountService.decreaseAccountBalance(parentAccount.accountReference, total.toNumber()),
|
||||
this.accountService.decrementReservedBalance(parentAccount, total.toNumber()),
|
||||
]);
|
||||
} else {
|
||||
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());
|
||||
}
|
||||
@ -84,15 +92,28 @@ export class TransactionService {
|
||||
return existingTransaction;
|
||||
}
|
||||
|
||||
async getWeeklySummary(juniorId: string) {
|
||||
const startOfWeek = moment().startOf('week').toDate();
|
||||
const endOfWeek = moment().endOf('week').toDate();
|
||||
async getWeeklySummary(juniorId: string, startDate?: Date, endDate?: Date) {
|
||||
let startOfWeek: Date;
|
||||
let endOfWeek: Date;
|
||||
|
||||
if (startDate && endDate) {
|
||||
startOfWeek = startDate;
|
||||
endOfWeek = endDate;
|
||||
} else {
|
||||
const now = moment();
|
||||
const dayOfWeek = now.day();
|
||||
|
||||
startOfWeek = moment().subtract(dayOfWeek, 'days').startOf('day').toDate();
|
||||
|
||||
endOfWeek = moment().add(6 - dayOfWeek, 'days').endOf('day').toDate();
|
||||
}
|
||||
|
||||
const transactions = await this.transactionRepository.getTransactionsForCardWithinDateRange(
|
||||
juniorId,
|
||||
startOfWeek,
|
||||
endOfWeek,
|
||||
);
|
||||
|
||||
const summary = {
|
||||
startOfWeek: startOfWeek,
|
||||
endOfWeek: endOfWeek,
|
||||
|
||||
@ -19,6 +19,10 @@
|
||||
"TOKEN_EXPIRED": "رمز المستخدم منتهي الصلاحية."
|
||||
},
|
||||
|
||||
"QR": {
|
||||
"CODE_USED_OR_EXPIRED": "تم استخدام رمز QR مسبقًا أو انتهت صلاحيته."
|
||||
},
|
||||
|
||||
"USER": {
|
||||
"PHONE_ALREADY_VERIFIED": "تم التحقق من رقم الهاتف بالفعل.",
|
||||
"EMAIL_ALREADY_VERIFIED": "تم التحقق من عنوان البريد الإلكتروني بالفعل.",
|
||||
|
||||
@ -19,6 +19,10 @@
|
||||
"TOKEN_EXPIRED": "The user token has expired."
|
||||
},
|
||||
|
||||
"QR": {
|
||||
"CODE_USED_OR_EXPIRED": "The QR code has already been used or expired."
|
||||
},
|
||||
|
||||
"USER": {
|
||||
"PHONE_ALREADY_VERIFIED": "The phone number has already been verified.",
|
||||
"EMAIL_ALREADY_VERIFIED": "The email address has already been verified.",
|
||||
|
||||
@ -151,11 +151,17 @@ export class JuniorController {
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@ApiDataResponse(WeeklySummaryResponseDto)
|
||||
@ApiQuery({ name: 'startUtc', required: false, type: String, example: '2025-10-20T00:00:00.000Z', description: 'Start date (defaults to start of current week)' })
|
||||
@ApiQuery({ name: 'endUtc', required: false, type: String, example: '2025-10-26T23:59:59.999Z', description: 'End date (defaults to end of current week)' })
|
||||
async getWeeklySummary(
|
||||
@Param('juniorId', CustomParseUUIDPipe) juniorId: string,
|
||||
@AuthenticatedUser() user: IJwtPayload,
|
||||
@Query('startUtc') startUtc?: string,
|
||||
@Query('endUtc') endUtc?: string,
|
||||
) {
|
||||
const summary = await this.juniorService.getWeeklySummary(juniorId, user.sub);
|
||||
const startDate = startUtc ? new Date(startUtc) : undefined;
|
||||
const endDate = endUtc ? new Date(endUtc) : undefined;
|
||||
const summary = await this.juniorService.getWeeklySummary(juniorId, user.sub, startDate, endDate);
|
||||
return ResponseFactory.data(summary);
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import { Roles } from '~/auth/enums';
|
||||
import { CardService, TransactionService } from '~/card/services';
|
||||
import { NeoLeapService } from '~/common/modules/neoleap/services';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { ErrorCategory } from '~/core/enums';
|
||||
import { setIf } from '~/core/utils';
|
||||
import { CustomerService } from '~/customer/services';
|
||||
import { DocumentService, OciService } from '~/document/services';
|
||||
@ -158,7 +159,14 @@ export class JuniorService {
|
||||
async validateToken(token: string) {
|
||||
this.logger.log(`Validating token ${token}`);
|
||||
const juniorId = await this.userTokenService.validateToken(token, UserType.JUNIOR);
|
||||
return this.findJuniorById(juniorId!, true);
|
||||
const junior = await this.findJuniorById(juniorId!, true);
|
||||
|
||||
if (junior.customer?.user?.password) {
|
||||
this.logger.error(`Token ${token} already used for junior ${juniorId}`);
|
||||
throw new BadRequestException({ message: 'QR.CODE_USED_OR_EXPIRED', category: ErrorCategory.BUSINESS_ERROR });
|
||||
}
|
||||
|
||||
return junior;
|
||||
}
|
||||
|
||||
async generateToken(juniorId: string) {
|
||||
@ -212,8 +220,8 @@ export class JuniorService {
|
||||
this.logger.log(`Junior ${juniorId} deleted successfully`);
|
||||
}
|
||||
|
||||
getWeeklySummary(juniorId: string, guardianId: string) {
|
||||
const doesBelong = this.doesJuniorBelongToGuardian(guardianId, juniorId);
|
||||
async getWeeklySummary(juniorId: string, guardianId: string, startDate?: Date, endDate?: Date) {
|
||||
const doesBelong = await this.doesJuniorBelongToGuardian(guardianId, juniorId);
|
||||
|
||||
if (!doesBelong) {
|
||||
this.logger.error(`Junior ${juniorId} does not belong to guardian ${guardianId}`);
|
||||
@ -221,7 +229,7 @@ export class JuniorService {
|
||||
}
|
||||
|
||||
this.logger.log(`Getting weekly summary for junior ${juniorId}`);
|
||||
return this.cardService.getWeeklySummary(juniorId);
|
||||
return this.cardService.getWeeklySummary(juniorId, startDate, endDate);
|
||||
}
|
||||
|
||||
async getJuniorHome(juniorId: string, userId: string, size: number): Promise<JuniorHomeResponseDto> {
|
||||
|
||||
@ -226,7 +226,15 @@ export class UserService {
|
||||
|
||||
if (userWithEmail) {
|
||||
if (userWithEmail.id === userId) {
|
||||
return;
|
||||
this.logger.log(`Generating OTP for current email ${email} for user ${userId}`);
|
||||
await this.userRepository.update(userId, { isEmailVerified: false });
|
||||
|
||||
return this.otpService.generateAndSendOtp({
|
||||
userId,
|
||||
recipient: email,
|
||||
otpType: OtpType.EMAIL,
|
||||
scope: OtpScope.VERIFY_EMAIL,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.error(`Email ${email} is already taken by another user`);
|
||||
|
||||
Reference in New Issue
Block a user