186 lines
6.7 KiB
TypeScript
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;
|
|
});
|
|
}
|
|
}
|