diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 47ce18a..8f7358a 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -317,6 +317,8 @@ export const enum WalletMessage { INVOICE_WALLET_TRANSFER = "پرداخت صورت حساب از طریق کیف پول", TRANSACTION_NOT_FOUND = "تراکنش پیدا نشد", REFERRAL_REWARD_WALLET_TRANSFER = "پرداخت پاداش ارجاع از طریق کیف پول", + INVOICE_ID_REQUIRED = "شناسه صورت حساب مورد نیاز است", + INVOICE_ID_SHOULD_BE_UUID = "شناسه صورت حساب باید یک UUID معتبر باشد", } export const enum PaymentMessage { diff --git a/src/configs/typeorm.config.ts b/src/configs/typeorm.config.ts index 8d8378f..9391ab2 100755 --- a/src/configs/typeorm.config.ts +++ b/src/configs/typeorm.config.ts @@ -13,7 +13,7 @@ export function databaseConfigs(): TypeOrmModuleAsyncOptions { username: configService.getOrThrow("DB_USER"), password: configService.getOrThrow("DB_PASS"), autoLoadEntities: true, - synchronize: configService.getOrThrow("NODE_ENV") == "production" ? false : true, + synchronize: configService.getOrThrow("NODE_ENV") == "production" ? false : false, logging: configService.getOrThrow("NODE_ENV") == "production" ? false : true, migrationsTableName: "typeorm_migrations", migrationsRun: false, diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index 29aac13..1f2e8a7 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -481,6 +481,33 @@ export class InvoicesService { if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); + let shouldCharge = false; + let remainingToCharge = 0; + if (!isAdmin) { + if (invoice.status === InvoiceStatus.WAIT_PAYMENT) { + const queryRunner = this.dataSource.createQueryRunner(); + try { + await queryRunner.connect(); + const userWallet = await this.walletsService.getWalletByUserId(userId, queryRunner); + const invoicePrice = new Decimal(invoice.totalPrice); + const walletBalance = new Decimal(userWallet.balance); + if (walletBalance.lessThan(invoicePrice)) { + remainingToCharge = invoicePrice.sub(walletBalance).round().toNumber(); + shouldCharge = true; + } else { + remainingToCharge = 0; + } + } finally { + await queryRunner.release(); + } + } + return { + invoice, + shouldCharge, + remainingToCharge, + }; + } + return { invoice, }; diff --git a/src/modules/payments/DTO/deposit-wallet.dto.ts b/src/modules/payments/DTO/deposit-wallet.dto.ts index 554574f..a6be0b5 100755 --- a/src/modules/payments/DTO/deposit-wallet.dto.ts +++ b/src/modules/payments/DTO/deposit-wallet.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsEnum, IsInt, IsNotEmpty, IsUUID, IsUrl, Min } from "class-validator"; +import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsUUID, IsUrl, Min } from "class-validator"; import { WalletMessage } from "../../../common/enums/message.enum"; import { TransferType } from "../enums/payment-type.enum"; @@ -22,6 +22,12 @@ export class GatewayDepositDto extends DepositDto { @IsUUID("4", { message: WalletMessage.GATEWAY_ID_SHOULD_BE_UUID }) @ApiProperty({ description: "Gateway id to pay", example: "e6fdce2a-9f91-47c4-8561-48368fc275b5" }) gatewayId: string; + + @IsOptional() + @IsNotEmpty({ message: WalletMessage.INVOICE_ID_REQUIRED }) + @IsUUID("4", { message: WalletMessage.INVOICE_ID_SHOULD_BE_UUID }) + @ApiProperty({ description: "Invoice id to pay", example: "e6fdce2a-9f91-47c4-8561-48368fc275b5" }) + invoiceId?: string; } export class TransferDepositDto extends DepositDto { diff --git a/src/modules/payments/DTO/verify-payment.dto.ts b/src/modules/payments/DTO/verify-payment.dto.ts index e730b40..eb820c9 100755 --- a/src/modules/payments/DTO/verify-payment.dto.ts +++ b/src/modules/payments/DTO/verify-payment.dto.ts @@ -1,4 +1,4 @@ -import { IsEnum, IsNotEmpty } from "class-validator"; +import { IsEnum, IsNotEmpty, IsOptional } from "class-validator"; import { GatewayEnum } from "../enums/gateway.enum"; import { GatewayType } from "../types/gateway.type"; @@ -8,6 +8,10 @@ export class VerifyParamDto { @IsNotEmpty() @IsEnum(GatewayEnum) gateway: GatewayType; + + @IsOptional() + @IsNotEmpty() + invoiceId?: string; } export class VerifyQueryDto { diff --git a/src/modules/payments/gateways/zarinpal.gateway.ts b/src/modules/payments/gateways/zarinpal.gateway.ts index 97b6532..1773ffa 100755 --- a/src/modules/payments/gateways/zarinpal.gateway.ts +++ b/src/modules/payments/gateways/zarinpal.gateway.ts @@ -34,7 +34,7 @@ export class ZarinpalGateway implements IPaymentGateway { const purchaseData: ZarinPalPGNewArgs = { merchant_id: this.config.merchantId, amount: processParams.amount, - callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`, + callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}${processParams.invoiceId ? "/" + processParams.invoiceId : ""}`, description: processParams.description, currency: "IRT", metadata: { email: processParams.email, mobile: processParams.mobile }, diff --git a/src/modules/payments/interfaces/IPayment.ts b/src/modules/payments/interfaces/IPayment.ts index b198acc..fccd737 100755 --- a/src/modules/payments/interfaces/IPayment.ts +++ b/src/modules/payments/interfaces/IPayment.ts @@ -13,6 +13,7 @@ export interface IProcessPaymentParams { // callBackPath: string; email?: string; mobile?: string; + invoiceId?: string; } export interface IProcessPaymentData { diff --git a/src/modules/payments/payments.controller.ts b/src/modules/payments/payments.controller.ts index e261c1b..7611ff8 100755 --- a/src/modules/payments/payments.controller.ts +++ b/src/modules/payments/payments.controller.ts @@ -87,9 +87,22 @@ export class PaymentsController { return this.paymentsService.getTransactions(queryDto); } + @Get("verify/:gateway/:invoiceId") + verifyPaymentWithInvoice( + @Param() paramDto: VerifyParamDto, + @Query() queryDto: VerifyQueryDto, + @Res({ passthrough: true }) rep: FastifyReply, + ) { + return this.paymentsService.verifyPayment(paramDto, queryDto, rep); + } + @Get("verify/:gateway") - verifyPayment(@Param() paramDto: VerifyParamDto, @Query() queryDto: VerifyQueryDto, @Res({ passthrough: true }) rep: FastifyReply) { - return this.paymentsService.verifyPayment(paramDto.gateway, queryDto, rep); + verifyPaymentWithoutInvoice( + @Param() paramDto: VerifyParamDto, + @Query() queryDto: VerifyQueryDto, + @Res({ passthrough: true }) rep: FastifyReply, + ) { + return this.paymentsService.verifyPayment(paramDto, queryDto, rep); } ///------------------- bank account ----------------------------- diff --git a/src/modules/payments/providers/payments.service.ts b/src/modules/payments/providers/payments.service.ts index 3d3c1e1..3c2636c 100755 --- a/src/modules/payments/providers/payments.service.ts +++ b/src/modules/payments/providers/payments.service.ts @@ -23,7 +23,7 @@ import { PaymentTransactionQueryDto } from "../DTO/payment-transaction-query.dto import { SearchTransactionQueryDto } from "../DTO/search-transaction-query.dto"; import { UpdateBankAccountDto } from "../DTO/update-bankaccount.dto"; import { RejectDepositRequestDto } from "../DTO/update-deposit-request-status.dto"; -import { VerifyQueryDto } from "../DTO/verify-payment.dto"; +import { VerifyParamDto, VerifyQueryDto } from "../DTO/verify-payment.dto"; import { BankAccount } from "../entities/bank-account.entity"; import { DepositRequest } from "../entities/deposit-request.entity"; import { PaymentGateway } from "../entities/payment-gateway.entity"; @@ -32,7 +32,7 @@ import { DepositRequestStatus } from "../enums/deposit-request-status.enum"; import { PaymentStatus } from "../enums/payment-status.enum"; import { TransferType } from "../enums/payment-type.enum"; import { PaymentGatewayFactory } from "../factories/payment.factory"; -import { IVerifyPayment } from "../interfaces/IPayment"; +import { IProcessPaymentParams, IVerifyPayment } from "../interfaces/IPayment"; import { BankAccountsRepository } from "../repositories/bank-accounts.repository"; import { DepositRequestsRepository } from "../repositories/deposit-requests.repository"; import { PaymentGatewaysRepository } from "../repositories/payment-gateway.repository"; @@ -58,16 +58,22 @@ export class PaymentsService { //=============================================== async chargeWalletWithGateway(chargeDto: GatewayDepositDto, userId: string) { const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); try { - const { amount, gatewayId } = chargeDto; + await queryRunner.connect(); + await queryRunner.startTransaction(); + const { amount, gatewayId, invoiceId } = chargeDto; const user = await this.getUserById(userId, queryRunner); const paymentGateway = await this.getPaymentGatewayById(gatewayId, queryRunner); - const gatewayData = await this.processPayment(paymentGateway.name, amount, WalletMessage.DEPOSIT_WALLET_IPG, user.email, user.phone); + const gatewayData = await this.processPayment(paymentGateway.name, { + amount, + description: WalletMessage.DEPOSIT_WALLET_IPG, + email: user.email, + mobile: user.phone, + invoiceId, + }); const payment = await this.createGatewayPaymentForUser(user, amount, gatewayData.reference, paymentGateway.id, queryRunner); @@ -113,21 +119,21 @@ export class PaymentsService { } //=============================================== - async processPayment(provider: GatewayType, amount: number, description: string, email?: string, mobile?: string) { + async processPayment(provider: GatewayType, params: IProcessPaymentParams) { const paymentGateway = this.gatewayFactory.getPaymentGateway(provider); - return paymentGateway.processPayment({ amount, description, email, mobile }); + return paymentGateway.processPayment(params); } //=============================================== - async verifyPayment(gateway: GatewayType, queryDto: VerifyQueryDto, rep: FRply) { - const frontUrl = this.buildFrontendRedirectUrl(); + async verifyPayment(verifyParamDto: VerifyParamDto, queryDto: VerifyQueryDto, rep: FRply) { + const frontUrl = this.buildFrontendRedirectUrl(verifyParamDto.invoiceId); const queryRunner = this.dataSource.createQueryRunner(); try { await queryRunner.connect(); await queryRunner.startTransaction(); - const paymentGateway = this.gatewayFactory.getPaymentGateway(gateway); + const paymentGateway = this.gatewayFactory.getPaymentGateway(verifyParamDto.gateway); const payment = await this.getPaymentByReference(queryDto.Authority, queryRunner); if (payment.status === PaymentStatus.COMPLETED) throw new BadRequestException(PaymentMessage.VALIDATED_BEFORE); @@ -409,9 +415,13 @@ export class PaymentsService { //-private methods //---------------------------------------- - private buildFrontendRedirectUrl(): URL { + private buildFrontendRedirectUrl(invoiceId?: string): URL { const frontUrl = new URL(this.configService.getOrThrow("SITE_URL")); - frontUrl.pathname = "/payment"; + if (invoiceId) { + frontUrl.pathname = "/receipts/" + invoiceId; + } else { + frontUrl.pathname = "/payment"; + } return frontUrl; }