This commit is contained in:
mahyargdz
2025-09-14 15:15:03 +03:30
commit be82059172
534 changed files with 51310 additions and 0 deletions
@@ -0,0 +1,392 @@
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: { authority: string; status: string }) {
const { status, authority } = verifyCallbackData;
const session = await startSession();
session.startTransaction();
try {
const paymentData = await this.cartPaymentRepo.model.findOne({ authority }).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") {
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 data = await this.paymentGateway.verifyPayment(gateway, { authority, amount: paymentData.totalPrice });
if (data.code === 100 || data.code === 101) {
await this.handleSellerNotify(order._id, user, session);
await this.handleSuccessfulPayment(paymentData._id.toString(), order._id, data.ref_id, 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, refId: number, session: ClientSession) {
await this.updatePaymentStatusAndReference(paymentId, refId, 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<string, number> = 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<IPriceDetails> = {
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, refId: number | null, status: PaymentStatus, session: ClientSession) {
return await this.cartPaymentRepo.model.findByIdAndUpdate(paymentId, { paymentStatus: status, transaction_id: refId }, { session });
}
}
export { PaymentService };