import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { EntityManager } from '@mikro-orm/postgresql'; import { AxiosError } from 'axios'; import { catchError, firstValueFrom, throwError } from 'rxjs'; import { Restaurant } from '../entities/restaurant.entity'; import { RestaurantCreditRequest } from '../entities/restaurant-credit-request.entity'; import { RestaurantCreditTransaction } from '../entities/restaurant-credit-transaction.entity'; import { RestaurantCreditTransactionReason, RestaurantCreditTransactionType, } from '../interface/restaurant-credit-transaction.interface'; import { RestaurantCreditRequestStatus } from '../interface/restaurant-credit-request.interface'; import { CreateExternalInvoice } from '../interface/external-invoice.interface'; import { ChargeRequestDto } from '../dto/charge-request.dto'; import { FinishChargeRequestDto } from '../dto/finish-charge-request.dto'; import { FindRestaurantWalletTransactionsDto } from '../dto/find-restaurant-wallet-transactions.dto'; import { FindRestaurantCreditRequestsDto } from '../dto/find-restaurant-credit-requests.dto'; import { RestaurantCreditTransactionRepository } from '../repositories/restaurant-credit-transaction.repository'; import { RestaurantCreditRequestRepository } from '../repositories/restaurant-credit-request.repository'; import { RestMessage } from 'src/common/enums/message.enum'; @Injectable() export class RestaurantWalletService { private readonly logger = new Logger(RestaurantWalletService.name); constructor( private readonly defaultEm: EntityManager, private readonly httpService: HttpService, private readonly configService: ConfigService, private readonly restaurantCreditTransactionRepository: RestaurantCreditTransactionRepository, private readonly restaurantCreditRequestRepository: RestaurantCreditRequestRepository, ) {} async getCurrentBalance(restaurantId: string, em: EntityManager = this.defaultEm): Promise { const latestTransaction = await em.findOne( RestaurantCreditTransaction, { restaurant: { id: restaurantId } }, { orderBy: { createdAt: 'desc' } }, ); return Number(latestTransaction?.balance ?? 0); } getTransactions(restaurantId: string, dto: FindRestaurantWalletTransactionsDto) { return this.restaurantCreditTransactionRepository.findAllPaginated(restaurantId, dto); } getCreditRequests(restaurantId: string, dto: FindRestaurantCreditRequestsDto) { return this.restaurantCreditRequestRepository.findAllPaginated(restaurantId, dto); } async createTransaction(params: { restaurant: Restaurant; amount: number; type: RestaurantCreditTransactionType; reason: RestaurantCreditTransactionReason; insufficientBalanceMessage?: string; em?: EntityManager; }): Promise { const { restaurant, amount, type, reason, insufficientBalanceMessage, em = this.defaultEm } = params; if (amount <= 0) { return; } const currentBalance = await this.getCurrentBalance(restaurant.id, em); if (type === RestaurantCreditTransactionType.DEBIT && currentBalance < amount) { throw new BadRequestException(insufficientBalanceMessage ?? 'اعتبار کیف پول رستوران کافی نیست'); } const newBalance = type === RestaurantCreditTransactionType.DEBIT ? currentBalance - amount : currentBalance + amount; const creditTransaction = em.create(RestaurantCreditTransaction, { restaurant, amount, balance: newBalance, type, reason, }); await em.persistAndFlush(creditTransaction); } async chargeRequest(restId: string, dto: ChargeRequestDto): Promise { const restaurant = await this.defaultEm.findOne(Restaurant, { id: restId }); if (!restaurant) { throw new NotFoundException(RestMessage.NOT_FOUND); } if (!restaurant.subscriptionId) { throw new BadRequestException('اشتراک رستوران یافت نشد'); } const params: CreateExternalInvoice = { danakSubscriptionId: restaurant.subscriptionId, businessId: restId, items: [ { name: 'dmenu_wallet_charge', count: 1, unitPrice: dto.amount, discount: 0, }, ], }; const danakApiUrl = this.configService.getOrThrow('DANAK_API_URL'); const xApiKey = this.configService.getOrThrow('EXTERNAL_API_KEYS'); let invoiceId: string; try { const { data } = await firstValueFrom( this.httpService .post<{ data: { invoice: { id: string } } }>(`${danakApiUrl}/invoices/external/create`, params, { headers: { 'Content-Type': 'application/json', 'X-API-Key': xApiKey, }, }) .pipe( catchError((err: AxiosError) => { this.logger.error(`Failed to create invoice: ${err.message}`, err.stack); return throwError(() => err); }), ), ); invoiceId = data.data.invoice.id; } catch (error: unknown) { this.logger.error( `Error creating invoice: ${error instanceof Error ? error.message : 'Unknown error'}`, ); throw new BadRequestException('ایجاد فاکتور با خطا مواجه شد'); } const creditRequest = this.defaultEm.create(RestaurantCreditRequest, { restaurant, amount: dto.amount, invoiceId, status: RestaurantCreditRequestStatus.PENDING, }); await this.defaultEm.persistAndFlush(creditRequest); return creditRequest; } async finishChargeRequest(dto: FinishChargeRequestDto): Promise { return this.defaultEm.transactional(async (em) => { const creditRequest = await em.findOne( RestaurantCreditRequest, { invoiceId: dto.invoiceId }, { populate: ['restaurant'] }, ); if (!creditRequest) { throw new NotFoundException(RestMessage.NOT_FOUND); } if (creditRequest.status === RestaurantCreditRequestStatus.PAID) { return creditRequest; } if (creditRequest.status !== RestaurantCreditRequestStatus.PENDING) { throw new BadRequestException('درخواست شارژ کیف پول قابل پردازش نیست'); } await this.createTransaction({ restaurant: creditRequest.restaurant, amount: Number(creditRequest.amount), type: RestaurantCreditTransactionType.CREDIT, reason: RestaurantCreditTransactionReason.DEPOSIT, em, }); creditRequest.status = RestaurantCreditRequestStatus.PAID; await em.flush(); return creditRequest; }); } }