first
This commit is contained in:
@@ -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 };
|
||||
Reference in New Issue
Block a user