round money
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Round monetary amounts to whole currency units (DB stores scale 0).
|
||||
*/
|
||||
export function roundMoney(value: number | string | null | undefined): number {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.round(numeric);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format monetary amounts for display/SMS without floating-point artifacts.
|
||||
*/
|
||||
export function formatMoneyAmount(value: number | string | null | undefined): string {
|
||||
return roundMoney(value).toString();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Post, Body, Patch, Delete, UseGuards, Param } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Patch, Delete, UseGuards, Param, ValidationPipe } from '@nestjs/common';
|
||||
import { CartService } from '../providers/cart.service';
|
||||
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
|
||||
import { ApplyCouponDto } from '../dto/apply-coupon.dto';
|
||||
@@ -38,12 +38,12 @@ export class CartController {
|
||||
}
|
||||
|
||||
@Post('variant/bulk')
|
||||
@ApiOperation({ summary: 'Bulk add items to cart (increments quantity if items exist)' })
|
||||
@ApiOperation({ summary: 'Bulk set item quantities in cart (replaces quantity if item exists)' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
bulkAddItems(
|
||||
@UserId() userId: string,
|
||||
@ShopId() shopId: string,
|
||||
@Body() bulkAddItemsDto: BulkAddItemsToCartDto,
|
||||
@Body(new ValidationPipe({ transform: true, whitelist: true })) bulkAddItemsDto: BulkAddItemsToCartDto,
|
||||
) {
|
||||
return this.cartService.bulkAddItems(userId, shopId, bulkAddItemsDto);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { Product } from '../../products/entities/product.entity';
|
||||
import { Cart } from '../interfaces/cart.interface';
|
||||
import { CouponService } from 'src/modules/coupons/providers/coupon.service';
|
||||
import { GeographicUtils } from '../utils/geographic.utils';
|
||||
import { roundQuantity } from 'src/modules/products/utils/quantity.utils';
|
||||
import { roundMoney } from 'src/common/utils/money.utils';
|
||||
|
||||
@Injectable()
|
||||
export class CartCalculationService {
|
||||
@@ -24,7 +26,7 @@ export class CartCalculationService {
|
||||
const safeUnitDiscount = Math.min(Number(unitDiscount) || 0, safeUnitPrice);
|
||||
const itemTotalPrice = safeUnitPrice * quantity;
|
||||
const itemTotalDiscount = safeUnitDiscount * quantity;
|
||||
return itemTotalPrice - itemTotalDiscount;
|
||||
return roundMoney(itemTotalPrice - itemTotalDiscount);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,17 +38,23 @@ export class CartCalculationService {
|
||||
let totalItems = 0;
|
||||
|
||||
for (const item of cart.items) {
|
||||
const quantity = roundQuantity(item.quantity);
|
||||
const unitPrice = Number(item.price) || 0;
|
||||
const unitDiscount = Math.min(Number(item.discount) || 0, unitPrice);
|
||||
const itemPrice = unitPrice * item.quantity;
|
||||
const itemDiscount = unitDiscount * item.quantity;
|
||||
const itemPrice = unitPrice * quantity;
|
||||
const itemDiscount = unitDiscount * quantity;
|
||||
subTotal += itemPrice;
|
||||
itemsDiscount += itemDiscount;
|
||||
totalItems += item.quantity;
|
||||
item.totalPrice = itemPrice - itemDiscount;
|
||||
totalItems += quantity;
|
||||
item.quantity = quantity;
|
||||
item.totalPrice = roundMoney(itemPrice - itemDiscount);
|
||||
}
|
||||
|
||||
return { subTotal, itemsDiscount, totalItems };
|
||||
return {
|
||||
subTotal: roundMoney(subTotal),
|
||||
itemsDiscount: roundMoney(itemsDiscount),
|
||||
totalItems,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,10 +102,10 @@ export class CartCalculationService {
|
||||
if (cart.coupon.maxDiscount && discount > cart.coupon.maxDiscount) {
|
||||
discount = cart.coupon.maxDiscount;
|
||||
}
|
||||
return discount;
|
||||
return roundMoney(discount);
|
||||
}
|
||||
|
||||
return Math.min(cart.coupon.value, priceAfterItemDiscount);
|
||||
return roundMoney(Math.min(cart.coupon.value, priceAfterItemDiscount));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,7 +115,7 @@ export class CartCalculationService {
|
||||
const shop = await this.em.findOne(Shop, { id: shopId });
|
||||
const vat = shop?.vat ? Number(shop.vat) : 0;
|
||||
if (!vat || vat <= 0) return 0;
|
||||
return (Math.max(0, amountAfterDiscounts) * vat) / 100;
|
||||
return roundMoney((Math.max(0, amountAfterDiscounts) * vat) / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,7 +130,7 @@ export class CartCalculationService {
|
||||
|
||||
// If not distance based, return fixed delivery fee
|
||||
if (deliveryMethod.deliveryFeeType !== DeliveryFeeTypeEnum.DISTANCE_BASED) {
|
||||
return Number(deliveryMethod.deliveryFee) || 0;
|
||||
return roundMoney(Number(deliveryMethod.deliveryFee) || 0);
|
||||
}
|
||||
|
||||
// For distance based calculation we need shop and user coordinates
|
||||
@@ -131,11 +139,11 @@ export class CartCalculationService {
|
||||
|
||||
if (!shop || shop.latitude == null || shop.longitude == null) {
|
||||
// fallback to configured fixed fee when coordinates are missing
|
||||
return Number(deliveryMethod.deliveryFee) || 0;
|
||||
return roundMoney(Number(deliveryMethod.deliveryFee) || 0);
|
||||
}
|
||||
|
||||
if (!userAddr || userAddr.latitude == null || userAddr.longitude == null) {
|
||||
return Number(deliveryMethod.deliveryFee) || 0;
|
||||
return roundMoney(Number(deliveryMethod.deliveryFee) || 0);
|
||||
}
|
||||
|
||||
const restLat = Number(shop.latitude);
|
||||
@@ -158,7 +166,7 @@ export class CartCalculationService {
|
||||
|
||||
if (minFee > 0 && fee < minFee) fee = minFee;
|
||||
|
||||
return Math.max(0, Number(fee));
|
||||
return roundMoney(Math.max(0, Number(fee)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,13 +179,13 @@ export class CartCalculationService {
|
||||
|
||||
const couponDiscount = await this.calculateCouponDiscount(cart, subTotal, itemsDiscount);
|
||||
cart.couponDiscount = couponDiscount;
|
||||
cart.totalDiscount = couponDiscount + itemsDiscount;
|
||||
cart.totalDiscount = roundMoney(couponDiscount + itemsDiscount);
|
||||
|
||||
cart.tax = await this.calculateTax(cart.shopId, Math.max(0, subTotal - cart.totalDiscount));
|
||||
cart.deliveryFee = await this.calculateDeliveryFee(cart);
|
||||
|
||||
// total = subtotal – totalDiscount + tax + deliveryFee
|
||||
cart.total = Math.max(0, subTotal - cart.totalDiscount) + cart.tax + cart.deliveryFee;
|
||||
cart.total = roundMoney(Math.max(0, subTotal - cart.totalDiscount) + cart.tax + cart.deliveryFee);
|
||||
cart.totalItems = totalItems;
|
||||
cart.updatedAt = this.nowIso();
|
||||
}
|
||||
|
||||
@@ -69,28 +69,54 @@ export class CartItemService {
|
||||
* Add or increment item in cart (validates stock against existing + new quantity)
|
||||
*/
|
||||
async addOrIncrementItem(cart: Cart, variantId: string, shopId: string, quantityToAdd: number): Promise<void> {
|
||||
const normalizedQuantityToAdd = roundQuantity(quantityToAdd);
|
||||
const index = this.getItemIndex(cart, variantId);
|
||||
const existingQuantityInCart = index >= 0 ? cart.items[index].quantity : 0;
|
||||
const existingQuantityInCart = index >= 0 ? roundQuantity(cart.items[index].quantity) : 0;
|
||||
|
||||
const variant = await this.validationService.validateAndGetVariant(
|
||||
variantId,
|
||||
shopId,
|
||||
quantityToAdd,
|
||||
normalizedQuantityToAdd,
|
||||
existingQuantityInCart,
|
||||
cart,
|
||||
);
|
||||
|
||||
if (index < 0) {
|
||||
cart.items.push(this.createCartItem(variant, quantityToAdd));
|
||||
cart.items.push(this.createCartItem(variant, normalizedQuantityToAdd));
|
||||
return;
|
||||
}
|
||||
|
||||
const existingItem = cart.items[index];
|
||||
const newQuantity = roundQuantity(existingItem.quantity + quantityToAdd);
|
||||
const newQuantity = roundQuantity(existingQuantityInCart + normalizedQuantityToAdd);
|
||||
|
||||
cart.items[index] = this.buildCartItemFromFood(variant, newQuantity, existingItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set item quantity in cart (adds item if it does not exist)
|
||||
*/
|
||||
async setItemQuantity(cart: Cart, variantId: string, shopId: string, quantity: number): Promise<void> {
|
||||
const normalizedQuantity = roundQuantity(quantity);
|
||||
const index = this.getItemIndex(cart, variantId);
|
||||
const existingQuantityInCart = index >= 0 ? roundQuantity(cart.items[index].quantity) : 0;
|
||||
|
||||
const variant = await this.validationService.validateAndGetVariant(
|
||||
variantId,
|
||||
shopId,
|
||||
normalizedQuantity,
|
||||
existingQuantityInCart,
|
||||
cart,
|
||||
{ mode: 'set' },
|
||||
);
|
||||
|
||||
if (index < 0) {
|
||||
cart.items.push(this.createCartItem(variant, normalizedQuantity));
|
||||
return;
|
||||
}
|
||||
|
||||
cart.items[index] = this.buildCartItemFromFood(variant, normalizedQuantity, cart.items[index]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment item by product purchase step (1 for fixed, purchasePitch for variable)
|
||||
*/
|
||||
@@ -123,7 +149,7 @@ export class CartItemService {
|
||||
const existingItem = cart.items[itemIndex];
|
||||
const variant = await this.productService.findOrFailVariant(variantId);
|
||||
const step = getQuantityStep(variant.product);
|
||||
const newQuantity = roundQuantity(existingItem.quantity - step);
|
||||
const newQuantity = roundQuantity(roundQuantity(existingItem.quantity) - step);
|
||||
|
||||
if (newQuantity <= 0) {
|
||||
cart.items.splice(itemIndex, 1);
|
||||
|
||||
@@ -37,34 +37,42 @@ export class CartValidationService {
|
||||
async validateAndGetVariant(
|
||||
variantId: string,
|
||||
shopId: string,
|
||||
quantityToAdd: number,
|
||||
quantity: number,
|
||||
existingQuantityInCart: number = 0,
|
||||
cart?: { items: { variantId: string; quantity: number }[] },
|
||||
options?: { mode?: 'increment' | 'set' },
|
||||
): Promise<Variant> {
|
||||
const mode = options?.mode ?? 'increment';
|
||||
const normalizedQuantity = roundQuantity(quantity);
|
||||
const normalizedExistingQuantity = roundQuantity(existingQuantityInCart);
|
||||
const variant = await this.productService.findOrFailVariant(variantId);
|
||||
|
||||
if (variant.product.shop.id !== shopId) {
|
||||
throw new BadRequestException(CartMessage.PRODUCT_NOT_BELONGS_TO_SHOP);
|
||||
}
|
||||
|
||||
const totalRequested =
|
||||
mode === 'set' ? normalizedQuantity : normalizedExistingQuantity + normalizedQuantity;
|
||||
|
||||
if (variant.stock != null) {
|
||||
const totalRequested = existingQuantityInCart + quantityToAdd;
|
||||
if (totalRequested > variant.stock) {
|
||||
if (totalRequested > roundQuantity(variant.stock)) {
|
||||
throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK);
|
||||
}
|
||||
}
|
||||
|
||||
this.assertValidQuantity(variant, existingQuantityInCart + quantityToAdd);
|
||||
this.assertValidQuantity(variant, totalRequested);
|
||||
|
||||
const maxPurchase = variant.product.maxPurchaseQuantity;
|
||||
if (maxPurchase != null && maxPurchase > 0) {
|
||||
const productVariantIds = variant.product.variants.getItems().map((v) => v.id);
|
||||
const totalProductQuantityInCart =
|
||||
cart?.items?.reduce(
|
||||
(sum, item) => (productVariantIds.includes(item.variantId) ? sum + item.quantity : sum),
|
||||
(sum, item) =>
|
||||
productVariantIds.includes(item.variantId) ? sum + roundQuantity(item.quantity) : sum,
|
||||
0,
|
||||
) ?? 0;
|
||||
const newTotalProductQuantity = totalProductQuantityInCart - existingQuantityInCart + quantityToAdd;
|
||||
const newTotalProductQuantity =
|
||||
totalProductQuantityInCart - normalizedExistingQuantity + totalRequested;
|
||||
if (newTotalProductQuantity > maxPurchase) {
|
||||
throw new BadRequestException(CartMessage.MAX_PURCHASE_EXCEEDED);
|
||||
}
|
||||
|
||||
@@ -181,13 +181,13 @@ export class CartService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk add items to cart (increments quantity if items exist)
|
||||
* Bulk set item quantities in cart (replaces quantity if item exists)
|
||||
*/
|
||||
async bulkAddItems(userId: string, shopId: string, bulkAddItemsDto: BulkAddItemsToCartDto): Promise<Cart> {
|
||||
const cart = await this.getOrCreateCart(userId, shopId);
|
||||
|
||||
for (const addItemDto of bulkAddItemsDto.items) {
|
||||
await this.itemService.addOrIncrementItem(cart, addItemDto.variantId, shopId, addItemDto.quantity);
|
||||
await this.itemService.setItemQuantity(cart, addItemDto.variantId, shopId, addItemDto.quantity);
|
||||
}
|
||||
|
||||
return this.recalculateAndSaveCart(cart);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CacheService } from 'src/modules/utils/cache.service';
|
||||
import { roundQuantity } from 'src/modules/products/utils/quantity.utils';
|
||||
import { Cart } from '../interfaces/cart.interface';
|
||||
|
||||
@Injectable()
|
||||
@@ -20,7 +21,7 @@ export class CartRepository {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(cachedCart);
|
||||
if (this.isCart(parsed) && parsed.userId === userId && parsed.shopId === shopId) {
|
||||
return parsed;
|
||||
return this.normalizeCartQuantities(parsed);
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, return null
|
||||
@@ -53,6 +54,17 @@ export class CartRepository {
|
||||
return `${this.CART_KEY_PREFIX}:${userId}:${shopId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce cached item quantities to numbers (guards against string concat bugs).
|
||||
*/
|
||||
private normalizeCartQuantities(cart: Cart): Cart {
|
||||
for (const item of cart.items) {
|
||||
item.quantity = roundQuantity(item.quantity);
|
||||
}
|
||||
cart.totalItems = roundQuantity(cart.totalItems);
|
||||
return cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an object is a Cart
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ import { OrderStatus } from '../interface/order.interface';
|
||||
import { PaymentMethodEnum } from 'src/modules/payments/interface/payment';
|
||||
import { UserService } from 'src/modules/users/providers/user.service';
|
||||
import { WalletTransactionReason, WalletTransactionType } from 'src/modules/users/interface/wallet';
|
||||
import { formatMoneyAmount } from 'src/common/utils/money.utils';
|
||||
|
||||
@Injectable()
|
||||
export class OrderListeners {
|
||||
@@ -69,17 +70,17 @@ export class OrderListeners {
|
||||
shopId: event.shopId,
|
||||
message: {
|
||||
title: NotifTitleEnum.ORDER_CREATED,
|
||||
content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`,
|
||||
content: `سفارش شماره ${event.orderNumber} با مبلغ ${formatMoneyAmount(event.total)} تومان با موفقیت ایجاد شد`,
|
||||
sms: {
|
||||
templateId: this.orderCreatedSmsTemplateId,
|
||||
parameters: {
|
||||
orderNumber: event.orderNumber,
|
||||
total: event.total.toString(),
|
||||
total: formatMoneyAmount(event.total),
|
||||
},
|
||||
},
|
||||
pushNotif: {
|
||||
title: `سفارش جدید`,
|
||||
content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`,
|
||||
content: `سفارش شماره ${event.orderNumber} با مبلغ ${formatMoneyAmount(event.total)} تومان با موفقیت ایجاد شد`,
|
||||
icon: `/`,
|
||||
action: {
|
||||
type: NotifTitleEnum.ORDER_CREATED,
|
||||
|
||||
@@ -27,6 +27,7 @@ import { OrderItemStatus } from '../enum/order-item.enum';
|
||||
import { getMinQuantity, isValidProductQuantity, roundQuantity } from '../../products/utils/quantity.utils';
|
||||
import { CouponService } from 'src/modules/coupons/providers/coupon.service';
|
||||
import { CouponType } from 'src/modules/coupons/interface/coupon';
|
||||
import { roundMoney } from 'src/common/utils/money.utils';
|
||||
|
||||
type OrderItemData = { variant: Variant; quantity: number; unitPrice: number; discount: number };
|
||||
|
||||
@@ -101,7 +102,7 @@ export class OrdersService {
|
||||
for (const itemData of validated.orderItemsData) {
|
||||
const { variant, quantity, unitPrice, discount } = itemData;
|
||||
|
||||
const totalPrice = (unitPrice - discount) * quantity;
|
||||
const totalPrice = roundMoney((unitPrice - discount) * quantity);
|
||||
|
||||
const orderItem = em.create(OrderItem, {
|
||||
order,
|
||||
@@ -127,7 +128,7 @@ export class OrdersService {
|
||||
|
||||
this.eventEmitter.emit(
|
||||
OrderCreatedEvent.name,
|
||||
new OrderCreatedEvent(order.id, shopId, String(order.orderNumber), order.total),
|
||||
new OrderCreatedEvent(order.id, shopId, String(order.orderNumber), roundMoney(order.total)),
|
||||
);
|
||||
|
||||
return { order };
|
||||
@@ -212,6 +213,7 @@ export class OrdersService {
|
||||
'payments',
|
||||
'items',
|
||||
'items.variant',
|
||||
'items.variant.product',
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -335,19 +337,19 @@ export class OrdersService {
|
||||
subTotal += unitPrice * item.quantity;
|
||||
itemsDiscount += unitDiscount * item.quantity;
|
||||
totalItems += item.quantity;
|
||||
item.totalPrice = (unitPrice - unitDiscount) * item.quantity;
|
||||
item.totalPrice = roundMoney((unitPrice - unitDiscount) * item.quantity);
|
||||
}
|
||||
|
||||
order.subTotal = subTotal;
|
||||
order.itemsDiscount = itemsDiscount;
|
||||
order.subTotal = roundMoney(subTotal);
|
||||
order.itemsDiscount = roundMoney(itemsDiscount);
|
||||
order.totalItems = totalItems;
|
||||
order.couponDiscount = await this.calculateOrderCouponDiscount(order);
|
||||
order.totalDiscount = order.couponDiscount + order.itemsDiscount;
|
||||
order.totalDiscount = roundMoney(order.couponDiscount + order.itemsDiscount);
|
||||
|
||||
const amountAfterDiscounts = Math.max(0, subTotal - order.totalDiscount);
|
||||
const amountAfterDiscounts = Math.max(0, order.subTotal - order.totalDiscount);
|
||||
const vat = order.shop?.vat ? Number(order.shop.vat) : 0;
|
||||
order.tax = vat > 0 ? (amountAfterDiscounts * vat) / 100 : 0;
|
||||
order.total = amountAfterDiscounts + order.tax + Number(order.deliveryFee || 0);
|
||||
order.tax = vat > 0 ? roundMoney((amountAfterDiscounts * vat) / 100) : 0;
|
||||
order.total = roundMoney(amountAfterDiscounts + order.tax + Number(order.deliveryFee || 0));
|
||||
}
|
||||
|
||||
private async calculateOrderCouponDiscount(order: Order): Promise<number> {
|
||||
@@ -395,10 +397,10 @@ export class OrdersService {
|
||||
if (couponDetail.maxDiscount && discount > couponDetail.maxDiscount) {
|
||||
discount = couponDetail.maxDiscount;
|
||||
}
|
||||
return discount;
|
||||
return roundMoney(discount);
|
||||
}
|
||||
|
||||
return Math.min(couponDetail.value, priceAfterItemDiscount);
|
||||
return roundMoney(Math.min(couponDetail.value, priceAfterItemDiscount));
|
||||
}
|
||||
|
||||
private assertStatusTransitionAllowed(order: Order, to: OrderStatus, ref: StatusTransitionRef) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { NotifTitleEnum } from 'src/modules/notifications/interfaces/notificatio
|
||||
import { NotificationService } from 'src/modules/notifications/services/notification.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { OrdersService } from 'src/modules/orders/providers/orders.service';
|
||||
import { formatMoneyAmount } from 'src/common/utils/money.utils';
|
||||
|
||||
@Injectable()
|
||||
export class PaymentListeners {
|
||||
@@ -48,7 +49,7 @@ export class PaymentListeners {
|
||||
shopId: event.shopId,
|
||||
message: {
|
||||
title: NotifTitleEnum.PAYMENT_SUCCESS,
|
||||
content: `پرداخت سفارش شماره ${event.orderNumber} با روش ${this.getPaymentMethodFarsi(event.paymentMethod)} و مبلغ ${event.total} تومان با موفقیت انجام شد`,
|
||||
content: `پرداخت سفارش شماره ${event.orderNumber} با روش ${this.getPaymentMethodFarsi(event.paymentMethod)} و مبلغ ${formatMoneyAmount(event.total)} تومان با موفقیت انجام شد`,
|
||||
sms: {
|
||||
templateId: this.orderCreatedSmsTemplateId,
|
||||
parameters: {
|
||||
@@ -58,7 +59,7 @@ export class PaymentListeners {
|
||||
},
|
||||
pushNotif: {
|
||||
title: `پرداخت موفق`,
|
||||
content: `پرداخت سفارش شماره ${event.orderNumber} با روش ${this.getPaymentMethodFarsi(event.paymentMethod)} و مبلغ ${event.total} تومان با موفقیت انجام شد`,
|
||||
content: `پرداخت سفارش شماره ${event.orderNumber} با روش ${this.getPaymentMethodFarsi(event.paymentMethod)} و مبلغ ${formatMoneyAmount(event.total)} تومان با موفقیت انجام شد`,
|
||||
icon: ``,
|
||||
action: {
|
||||
type: NotifTitleEnum.PAYMENT_SUCCESS,
|
||||
@@ -96,17 +97,17 @@ export class PaymentListeners {
|
||||
shopId: event.shopId,
|
||||
message: {
|
||||
title: NotifTitleEnum.ORDER_CREATED,
|
||||
content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`,
|
||||
content: `سفارش شماره ${event.orderNumber} با مبلغ ${formatMoneyAmount(event.total)} تومان با موفقیت ایجاد شد`,
|
||||
sms: {
|
||||
templateId: this.orderCreatedSmsTemplateId,
|
||||
parameters: {
|
||||
orderNumber: event.orderNumber,
|
||||
total: event.total.toString(),
|
||||
total: formatMoneyAmount(event.total),
|
||||
},
|
||||
},
|
||||
pushNotif: {
|
||||
title: `سفارش جدید`,
|
||||
content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`,
|
||||
content: `سفارش شماره ${event.orderNumber} با مبلغ ${formatMoneyAmount(event.total)} تومان با موفقیت ایجاد شد`,
|
||||
icon: `/`,
|
||||
action: {
|
||||
type: NotifTitleEnum.ORDER_CREATED,
|
||||
@@ -137,17 +138,17 @@ export class PaymentListeners {
|
||||
shopId: event.shopId,
|
||||
message: {
|
||||
title: NotifTitleEnum.PAYMENT_SUCCESS,
|
||||
content: `پرداخت سفارش شماره ${order.orderNumber} با روش ${this.getPaymentMethodFarsi(order.paymentMethod.method)} و مبلغ ${order.total} تومان با موفقیت انجام شد`,
|
||||
content: `پرداخت سفارش شماره ${order.orderNumber} با روش ${this.getPaymentMethodFarsi(order.paymentMethod.method)} و مبلغ ${formatMoneyAmount(order.total)} تومان با موفقیت انجام شد`,
|
||||
sms: {
|
||||
templateId: this.paymentSucceedSmsTemplateId,
|
||||
parameters: {
|
||||
orderNumber: event.orderNumber,
|
||||
total: event.total.toString(),
|
||||
total: formatMoneyAmount(event.total),
|
||||
},
|
||||
},
|
||||
pushNotif: {
|
||||
title: `پرداخت موفق`,
|
||||
content: `پرداخت سفارش شماره ${order.orderNumber} با روش ${this.getPaymentMethodFarsi(order.paymentMethod.method)} و مبلغ ${order.total} تومان با موفقیت انجام شد`,
|
||||
content: `پرداخت سفارش شماره ${order.orderNumber} با روش ${this.getPaymentMethodFarsi(order.paymentMethod.method)} و مبلغ ${formatMoneyAmount(order.total)} تومان با موفقیت انجام شد`,
|
||||
icon: `/`,
|
||||
action: {
|
||||
type: NotifTitleEnum.PAYMENT_SUCCESS,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { onlinePaymentSucceedEvent, paymentSucceedEvent } from '../events/payment.events';
|
||||
import { WalletTransactionReason, WalletTransactionType } from 'src/modules/users/interface/wallet';
|
||||
import { PaymentRepository } from '../repositories/payment.repository';
|
||||
import { roundMoney } from 'src/common/utils/money.utils';
|
||||
|
||||
@Injectable()
|
||||
export class PaymentsService {
|
||||
@@ -83,7 +84,7 @@ export class PaymentsService {
|
||||
|
||||
return {
|
||||
order,
|
||||
amount: order.total,
|
||||
amount: roundMoney(order.total),
|
||||
method: pm.method,
|
||||
gateway: pm.gateway ?? null,
|
||||
merchantId: pm.merchantId ?? null,
|
||||
@@ -152,7 +153,7 @@ export class PaymentsService {
|
||||
});
|
||||
this.eventEmitter.emit(
|
||||
paymentSucceedEvent.name,
|
||||
new paymentSucceedEvent(ctx.order.id, ctx.order.shop.id, String(ctx.order?.orderNumber) || '', ctx.method, ctx.amount),
|
||||
new paymentSucceedEvent(ctx.order.id, ctx.order.shop.id, String(ctx.order?.orderNumber) || '', ctx.method, roundMoney(ctx.amount)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -234,7 +235,7 @@ export class PaymentsService {
|
||||
});
|
||||
this.eventEmitter.emit(
|
||||
onlinePaymentSucceedEvent.name,
|
||||
new onlinePaymentSucceedEvent(payment.id, payment.order.id, payment.order.shop.id, String(payment.order?.orderNumber) || '', payment.amount),
|
||||
new onlinePaymentSucceedEvent(payment.id, payment.order.id, payment.order.shop.id, String(payment.order?.orderNumber) || '', roundMoney(payment.amount)),
|
||||
);
|
||||
return payment;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,12 @@ export function getMinQuantity(product: Product): number {
|
||||
return product.pricingType === PricingType.VARIABLE ? getQuantityStep(product) : 1;
|
||||
}
|
||||
|
||||
export function roundQuantity(value: number): number {
|
||||
return Math.round(value * SCALE_FACTOR) / SCALE_FACTOR;
|
||||
export function roundQuantity(value: number | string): number {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.round(numeric * SCALE_FACTOR) / SCALE_FACTOR;
|
||||
}
|
||||
|
||||
export function isQuantityAlignedToPitch(quantity: number, pitch: number): boolean {
|
||||
|
||||
Reference in New Issue
Block a user