verify payment
This commit is contained in:
@@ -5,6 +5,7 @@ import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
|||||||
import { PaymentMethodService } from '../services/payment-method.service';
|
import { PaymentMethodService } from '../services/payment-method.service';
|
||||||
import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto';
|
import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto';
|
||||||
import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto';
|
import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto';
|
||||||
|
import { VerifyPaymentDto } from '../dto/verify-payment.dto';
|
||||||
import { RestId } from 'src/common/decorators';
|
import { RestId } from 'src/common/decorators';
|
||||||
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
|
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
|
||||||
|
|
||||||
@@ -25,6 +26,18 @@ export class PaymentsController {
|
|||||||
return this.paymentMethodService.findByRestaurant(restId);
|
return this.paymentMethodService.findByRestaurant(restId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('public/payments/verify')
|
||||||
|
@ApiOperation({ summary: 'Verify a payment' })
|
||||||
|
@ApiBody({ type: VerifyPaymentDto })
|
||||||
|
@ApiNotFoundResponse({ description: 'Payment not found' })
|
||||||
|
verifyPayment(@Body() verifyPaymentDto: VerifyPaymentDto) {
|
||||||
|
return this.paymentsService.verifyPayment(
|
||||||
|
verifyPaymentDto.authority,
|
||||||
|
verifyPaymentDto.amount,
|
||||||
|
verifyPaymentDto.orderId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// @UseGuards(AdminAuthGuard)
|
// @UseGuards(AdminAuthGuard)
|
||||||
// @ApiBearerAuth()
|
// @ApiBearerAuth()
|
||||||
// @Get('admin/payments')
|
// @Get('admin/payments')
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { IsNumber, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class VerifyPaymentDto {
|
||||||
|
@ApiProperty({ description: 'Payment authority token' })
|
||||||
|
@IsString()
|
||||||
|
authority: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Payment amount (optional, will use payment amount if not provided)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
amount?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Payment order id' })
|
||||||
|
@IsString()
|
||||||
|
orderId: string;
|
||||||
|
}
|
||||||
@@ -30,3 +30,19 @@ export interface IPaymentResponse {
|
|||||||
fee_type: string;
|
fee_type: string;
|
||||||
fee: number;
|
fee: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPaymentVerifyRequest {
|
||||||
|
merchantId: string;
|
||||||
|
amount: number;
|
||||||
|
authority: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPaymentVerifyResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
refId: number;
|
||||||
|
cardPan: string;
|
||||||
|
cardHash: string;
|
||||||
|
fee_type: string;
|
||||||
|
fee: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
IPaymentRequest,
|
IPaymentRequest,
|
||||||
IPaymentResponse,
|
IPaymentResponse,
|
||||||
|
IPaymentVerifyRequest,
|
||||||
|
IPaymentVerifyResponse,
|
||||||
PaymentGatewayEnum,
|
PaymentGatewayEnum,
|
||||||
PaymentMethodEnum,
|
PaymentMethodEnum,
|
||||||
PaymentStatusEnum,
|
PaymentStatusEnum,
|
||||||
@@ -45,7 +47,7 @@ export class PaymentsService {
|
|||||||
|
|
||||||
// Create payment record and save authority
|
// Create payment record and save authority
|
||||||
const restaurantDomain = paymentMethod.restaurant.domain;
|
const restaurantDomain = paymentMethod.restaurant.domain;
|
||||||
const gateway = paymentMethod.gateway as PaymentGatewayEnum | null;
|
const gateway = paymentMethod.gateway;
|
||||||
const { authority } = await this.createPayment(restaurantDomain, {
|
const { authority } = await this.createPayment(restaurantDomain, {
|
||||||
amount,
|
amount,
|
||||||
orderId,
|
orderId,
|
||||||
@@ -98,10 +100,9 @@ export class PaymentsService {
|
|||||||
throw new BadRequestException('Payment gateway is required for online payments');
|
throw new BadRequestException('Payment gateway is required for online payments');
|
||||||
}
|
}
|
||||||
|
|
||||||
const callbackUrl = `${domain}/payments/callback`;
|
|
||||||
|
|
||||||
// Handle ZarinPal gateway
|
// Handle ZarinPal gateway
|
||||||
if (gateway === PaymentGatewayEnum.ZarinPal) {
|
if (gateway === PaymentGatewayEnum.ZarinPal) {
|
||||||
|
const callbackUrl = `${domain}/payments/${PaymentGatewayEnum.ZarinPal}/${orderId}/callback`;
|
||||||
try {
|
try {
|
||||||
const gatewayResponse = await this.requestToZarinPalGateway({
|
const gatewayResponse = await this.requestToZarinPalGateway({
|
||||||
amount,
|
amount,
|
||||||
@@ -148,6 +149,126 @@ export class PaymentsService {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async verifyPayment(authority: string, amount: number | undefined, orderId: string): Promise<Payment> {
|
||||||
|
// Find payment by authority and orderId
|
||||||
|
const payment = await this.em.findOne(
|
||||||
|
Payment,
|
||||||
|
{ authority, order: { id: orderId } },
|
||||||
|
{ populate: ['order', 'order.paymentMethod'] },
|
||||||
|
);
|
||||||
|
if (!payment) {
|
||||||
|
throw new NotFoundException('Payment not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If payment is already verified, return it
|
||||||
|
if (payment.status === PaymentStatusEnum.Paid) {
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get payment method from order (already populated)
|
||||||
|
if (!payment.order.paymentMethod) {
|
||||||
|
throw new NotFoundException('Payment method not found for this order');
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentMethod = await this.em.findOne(PaymentMethod, { id: payment.order.paymentMethod.id });
|
||||||
|
if (!paymentMethod) {
|
||||||
|
throw new NotFoundException('Payment method not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-online payments, mark as paid directly
|
||||||
|
if (paymentMethod.method !== PaymentMethodEnum.Online) {
|
||||||
|
payment.status = PaymentStatusEnum.Paid;
|
||||||
|
payment.paidAt = new Date();
|
||||||
|
await this.em.persistAndFlush(payment);
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Online payments require gateway verification
|
||||||
|
if (!paymentMethod.merchantId) {
|
||||||
|
throw new BadRequestException('Merchant ID is required for online payment verification');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payment.gateway) {
|
||||||
|
throw new BadRequestException('Payment gateway is required for online payment verification');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use provided amount or payment amount
|
||||||
|
const verifyAmount = amount || payment.amount;
|
||||||
|
|
||||||
|
// Handle ZarinPal gateway verification
|
||||||
|
if (payment.gateway === PaymentGatewayEnum.ZarinPal) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const verifyResponse = await this.verifyZarinPalPayment({
|
||||||
|
merchantId: paymentMethod.merchantId,
|
||||||
|
amount: verifyAmount,
|
||||||
|
authority,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Type guard to ensure we have a valid response
|
||||||
|
if (!verifyResponse || typeof verifyResponse !== 'object') {
|
||||||
|
throw new BadRequestException('Invalid verification response from payment gateway');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type assertion after type guard
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const response: IPaymentVerifyResponse = verifyResponse as IPaymentVerifyResponse;
|
||||||
|
|
||||||
|
// Check verification response code (100 means success for ZarinPal)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
if (response.code === 100) {
|
||||||
|
// Payment successful
|
||||||
|
payment.status = PaymentStatusEnum.Paid;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
payment.refId = String(response.refId);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
|
||||||
|
payment.cardPan = response.cardPan;
|
||||||
|
payment.verifyResponse = response as unknown as Record<string, any>;
|
||||||
|
payment.paidAt = new Date();
|
||||||
|
payment.order.paymentStatus = PaymentStatusEnum.Paid;
|
||||||
|
} else {
|
||||||
|
// Payment failed
|
||||||
|
payment.status = PaymentStatusEnum.Failed;
|
||||||
|
payment.verifyResponse = response as unknown as Record<string, any>;
|
||||||
|
payment.failedAt = new Date();
|
||||||
|
payment.order.paymentStatus = PaymentStatusEnum.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.em.persistAndFlush(payment);
|
||||||
|
return payment;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// Mark payment as failed on error
|
||||||
|
payment.status = PaymentStatusEnum.Failed;
|
||||||
|
payment.failedAt = new Date();
|
||||||
|
payment.order.paymentStatus = PaymentStatusEnum.Failed;
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
payment.verifyResponse = {
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
await this.em.persistAndFlush(payment);
|
||||||
|
|
||||||
|
throw new BadRequestException(`Payment verification failed: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If gateway is not supported, throw an error
|
||||||
|
throw new BadRequestException(`Unsupported payment gateway: ${String(payment.gateway)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verifyZarinPalPayment(verifyRequest: IPaymentVerifyRequest): Promise<IPaymentVerifyResponse> {
|
||||||
|
const gatewayVerifyUrl = 'https://payment.zarinpal.com/pg/v4/payment/verify.json';
|
||||||
|
try {
|
||||||
|
const response = await axios.post<IPaymentVerifyResponse>(gatewayVerifyUrl, verifyRequest);
|
||||||
|
if (!response.data) {
|
||||||
|
throw new BadRequestException('Empty response from payment gateway');
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
throw new BadRequestException(`Failed to verify payment with gateway: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
update(_id: number, _updatePaymentDto: unknown) {
|
update(_id: number, _updatePaymentDto: unknown) {
|
||||||
return `This action updates a #${_id} payment`;
|
return `This action updates a #${_id} payment`;
|
||||||
|
|||||||
Reference in New Issue
Block a user