From 6bfef28886624319295824d4237425b9a509d2d5 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sat, 20 Dec 2025 16:42:54 +0330 Subject: [PATCH] calculate delivery fee based on distance --- .../cart/controllers/cart.controller.ts | 124 +++++++++--------- src/modules/cart/providers/cart.service.ts | 75 ++++++++++- 2 files changed, 131 insertions(+), 68 deletions(-) diff --git a/src/modules/cart/controllers/cart.controller.ts b/src/modules/cart/controllers/cart.controller.ts index db113b7..03c0af0 100644 --- a/src/modules/cart/controllers/cart.controller.ts +++ b/src/modules/cart/controllers/cart.controller.ts @@ -87,73 +87,73 @@ export class CartController { return this.cartService.removeCoupon(userId, restaurantId); } - @Patch('address') - @ApiOperation({ summary: 'Set address for cart' }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: SetAddressDto }) - async setAddress(@UserId() userId: string, @RestId() restaurantId: string, @Body() setAddressDto: SetAddressDto) { - return this.cartService.setAddress(userId, restaurantId, setAddressDto); - } + // @Patch('address') + // @ApiOperation({ summary: 'Set address for cart' }) + // @ApiHeader(API_HEADER_SLUG) + // @ApiBody({ type: SetAddressDto }) + // async setAddress(@UserId() userId: string, @RestId() restaurantId: string, @Body() setAddressDto: SetAddressDto) { + // return this.cartService.setAddress(userId, restaurantId, setAddressDto); + // } - @Patch('payment-method') - @ApiOperation({ summary: 'Set payment method for cart' }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: SetPaymentMethodDto }) - async setPaymentMethod( - @UserId() userId: string, - @RestId() restaurantId: string, - @Body() setPaymentMethodDto: SetPaymentMethodDto, - ) { - return this.cartService.setPaymentMethod(userId, restaurantId, setPaymentMethodDto); - } + // @Patch('payment-method') + // @ApiOperation({ summary: 'Set payment method for cart' }) + // @ApiHeader(API_HEADER_SLUG) + // @ApiBody({ type: SetPaymentMethodDto }) + // async setPaymentMethod( + // @UserId() userId: string, + // @RestId() restaurantId: string, + // @Body() setPaymentMethodDto: SetPaymentMethodDto, + // ) { + // return this.cartService.setPaymentMethod(userId, restaurantId, setPaymentMethodDto); + // } - @Patch('delivery-method') - @ApiOperation({ summary: 'Set delivery method for cart' }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: SetDeliveryMethodDto }) - setDeliveryMethod( - @UserId() userId: string, - @RestId() restaurantId: string, - @Body() setDeliveryMethodDto: SetDeliveryMethodDto, - ) { - return this.cartService.setDeliveryMethod(userId, restaurantId, setDeliveryMethodDto); - } + // @Patch('delivery-method') + // @ApiOperation({ summary: 'Set delivery method for cart' }) + // @ApiHeader(API_HEADER_SLUG) + // @ApiBody({ type: SetDeliveryMethodDto }) + // setDeliveryMethod( + // @UserId() userId: string, + // @RestId() restaurantId: string, + // @Body() setDeliveryMethodDto: SetDeliveryMethodDto, + // ) { + // return this.cartService.setDeliveryMethod(userId, restaurantId, setDeliveryMethodDto); + // } - @Patch('description') - @ApiOperation({ summary: 'Set description for cart' }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: SetDescriptionDto }) - setDescription( - @UserId() userId: string, - @RestId() restaurantId: string, - @Body() setDescriptionDto: SetDescriptionDto, - ) { - return this.cartService.setDescription(userId, restaurantId, setDescriptionDto); - } + // @Patch('description') + // @ApiOperation({ summary: 'Set description for cart' }) + // @ApiHeader(API_HEADER_SLUG) + // @ApiBody({ type: SetDescriptionDto }) + // setDescription( + // @UserId() userId: string, + // @RestId() restaurantId: string, + // @Body() setDescriptionDto: SetDescriptionDto, + // ) { + // return this.cartService.setDescription(userId, restaurantId, setDescriptionDto); + // } - @Patch('table-number') - @ApiOperation({ summary: 'Set table number for cart (required when delivery method is DineIn)' }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: SetTableNumberDto }) - setTableNumber( - @UserId() userId: string, - @RestId() restaurantId: string, - @Body() setTableNumberDto: SetTableNumberDto, - ) { - return this.cartService.setTableNumber(userId, restaurantId, setTableNumberDto); - } + // @Patch('table-number') + // @ApiOperation({ summary: 'Set table number for cart (required when delivery method is DineIn)' }) + // @ApiHeader(API_HEADER_SLUG) + // @ApiBody({ type: SetTableNumberDto }) + // setTableNumber( + // @UserId() userId: string, + // @RestId() restaurantId: string, + // @Body() setTableNumberDto: SetTableNumberDto, + // ) { + // return this.cartService.setTableNumber(userId, restaurantId, setTableNumberDto); + // } - @Patch('car-delivery') - @ApiOperation({ summary: 'Set car delivery for cart' }) - @ApiHeader(API_HEADER_SLUG) - @ApiBody({ type: SetCarDeliveryDto }) - setCarDelivery( - @UserId() userId: string, - @RestId() restaurantId: string, - @Body() setCarDeliveryDto: SetCarDeliveryDto, - ) { - return this.cartService.setCarDelivery(userId, restaurantId, setCarDeliveryDto); - } + // @Patch('car-delivery') + // @ApiOperation({ summary: 'Set car delivery for cart' }) + // @ApiHeader(API_HEADER_SLUG) + // @ApiBody({ type: SetCarDeliveryDto }) + // setCarDelivery( + // @UserId() userId: string, + // @RestId() restaurantId: string, + // @Body() setCarDeliveryDto: SetCarDeliveryDto, + // ) { + // return this.cartService.setCarDelivery(userId, restaurantId, setCarDeliveryDto); + // } @Patch('all') @ApiOperation({ summary: 'Set all cart params' }) @ApiHeader(API_HEADER_SLUG) diff --git a/src/modules/cart/providers/cart.service.ts b/src/modules/cart/providers/cart.service.ts index 5c1dda3..7c1dff0 100644 --- a/src/modules/cart/providers/cart.service.ts +++ b/src/modules/cart/providers/cart.service.ts @@ -15,7 +15,7 @@ import { UserAddress } from '../../users/entities/user-address.entity'; import { User } from '../../users/entities/user.entity'; import { PaymentMethod } from '../../payments/entities/payment-method.entity'; import { Delivery } from '../../delivery/entities/delivery.entity'; -import { DeliveryMethodEnum } from '../../delivery/interface/delivery'; +import { DeliveryMethodEnum, DeliveryFeeTypeEnum } from '../../delivery/interface/delivery'; import { Cart, CartItem } from '../interfaces/cart.interface'; import { CouponService } from 'src/modules/coupons/providers/coupon.service'; import { OrderCouponDetail, OrderStatus } from 'src/modules/orders/interface/order.interface'; @@ -24,7 +24,7 @@ import { UserWallet } from 'src/modules/users/entities/user-wallet.entity'; import { PaymentMethodEnum } from 'src/modules/payments/interface/payment'; import { SetAllCartParmsDto } from '../dto/set-all-cart-params.dto'; import { Order } from 'src/modules/orders/entities/order.entity'; - + @Injectable() export class CartService { private readonly CART_TTL = 3600; // 1 hour in seconds @@ -489,7 +489,7 @@ export class CartService { cart.totalDiscount = couponDiscount + itemsDiscount; cart.tax = await this.calculateTax(cart.restaurantId, Math.max(0, subTotal - cart.totalDiscount)); - cart.deliveryFee = await this.calculateDeliveryFee(cart.deliveryMethodId); + cart.deliveryFee = await this.calculateDeliveryFee(cart); // total = subtotal – totalDiscount + tax + deliveryFee cart.total = Math.max(0, subTotal - cart.totalDiscount) + cart.tax + cart.deliveryFee; @@ -988,10 +988,73 @@ export class CartService { return (Math.max(0, amountAfterDiscounts) * vat) / 100; } - private async calculateDeliveryFee(deliveryMethodId?: string): Promise { + private async calculateDeliveryFee(cart: Cart): Promise { + const deliveryMethodId = cart.deliveryMethodId; if (!deliveryMethodId) return 0; - const deliveryMethod = await this.em.findOne(Delivery, { id: deliveryMethodId }); + + const deliveryMethod = await this.em.findOne(Delivery, { id: deliveryMethodId }, { populate: ['restaurant'] }); if (!deliveryMethod || !deliveryMethod.enabled) return 0; - return Number(deliveryMethod.deliveryFee) || 0; + + // If not distance based, return fixed delivery fee + if (deliveryMethod.deliveryFeeType !== DeliveryFeeTypeEnum.DISTANCE_BASED) { + return Number(deliveryMethod.deliveryFee) || 0; + } + + // For distance based calculation we need restaurant and user coordinates + const restaurant = (deliveryMethod as any).restaurant as Restaurant | undefined; + const userAddr = cart.userAddress; + + if (!restaurant || restaurant.latitude == null || restaurant.longitude == null) { + // fallback to configured fixed fee when coordinates are missing + return Number(deliveryMethod.deliveryFee) || 0; + } + + if (!userAddr || userAddr.latitude == null || userAddr.longitude == null) { + return Number(deliveryMethod.deliveryFee) || 0; + } + + const restLat = Number(restaurant.latitude); + const restLng = Number(restaurant.longitude); + const userLat = Number(userAddr.latitude); + const userLng = Number(userAddr.longitude); + + const distanceKm = this.getDistanceKmRounded(restLat, restLng, userLat, userLng); + + // Try to read either possible property names (legacy vs new) + const perKm = Number(deliveryMethod.perKilometerFee); + const minFee = Number(deliveryMethod.distanceBasedMinCost); + + let fee = 0; + if (perKm <= 0) { + fee = Number(deliveryMethod.deliveryFee) || 0; + } else { + fee = distanceKm * perKm; + } + + if (minFee > 0 && fee < minFee) fee = minFee; + + return Math.max(0, Number(fee)); + } + + private toRad(value: number): number { + return (value * Math.PI) / 180; + } + + private getDistanceKm(lat1: number, lng1: number, lat2: number, lng2: number): number { + const R = 6371; // Earth radius in KM + + const dLat = this.toRad(lat2 - lat1); + const dLng = this.toRad(lng2 - lng1); + + const a = + Math.sin(dLat / 2) ** 2 + Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) * Math.sin(dLng / 2) ** 2; + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return R * c; + } + private getDistanceKmRounded(lat1: number, lng1: number, lat2: number, lng2: number, precision = 2): number { + const distance = this.getDistanceKm(lat1, lng1, lat2, lng2); + return Number(distance.toFixed(precision)); } }