Compare commits

...

10 Commits

Author SHA1 Message Date
506974afc8 Enhance profile picture handling in JuniorService to ensure foreign key consistency and validate document ownership before assignment. 2025-11-09 12:42:48 +03:00
8b00cda23d Update return value in updateJunior method to fetch updated junior details by ID instead of returning the junior object directly. 2025-11-09 12:25:37 +03:00
12cc88a50e Merge pull request #59 from HamzaSha1/money-request-to-use-the-parint-account
Refactor balance check in increaseReservedBalance method to delegate …
2025-11-02 12:41:51 +03:00
2172051093 Refactor balance check in increaseReservedBalance method to delegate validation to the caller, improving clarity and responsibility separation. 2025-11-02 12:41:16 +03:00
a6a573957c Merge pull request #58 from HamzaSha1/money-request-to-use-the-parint-account
add more loggs
2025-11-02 12:35:31 +03:00
d6fb5f48d9 add more loggs 2025-11-02 12:34:41 +03:00
b0011eb7cc Merge pull request #57 from HamzaSha1/money-request-to-use-the-parint-account
Money request to use the parint account
2025-11-02 12:07:13 +03:00
99af65a300 money-request to use the parent card 2025-11-02 11:57:41 +03:00
0c9b40132a Merge pull request #56 from HamzaSha1/ZOD-344-after-a-child-completes-registration-using-the-qr-code-the-same-qr-code-remains-valid-and-allows-the-child-to-register-again-instead-of-expiring
ZOD-344-Add QR code validation error handling and localization support
2025-11-02 11:02:25 +03:00
3b295ea79f ZOD-344-Add QR code validation error handling and localization support
- Introduced new error handling for already used or expired QR codes in JuniorService.
- Added corresponding localization entries in Arabic and English app.json files for QR code validation messages.
2025-11-02 10:52:43 +03:00
6 changed files with 67 additions and 12 deletions

View File

@ -64,9 +64,8 @@ export class AccountService {
} }
increaseReservedBalance(account: Account, amount: number) { increaseReservedBalance(account: Account, amount: number) {
if (account.balance < account.reservedBalance + amount) { // Balance check is performed by the caller (e.g., transferToChild)
throw new UnprocessableEntityException('CARD.INSUFFICIENT_BALANCE'); // to ensure correct account (guardian vs child) is validated
}
return this.accountRepository.increaseReservedBalance(account.id, amount); return this.accountRepository.increaseReservedBalance(account.id, amount);
} }

View File

@ -148,7 +148,18 @@ export class CardService {
async transferToChild(juniorId: string, amount: number) { async transferToChild(juniorId: string, amount: number) {
const card = await this.getCardByCustomerId(juniorId); 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'); throw new BadRequestException('CARD.INSUFFICIENT_BALANCE');
} }
@ -156,7 +167,7 @@ export class CardService {
await Promise.all([ await Promise.all([
this.neoleapService.updateCardControl(card.cardReference, finalAmount.toNumber()), this.neoleapService.updateCardControl(card.cardReference, finalAmount.toNumber()),
this.updateCardLimit(card.id, 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), this.transactionService.createInternalChildTransaction(card.id, amount),
]); ]);

View File

@ -42,10 +42,18 @@ export class TransactionService {
const total = new Decimal(body.transactionAmount).plus(body.billingAmount).plus(body.fees).plus(body.vatOnFees); const total = new Decimal(body.transactionAmount).plus(body.billingAmount).plus(body.fees).plus(body.vatOnFees);
if (card.customerType === CustomerType.CHILD) { 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([ await Promise.all([
this.accountService.decreaseAccountBalance(card.account.accountReference, total.toNumber()), this.accountService.decreaseAccountBalance(card.account.accountReference, total.toNumber()),
this.accountService.decrementReservedBalance(card.account, total.toNumber()), this.accountService.decrementReservedBalance(card.account, total.toNumber()),
]); ]);
}
} else { } else {
await this.accountService.decreaseAccountBalance(card.account.accountReference, total.toNumber()); await this.accountService.decreaseAccountBalance(card.account.accountReference, total.toNumber());
} }

View File

@ -19,6 +19,10 @@
"TOKEN_EXPIRED": "رمز المستخدم منتهي الصلاحية." "TOKEN_EXPIRED": "رمز المستخدم منتهي الصلاحية."
}, },
"QR": {
"CODE_USED_OR_EXPIRED": "تم استخدام رمز QR مسبقًا أو انتهت صلاحيته."
},
"USER": { "USER": {
"PHONE_ALREADY_VERIFIED": "تم التحقق من رقم الهاتف بالفعل.", "PHONE_ALREADY_VERIFIED": "تم التحقق من رقم الهاتف بالفعل.",
"EMAIL_ALREADY_VERIFIED": "تم التحقق من عنوان البريد الإلكتروني بالفعل.", "EMAIL_ALREADY_VERIFIED": "تم التحقق من عنوان البريد الإلكتروني بالفعل.",

View File

@ -19,6 +19,10 @@
"TOKEN_EXPIRED": "The user token has expired." "TOKEN_EXPIRED": "The user token has expired."
}, },
"QR": {
"CODE_USED_OR_EXPIRED": "The QR code has already been used or expired."
},
"USER": { "USER": {
"PHONE_ALREADY_VERIFIED": "The phone number has already been verified.", "PHONE_ALREADY_VERIFIED": "The phone number has already been verified.",
"EMAIL_ALREADY_VERIFIED": "The email address has already been verified.", "EMAIL_ALREADY_VERIFIED": "The email address has already been verified.",

View File

@ -5,6 +5,7 @@ import { Roles } from '~/auth/enums';
import { CardService, TransactionService } from '~/card/services'; import { CardService, TransactionService } from '~/card/services';
import { NeoLeapService } from '~/common/modules/neoleap/services'; import { NeoLeapService } from '~/common/modules/neoleap/services';
import { PageOptionsRequestDto } from '~/core/dtos'; import { PageOptionsRequestDto } from '~/core/dtos';
import { ErrorCategory } from '~/core/enums';
import { setIf } from '~/core/utils'; import { setIf } from '~/core/utils';
import { CustomerService } from '~/customer/services'; import { CustomerService } from '~/customer/services';
import { DocumentService, OciService } from '~/document/services'; import { DocumentService, OciService } from '~/document/services';
@ -113,7 +114,28 @@ export class JuniorService {
} }
junior.customer.user.email = body.email; junior.customer.user.email = body.email;
} }
setIf(user, 'profilePictureId', body.profilePictureId); // Update profile picture: ensure FK and relation are consistent to avoid TypeORM overriding the FK
if (typeof body.profilePictureId !== 'undefined') {
if (body.profilePictureId) {
const document = await this.documentService.findDocumentById(body.profilePictureId);
if (!document) {
this.logger.error(`Document with id ${body.profilePictureId} not found`);
throw new BadRequestException('DOCUMENT.NOT_FOUND');
}
if (document.createdById !== juniorId) {
this.logger.error(
`Document with id ${body.profilePictureId} does not belong to user ${juniorId}`,
);
}
user.profilePictureId = body.profilePictureId;
// assign relation to keep it consistent with FK during save
user.profilePicture = document as any;
} else {
// if empty string provided (unlikely), clear relation and FK
user.profilePicture = null as any;
user.profilePictureId = null as any;
}
}
setIf(user, 'firstName', body.firstName); setIf(user, 'firstName', body.firstName);
setIf(user, 'lastName', body.lastName); setIf(user, 'lastName', body.lastName);
@ -125,7 +147,7 @@ export class JuniorService {
setIf(junior, 'relationship', body.relationship); setIf(junior, 'relationship', body.relationship);
await Promise.all([junior.save(), customer.save(), user.save()]); await Promise.all([junior.save(), customer.save(), user.save()]);
this.logger.log(`Junior ${juniorId} updated successfully`); this.logger.log(`Junior ${juniorId} updated successfully`);
return junior; return this.findJuniorById(juniorId, false, guardianId);
} }
@Transactional() @Transactional()
@ -158,7 +180,14 @@ export class JuniorService {
async validateToken(token: string) { async validateToken(token: string) {
this.logger.log(`Validating token ${token}`); this.logger.log(`Validating token ${token}`);
const juniorId = await this.userTokenService.validateToken(token, UserType.JUNIOR); 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) { async generateToken(juniorId: string) {