Files
dmenu-api/src/modules/restaurants/providers/restaurant-wallet.service.ts
T
2026-07-04 12:49:00 +03:30

186 lines
6.7 KiB
TypeScript

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<number> {
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<void> {
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<RestaurantCreditRequest> {
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<string>('DANAK_API_URL');
const xApiKey = this.configService.getOrThrow<string>('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<RestaurantCreditRequest> {
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;
});
}
}