import { inject, injectable } from "inversify"; import { ClientSession, Types, isValidObjectId, startSession } from "mongoose"; import { CartMessage, PaymentMessage } from "../../../common/enums/message.enum"; import { OrderItemsStatus, OrdersStatus, PaymentStatus } from "../../../common/enums/order.enum"; import { GatewayProvider } from "../../../common/enums/payment.enum"; import { PaymentsQueries } from "../../../common/types/query.type"; import { BadRequestError } from "../../../core/app/app.errors"; import { Logger } from "../../../core/logging/logger"; import { IOCTYPES } from "../../../IOC/ioc.types"; import { SMS } from "../../../utils/sms.service"; import { CartRepository, CartShipItemRepo } from "../../cart/cart.repository"; import { CartService } from "../../cart/cart.service"; import { CartDTO } from "../../cart/DTO/cart.dto"; import { ICartShipmentItem } from "../../cart/models/Abstraction/ICartShipmentItem"; import { PaymentGateway } from "../../IPG/PaymentGateway"; import { NotificationService } from "../../notification/notification.service"; import { IOrderItem } from "../../order/models/Abstraction/IOrderItem"; import { OrderItemRepo } from "../../order/order.repository"; import { OrderService } from "../../order/order.service"; import { PricingRepository } from "../../pricing/pricing.repository"; import { ProductService } from "../../product/providers/product.service"; import { ShopRepo } from "../../shop/shop.repository"; import { IUser } from "../../user/models/Abstraction/IUser"; import { WalletService } from "../../wallet/wallet.service"; import { CartCheckoutDTO } from "../DTO/cartCheckout.dto"; import { PaymentDTO } from "../DTO/payment.dto"; import { IPaymentMethod } from "../models/Abstraction/IPaymentMethod"; import { IPriceDetails } from "../models/Abstraction/IPayments"; import { CartPaymentRepository, PaymentMethodRepo } from "../payment.repository"; @injectable() class PaymentService { private logger = new Logger(PaymentService.name); @inject(IOCTYPES.CartService) cartService: CartService; @inject(IOCTYPES.OrderService) orderService: OrderService; @inject(IOCTYPES.ProductService) productService: ProductService; @inject(IOCTYPES.CartPaymentRepo) cartPaymentRepo: CartPaymentRepository; @inject(IOCTYPES.PricingRepo) priceRepo: PricingRepository; @inject(IOCTYPES.OrderItemRepo) orderItemRepo: OrderItemRepo; @inject(IOCTYPES.CartRepository) cartRepo: CartRepository; @inject(IOCTYPES.PaymentGateway) paymentGateway: PaymentGateway; @inject(IOCTYPES.CartShipItemRepo) cartShipItemRepo: CartShipItemRepo; @inject(IOCTYPES.PaymentMethodRepo) paymentMethodRepo: PaymentMethodRepo; @inject(IOCTYPES.WalletService) walletService: WalletService; @inject(IOCTYPES.NotificationService) notificationService: NotificationService; @inject(IOCTYPES.ShopRepo) shopRepo: ShopRepo; //################################################################## //################################################################## async getAllPaymentsForAdminPanelS(queries: PaymentsQueries) { const { docs, count } = await this.cartPaymentRepo.getPaymentsForAdminPanel(queries); const payments = docs.map((doc: any) => PaymentDTO.transformPayment(doc)); return { payments, count, }; } async getPaymentInfoS(userId: string) { const { cart } = await this.cartService.getCartS(userId); const paymentMethods = await this.paymentMethodRepo.model.find().select("-createdAt -updatedAt"); if (!cart) throw new BadRequestError(CartMessage.CartNotFound); const cartShipmentItems = await this.cartShipItemRepo.model.aggregate([ { $match: { cart: new Types.ObjectId(cart._id.toString()), }, }, { $lookup: { from: "shops", localField: "shop", foreignField: "_id", as: "shop", }, }, { $unwind: "$shop", }, { $addFields: { shopName: "$shop.shopName", }, }, { $project: { shop: 0, __v: 0, }, }, ]); if (!cartShipmentItems.length) throw new BadRequestError(CartMessage.CartShipmentItemsNotFound); if (!paymentMethods) throw new BadRequestError(PaymentMessage.PaymentMethodsNotFound); const { price } = this.calculateTotalPrice(cart, cartShipmentItems); return { cart, cartShipmentItems, paymentMethods, price, }; } //############################## async getPricingDetails() { return { pricing: await this.priceRepo.findAll(), }; } //############################## async getPaymentMethods() { return { paymentMethods: await this.paymentMethodRepo.model.find({ isActive: true }), }; } //################################################################## //################################################################## async createCartPaymentS(cartCheckoutDto: CartCheckoutDTO, user: IUser) { const session = await startSession(); session.startTransaction(); try { const { cart } = await this.cartService.getCartS(user._id.toString()); if (!cart) throw new BadRequestError(CartMessage.CartNotFound); const cartShipmentItems = await this.cartShipItemRepo.model.find({ cart: cart._id.toString() }); if (!cartShipmentItems.length) throw new BadRequestError(CartMessage.CartShipmentItemsNotFound); const paymentMethod = await this.paymentMethodRepo.findById(cartCheckoutDto.payment_method_id); if (!paymentMethod) throw new BadRequestError(PaymentMessage.PaymentMethodNotFound); const { price, description } = this.calculateTotalPrice(cart, cartShipmentItems); // extracted to handle payment creation const { payment, paymentData } = await this.handlePaymentCreation(paymentMethod, price, user, description, session); // create order and order items const { order, orderItems } = await this.orderService.createOrder(user, payment, cartShipmentItems, session); // clear cart and adjust stock await this.finalizeOrder(user, cartShipmentItems, session); await session.commitTransaction(); await session.endSession(); return { paymentData, order, orderItems }; } catch (error) { await session.abortTransaction(); await session.endSession(); this.logger.warn(error); throw error; // throw new InternalError(["Order processing failed, please try again."]); } } //################################################################## //################################################################## //verify callback of cart payment async verifyCartPayment(gateway: GatewayProvider, verifyCallbackData: { status: string; authority?: string; refNum?: string }) { const { status, authority, refNum } = verifyCallbackData; const session = await startSession(); session.startTransaction(); try { // For SEP gateway, refNum is used as authority in database const authorityToFind = gateway === GatewayProvider.SEP ? refNum : authority; if (!authorityToFind) throw new BadRequestError(PaymentMessage.PaymentNotFound); // For SEP gateway, check if refNum has already been used as transaction_id if (gateway === GatewayProvider.SEP && refNum) { const existingPayment = await this.cartPaymentRepo.model.findOne({ transaction_id: refNum }).session(session); if (existingPayment) throw new BadRequestError(PaymentMessage.RefNumIsAlreadyUsed); } const paymentData = await this.cartPaymentRepo.model.findOne({ authority: authorityToFind }).session(session); if (!paymentData) throw new BadRequestError(PaymentMessage.PaymentNotFound); const order = await this.orderService.getOrderByPaymentId(paymentData._id.toString(), session); if (!order) throw new BadRequestError(PaymentMessage.PaymentNotFound); const user = order.user as unknown as IUser; const orderItems = await this.orderService.getOrderItemByOrderId(order._id, session); // Handle unsuccessful payment status if (status !== "OK") { // user can retry // await this.handleFailedPayment(paymentData._id.toString(), order._id, orderItems, session); return this.buildPaymentResponse(status, PaymentMessage.PaymentNotSuccessful, paymentData._id.toString(), order._id); } // Verify payment through gateway const verifyData = gateway === GatewayProvider.SEP ? { refNum: authority, amount: paymentData.totalPrice } : { authority, amount: paymentData.totalPrice }; const data = await this.paymentGateway.verifyPayment(gateway, verifyData); if (data.code === 100 || data.code === 101) { await this.handleSellerNotify(order._id, user, session); // For SEP gateway, save refNum as transaction_id; for Zarinpal, save ref_id const transactionId = gateway === GatewayProvider.SEP ? refNum : data.ref_id; await this.handleSuccessfulPayment(paymentData._id.toString(), order._id, transactionId, session); return this.buildPaymentResponse(status, PaymentMessage.PaymentSuccessful, paymentData._id.toString(), order._id); } // Handle verification failure await this.handleFailedPayment(paymentData._id.toString(), order._id, orderItems, session); return this.buildPaymentResponse(status, PaymentMessage.PaymentNotSuccessful, paymentData._id.toString(), order._id); } catch (error) { await session.abortTransaction(); throw error; } finally { await session.endSession(); } } //################################################################## //################################################################## //retry payment async retryPayment(paymentId: string, user: IUser) { if (!isValidObjectId(paymentId)) throw new BadRequestError(PaymentMessage.PaymentNotFound); const paymentData = await this.cartPaymentRepo.findById(paymentId); if (!paymentData) throw new BadRequestError(PaymentMessage.PaymentNotFound); if (user._id.toString() !== paymentData.user_id.toString()) throw new BadRequestError(PaymentMessage.PaymentNotBelongToUser); const paymentMethod = await this.paymentMethodRepo.model.findById(paymentData.payment_method.toString()).lean(); if (!paymentMethod) throw new BadRequestError(PaymentMessage.PaymentMethodNotFound); const data = await this.paymentGateway.retryPayment(paymentMethod.provider as GatewayProvider, paymentData.authority); return { ...data, }; } //################################################################## //################################################################## // Helper method to calculate the total price of cart items private calculateTotalPrice(cart: CartDTO, cartShipItem: ICartShipmentItem[]) { const totalShippingCost = cartShipItem.reduce( (sum, item) => sum + item.shipmentItems.reduce((subSum, shipmentItem) => subSum + shipmentItem.shipmentCost, 0), 0, ); // const totalCouponDiscount = cartShipItem.reduce((sum, item) => sum + item.totalCouponDiscount, 0); // const totalShippingCost = cartShipItem.reduce((sum, item) => sum + item.shipmentCost, 0); const price = { shipping_cost: totalShippingCost, // process_cost: totalShippingCost, process_cost: 0, cart_retail_price: cart.retail_price, // cart_payable_price: cart.payable_price - cart.coupon_discount, cart_payable_price: cart.payable_price, total_discount: cart.total_discount, coupon_discount: cart.coupon_discount, // total_payable_price: cart.payable_price + totalShippingCost * 2, total_payable_price: cart.isWholeSale ? cart.retail_price + totalShippingCost : cart.payable_price + totalShippingCost, }; const items = cart.items; let description = "خرید کالا:"; items.forEach((item) => { description += `${item.product.model} (${item.quantity}), `; }); description = description.slice(0, -2); return { price, description }; } //################################################################## //################################################################## private async handleFailedPayment(paymentId: string, orderId: number, orderItems: IOrderItem[], session: ClientSession) { await this.updatePaymentStatusAndReference(paymentId, null, PaymentStatus.Cancelled, session); await this.orderItemRepo.model.updateMany({ order: orderId }, { status: OrderItemsStatus.cancelled_system }, { session }); //increase the product stock because of failed payment const itemsToUpdateStock = orderItems.flatMap((sellerItem) => sellerItem.shipmentItems.map((shipmentItem) => ({ variantId: shipmentItem.variant.toString(), quantity: shipmentItem.quantity, })), ); await this.productService.updateStock(itemsToUpdateStock, "increase", session); await this.orderService.updateOrderStatus(orderId, OrdersStatus.cancelled_system, session); await session.commitTransaction(); } //################################################################## //################################################################## private async handleSuccessfulPayment(paymentId: string, orderId: number, transactionId: string, session: ClientSession) { await this.updatePaymentStatusAndReference(paymentId, transactionId, PaymentStatus.Completed, session); await this.orderService.updateOrderStatus(orderId, OrdersStatus.process_by_seller, session); await session.commitTransaction(); } //################################################################## //################################################################## private async handleSellerNotify(orderId: number, user: IUser, session: ClientSession) { const orderItems = await this.orderItemRepo.model.find({ order: orderId }).lean(); await SMS.sendOrderCreatedSms(user.phoneNumber, user.fullName, orderId); // group items by seller and calculate total price for each seller const shopTotalMap: Map = new Map(); for (const item of orderItems) { const shopId = item.shop.toString(); // const totalPriceForItem = item.shipmentItems.reduce((acc, shippingItem) => { // const totalCost = shippingItem.selling_price + shippingItem.shipmentCost; // return totalCost + acc; // }, 0); const totalPriceForItem = item.totalPaymentPrice; if (shopTotalMap.has(shopId)) { shopTotalMap.set(shopId, shopTotalMap.get(shopId)! + totalPriceForItem); } else { shopTotalMap.set(shopId, totalPriceForItem); } } // credit each sellers wallet with their corresponding total price for (const [shopId, totalPrice] of shopTotalMap.entries()) { const shop = await this.shopRepo.findById(shopId); await this.notificationService.notifyNewOrder(shop!.owner.toString(), orderId, totalPrice, session); } } //################################################################## //################################################################## //method to return response for template rendering private buildPaymentResponse(status: string, message: string, paymentId: string, orderId: number | string) { return { status, message, store_url: process.env.STORE_URL, payment_id: paymentId, order_id: orderId, }; } //################################################################## //################################################################## private async handlePaymentCreation(paymentMethod: IPaymentMethod, price: any, user: IUser, description: string, session: ClientSession) { const paymentData = await this.paymentGateway.requestPayment(paymentMethod.provider as GatewayProvider, { amount: price.total_payable_price, description, email: user.email, mobile: user.phoneNumber, callbackPath: "cart", }); if (!paymentData) throw new BadRequestError(PaymentMessage.PaymentRequestFailed); const priceDetails: Partial = { shipping_cost: price.shipping_cost, process_cost: price.process_cost, total_retail_price: price.cart_retail_price, total_discount: price.total_discount, coupon_discount: price.coupon_discount, total_payable_price: price.total_payable_price, }; const payment = await this.cartPaymentRepo.model.create( [ { user_id: user._id.toString(), authority: paymentData.authority, payment_method: paymentMethod._id, priceDetails, totalPrice: priceDetails.total_payable_price, }, ], { session }, ); return { payment: payment[0], paymentData }; } //################################################################## //################################################################## private async finalizeOrder(user: IUser, cartShipmentItems: ICartShipmentItem[], session: ClientSession) { // empty the cart and cartShipment after successful order creation await this.cartService.clearCartAndShipmentItems(user._id.toString(), session); // reduce stock for ordered items const itemsToUpdateStock = cartShipmentItems.flatMap((cartItem) => cartItem.shipmentItems.map((shipmentItem) => ({ variantId: shipmentItem.variant.toString(), quantity: shipmentItem.quantity, })), ); await this.productService.updateStock(itemsToUpdateStock, "decrease", session); } //################################################################## //################################################################## private async updatePaymentStatusAndReference(paymentId: string, transactionId: string | null, status: PaymentStatus, session: ClientSession) { return await this.cartPaymentRepo.model.findByIdAndUpdate(paymentId, { paymentStatus: status, transaction_id: transactionId }, { session }); } } export { PaymentService };