round money

This commit is contained in:
2026-06-28 16:18:45 +03:30
parent 5e9125ea44
commit 1b7ccaf643
12 changed files with 139 additions and 59 deletions
+17
View File
@@ -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 { CartService } from '../providers/cart.service';
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto'; import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
import { ApplyCouponDto } from '../dto/apply-coupon.dto'; import { ApplyCouponDto } from '../dto/apply-coupon.dto';
@@ -38,12 +38,12 @@ export class CartController {
} }
@Post('variant/bulk') @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) @ApiHeader(API_HEADER_SLUG)
bulkAddItems( bulkAddItems(
@UserId() userId: string, @UserId() userId: string,
@ShopId() shopId: string, @ShopId() shopId: string,
@Body() bulkAddItemsDto: BulkAddItemsToCartDto, @Body(new ValidationPipe({ transform: true, whitelist: true })) bulkAddItemsDto: BulkAddItemsToCartDto,
) { ) {
return this.cartService.bulkAddItems(userId, shopId, bulkAddItemsDto); 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 { Cart } from '../interfaces/cart.interface';
import { CouponService } from 'src/modules/coupons/providers/coupon.service'; import { CouponService } from 'src/modules/coupons/providers/coupon.service';
import { GeographicUtils } from '../utils/geographic.utils'; import { GeographicUtils } from '../utils/geographic.utils';
import { roundQuantity } from 'src/modules/products/utils/quantity.utils';
import { roundMoney } from 'src/common/utils/money.utils';
@Injectable() @Injectable()
export class CartCalculationService { export class CartCalculationService {
@@ -24,7 +26,7 @@ export class CartCalculationService {
const safeUnitDiscount = Math.min(Number(unitDiscount) || 0, safeUnitPrice); const safeUnitDiscount = Math.min(Number(unitDiscount) || 0, safeUnitPrice);
const itemTotalPrice = safeUnitPrice * quantity; const itemTotalPrice = safeUnitPrice * quantity;
const itemTotalDiscount = safeUnitDiscount * quantity; const itemTotalDiscount = safeUnitDiscount * quantity;
return itemTotalPrice - itemTotalDiscount; return roundMoney(itemTotalPrice - itemTotalDiscount);
} }
/** /**
@@ -36,17 +38,23 @@ export class CartCalculationService {
let totalItems = 0; let totalItems = 0;
for (const item of cart.items) { for (const item of cart.items) {
const quantity = roundQuantity(item.quantity);
const unitPrice = Number(item.price) || 0; const unitPrice = Number(item.price) || 0;
const unitDiscount = Math.min(Number(item.discount) || 0, unitPrice); const unitDiscount = Math.min(Number(item.discount) || 0, unitPrice);
const itemPrice = unitPrice * item.quantity; const itemPrice = unitPrice * quantity;
const itemDiscount = unitDiscount * item.quantity; const itemDiscount = unitDiscount * quantity;
subTotal += itemPrice; subTotal += itemPrice;
itemsDiscount += itemDiscount; itemsDiscount += itemDiscount;
totalItems += item.quantity; totalItems += quantity;
item.totalPrice = itemPrice - itemDiscount; 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) { if (cart.coupon.maxDiscount && discount > 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 shop = await this.em.findOne(Shop, { id: shopId });
const vat = shop?.vat ? Number(shop.vat) : 0; const vat = shop?.vat ? Number(shop.vat) : 0;
if (!vat || vat <= 0) return 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 not distance based, return fixed delivery fee
if (deliveryMethod.deliveryFeeType !== DeliveryFeeTypeEnum.DISTANCE_BASED) { 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 // 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) { if (!shop || shop.latitude == null || shop.longitude == null) {
// fallback to configured fixed fee when coordinates are missing // 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) { if (!userAddr || userAddr.latitude == null || userAddr.longitude == null) {
return Number(deliveryMethod.deliveryFee) || 0; return roundMoney(Number(deliveryMethod.deliveryFee) || 0);
} }
const restLat = Number(shop.latitude); const restLat = Number(shop.latitude);
@@ -158,7 +166,7 @@ export class CartCalculationService {
if (minFee > 0 && fee < minFee) fee = minFee; 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); const couponDiscount = await this.calculateCouponDiscount(cart, subTotal, itemsDiscount);
cart.couponDiscount = couponDiscount; 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.tax = await this.calculateTax(cart.shopId, Math.max(0, subTotal - cart.totalDiscount));
cart.deliveryFee = await this.calculateDeliveryFee(cart); cart.deliveryFee = await this.calculateDeliveryFee(cart);
// total = subtotal totalDiscount + tax + deliveryFee // 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.totalItems = totalItems;
cart.updatedAt = this.nowIso(); cart.updatedAt = this.nowIso();
} }
@@ -69,28 +69,54 @@ export class CartItemService {
* Add or increment item in cart (validates stock against existing + new quantity) * Add or increment item in cart (validates stock against existing + new quantity)
*/ */
async addOrIncrementItem(cart: Cart, variantId: string, shopId: string, quantityToAdd: number): Promise<void> { async addOrIncrementItem(cart: Cart, variantId: string, shopId: string, quantityToAdd: number): Promise<void> {
const normalizedQuantityToAdd = roundQuantity(quantityToAdd);
const index = this.getItemIndex(cart, variantId); 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( const variant = await this.validationService.validateAndGetVariant(
variantId, variantId,
shopId, shopId,
quantityToAdd, normalizedQuantityToAdd,
existingQuantityInCart, existingQuantityInCart,
cart, cart,
); );
if (index < 0) { if (index < 0) {
cart.items.push(this.createCartItem(variant, quantityToAdd)); cart.items.push(this.createCartItem(variant, normalizedQuantityToAdd));
return; return;
} }
const existingItem = cart.items[index]; const existingItem = cart.items[index];
const newQuantity = roundQuantity(existingItem.quantity + quantityToAdd); const newQuantity = roundQuantity(existingQuantityInCart + normalizedQuantityToAdd);
cart.items[index] = this.buildCartItemFromFood(variant, newQuantity, existingItem); 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) * 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 existingItem = cart.items[itemIndex];
const variant = await this.productService.findOrFailVariant(variantId); const variant = await this.productService.findOrFailVariant(variantId);
const step = getQuantityStep(variant.product); const step = getQuantityStep(variant.product);
const newQuantity = roundQuantity(existingItem.quantity - step); const newQuantity = roundQuantity(roundQuantity(existingItem.quantity) - step);
if (newQuantity <= 0) { if (newQuantity <= 0) {
cart.items.splice(itemIndex, 1); cart.items.splice(itemIndex, 1);
@@ -37,34 +37,42 @@ export class CartValidationService {
async validateAndGetVariant( async validateAndGetVariant(
variantId: string, variantId: string,
shopId: string, shopId: string,
quantityToAdd: number, quantity: number,
existingQuantityInCart: number = 0, existingQuantityInCart: number = 0,
cart?: { items: { variantId: string; quantity: number }[] }, cart?: { items: { variantId: string; quantity: number }[] },
options?: { mode?: 'increment' | 'set' },
): Promise<Variant> { ): Promise<Variant> {
const mode = options?.mode ?? 'increment';
const normalizedQuantity = roundQuantity(quantity);
const normalizedExistingQuantity = roundQuantity(existingQuantityInCart);
const variant = await this.productService.findOrFailVariant(variantId); const variant = await this.productService.findOrFailVariant(variantId);
if (variant.product.shop.id !== shopId) { if (variant.product.shop.id !== shopId) {
throw new BadRequestException(CartMessage.PRODUCT_NOT_BELONGS_TO_SHOP); throw new BadRequestException(CartMessage.PRODUCT_NOT_BELONGS_TO_SHOP);
} }
const totalRequested =
mode === 'set' ? normalizedQuantity : normalizedExistingQuantity + normalizedQuantity;
if (variant.stock != null) { if (variant.stock != null) {
const totalRequested = existingQuantityInCart + quantityToAdd; if (totalRequested > roundQuantity(variant.stock)) {
if (totalRequested > variant.stock) {
throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK); throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK);
} }
} }
this.assertValidQuantity(variant, existingQuantityInCart + quantityToAdd); this.assertValidQuantity(variant, totalRequested);
const maxPurchase = variant.product.maxPurchaseQuantity; const maxPurchase = variant.product.maxPurchaseQuantity;
if (maxPurchase != null && maxPurchase > 0) { if (maxPurchase != null && maxPurchase > 0) {
const productVariantIds = variant.product.variants.getItems().map((v) => v.id); const productVariantIds = variant.product.variants.getItems().map((v) => v.id);
const totalProductQuantityInCart = const totalProductQuantityInCart =
cart?.items?.reduce( 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,
) ?? 0; ) ?? 0;
const newTotalProductQuantity = totalProductQuantityInCart - existingQuantityInCart + quantityToAdd; const newTotalProductQuantity =
totalProductQuantityInCart - normalizedExistingQuantity + totalRequested;
if (newTotalProductQuantity > maxPurchase) { if (newTotalProductQuantity > maxPurchase) {
throw new BadRequestException(CartMessage.MAX_PURCHASE_EXCEEDED); throw new BadRequestException(CartMessage.MAX_PURCHASE_EXCEEDED);
} }
+2 -2
View File
@@ -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> { async bulkAddItems(userId: string, shopId: string, bulkAddItemsDto: BulkAddItemsToCartDto): Promise<Cart> {
const cart = await this.getOrCreateCart(userId, shopId); const cart = await this.getOrCreateCart(userId, shopId);
for (const addItemDto of bulkAddItemsDto.items) { 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); return this.recalculateAndSaveCart(cart);
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { CacheService } from 'src/modules/utils/cache.service'; import { CacheService } from 'src/modules/utils/cache.service';
import { roundQuantity } from 'src/modules/products/utils/quantity.utils';
import { Cart } from '../interfaces/cart.interface'; import { Cart } from '../interfaces/cart.interface';
@Injectable() @Injectable()
@@ -20,7 +21,7 @@ export class CartRepository {
try { try {
const parsed: unknown = JSON.parse(cachedCart); const parsed: unknown = JSON.parse(cachedCart);
if (this.isCart(parsed) && parsed.userId === userId && parsed.shopId === shopId) { if (this.isCart(parsed) && parsed.userId === userId && parsed.shopId === shopId) {
return parsed; return this.normalizeCartQuantities(parsed);
} }
} catch { } catch {
// If parsing fails, return null // If parsing fails, return null
@@ -53,6 +54,17 @@ export class CartRepository {
return `${this.CART_KEY_PREFIX}:${userId}:${shopId}`; 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 * 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 { PaymentMethodEnum } from 'src/modules/payments/interface/payment';
import { UserService } from 'src/modules/users/providers/user.service'; import { UserService } from 'src/modules/users/providers/user.service';
import { WalletTransactionReason, WalletTransactionType } from 'src/modules/users/interface/wallet'; import { WalletTransactionReason, WalletTransactionType } from 'src/modules/users/interface/wallet';
import { formatMoneyAmount } from 'src/common/utils/money.utils';
@Injectable() @Injectable()
export class OrderListeners { export class OrderListeners {
@@ -69,17 +70,17 @@ export class OrderListeners {
shopId: event.shopId, shopId: event.shopId,
message: { message: {
title: NotifTitleEnum.ORDER_CREATED, title: NotifTitleEnum.ORDER_CREATED,
content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`, content: `سفارش شماره ${event.orderNumber} با مبلغ ${formatMoneyAmount(event.total)} تومان با موفقیت ایجاد شد`,
sms: { sms: {
templateId: this.orderCreatedSmsTemplateId, templateId: this.orderCreatedSmsTemplateId,
parameters: { parameters: {
orderNumber: event.orderNumber, orderNumber: event.orderNumber,
total: event.total.toString(), total: formatMoneyAmount(event.total),
}, },
}, },
pushNotif: { pushNotif: {
title: `سفارش جدید`, title: `سفارش جدید`,
content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`, content: `سفارش شماره ${event.orderNumber} با مبلغ ${formatMoneyAmount(event.total)} تومان با موفقیت ایجاد شد`,
icon: `/`, icon: `/`,
action: { action: {
type: NotifTitleEnum.ORDER_CREATED, type: NotifTitleEnum.ORDER_CREATED,
+13 -11
View File
@@ -27,6 +27,7 @@ import { OrderItemStatus } from '../enum/order-item.enum';
import { getMinQuantity, isValidProductQuantity, roundQuantity } from '../../products/utils/quantity.utils'; import { getMinQuantity, isValidProductQuantity, roundQuantity } from '../../products/utils/quantity.utils';
import { CouponService } from 'src/modules/coupons/providers/coupon.service'; import { CouponService } from 'src/modules/coupons/providers/coupon.service';
import { CouponType } from 'src/modules/coupons/interface/coupon'; 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 }; type OrderItemData = { variant: Variant; quantity: number; unitPrice: number; discount: number };
@@ -101,7 +102,7 @@ export class OrdersService {
for (const itemData of validated.orderItemsData) { for (const itemData of validated.orderItemsData) {
const { variant, quantity, unitPrice, discount } = itemData; const { variant, quantity, unitPrice, discount } = itemData;
const totalPrice = (unitPrice - discount) * quantity; const totalPrice = roundMoney((unitPrice - discount) * quantity);
const orderItem = em.create(OrderItem, { const orderItem = em.create(OrderItem, {
order, order,
@@ -127,7 +128,7 @@ export class OrdersService {
this.eventEmitter.emit( this.eventEmitter.emit(
OrderCreatedEvent.name, 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 }; return { order };
@@ -212,6 +213,7 @@ export class OrdersService {
'payments', 'payments',
'items', 'items',
'items.variant', 'items.variant',
'items.variant.product',
], ],
}, },
); );
@@ -335,19 +337,19 @@ export class OrdersService {
subTotal += unitPrice * item.quantity; subTotal += unitPrice * item.quantity;
itemsDiscount += unitDiscount * item.quantity; itemsDiscount += unitDiscount * item.quantity;
totalItems += item.quantity; totalItems += item.quantity;
item.totalPrice = (unitPrice - unitDiscount) * item.quantity; item.totalPrice = roundMoney((unitPrice - unitDiscount) * item.quantity);
} }
order.subTotal = subTotal; order.subTotal = roundMoney(subTotal);
order.itemsDiscount = itemsDiscount; order.itemsDiscount = roundMoney(itemsDiscount);
order.totalItems = totalItems; order.totalItems = totalItems;
order.couponDiscount = await this.calculateOrderCouponDiscount(order); 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; const vat = order.shop?.vat ? Number(order.shop.vat) : 0;
order.tax = vat > 0 ? (amountAfterDiscounts * vat) / 100 : 0; order.tax = vat > 0 ? roundMoney((amountAfterDiscounts * vat) / 100) : 0;
order.total = amountAfterDiscounts + order.tax + Number(order.deliveryFee || 0); order.total = roundMoney(amountAfterDiscounts + order.tax + Number(order.deliveryFee || 0));
} }
private async calculateOrderCouponDiscount(order: Order): Promise<number> { private async calculateOrderCouponDiscount(order: Order): Promise<number> {
@@ -395,10 +397,10 @@ export class OrdersService {
if (couponDetail.maxDiscount && discount > couponDetail.maxDiscount) { if (couponDetail.maxDiscount && discount > 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) { 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 { NotificationService } from 'src/modules/notifications/services/notification.service';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { OrdersService } from 'src/modules/orders/providers/orders.service'; import { OrdersService } from 'src/modules/orders/providers/orders.service';
import { formatMoneyAmount } from 'src/common/utils/money.utils';
@Injectable() @Injectable()
export class PaymentListeners { export class PaymentListeners {
@@ -48,7 +49,7 @@ export class PaymentListeners {
shopId: event.shopId, shopId: event.shopId,
message: { message: {
title: NotifTitleEnum.PAYMENT_SUCCESS, title: NotifTitleEnum.PAYMENT_SUCCESS,
content: `پرداخت سفارش شماره ${event.orderNumber} با روش ${this.getPaymentMethodFarsi(event.paymentMethod)} و مبلغ ${event.total} تومان با موفقیت انجام شد`, content: `پرداخت سفارش شماره ${event.orderNumber} با روش ${this.getPaymentMethodFarsi(event.paymentMethod)} و مبلغ ${formatMoneyAmount(event.total)} تومان با موفقیت انجام شد`,
sms: { sms: {
templateId: this.orderCreatedSmsTemplateId, templateId: this.orderCreatedSmsTemplateId,
parameters: { parameters: {
@@ -58,7 +59,7 @@ export class PaymentListeners {
}, },
pushNotif: { pushNotif: {
title: `پرداخت موفق`, title: `پرداخت موفق`,
content: `پرداخت سفارش شماره ${event.orderNumber} با روش ${this.getPaymentMethodFarsi(event.paymentMethod)} و مبلغ ${event.total} تومان با موفقیت انجام شد`, content: `پرداخت سفارش شماره ${event.orderNumber} با روش ${this.getPaymentMethodFarsi(event.paymentMethod)} و مبلغ ${formatMoneyAmount(event.total)} تومان با موفقیت انجام شد`,
icon: ``, icon: ``,
action: { action: {
type: NotifTitleEnum.PAYMENT_SUCCESS, type: NotifTitleEnum.PAYMENT_SUCCESS,
@@ -96,17 +97,17 @@ export class PaymentListeners {
shopId: event.shopId, shopId: event.shopId,
message: { message: {
title: NotifTitleEnum.ORDER_CREATED, title: NotifTitleEnum.ORDER_CREATED,
content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`, content: `سفارش شماره ${event.orderNumber} با مبلغ ${formatMoneyAmount(event.total)} تومان با موفقیت ایجاد شد`,
sms: { sms: {
templateId: this.orderCreatedSmsTemplateId, templateId: this.orderCreatedSmsTemplateId,
parameters: { parameters: {
orderNumber: event.orderNumber, orderNumber: event.orderNumber,
total: event.total.toString(), total: formatMoneyAmount(event.total),
}, },
}, },
pushNotif: { pushNotif: {
title: `سفارش جدید`, title: `سفارش جدید`,
content: `سفارش شماره ${event.orderNumber} با مبلغ ${event.total} تومان با موفقیت ایجاد شد`, content: `سفارش شماره ${event.orderNumber} با مبلغ ${formatMoneyAmount(event.total)} تومان با موفقیت ایجاد شد`,
icon: `/`, icon: `/`,
action: { action: {
type: NotifTitleEnum.ORDER_CREATED, type: NotifTitleEnum.ORDER_CREATED,
@@ -137,17 +138,17 @@ export class PaymentListeners {
shopId: event.shopId, shopId: event.shopId,
message: { message: {
title: NotifTitleEnum.PAYMENT_SUCCESS, 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: { sms: {
templateId: this.paymentSucceedSmsTemplateId, templateId: this.paymentSucceedSmsTemplateId,
parameters: { parameters: {
orderNumber: event.orderNumber, orderNumber: event.orderNumber,
total: event.total.toString(), total: formatMoneyAmount(event.total),
}, },
}, },
pushNotif: { pushNotif: {
title: `پرداخت موفق`, title: `پرداخت موفق`,
content: `پرداخت سفارش شماره ${order.orderNumber} با روش ${this.getPaymentMethodFarsi(order.paymentMethod.method)} و مبلغ ${order.total} تومان با موفقیت انجام شد`, content: `پرداخت سفارش شماره ${order.orderNumber} با روش ${this.getPaymentMethodFarsi(order.paymentMethod.method)} و مبلغ ${formatMoneyAmount(order.total)} تومان با موفقیت انجام شد`,
icon: `/`, icon: `/`,
action: { action: {
type: NotifTitleEnum.PAYMENT_SUCCESS, type: NotifTitleEnum.PAYMENT_SUCCESS,
@@ -14,6 +14,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { onlinePaymentSucceedEvent, paymentSucceedEvent } from '../events/payment.events'; import { onlinePaymentSucceedEvent, paymentSucceedEvent } from '../events/payment.events';
import { WalletTransactionReason, WalletTransactionType } from 'src/modules/users/interface/wallet'; import { WalletTransactionReason, WalletTransactionType } from 'src/modules/users/interface/wallet';
import { PaymentRepository } from '../repositories/payment.repository'; import { PaymentRepository } from '../repositories/payment.repository';
import { roundMoney } from 'src/common/utils/money.utils';
@Injectable() @Injectable()
export class PaymentsService { export class PaymentsService {
@@ -83,7 +84,7 @@ export class PaymentsService {
return { return {
order, order,
amount: order.total, amount: roundMoney(order.total),
method: pm.method, method: pm.method,
gateway: pm.gateway ?? null, gateway: pm.gateway ?? null,
merchantId: pm.merchantId ?? null, merchantId: pm.merchantId ?? null,
@@ -152,7 +153,7 @@ export class PaymentsService {
}); });
this.eventEmitter.emit( this.eventEmitter.emit(
paymentSucceedEvent.name, 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( this.eventEmitter.emit(
onlinePaymentSucceedEvent.name, 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; return payment;
} }
+6 -2
View File
@@ -18,8 +18,12 @@ export function getMinQuantity(product: Product): number {
return product.pricingType === PricingType.VARIABLE ? getQuantityStep(product) : 1; return product.pricingType === PricingType.VARIABLE ? getQuantityStep(product) : 1;
} }
export function roundQuantity(value: number): number { export function roundQuantity(value: number | string): number {
return Math.round(value * SCALE_FACTOR) / SCALE_FACTOR; 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 { export function isQuantityAlignedToPitch(quantity: number, pitch: number): boolean {