diff --git a/database/migrations/Migration20260704130000_removeRestaurantCreditColumn.ts b/database/migrations/Migration20260704130000_removeRestaurantCreditColumn.ts new file mode 100644 index 0000000..a43cda4 --- /dev/null +++ b/database/migrations/Migration20260704130000_removeRestaurantCreditColumn.ts @@ -0,0 +1,13 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20260704130000_removeRestaurantCreditColumn extends Migration { + + override async up(): Promise { + this.addSql(`alter table "restaurants" drop column if exists "credit";`); + } + + override async down(): Promise { + this.addSql(`alter table "restaurants" add column "credit" numeric(10,0) not null default 0;`); + } + +} diff --git a/src/modules/restaurants/controllers/restaurants.controller.ts b/src/modules/restaurants/controllers/restaurants.controller.ts index 3c67fe9..4d76e59 100644 --- a/src/modules/restaurants/controllers/restaurants.controller.ts +++ b/src/modules/restaurants/controllers/restaurants.controller.ts @@ -25,6 +25,7 @@ import { CacheResponse } from 'src/common/decorators/cache-response.decorator'; import { CacheKeyPrefixes } from 'src/common/constants/cache-keys.constant'; import { UpdateRestaurantBgDto } from '../dto/update-restaurant-bg.dto'; import { ChargeRequestDto } from '../dto/charge-request.dto'; +import { FinishChargeRequestDto } from '../dto/finish-charge-request.dto'; @ApiTags('restaurants') @Controller() @@ -167,6 +168,15 @@ export class RestaurantsController { return this.restaurantsService.upgradeSubscription(subscriptionId, upgradeSubscriptionDto); } + @UseGuards(SuperAdminAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Finalize wallet charge after invoice payment' }) + @ApiBody({ type: FinishChargeRequestDto }) + @Post('super-admin/restaurants/charge-request/finish') + finishChargeRequest(@Body() dto: FinishChargeRequestDto) { + return this.restaurantsService.finishChargeRequest(dto); + } + @UseGuards(SuperAdminAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Hard delete a restaurant and all related data' }) diff --git a/src/modules/restaurants/dto/finish-charge-request.dto.ts b/src/modules/restaurants/dto/finish-charge-request.dto.ts new file mode 100644 index 0000000..b0ec30f --- /dev/null +++ b/src/modules/restaurants/dto/finish-charge-request.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class FinishChargeRequestDto { + @ApiProperty({ description: 'External invoice ID returned after payment' }) + @IsString() + @IsNotEmpty() + invoiceId!: string; +} diff --git a/src/modules/restaurants/entities/restaurant.entity.ts b/src/modules/restaurants/entities/restaurant.entity.ts index 7d016ae..1ca8e7d 100644 --- a/src/modules/restaurants/entities/restaurant.entity.ts +++ b/src/modules/restaurants/entities/restaurant.entity.ts @@ -77,9 +77,6 @@ export class Restaurant extends BaseEntity { @Property({ type: 'decimal', default: 0 }) vat?: number = 0; - @Property({ type: 'decimal', precision: 10, scale: 0, default: 0 }) - credit?: number = 0; - @Property() domain!: string; diff --git a/src/modules/restaurants/providers/restaurant-wallet.service.ts b/src/modules/restaurants/providers/restaurant-wallet.service.ts index 6226fa4..b7981cb 100644 --- a/src/modules/restaurants/providers/restaurant-wallet.service.ts +++ b/src/modules/restaurants/providers/restaurant-wallet.service.ts @@ -14,6 +14,7 @@ import { 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 { RestMessage } from 'src/common/enums/message.enum'; @Injectable() @@ -26,6 +27,16 @@ export class RestaurantWalletService { private readonly configService: ConfigService, ) {} + 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); + } + async createTransaction(params: { restaurant: Restaurant; amount: number; @@ -40,15 +51,13 @@ export class RestaurantWalletService { return; } - const currentCredit = Number(restaurant.credit ?? 0); - if (type === RestaurantCreditTransactionType.DEBIT && currentCredit < amount) { + const currentBalance = await this.getCurrentBalance(restaurant.id, em); + if (type === RestaurantCreditTransactionType.DEBIT && currentBalance < amount) { throw new BadRequestException(insufficientBalanceMessage ?? 'اعتبار کیف پول رستوران کافی نیست'); } const newBalance = - type === RestaurantCreditTransactionType.DEBIT ? currentCredit - amount : currentCredit + amount; - - restaurant.credit = newBalance; + type === RestaurantCreditTransactionType.DEBIT ? currentBalance - amount : currentBalance + amount; const creditTransaction = em.create(RestaurantCreditTransaction, { restaurant, @@ -58,7 +67,7 @@ export class RestaurantWalletService { reason, }); - await em.persistAndFlush([restaurant, creditTransaction]); + await em.persistAndFlush(creditTransaction); } async chargeRequest(restId: string, dto: ChargeRequestDto): Promise { @@ -124,4 +133,39 @@ export class RestaurantWalletService { 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; + }); + } } diff --git a/src/modules/restaurants/providers/restaurants.service.ts b/src/modules/restaurants/providers/restaurants.service.ts index f49da7d..4a9d3db 100644 --- a/src/modules/restaurants/providers/restaurants.service.ts +++ b/src/modules/restaurants/providers/restaurants.service.ts @@ -34,6 +34,7 @@ import { BackgroundRepository } from '../repositories/background.repository'; import { ActiveRestaurantListItemDto } from '../dto/active-restaurant-list-item.dto'; import { RestaurantWalletService } from './restaurant-wallet.service'; import { ChargeRequestDto } from '../dto/charge-request.dto'; +import { FinishChargeRequestDto } from '../dto/finish-charge-request.dto'; @Injectable() @@ -333,6 +334,10 @@ export class RestaurantsService { return this.restaurantWalletService.chargeRequest(restId, dto); } + finishChargeRequest(dto: FinishChargeRequestDto) { + return this.restaurantWalletService.finishChargeRequest(dto); + } + async updateBackground(id: string, dto: UpdateRestaurantBgDto): Promise { const restaurant = await this.restRepository.findOne({ id });