create wallet transaction after order created

This commit is contained in:
2025-12-29 10:43:42 +03:30
parent 89adbfdb33
commit b9ef611a76
5 changed files with 107 additions and 70 deletions
@@ -9,22 +9,25 @@ import { ConfigService } from '@nestjs/config';
import { OrderRepository } from '../repositories/order.repository'; import { OrderRepository } from '../repositories/order.repository';
import { OrderStatus } from '../interface/order.interface'; import { OrderStatus } from '../interface/order.interface';
import { PaymentMethodEnum } from 'src/modules/payments/interface/payment'; import { PaymentMethodEnum } from 'src/modules/payments/interface/payment';
import { UserService } from 'src/modules/users/providers/user.service';
import { WalletTransactionReason, WalletTransactionType } from 'src/modules/users/interface/wallet';
@Injectable() @Injectable()
export class OrderListeners { export class OrderListeners {
private readonly logger = new Logger(OrderListeners.name); private readonly logger = new Logger(OrderListeners.name);
private orderCreatedSmsTemplateId: string; private orderCreatedSmsTemplateId: string;
private orderStatusChangedSmsTemplateId: string; private orderStatusChangedSmsTemplateId: string;
private orderCompletedSmsTemplateId: string; // private orderCompletedSmsTemplateId: string;
constructor( constructor(
private readonly adminService: AdminRepository, private readonly adminService: AdminRepository,
private readonly OrderRepository: OrderRepository, private readonly OrderRepository: OrderRepository,
private readonly notificationService: NotificationService, private readonly notificationService: NotificationService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly userService: UserService,
) { ) {
this.orderCreatedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_CREATED') ?? '123'; this.orderCreatedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_CREATED') ?? '123';
this.orderStatusChangedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_STATUS_CHANGE') ?? '123'; this.orderStatusChangedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_STATUS_CHANGE') ?? '123';
this.orderCompletedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_STATUS_COMPLETED') ?? '123'; // this.orderCompletedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_STATUS_COMPLETED') ?? '123';
} }
private getStatusFarsi(status: OrderStatus): string { private getStatusFarsi(status: OrderStatus): string {
@@ -52,6 +55,8 @@ export class OrderListeners {
if (order?.paymentMethod.method === PaymentMethodEnum.Online) { if (order?.paymentMethod.method === PaymentMethodEnum.Online) {
return; return;
} }
// get admnin os restuaraant that have order permissuins // get admnin os restuaraant that have order permissuins
const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.MANAGE_ORDERS); const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.MANAGE_ORDERS);
const recipients = admins.map(admin => ({ const recipients = admins.map(admin => ({
@@ -99,20 +104,39 @@ export class OrderListeners {
this.logger.log( this.logger.log(
`Order status changed event received: ${event.orderId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`, `Order status changed event received: ${event.orderId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`,
); );
//TODO : REFACTOR to use queue or other way to handle this
const recipients = [ const recipients = [
{ {
userId: event.userId, userId: event.userId,
}, },
]; ];
if(event.newStatus === OrderStatus.COMPLETED) { if (event.newStatus === OrderStatus.COMPLETED) {
if (!event?.userId) {
this.logger.log(
`User not found for order: ${event.orderId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`,
);
}
const order = await this.OrderRepository.findOne(event.orderId);
if (!order) {
this.logger.log(
`Order not found for order: ${event.orderId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`,
);
return;
}
this.userService.createWalletTransaction(event.userId, event.restaurantId, {
amount: order.subTotal,
type: WalletTransactionType.CREDIT,
reason: WalletTransactionReason.ORDER_COMPLETED_DEPOSIT,
});
await this.notificationService.sendNotification({ await this.notificationService.sendNotification({
restaurantId: event.restaurantId, restaurantId: event.restaurantId,
message: { message: {
title: NotifTitleEnum.ORDER_STATUS_CHANGED, title: NotifTitleEnum.ORDER_STATUS_CHANGED,
content: `لطفابرای ثبت نظر سفارش ${event.orderNumber} به اپ مراجعه کنید`, content: `لطفابرای ثبت نظر سفارش ${event.orderNumber} به اپ مراجعه کنید`,
sms: { sms: {
templateId: this.orderCreatedSmsTemplateId , templateId: this.orderCreatedSmsTemplateId,
parameters: { parameters: {
orderNumber: event.orderNumber, orderNumber: event.orderNumber,
}, },
@@ -132,7 +156,7 @@ export class OrderListeners {
priority: 1, priority: 1,
}, },
}); });
}else{ } else {
await this.notificationService.sendNotification({ await this.notificationService.sendNotification({
restaurantId: event.restaurantId, restaurantId: event.restaurantId,
message: { message: {
@@ -169,4 +193,4 @@ export class OrderListeners {
} }
} }
} }
+4 -2
View File
@@ -20,7 +20,8 @@ import { OrdersCrone } from './crone/order.crone';
import { AdminModule } from '../admin/admin.module'; import { AdminModule } from '../admin/admin.module';
import { NotificationsModule } from '../notifications/notifications.module'; import { NotificationsModule } from '../notifications/notifications.module';
import { InventoryModule } from '../inventory/inventory.module'; import { InventoryModule } from '../inventory/inventory.module';
import { UserModule } from '../users/user.module';
@Module({ @Module({
imports: [ imports: [
MikroOrmModule.forFeature([Order, OrderItem, User, Restaurant, Food, UserAddress, PaymentMethod]), MikroOrmModule.forFeature([Order, OrderItem, User, Restaurant, Food, UserAddress, PaymentMethod]),
@@ -31,7 +32,8 @@ import { InventoryModule } from '../inventory/inventory.module';
JwtModule, JwtModule,
AdminModule, AdminModule,
NotificationsModule, NotificationsModule,
InventoryModule InventoryModule,
forwardRef(() => UserModule)
], ],
controllers: [OrdersController], controllers: [OrdersController],
providers: [OrdersService, OrderRepository, OrderListeners, OrdersCrone], providers: [OrdersService, OrderRepository, OrderListeners, OrdersCrone],
+1
View File
@@ -10,4 +10,5 @@ export enum WalletTransactionReason {
CONVERT_SCORE_TO_WALLET = 'convert_score_to_wallet', CONVERT_SCORE_TO_WALLET = 'convert_score_to_wallet',
WITHDRAW = 'withdraw', WITHDRAW = 'withdraw',
DEPOSIT = 'deposit', DEPOSIT = 'deposit',
ORDER_COMPLETED_DEPOSIT = 'order_completed_deposit',
} }
+55 -43
View File
@@ -2,6 +2,7 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/comm
import { FilterQuery, RequiredEntityData } from '@mikro-orm/core'; import { FilterQuery, RequiredEntityData } from '@mikro-orm/core';
import { User } from '../entities/user.entity'; import { User } from '../entities/user.entity';
import { UserAddress } from '../entities/user-address.entity'; import { UserAddress } from '../entities/user-address.entity';
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
import { EntityManager } from '@mikro-orm/postgresql'; import { EntityManager } from '@mikro-orm/postgresql';
import { UpdateUserDto } from '../dto/update-user.dto'; import { UpdateUserDto } from '../dto/update-user.dto';
import { FindUsersDto } from '../dto/find-user.dto'; import { FindUsersDto } from '../dto/find-user.dto';
@@ -16,6 +17,7 @@ import { RestRepository } from 'src/modules/restaurants/repositories/rest.reposi
import { UserWallet } from '../entities/user-wallet.entity'; import { UserWallet } from '../entities/user-wallet.entity';
import { UserWalletRepository } from '../repositories/user-wallet.repository'; import { UserWalletRepository } from '../repositories/user-wallet.repository';
import { WalletService } from './wallet.service'; import { WalletService } from './wallet.service';
import { WalletTransactionReason, WalletTransactionType } from '../interface/wallet';
@Injectable() @Injectable()
export class UserService { export class UserService {
@@ -298,54 +300,62 @@ export class UserService {
} }
async convertScoreToWallet(userId: string, restId: string) { async convertScoreToWallet(userId: string, restId: string) {
const user = await this.userRepository.findOne({ id: userId }); return this.em.transactional(async em => {
if (!user) { const user = await em.findOne(User, { id: userId });
throw new NotFoundException(`User with ID ${userId} not found.`); if (!user) {
} throw new NotFoundException(`User with ID ${userId} not found.`);
const restaurant = await this.restaurantRepository.findOne({ id: restId }); }
if (!restaurant) { const restaurant = await em.findOne(Restaurant, { id: restId });
throw new NotFoundException(`Restaurant with ID ${restId} not found.`); if (!restaurant) {
} throw new NotFoundException(`Restaurant with ID ${restId} not found.`);
let userWallet = await this.userWalletRepository.findOne({ user: { id: userId }, restaurant: { id: restId } }); }
if (!userWallet) { let userWallet = await em.findOne(UserWallet, { user: { id: userId }, restaurant: { id: restId } });
userWallet = this.userWalletRepository.create({ user: user, restaurant, wallet: 0, points: 0 }); if (!userWallet) {
await this.em.persistAndFlush(userWallet); userWallet = em.create(UserWallet, { user: user, restaurant, wallet: 0, points: 0 });
} await em.persistAndFlush(userWallet);
// Determine how many points to convert }
const pointsToConvert = userWallet.points; // Determine how many points to convert
const pointsToConvert = userWallet.points;
// Validate points to convert // Validate points to convert
if (pointsToConvert <= 0) { if (pointsToConvert <= 0) {
throw new BadRequestException('Points to convert must be greater than 0.'); throw new BadRequestException('Points to convert must be greater than 0.');
} }
if (userWallet.points < pointsToConvert) { if (userWallet.points < pointsToConvert) {
throw new BadRequestException( throw new BadRequestException(
`Insufficient points. Available: ${userWallet.points}, Requested: ${pointsToConvert}.`, `Insufficient points. Available: ${userWallet.points}, Requested: ${pointsToConvert}.`,
); );
} }
if (!restaurant.score) { if (!restaurant.score) {
throw new BadRequestException('Restaurant score not found.'); throw new BadRequestException('Restaurant score not found.');
} }
if (!restaurant.score.purchaseScore) { if (!restaurant.score.purchaseScore) {
throw new BadRequestException('Restaurant purchase score not found.'); throw new BadRequestException('Restaurant purchase score not found.');
} }
if (!restaurant.score.purchaseAmount) { if (!restaurant.score.purchaseAmount) {
throw new BadRequestException('Restaurant purchase amount not found.'); throw new BadRequestException('Restaurant purchase amount not found.');
} }
const walletIncreaseAmount = const walletIncreaseAmount =
(Number(pointsToConvert) * Number(restaurant.score.purchaseAmount)) / Number(restaurant.score.purchaseScore); (Number(pointsToConvert) * Number(restaurant.score.purchaseAmount)) / Number(restaurant.score.purchaseScore);
// Convert points to wallet (1:1 ratio - can be made configurable) // Convert points to wallet (1:1 ratio - can be made configurable)
// Update user's wallet and points this.walletService.createTransaction(em, userId, restId, {
userWallet.wallet = (userWallet.wallet || 0) + walletIncreaseAmount; amount: walletIncreaseAmount,
userWallet.points = (userWallet.points || 0) - pointsToConvert; type: WalletTransactionType.CREDIT,
reason: WalletTransactionReason.CONVERT_SCORE_TO_WALLET,
});
// Update user's wallet and points
userWallet.wallet = (userWallet.wallet || 0) + walletIncreaseAmount;
userWallet.points = (userWallet.points || 0) - pointsToConvert;
await this.em.flush(); await em.flush();
return user; return user;
});
} }
getUserWallet(userId: string, restId: string): Promise<UserWallet | null> { getUserWallet(userId: string, restId: string): Promise<UserWallet | null> {
return this.walletService.getUserWallet(userId, restId); return this.walletService.getUserWallet(userId, restId);
} }
@@ -354,8 +364,10 @@ export class UserService {
return this.walletService.getUserWalletTransactions(userId, restId, dto); return this.walletService.getUserWalletTransactions(userId, restId, dto);
} }
createWalletTransaction(userId: string, restId: string, dto: CreateWalletTransactionDto) { async createWalletTransaction(userId: string, restId: string, dto: CreateWalletTransactionDto) {
return this.walletService.createTransaction(userId, restId, dto); return this.em.transactional(async em => {
return this.walletService.createTransaction(em, userId, restId, dto);
});
} }
} }
+16 -18
View File
@@ -113,6 +113,7 @@ export class WalletService {
} }
async createTransaction( async createTransaction(
em: EntityManager,
userId: string, userId: string,
restId: string, restId: string,
dto: CreateWalletTransactionDto, dto: CreateWalletTransactionDto,
@@ -125,7 +126,7 @@ export class WalletService {
} }
// Find the user's wallet for this restaurant // Find the user's wallet for this restaurant
const userWallet = await this.userWalletRepository.findOne({ const userWallet = await em.findOne(UserWallet, {
user: { id: userId }, user: { id: userId },
restaurant: { id: restId }, restaurant: { id: restId },
}); });
@@ -151,24 +152,21 @@ export class WalletService {
throw new BadRequestException(`Invalid transaction type: ${type}`); throw new BadRequestException(`Invalid transaction type: ${type}`);
} }
// Use transaction to ensure consistency // Update wallet balance
return await this.em.transactional(async (em) => { userWallet.wallet = newBalance;
// Update wallet balance await em.persistAndFlush(userWallet);
userWallet.wallet = newBalance;
await em.persistAndFlush(userWallet);
// Create transaction record // Create transaction record
const transaction = em.create(WalletTransaction, { const transaction = em.create(WalletTransaction, {
userWallet, userWallet,
amount, amount,
balance: newBalance, balance: newBalance,
type, type,
reason, reason,
});
await em.persistAndFlush(transaction);
return transaction;
}); });
await em.persistAndFlush(transaction);
return transaction;
} }
} }