payment redirect
This commit is contained in:
@@ -526,4 +526,9 @@ export class CartService {
|
|||||||
const cacheKey = `${this.CART_KEY_PREFIX}:${cart.userId}:${cart.restaurantId}`;
|
const cacheKey = `${this.CART_KEY_PREFIX}:${cart.userId}:${cart.restaurantId}`;
|
||||||
await this.cacheService.set(cacheKey, JSON.stringify(cart), this.CART_TTL);
|
await this.cacheService.set(cacheKey, JSON.stringify(cart), this.CART_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clearCart(userId: string, restaurantId: string): Promise<void> {
|
||||||
|
const cacheKey = `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`;
|
||||||
|
return this.cacheService.del(cacheKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { RestaurantPaymentMethod } from '../payments/entities/restaurant-payment
|
|||||||
import { CartModule } from '../cart/cart.module';
|
import { CartModule } from '../cart/cart.module';
|
||||||
import { UtilsModule } from '../utils/utils.module';
|
import { UtilsModule } from '../utils/utils.module';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
import { PaymentsModule } from '../payments/payments.module';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -20,6 +21,7 @@ import { JwtModule } from '@nestjs/jwt';
|
|||||||
CartModule,
|
CartModule,
|
||||||
UtilsModule,
|
UtilsModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
PaymentsModule,
|
||||||
JwtModule,
|
JwtModule,
|
||||||
],
|
],
|
||||||
controllers: [OrdersController],
|
controllers: [OrdersController],
|
||||||
|
|||||||
@@ -8,22 +8,22 @@ import { Food } from '../foods/entities/food.entity';
|
|||||||
import { UserAddress } from '../users/entities/user-address.entity';
|
import { UserAddress } from '../users/entities/user-address.entity';
|
||||||
import { RestaurantPaymentMethod } from '../payments/entities/restaurant-payment-method.entity';
|
import { RestaurantPaymentMethod } from '../payments/entities/restaurant-payment-method.entity';
|
||||||
import { CartService } from '../cart/providers/cart.service';
|
import { CartService } from '../cart/providers/cart.service';
|
||||||
import { CacheService } from '../utils/cache.service';
|
|
||||||
import { OrderStatus } from './interface/order-status';
|
import { OrderStatus } from './interface/order-status';
|
||||||
import { PaymentStatus } from '../payments/interface/payment-status';
|
import { PaymentStatus } from '../payments/interface/payment-status';
|
||||||
import { Cart } from '../cart/interfaces/cart.interface';
|
import { Cart } from '../cart/interfaces/cart.interface';
|
||||||
|
import { RestaurantPaymentMethodRepository } from '../payments/repositories/restaurant-payment-method.repository';
|
||||||
|
import { PaymentGatewayService } from '../payments/services/payment-gateway.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrdersService {
|
export class OrdersService {
|
||||||
private readonly CART_KEY_PREFIX = 'cart';
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
private readonly cartService: CartService,
|
private readonly cartService: CartService,
|
||||||
private readonly cacheService: CacheService,
|
private readonly RestaurantPaymentMethodRepository: RestaurantPaymentMethodRepository,
|
||||||
|
private readonly paymentGatewayService: PaymentGatewayService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(userId: string, restaurantId: string): Promise<Order> {
|
async create(userId: string, restaurantId: string): Promise<Order | { order: Order; redirectUrl: string }> {
|
||||||
// Get cart
|
// Get cart
|
||||||
const cart = await this.cartService.findOne(userId, restaurantId);
|
const cart = await this.cartService.findOne(userId, restaurantId);
|
||||||
|
|
||||||
@@ -77,8 +77,25 @@ export class OrdersService {
|
|||||||
await em.flush();
|
await em.flush();
|
||||||
|
|
||||||
// Clear cart from cache after successful order creation
|
// Clear cart from cache after successful order creation
|
||||||
const cacheKey = `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`;
|
await this.cartService.clearCart(userId, restaurantId);
|
||||||
await this.cacheService.del(cacheKey);
|
|
||||||
|
// Check if payment method is online and redirect to payment gateway
|
||||||
|
if (validationResult.paymentMethod.paymentMethod.isOnline) {
|
||||||
|
const paymentRedirect = await this.paymentGatewayService.generateRedirectUrl(
|
||||||
|
order,
|
||||||
|
validationResult.paymentMethod,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return order with redirect URL for online payment
|
||||||
|
return {
|
||||||
|
order,
|
||||||
|
redirectUrl: paymentRedirect.redirectUrl,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// For offline payment methods (cash on delivery, etc.), order is created and pending
|
||||||
|
// Payment status will be updated when payment is confirmed
|
||||||
|
// TODO: Implement offline payment confirmation flow if needed
|
||||||
|
}
|
||||||
|
|
||||||
return order;
|
return order;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { PaymentMethodService } from './services/payment-method.service';
|
|||||||
import { RestaurantPaymentMethod } from './entities/restaurant-payment-method.entity';
|
import { RestaurantPaymentMethod } from './entities/restaurant-payment-method.entity';
|
||||||
import { RestaurantPaymentMethodRepository } from './repositories/restaurant-payment-method.repository';
|
import { RestaurantPaymentMethodRepository } from './repositories/restaurant-payment-method.repository';
|
||||||
import { RestaurantPaymentMethodService } from './services/restaurant-payment-method.service';
|
import { RestaurantPaymentMethodService } from './services/restaurant-payment-method.service';
|
||||||
|
import { PaymentGatewayService } from './services/payment-gateway.service';
|
||||||
import { Restaurant } from '../restaurants/entities/restaurant.entity';
|
import { Restaurant } from '../restaurants/entities/restaurant.entity';
|
||||||
import { PaymentsController } from './controllers/payments.controller';
|
import { PaymentsController } from './controllers/payments.controller';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
@@ -21,12 +22,15 @@ import { JwtModule } from '@nestjs/jwt';
|
|||||||
PaymentMethodRepository,
|
PaymentMethodRepository,
|
||||||
RestaurantPaymentMethodService,
|
RestaurantPaymentMethodService,
|
||||||
RestaurantPaymentMethodRepository,
|
RestaurantPaymentMethodRepository,
|
||||||
|
PaymentGatewayService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
PaymentMethodRepository,
|
PaymentMethodRepository,
|
||||||
PaymentMethodService,
|
PaymentMethodService,
|
||||||
RestaurantPaymentMethodRepository,
|
RestaurantPaymentMethodRepository,
|
||||||
RestaurantPaymentMethodService,
|
RestaurantPaymentMethodService,
|
||||||
|
PaymentsService,
|
||||||
|
PaymentGatewayService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PaymentsModule {}
|
export class PaymentsModule {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||||
import { RestaurantPaymentMethod } from '../entities/restaurant-payment-method.entity';
|
import { RestaurantPaymentMethod } from '../entities/restaurant-payment-method.entity';
|
||||||
|
|
||||||
@@ -7,4 +7,13 @@ export class RestaurantPaymentMethodRepository extends EntityRepository<Restaura
|
|||||||
constructor(readonly em: EntityManager) {
|
constructor(readonly em: EntityManager) {
|
||||||
super(em, RestaurantPaymentMethod);
|
super(em, RestaurantPaymentMethod);
|
||||||
}
|
}
|
||||||
|
async findOneWithPopulate(id: string): Promise<RestaurantPaymentMethod> {
|
||||||
|
const restaurantPaymentMethod = await this.findOne({ id }, { populate: ['restaurant', 'paymentMethod'] });
|
||||||
|
if (!restaurantPaymentMethod) {
|
||||||
|
throw new NotFoundException('Restaurant payment method not found');
|
||||||
|
}
|
||||||
|
await restaurantPaymentMethod.restaurant.load();
|
||||||
|
await restaurantPaymentMethod.paymentMethod.load();
|
||||||
|
return restaurantPaymentMethod;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { RestaurantPaymentMethod } from '../entities/restaurant-payment-method.entity';
|
||||||
|
import { Order } from '../../orders/entities/order.entity';
|
||||||
|
|
||||||
|
export interface PaymentGatewayRedirect {
|
||||||
|
redirectUrl: string;
|
||||||
|
orderId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PaymentGatewayService {
|
||||||
|
private readonly logger = new Logger(PaymentGatewayService.name);
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate payment gateway redirect URL for online payment methods
|
||||||
|
*/
|
||||||
|
generateRedirectUrl(order: Order, restaurantPaymentMethod: RestaurantPaymentMethod): Promise<PaymentGatewayRedirect> {
|
||||||
|
const paymentMethod = restaurantPaymentMethod.paymentMethod;
|
||||||
|
|
||||||
|
if (!paymentMethod.isOnline) {
|
||||||
|
throw new BadRequestException('Payment method is not online');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get base URL for callback
|
||||||
|
const baseUrl = this.configService.get<string>('APP_BASE_URL') || 'http://localhost:4000';
|
||||||
|
const callbackUrl = paymentMethod.callbackUrl || `${baseUrl}/public/payments/callback`;
|
||||||
|
|
||||||
|
// Generate payment gateway URL based on payment method keyName
|
||||||
|
const redirectUrl = this.generateGatewayUrl(paymentMethod.keyName, {
|
||||||
|
orderId: order.id,
|
||||||
|
amount: order.total,
|
||||||
|
merchantId: restaurantPaymentMethod.merchantId,
|
||||||
|
callbackUrl,
|
||||||
|
description: `Order #${order.id}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
redirectUrl,
|
||||||
|
orderId: order.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate gateway URL based on payment provider
|
||||||
|
* This can be extended to support multiple payment gateways (ZarinPal, Mellat, etc.)
|
||||||
|
*/
|
||||||
|
private generateGatewayUrl(
|
||||||
|
paymentMethodKey: string,
|
||||||
|
params: {
|
||||||
|
orderId: string;
|
||||||
|
amount: number;
|
||||||
|
merchantId?: string;
|
||||||
|
callbackUrl: string;
|
||||||
|
description: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
// Convert amount to smallest currency unit (e.g., Toman to Rials for Iranian gateways)
|
||||||
|
const amountInSmallestUnit = Math.round(params.amount * 10); // Assuming Toman to Rials conversion
|
||||||
|
|
||||||
|
switch (paymentMethodKey.toLowerCase()) {
|
||||||
|
case 'zarinpal':
|
||||||
|
return this.generateZarinPalUrl(params, amountInSmallestUnit);
|
||||||
|
|
||||||
|
case 'mellat':
|
||||||
|
case 'bankmellat':
|
||||||
|
return this.generateMellatUrl(params, amountInSmallestUnit);
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Generic payment gateway URL structure
|
||||||
|
// You can customize this based on your payment gateway provider
|
||||||
|
this.logger.warn(`Unknown payment method: ${paymentMethodKey}, using generic URL structure`);
|
||||||
|
return this.generateGenericGatewayUrl(params, amountInSmallestUnit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate ZarinPal payment gateway URL
|
||||||
|
*/
|
||||||
|
private generateZarinPalUrl(
|
||||||
|
params: {
|
||||||
|
orderId: string;
|
||||||
|
merchantId?: string;
|
||||||
|
callbackUrl: string;
|
||||||
|
description: string;
|
||||||
|
},
|
||||||
|
amount: number,
|
||||||
|
): string {
|
||||||
|
if (!params.merchantId) {
|
||||||
|
throw new BadRequestException('Merchant ID is required for ZarinPal payment gateway');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZarinPal payment gateway URL structure
|
||||||
|
// This is a simplified version - you may need to make an API call to get the actual redirect URL
|
||||||
|
const zarinPalBaseUrl =
|
||||||
|
this.configService.get<string>('ZARINPAL_BASE_URL') || 'https://www.zarinpal.com/pg/StartPay';
|
||||||
|
|
||||||
|
// For ZarinPal, you typically need to:
|
||||||
|
// 1. Call their API to create a payment request
|
||||||
|
// 2. Get an Authority token
|
||||||
|
// 3. Redirect to their payment page with the Authority
|
||||||
|
|
||||||
|
// For now, returning a placeholder URL structure
|
||||||
|
// TODO: Implement actual ZarinPal API integration
|
||||||
|
return `${zarinPalBaseUrl}/${params.orderId}?Amount=${amount}&MerchantID=${params.merchantId}&CallbackURL=${encodeURIComponent(params.callbackUrl)}&Description=${encodeURIComponent(params.description)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate Bank Mellat payment gateway URL
|
||||||
|
*/
|
||||||
|
private generateMellatUrl(
|
||||||
|
params: {
|
||||||
|
orderId: string;
|
||||||
|
merchantId?: string;
|
||||||
|
callbackUrl: string;
|
||||||
|
description: string;
|
||||||
|
},
|
||||||
|
amount: number,
|
||||||
|
): string {
|
||||||
|
if (!params.merchantId) {
|
||||||
|
throw new BadRequestException('Merchant ID is required for Mellat payment gateway');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mellatBaseUrl =
|
||||||
|
this.configService.get<string>('MELLAT_BASE_URL') || 'https://bpm.shaparak.ir/pgwchannel/startpay.mellat';
|
||||||
|
|
||||||
|
// TODO: Implement actual Mellat API integration
|
||||||
|
return `${mellatBaseUrl}?orderId=${params.orderId}&amount=${amount}&merchantId=${params.merchantId}&callbackUrl=${encodeURIComponent(params.callbackUrl)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate generic payment gateway URL
|
||||||
|
*/
|
||||||
|
private generateGenericGatewayUrl(
|
||||||
|
params: {
|
||||||
|
orderId: string;
|
||||||
|
merchantId?: string;
|
||||||
|
callbackUrl: string;
|
||||||
|
description: string;
|
||||||
|
},
|
||||||
|
amount: number,
|
||||||
|
): string {
|
||||||
|
const genericBaseUrl =
|
||||||
|
this.configService.get<string>('PAYMENT_GATEWAY_BASE_URL') || 'https://payment.example.com/pay';
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
orderId: params.orderId,
|
||||||
|
amount: amount.toString(),
|
||||||
|
callbackUrl: params.callbackUrl,
|
||||||
|
description: params.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (params.merchantId) {
|
||||||
|
queryParams.append('merchantId', params.merchantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${genericBaseUrl}?${queryParams.toString()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user