diff --git a/src/modules/cart/providers/cart.service.ts b/src/modules/cart/providers/cart.service.ts index 697a270..6624ddb 100644 --- a/src/modules/cart/providers/cart.service.ts +++ b/src/modules/cart/providers/cart.service.ts @@ -526,4 +526,9 @@ export class CartService { const cacheKey = `${this.CART_KEY_PREFIX}:${cart.userId}:${cart.restaurantId}`; await this.cacheService.set(cacheKey, JSON.stringify(cart), this.CART_TTL); } + + async clearCart(userId: string, restaurantId: string): Promise { + const cacheKey = `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`; + return this.cacheService.del(cacheKey); + } } diff --git a/src/modules/orders/orders.module.ts b/src/modules/orders/orders.module.ts index 711ab6f..8b5a8e5 100644 --- a/src/modules/orders/orders.module.ts +++ b/src/modules/orders/orders.module.ts @@ -12,6 +12,7 @@ import { RestaurantPaymentMethod } from '../payments/entities/restaurant-payment import { CartModule } from '../cart/cart.module'; import { UtilsModule } from '../utils/utils.module'; import { AuthModule } from '../auth/auth.module'; +import { PaymentsModule } from '../payments/payments.module'; import { JwtModule } from '@nestjs/jwt'; @Module({ @@ -20,6 +21,7 @@ import { JwtModule } from '@nestjs/jwt'; CartModule, UtilsModule, AuthModule, + PaymentsModule, JwtModule, ], controllers: [OrdersController], diff --git a/src/modules/orders/orders.service.ts b/src/modules/orders/orders.service.ts index ada4e76..3047e99 100644 --- a/src/modules/orders/orders.service.ts +++ b/src/modules/orders/orders.service.ts @@ -8,22 +8,22 @@ import { Food } from '../foods/entities/food.entity'; import { UserAddress } from '../users/entities/user-address.entity'; import { RestaurantPaymentMethod } from '../payments/entities/restaurant-payment-method.entity'; import { CartService } from '../cart/providers/cart.service'; -import { CacheService } from '../utils/cache.service'; import { OrderStatus } from './interface/order-status'; import { PaymentStatus } from '../payments/interface/payment-status'; 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() export class OrdersService { - private readonly CART_KEY_PREFIX = 'cart'; - constructor( private readonly em: EntityManager, private readonly cartService: CartService, - private readonly cacheService: CacheService, + private readonly RestaurantPaymentMethodRepository: RestaurantPaymentMethodRepository, + private readonly paymentGatewayService: PaymentGatewayService, ) {} - async create(userId: string, restaurantId: string): Promise { + async create(userId: string, restaurantId: string): Promise { // Get cart const cart = await this.cartService.findOne(userId, restaurantId); @@ -77,8 +77,25 @@ export class OrdersService { await em.flush(); // Clear cart from cache after successful order creation - const cacheKey = `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`; - await this.cacheService.del(cacheKey); + await this.cartService.clearCart(userId, restaurantId); + + // 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; }); diff --git a/src/modules/payments/payments.module.ts b/src/modules/payments/payments.module.ts index 30c458e..3433051 100644 --- a/src/modules/payments/payments.module.ts +++ b/src/modules/payments/payments.module.ts @@ -7,6 +7,7 @@ import { PaymentMethodService } from './services/payment-method.service'; import { RestaurantPaymentMethod } from './entities/restaurant-payment-method.entity'; import { RestaurantPaymentMethodRepository } from './repositories/restaurant-payment-method.repository'; import { RestaurantPaymentMethodService } from './services/restaurant-payment-method.service'; +import { PaymentGatewayService } from './services/payment-gateway.service'; import { Restaurant } from '../restaurants/entities/restaurant.entity'; import { PaymentsController } from './controllers/payments.controller'; import { AuthModule } from '../auth/auth.module'; @@ -21,12 +22,15 @@ import { JwtModule } from '@nestjs/jwt'; PaymentMethodRepository, RestaurantPaymentMethodService, RestaurantPaymentMethodRepository, + PaymentGatewayService, ], exports: [ PaymentMethodRepository, PaymentMethodService, RestaurantPaymentMethodRepository, RestaurantPaymentMethodService, + PaymentsService, + PaymentGatewayService, ], }) export class PaymentsModule {} diff --git a/src/modules/payments/repositories/restaurant-payment-method.repository.ts b/src/modules/payments/repositories/restaurant-payment-method.repository.ts index 15971b5..dbcc77b 100644 --- a/src/modules/payments/repositories/restaurant-payment-method.repository.ts +++ b/src/modules/payments/repositories/restaurant-payment-method.repository.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; import { RestaurantPaymentMethod } from '../entities/restaurant-payment-method.entity'; @@ -7,4 +7,13 @@ export class RestaurantPaymentMethodRepository extends EntityRepository { + 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; + } } diff --git a/src/modules/payments/services/payment-gateway.service.ts b/src/modules/payments/services/payment-gateway.service.ts new file mode 100644 index 0000000..a723b28 --- /dev/null +++ b/src/modules/payments/services/payment-gateway.service.ts @@ -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 { + 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('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('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('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('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()}`; + } +}