mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2026-03-10 20:41:46 +00:00
Merge pull request #83 from Zod-Alkhair/feature/notification-system-fcm-registration
feat: add timezone support to user and device entities
This commit is contained in:
@ -23,4 +23,13 @@ export class JuniorLoginRequestDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.fcmToken' }) })
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.fcmToken' }) })
|
||||||
fcmToken?: string;
|
fcmToken?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'Asia/Riyadh',
|
||||||
|
description: 'Device timezone (auto-detected from device OS)',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.timezone' }) })
|
||||||
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,4 +35,13 @@ export class LoginRequestDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.fcmToken' }) })
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.fcmToken' }) })
|
||||||
fcmToken?: string;
|
fcmToken?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'Asia/Riyadh',
|
||||||
|
description: 'Device timezone (auto-detected from device OS)',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.timezone' }) })
|
||||||
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,4 +115,13 @@ export class VerifyUserRequestDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.fcmToken' }) })
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.fcmToken' }) })
|
||||||
fcmToken?: string;
|
fcmToken?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'Asia/Riyadh',
|
||||||
|
description: 'Device timezone (auto-detected from device OS)',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.timezone' }) })
|
||||||
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,9 +87,9 @@ export class AuthService {
|
|||||||
const tokens = await this.generateAuthToken(user);
|
const tokens = await this.generateAuthToken(user);
|
||||||
this.logger.log(`User with phone number ${user.fullPhoneNumber} verified successfully`);
|
this.logger.log(`User with phone number ${user.fullPhoneNumber} verified successfully`);
|
||||||
|
|
||||||
// Register/update device with FCM token if provided
|
// Register/update device with FCM token and timezone if provided
|
||||||
if (verifyUserDto.fcmToken && verifyUserDto.deviceId) {
|
if (verifyUserDto.fcmToken && verifyUserDto.deviceId) {
|
||||||
await this.registerDeviceToken(user.id, verifyUserDto.deviceId, verifyUserDto.fcmToken);
|
await this.registerDeviceToken(user.id, verifyUserDto.deviceId, verifyUserDto.fcmToken, verifyUserDto.timezone);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [tokens, user];
|
return [tokens, user];
|
||||||
@ -278,9 +278,9 @@ export class AuthService {
|
|||||||
const tokens = await this.generateAuthToken(user);
|
const tokens = await this.generateAuthToken(user);
|
||||||
this.logger.log(`Password validated successfully for user`);
|
this.logger.log(`Password validated successfully for user`);
|
||||||
|
|
||||||
// Register/update device with FCM token if provided
|
// Register/update device with FCM token and timezone if provided
|
||||||
if (loginDto.fcmToken && loginDto.deviceId) {
|
if (loginDto.fcmToken && loginDto.deviceId) {
|
||||||
await this.registerDeviceToken(user.id, loginDto.deviceId, loginDto.fcmToken);
|
await this.registerDeviceToken(user.id, loginDto.deviceId, loginDto.fcmToken, loginDto.timezone);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [tokens, user];
|
return [tokens, user];
|
||||||
@ -304,22 +304,22 @@ export class AuthService {
|
|||||||
const tokens = await this.generateAuthToken(user);
|
const tokens = await this.generateAuthToken(user);
|
||||||
this.logger.log(`Password validated successfully for user`);
|
this.logger.log(`Password validated successfully for user`);
|
||||||
|
|
||||||
// Register/update device with FCM token if provided
|
// Register/update device with FCM token and timezone if provided
|
||||||
if (juniorLoginDto.fcmToken && juniorLoginDto.deviceId) {
|
if (juniorLoginDto.fcmToken && juniorLoginDto.deviceId) {
|
||||||
await this.registerDeviceToken(user.id, juniorLoginDto.deviceId, juniorLoginDto.fcmToken);
|
await this.registerDeviceToken(user.id, juniorLoginDto.deviceId, juniorLoginDto.fcmToken, juniorLoginDto.timezone);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [tokens, user];
|
return [tokens, user];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register or update device with FCM token
|
* Register or update device with FCM token and timezone
|
||||||
* This method handles:
|
* This method handles:
|
||||||
* 1. Device already exists for this user → Update FCM token
|
* 1. Device already exists for this user → Update FCM token and timezone
|
||||||
* 2. Device exists for different user → Transfer device to new user
|
* 2. Device exists for different user → Transfer device to new user
|
||||||
* 3. Device doesn't exist → Create new device
|
* 3. Device doesn't exist → Create new device
|
||||||
*/
|
*/
|
||||||
private async registerDeviceToken(userId: string, deviceId: string, fcmToken: string): Promise<void> {
|
private async registerDeviceToken(userId: string, deviceId: string, fcmToken: string, timezone?: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.logger.log(`Registering/updating device ${deviceId} with FCM token for user ${userId}`);
|
this.logger.log(`Registering/updating device ${deviceId} with FCM token for user ${userId}`);
|
||||||
|
|
||||||
@ -327,13 +327,14 @@ export class AuthService {
|
|||||||
const existingDeviceForUser = await this.deviceService.findUserDeviceById(deviceId, userId);
|
const existingDeviceForUser = await this.deviceService.findUserDeviceById(deviceId, userId);
|
||||||
|
|
||||||
if (existingDeviceForUser) {
|
if (existingDeviceForUser) {
|
||||||
// Device exists for this user → Update FCM token and last access time
|
// Device exists for this user → Update FCM token, timezone, and last access time
|
||||||
await this.deviceService.updateDevice(deviceId, {
|
await this.deviceService.updateDevice(deviceId, {
|
||||||
fcmToken,
|
fcmToken,
|
||||||
userId,
|
userId,
|
||||||
|
timezone, // Update timezone if provided
|
||||||
lastAccessOn: new Date(),
|
lastAccessOn: new Date(),
|
||||||
});
|
});
|
||||||
this.logger.log(`Device ${deviceId} updated with new FCM token for user ${userId}`);
|
this.logger.log(`Device ${deviceId} updated with new FCM token and timezone for user ${userId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,6 +349,7 @@ export class AuthService {
|
|||||||
await this.deviceService.updateDevice(deviceId, {
|
await this.deviceService.updateDevice(deviceId, {
|
||||||
userId,
|
userId,
|
||||||
fcmToken,
|
fcmToken,
|
||||||
|
timezone, // Update timezone if provided
|
||||||
lastAccessOn: new Date(),
|
lastAccessOn: new Date(),
|
||||||
});
|
});
|
||||||
this.logger.log(`Device ${deviceId} transferred from user ${existingDevice.userId} to user ${userId}`);
|
this.logger.log(`Device ${deviceId} transferred from user ${existingDevice.userId} to user ${userId}`);
|
||||||
@ -359,6 +361,7 @@ export class AuthService {
|
|||||||
deviceId,
|
deviceId,
|
||||||
userId,
|
userId,
|
||||||
fcmToken,
|
fcmToken,
|
||||||
|
timezone, // Store timezone if provided
|
||||||
lastAccessOn: new Date(),
|
lastAccessOn: new Date(),
|
||||||
});
|
});
|
||||||
this.logger.log(`New device ${deviceId} registered with FCM token for user ${userId}`);
|
this.logger.log(`New device ${deviceId} registered with FCM token for user ${userId}`);
|
||||||
|
|||||||
@ -26,12 +26,13 @@ export class NotificationsResponseDto {
|
|||||||
|
|
||||||
// Use event timestamp from data if available, otherwise use notification creation time
|
// Use event timestamp from data if available, otherwise use notification creation time
|
||||||
// This ensures notifications show when the event occurred, not when notification was saved
|
// This ensures notifications show when the event occurred, not when notification was saved
|
||||||
// Note: Timestamps are stored in UTC in the database, and should be converted to local timezone on the client side
|
// Note: Timestamps are stored in UTC. The client should convert to the user's local timezone.
|
||||||
if (notification.data?.timestamp) {
|
if (notification.data?.timestamp) {
|
||||||
// Parse the ISO string timestamp (which is in UTC)
|
// Parse the ISO string timestamp (which is in UTC)
|
||||||
// The client should convert this to the user's local timezone
|
// The client should convert this to the user's local timezone based on their device settings
|
||||||
this.createdAt = new Date(notification.data.timestamp);
|
this.createdAt = new Date(notification.data.timestamp);
|
||||||
} else {
|
} else {
|
||||||
|
// Use notification creation time (also in UTC)
|
||||||
this.createdAt = notification.createdAt;
|
this.createdAt = notification.createdAt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -109,11 +109,17 @@ export class TransactionNotificationListener {
|
|||||||
const cardWithUpdatedBalance = await this.cardService.getCardById(card.id);
|
const cardWithUpdatedBalance = await this.cardService.getCardById(card.id);
|
||||||
const balance = cardWithUpdatedBalance.account?.balance || card.account?.balance || 0;
|
const balance = cardWithUpdatedBalance.account?.balance || card.account?.balance || 0;
|
||||||
|
|
||||||
|
const accountCurrency = cardWithUpdatedBalance.account?.currency || card.account?.currency;
|
||||||
const currency = getCurrency(
|
const currency = getCurrency(
|
||||||
cardWithUpdatedBalance.account?.currency || card.account?.currency,
|
accountCurrency,
|
||||||
transaction.transactionCurrency,
|
transaction.transactionCurrency,
|
||||||
'SAR'
|
'SAR'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`[Child Notification] Account currency: ${accountCurrency}, Transaction currency: ${transaction.transactionCurrency}, Final currency: ${currency}, Balance: ${balance}, Amount: ${amount}`
|
||||||
|
);
|
||||||
|
|
||||||
const formattedAmount = formatCurrencyAmount(amount, currency);
|
const formattedAmount = formatCurrencyAmount(amount, currency);
|
||||||
const formattedBalance = formatCurrencyAmount(balance, currency);
|
const formattedBalance = formatCurrencyAmount(balance, currency);
|
||||||
|
|
||||||
@ -166,14 +172,14 @@ export class TransactionNotificationListener {
|
|||||||
data: {
|
data: {
|
||||||
transactionId: transaction.id,
|
transactionId: transaction.id,
|
||||||
amount: formattedAmount,
|
amount: formattedAmount,
|
||||||
currency: currency,
|
currency: currency, // ISO currency code (SAR, USD, etc.)
|
||||||
merchant: merchant,
|
merchant: merchant,
|
||||||
merchantCategory: transaction.merchantCategoryCode || 'OTHER',
|
merchantCategory: transaction.merchantCategoryCode || 'OTHER',
|
||||||
balance: formattedBalance,
|
balance: formattedBalance,
|
||||||
timestamp: transaction.transactionDate.toISOString(),
|
timestamp: transaction.transactionDate.toISOString(),
|
||||||
type: isTopUp ? 'TOP_UP' : 'SPENDING',
|
type: isTopUp ? 'TOP_UP' : 'SPENDING',
|
||||||
action: 'OPEN_TRANSACTION',
|
action: 'OPEN_TRANSACTION',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`✅ Notified user ${user.id} for transaction ${transaction.id}`);
|
this.logger.log(`✅ Notified user ${user.id} for transaction ${transaction.id}`);
|
||||||
@ -208,30 +214,43 @@ export class TransactionNotificationListener {
|
|||||||
const amount = transaction.transactionAmount;
|
const amount = transaction.transactionAmount;
|
||||||
const merchant = transaction.merchantName || 'a merchant';
|
const merchant = transaction.merchantName || 'a merchant';
|
||||||
|
|
||||||
// Get parent's account balance (not child's balance)
|
// Get parent's account balance (not child's balance) - reload to get fresh balance
|
||||||
let parentAccountBalance = 0;
|
let parentAccountBalance = 0;
|
||||||
let parentAccountCurrency: string | undefined;
|
let parentAccountCurrency: string | undefined;
|
||||||
try {
|
try {
|
||||||
const parentCustomer = customer?.junior?.guardian?.customer;
|
if (card.parentId) {
|
||||||
if (parentCustomer?.cards?.[0]?.account) {
|
// Always reload parent account to get fresh balance after transaction
|
||||||
parentAccountBalance = parentCustomer.cards[0].account.balance;
|
|
||||||
parentAccountCurrency = parentCustomer.cards[0].account.currency;
|
|
||||||
} else if (card.parentId) {
|
|
||||||
const parentAccount = await this.accountService.getAccountByCustomerId(card.parentId);
|
const parentAccount = await this.accountService.getAccountByCustomerId(card.parentId);
|
||||||
parentAccountBalance = parentAccount.balance;
|
parentAccountBalance = parentAccount.balance;
|
||||||
parentAccountCurrency = parentAccount.currency;
|
parentAccountCurrency = parentAccount.currency;
|
||||||
|
this.logger.debug(`Fetched parent account balance: ${parentAccountBalance}, currency: ${parentAccountCurrency}`);
|
||||||
|
} else {
|
||||||
|
const parentCustomer = customer?.junior?.guardian?.customer;
|
||||||
|
if (parentCustomer?.cards?.[0]?.account) {
|
||||||
|
// Reload to get fresh balance
|
||||||
|
const parentAccount = await this.accountService.getAccountByCustomerId(parentCustomer.id);
|
||||||
|
parentAccountBalance = parentAccount.balance;
|
||||||
|
parentAccountCurrency = parentAccount.currency;
|
||||||
|
this.logger.debug(`Fetched parent account balance via customer: ${parentAccountBalance}, currency: ${parentAccountCurrency}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
this.logger.warn(`Could not fetch parent account for parent notification, using child account balance as fallback`);
|
this.logger.warn(`Could not fetch parent account for parent notification: ${error?.message}, using child account balance as fallback`);
|
||||||
parentAccountBalance = card.account?.balance || 0;
|
parentAccountBalance = card.account?.balance || 0;
|
||||||
parentAccountCurrency = card.account?.currency;
|
parentAccountCurrency = card.account?.currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accountCurrency = parentAccountCurrency || card.account?.currency;
|
||||||
const currency = getCurrency(
|
const currency = getCurrency(
|
||||||
parentAccountCurrency || card.account?.currency,
|
accountCurrency,
|
||||||
transaction.transactionCurrency,
|
transaction.transactionCurrency,
|
||||||
'SAR'
|
'SAR'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`[Parent Spending Notification] Parent account currency: ${parentAccountCurrency}, Account currency: ${accountCurrency}, Transaction currency: ${transaction.transactionCurrency}, Final currency: ${currency}, Parent balance: ${parentAccountBalance}, Amount: ${amount}`
|
||||||
|
);
|
||||||
|
|
||||||
const formattedAmount = formatCurrencyAmount(amount, currency);
|
const formattedAmount = formatCurrencyAmount(amount, currency);
|
||||||
const formattedBalance = formatCurrencyAmount(parentAccountBalance, currency);
|
const formattedBalance = formatCurrencyAmount(parentAccountBalance, currency);
|
||||||
|
|
||||||
@ -271,8 +290,8 @@ export class TransactionNotificationListener {
|
|||||||
transactionId: transaction.id,
|
transactionId: transaction.id,
|
||||||
childId: childUser.id,
|
childId: childUser.id,
|
||||||
childName: childName,
|
childName: childName,
|
||||||
amount: amount.toString(),
|
amount: formattedAmount, // Use formatted amount instead of raw amount
|
||||||
currency: currency,
|
currency: currency, // ISO currency code (SAR, USD, etc.)
|
||||||
merchant: merchant,
|
merchant: merchant,
|
||||||
merchantCategory: transaction.merchantCategoryCode || 'OTHER',
|
merchantCategory: transaction.merchantCategoryCode || 'OTHER',
|
||||||
balance: formattedBalance,
|
balance: formattedBalance,
|
||||||
@ -313,30 +332,41 @@ export class TransactionNotificationListener {
|
|||||||
const childName = childUser?.firstName || defaultChildName;
|
const childName = childUser?.firstName || defaultChildName;
|
||||||
const amount = transaction.transactionAmount;
|
const amount = transaction.transactionAmount;
|
||||||
|
|
||||||
const parentCustomer = customer?.junior?.guardian?.customer;
|
// Always reload parent account to get updated balance after transfer
|
||||||
let parentAccount = parentCustomer?.cards?.[0]?.account;
|
let parentAccount: any = null;
|
||||||
|
if (card.parentId) {
|
||||||
// Reload parent account to get updated balance after transfer
|
|
||||||
if (!parentAccount && card.parentId) {
|
|
||||||
try {
|
try {
|
||||||
parentAccount = await this.accountService.getAccountByCustomerId(card.parentId);
|
parentAccount = await this.accountService.getAccountByCustomerId(card.parentId);
|
||||||
} catch (error) {
|
this.logger.debug(`Fetched parent account for top-up notification - balance: ${parentAccount.balance}, currency: ${parentAccount.currency}`);
|
||||||
this.logger.warn(`Could not fetch parent account for customer ${card.parentId}, using child account balance`);
|
} catch (error: any) {
|
||||||
|
this.logger.warn(`Could not fetch parent account for customer ${card.parentId}: ${error?.message}, using child account balance`);
|
||||||
}
|
}
|
||||||
} else if (parentAccount && card.parentId) {
|
}
|
||||||
// Reload to get fresh balance
|
|
||||||
try {
|
// If parent account not found, try via customer relation
|
||||||
parentAccount = await this.accountService.getAccountByCustomerId(card.parentId);
|
if (!parentAccount) {
|
||||||
} catch (error) {
|
const parentCustomer = customer?.junior?.guardian?.customer;
|
||||||
this.logger.warn(`Could not reload parent account, using cached balance`);
|
if (parentCustomer?.id) {
|
||||||
|
try {
|
||||||
|
parentAccount = await this.accountService.getAccountByCustomerId(parentCustomer.id);
|
||||||
|
this.logger.debug(`Fetched parent account via customer relation - balance: ${parentAccount.balance}, currency: ${parentAccount.currency}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(`Could not fetch parent account via customer: ${error?.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const balance = parentAccount?.balance || card.account?.balance || 0;
|
const balance = parentAccount?.balance || card.account?.balance || 0;
|
||||||
|
const accountCurrency = parentAccount?.currency || card.account?.currency;
|
||||||
const currency = getCurrency(
|
const currency = getCurrency(
|
||||||
parentAccount?.currency || card.account?.currency,
|
accountCurrency,
|
||||||
transaction.transactionCurrency,
|
transaction.transactionCurrency,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`[Parent Top-Up Notification] Parent account currency: ${parentAccount?.currency}, Account currency: ${accountCurrency}, Transaction currency: ${transaction.transactionCurrency}, Final currency: ${currency}, Parent balance: ${balance}, Amount: ${amount}`
|
||||||
|
);
|
||||||
|
|
||||||
const formattedAmount = formatCurrencyAmount(amount, currency);
|
const formattedAmount = formatCurrencyAmount(amount, currency);
|
||||||
const formattedBalance = formatCurrencyAmount(balance, currency);
|
const formattedBalance = formatCurrencyAmount(balance, currency);
|
||||||
|
|
||||||
@ -376,7 +406,7 @@ export class TransactionNotificationListener {
|
|||||||
childId: childUser.id,
|
childId: childUser.id,
|
||||||
childName: childName,
|
childName: childName,
|
||||||
amount: formattedAmount,
|
amount: formattedAmount,
|
||||||
currency: currency,
|
currency: currency, // ISO currency code (SAR, USD, etc.)
|
||||||
balance: formattedBalance,
|
balance: formattedBalance,
|
||||||
timestamp: transaction.transactionDate.toISOString(),
|
timestamp: transaction.transactionDate.toISOString(),
|
||||||
type: 'TOP_UP',
|
type: 'TOP_UP',
|
||||||
|
|||||||
@ -14,7 +14,15 @@ export class RedisModule {
|
|||||||
{
|
{
|
||||||
provide: 'REDIS_PUBLISHER',
|
provide: 'REDIS_PUBLISHER',
|
||||||
useFactory: async (configService: ConfigService) => {
|
useFactory: async (configService: ConfigService) => {
|
||||||
const publisher = createClient({ url: configService.get<string>('REDIS_URL') });
|
// Skip Redis connection during migration generation
|
||||||
|
if (process.env.MIGRATIONS_RUN === 'false') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const redisUrl = configService.get<string>('REDIS_URL');
|
||||||
|
if (!redisUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const publisher = createClient({ url: redisUrl });
|
||||||
await publisher.connect();
|
await publisher.connect();
|
||||||
return publisher;
|
return publisher;
|
||||||
},
|
},
|
||||||
@ -24,7 +32,15 @@ export class RedisModule {
|
|||||||
{
|
{
|
||||||
provide: 'REDIS_SUBSCRIBER',
|
provide: 'REDIS_SUBSCRIBER',
|
||||||
useFactory: async (configService: ConfigService) => {
|
useFactory: async (configService: ConfigService) => {
|
||||||
const subscriber = createClient({ url: configService.get<string>('REDIS_URL') });
|
// Skip Redis connection during migration generation
|
||||||
|
if (process.env.MIGRATIONS_RUN === 'false') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const redisUrl = configService.get<string>('REDIS_URL');
|
||||||
|
if (!redisUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const subscriber = createClient({ url: redisUrl });
|
||||||
await subscriber.connect();
|
await subscriber.connect();
|
||||||
return subscriber;
|
return subscriber;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -15,6 +15,10 @@ export class RedisPubSubService implements OnModuleInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
|
// Skip subscription during migration generation
|
||||||
|
if (process.env.MIGRATIONS_RUN === 'false' || !this.subscriber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.subscriber.subscribe(EventType.NOTIFICATION_CREATED, async (message) => {
|
this.subscriber.subscribe(EventType.NOTIFICATION_CREATED, async (message) => {
|
||||||
const data = JSON.parse(message);
|
const data = JSON.parse(message);
|
||||||
this.logger.log('Received message on NOTIFICATION_CREATED channel:', data);
|
this.logger.log('Received message on NOTIFICATION_CREATED channel:', data);
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export function formatCurrencyAmount(amount: number | string, currency: string):
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get currency from account or transaction, with fallback
|
* Get currency from account or transaction, with fallback
|
||||||
* @param accountCurrency - Currency from account entity
|
* @param accountCurrency - Currency from account entity (may be numeric like '682')
|
||||||
* @param transactionCurrency - Currency from transaction entity (may be numeric)
|
* @param transactionCurrency - Currency from transaction entity (may be numeric)
|
||||||
* @param fallback - Fallback currency (default: 'SAR')
|
* @param fallback - Fallback currency (default: 'SAR')
|
||||||
* @returns ISO currency code
|
* @returns ISO currency code
|
||||||
@ -90,9 +90,16 @@ export function getCurrency(
|
|||||||
transactionCurrency?: string | null,
|
transactionCurrency?: string | null,
|
||||||
fallback: string = 'SAR'
|
fallback: string = 'SAR'
|
||||||
): string {
|
): string {
|
||||||
// Prefer account currency (already in ISO format)
|
// Convert account currency first (it may be numeric like '682')
|
||||||
if (accountCurrency) {
|
if (accountCurrency) {
|
||||||
return accountCurrency;
|
const converted = numericToCurrencyCode(accountCurrency);
|
||||||
|
if (converted && converted !== accountCurrency) {
|
||||||
|
return converted; // Successfully converted from numeric to ISO
|
||||||
|
}
|
||||||
|
// If already ISO format, return as is
|
||||||
|
if (/^[A-Z]{3}$/.test(accountCurrency)) {
|
||||||
|
return accountCurrency;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert transaction currency (may be numeric)
|
// Convert transaction currency (may be numeric)
|
||||||
|
|||||||
18
src/db/migrations/1768395622276-AddTimezoneFields.ts
Normal file
18
src/db/migrations/1768395622276-AddTimezoneFields.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddTimezoneFields1768395622276 implements MigrationInterface {
|
||||||
|
name = 'AddTimezoneFields1768395622276'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD "timezone" character varying(50)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "devices" ADD "timezone" character varying(50)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "is_push_enabled" SET DEFAULT true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "is_push_enabled" SET DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "devices" DROP COLUMN "timezone"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "timezone"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -11,3 +11,4 @@ export * from './1765804942393-AddKycFieldsAndTransactions';
|
|||||||
export * from './1765877128065-AddNationalIdToKycTransactions';
|
export * from './1765877128065-AddNationalIdToKycTransactions';
|
||||||
export * from './1765891028260-RemoveOldCustomerColumns';
|
export * from './1765891028260-RemoveOldCustomerColumns';
|
||||||
export * from './1765975126402-RemoveAddressColumns';
|
export * from './1765975126402-RemoveAddressColumns';
|
||||||
|
export * from './1768395622276-AddTimezoneFields';
|
||||||
@ -34,4 +34,12 @@ export class UpdateUserRequestDto {
|
|||||||
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
|
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
dateOfBirth!: Date;
|
dateOfBirth!: Date;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: 'Asia/Riyadh',
|
||||||
|
description: 'User preferred timezone for reports/statements (e.g., "Asia/Riyadh", "America/New_York"). Leave empty or "Auto" to use device timezone.',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'user.timezone' }) })
|
||||||
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,9 @@ export class Device {
|
|||||||
@Column('varchar', { name: 'fcm_token', nullable: true })
|
@Column('varchar', { name: 'fcm_token', nullable: true })
|
||||||
fcmToken?: string | null;
|
fcmToken?: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', { name: 'timezone', nullable: true, length: 50 })
|
||||||
|
timezone?: string | null; // e.g., "Asia/Riyadh", "America/New_York" - auto-detected from device
|
||||||
|
|
||||||
@Column('timestamp with time zone', { name: 'last_access_on', default: () => 'CURRENT_TIMESTAMP' })
|
@Column('timestamp with time zone', { name: 'last_access_on', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
lastAccessOn!: Date;
|
lastAccessOn!: Date;
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,9 @@ export class User extends BaseEntity {
|
|||||||
@Column({ name: 'is_sms_enabled', default: false })
|
@Column({ name: 'is_sms_enabled', default: false })
|
||||||
isSmsEnabled!: boolean;
|
isSmsEnabled!: boolean;
|
||||||
|
|
||||||
|
@Column('varchar', { name: 'timezone', nullable: true, length: 50 })
|
||||||
|
timezone?: string | null; // User's preferred timezone for reports/statements (e.g., "Asia/Riyadh")
|
||||||
|
|
||||||
@Column('text', { nullable: true, array: true, name: 'roles' })
|
@Column('text', { nullable: true, array: true, name: 'roles' })
|
||||||
roles!: Roles[];
|
roles!: Roles[];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user