calculate delivery fee based on distance

This commit is contained in:
2025-12-20 16:42:54 +03:30
parent 2bf4e81bbd
commit 6bfef28886
2 changed files with 131 additions and 68 deletions
+62 -62
View File
@@ -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)
+69 -6
View File
@@ -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<number> {
private async calculateDeliveryFee(cart: Cart): Promise<number> {
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));
}
}