|
|
|
@@ -5,18 +5,13 @@ import { OrderCouponDetail } from '../../orders/interface/order.interface';
|
|
|
|
|
import { Cart } from '../interfaces/cart.interface';
|
|
|
|
|
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
|
|
|
|
|
import { ApplyCouponDto } from '../dto/apply-coupon.dto';
|
|
|
|
|
import { SetAddressDto } from '../dto/set-address.dto';
|
|
|
|
|
import { SetPaymentMethodDto } from '../dto/set-payment-method.dto';
|
|
|
|
|
import { SetDeliveryMethodDto } from '../dto/set-delivery-method.dto';
|
|
|
|
|
import { SetDescriptionDto } from '../dto/set-description.dto';
|
|
|
|
|
import { SetTableNumberDto } from '../dto/set-table-number.dto';
|
|
|
|
|
import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto';
|
|
|
|
|
import { SetAllCartParmsDto } from '../dto/set-all-cart-params.dto';
|
|
|
|
|
import { SetAllCartParmsDto } from '../dto/set-all-cart-params.dto';
|
|
|
|
|
import { CouponService } from '../../coupons/providers/coupon.service';
|
|
|
|
|
import { CartRepository } from '../repositories/cart.repository';
|
|
|
|
|
import { CartValidationService } from './cart-validation.service';
|
|
|
|
|
import { CartCalculationService } from './cart-calculation.service';
|
|
|
|
|
import { CartItemService } from './cart-item.service';
|
|
|
|
|
import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto';
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class CartService {
|
|
|
|
@@ -26,127 +21,106 @@ export class CartService {
|
|
|
|
|
private readonly calculationService: CartCalculationService,
|
|
|
|
|
private readonly itemService: CartItemService,
|
|
|
|
|
private readonly couponService: CouponService,
|
|
|
|
|
) {}
|
|
|
|
|
) { }
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set all cart parameters at once
|
|
|
|
|
*/
|
|
|
|
|
async setAllCartParams(userId: string, restaurantId: string, params: SetAllCartParmsDto): Promise<Cart> {
|
|
|
|
|
const { deliveryMethodId, paymentMethodId, addressId, carAddress, description } = params;
|
|
|
|
|
const { deliveryMethodId, paymentMethodId, addressId, carAddress, description, tableNumber } = params;
|
|
|
|
|
|
|
|
|
|
// get existing cart (or throw)
|
|
|
|
|
const cart = await this.findOneOrFail(userId, restaurantId);
|
|
|
|
|
|
|
|
|
|
// If deliveryMethodId is provided -> validate and set it first
|
|
|
|
|
if (deliveryMethodId) {
|
|
|
|
|
const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail(
|
|
|
|
|
restaurantId,
|
|
|
|
|
deliveryMethodId,
|
|
|
|
|
);
|
|
|
|
|
cart.deliveryMethodId = deliveryMethodId;
|
|
|
|
|
// clear fields incompatible with the chosen delivery method
|
|
|
|
|
this.clearFieldsIncompatibleWithDeliveryMethod(cart, deliveryMethod.method);
|
|
|
|
|
// Validate and get delivery method
|
|
|
|
|
const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail(
|
|
|
|
|
restaurantId,
|
|
|
|
|
deliveryMethodId,
|
|
|
|
|
);
|
|
|
|
|
cart.deliveryMethodId = deliveryMethodId;
|
|
|
|
|
|
|
|
|
|
// Handle each delivery method with its specific requirements
|
|
|
|
|
switch (deliveryMethod.method) {
|
|
|
|
|
case DeliveryMethodEnum.DineIn:
|
|
|
|
|
// DineIn requires table number and clears incompatible fields
|
|
|
|
|
if (!tableNumber) {
|
|
|
|
|
throw new BadRequestException('Table number is required for dine-in orders');
|
|
|
|
|
}
|
|
|
|
|
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn);
|
|
|
|
|
cart.tableNumber = tableNumber;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case DeliveryMethodEnum.CustomerPickup:
|
|
|
|
|
// CustomerPickup just clears incompatible fields
|
|
|
|
|
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.CustomerPickup);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case DeliveryMethodEnum.DeliveryCar:
|
|
|
|
|
// DeliveryCar requires carAddress and clears incompatible fields
|
|
|
|
|
if (!carAddress) {
|
|
|
|
|
throw new BadRequestException('Car address is required for car delivery');
|
|
|
|
|
}
|
|
|
|
|
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar);
|
|
|
|
|
const user = await this.validationService.getUserOrFail(userId);
|
|
|
|
|
cart.carAddress = {
|
|
|
|
|
carModel: carAddress.carModel,
|
|
|
|
|
carColor: carAddress.carColor,
|
|
|
|
|
plateNumber: carAddress.plateNumber,
|
|
|
|
|
phone: user.phone,
|
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case DeliveryMethodEnum.DeliveryCourier:
|
|
|
|
|
// DeliveryCourier requires addressId and clears incompatible fields
|
|
|
|
|
if (!addressId) {
|
|
|
|
|
throw new BadRequestException('Address is required for courier delivery');
|
|
|
|
|
}
|
|
|
|
|
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
|
|
|
|
|
const address = await this.validationService.getUserAddressOrFail(addressId);
|
|
|
|
|
this.validationService.validateAddressOwnership(address, userId);
|
|
|
|
|
|
|
|
|
|
// ensure address is within restaurant service area
|
|
|
|
|
await this.validationService.assertAddressInsideServiceArea(
|
|
|
|
|
restaurantId,
|
|
|
|
|
address.latitude,
|
|
|
|
|
address.longitude,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
cart.userAddress = {
|
|
|
|
|
address: address.address,
|
|
|
|
|
latitude: address.latitude,
|
|
|
|
|
longitude: address.longitude,
|
|
|
|
|
city: address.city,
|
|
|
|
|
province: address.province || '',
|
|
|
|
|
postalCode: address.postalCode || undefined,
|
|
|
|
|
fullName: `${address.user.firstName || ''} ${address.user.lastName || ''}`.trim(),
|
|
|
|
|
phone: address.user.phone,
|
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If addressId is provided -> must have delivery method (either provided above or already on cart)
|
|
|
|
|
if (addressId) {
|
|
|
|
|
const effectiveDeliveryMethodId = deliveryMethodId ?? cart.deliveryMethodId;
|
|
|
|
|
if (!effectiveDeliveryMethodId) {
|
|
|
|
|
throw new BadRequestException('Delivery method must be set before setting address');
|
|
|
|
|
}
|
|
|
|
|
const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
|
|
|
|
|
restaurantId,
|
|
|
|
|
effectiveDeliveryMethodId,
|
|
|
|
|
);
|
|
|
|
|
this.validationService.assertDeliveryMethod(
|
|
|
|
|
deliveryMethod.method,
|
|
|
|
|
DeliveryMethodEnum.DeliveryCourier,
|
|
|
|
|
'Delivery method must be DeliveryCourier',
|
|
|
|
|
);
|
|
|
|
|
// Validate and set payment method
|
|
|
|
|
const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail(
|
|
|
|
|
restaurantId,
|
|
|
|
|
paymentMethodId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const address = await this.validationService.getUserAddressOrFail(addressId);
|
|
|
|
|
this.validationService.validateAddressOwnership(address, userId);
|
|
|
|
|
// Recalculate totals first so wallet check uses up-to-date total (delivery method may have changed above).
|
|
|
|
|
await this.calculationService.recalculateCartTotals(cart);
|
|
|
|
|
|
|
|
|
|
// ensure address is within restaurant service area
|
|
|
|
|
await this.validationService.assertAddressInsideServiceArea(
|
|
|
|
|
restaurantId,
|
|
|
|
|
address.latitude,
|
|
|
|
|
address.longitude,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ensure cart fields are compatible and set address
|
|
|
|
|
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
|
|
|
|
|
cart.userAddress = {
|
|
|
|
|
address: address.address,
|
|
|
|
|
latitude: address.latitude,
|
|
|
|
|
longitude: address.longitude,
|
|
|
|
|
city: address.city,
|
|
|
|
|
province: address.province || '',
|
|
|
|
|
postalCode: address.postalCode || undefined,
|
|
|
|
|
fullName: `${address.user.firstName || ''} ${address.user.lastName || ''}`.trim(),
|
|
|
|
|
phone: address.user.phone,
|
|
|
|
|
};
|
|
|
|
|
if (paymentMethod.method === PaymentMethodEnum.Wallet) {
|
|
|
|
|
await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If carAddress is provided -> must have delivery method DeliveryCar
|
|
|
|
|
if (carAddress) {
|
|
|
|
|
const effectiveDeliveryMethodId = deliveryMethodId ?? cart.deliveryMethodId;
|
|
|
|
|
if (!effectiveDeliveryMethodId) {
|
|
|
|
|
throw new BadRequestException('Delivery method must be set before setting car delivery');
|
|
|
|
|
}
|
|
|
|
|
const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
|
|
|
|
|
restaurantId,
|
|
|
|
|
effectiveDeliveryMethodId,
|
|
|
|
|
);
|
|
|
|
|
this.validationService.assertDeliveryMethod(
|
|
|
|
|
deliveryMethod.method,
|
|
|
|
|
DeliveryMethodEnum.DeliveryCar,
|
|
|
|
|
'Delivery method must be DeliveryCar',
|
|
|
|
|
);
|
|
|
|
|
cart.paymentMethodId = paymentMethodId;
|
|
|
|
|
|
|
|
|
|
const user = await this.validationService.getUserOrFail(userId);
|
|
|
|
|
|
|
|
|
|
// make sure incompatible fields are cleared and set car address (phone comes from user)
|
|
|
|
|
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar);
|
|
|
|
|
cart.carAddress = {
|
|
|
|
|
carModel: carAddress.carModel,
|
|
|
|
|
carColor: carAddress.carColor,
|
|
|
|
|
plateNumber: carAddress.plateNumber,
|
|
|
|
|
phone: user.phone,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If payment method is provided -> validate and (for wallet) ensure enough balance.
|
|
|
|
|
if (paymentMethodId) {
|
|
|
|
|
const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail(
|
|
|
|
|
restaurantId,
|
|
|
|
|
paymentMethodId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Recalculate totals first so wallet check uses up-to-date total (delivery method may have changed above).
|
|
|
|
|
await this.calculationService.recalculateCartTotals(cart);
|
|
|
|
|
|
|
|
|
|
if (paymentMethod.method === PaymentMethodEnum.Wallet) {
|
|
|
|
|
await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cart.paymentMethodId = paymentMethodId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Description (if provided)
|
|
|
|
|
// Set description (if provided)
|
|
|
|
|
if (description !== undefined) {
|
|
|
|
|
cart.description = description;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Final validation: if effective delivery method is courier, ensure all foods have pickupServe
|
|
|
|
|
const effectiveDeliveryMethodId = deliveryMethodId ?? cart.deliveryMethodId;
|
|
|
|
|
if (effectiveDeliveryMethodId) {
|
|
|
|
|
const effectiveDeliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
|
|
|
|
|
restaurantId,
|
|
|
|
|
effectiveDeliveryMethodId,
|
|
|
|
|
);
|
|
|
|
|
await this.validationService.assertAllFoodsHavePickupServeForCourier(cart, effectiveDeliveryMethod.method);
|
|
|
|
|
}
|
|
|
|
|
await this.validationService.assertAllFoodsHavePickupServeForCourier(cart, deliveryMethod.method);
|
|
|
|
|
|
|
|
|
|
// Final recalculation + save and return cart
|
|
|
|
|
return this.recalculateAndSaveCart(cart);
|
|
|
|
@@ -296,7 +270,7 @@ export class CartService {
|
|
|
|
|
/**
|
|
|
|
|
* Set address for cart
|
|
|
|
|
*/
|
|
|
|
|
async setAddress(userId: string, restaurantId: string, setAddressDto: SetAddressDto): Promise<Cart> {
|
|
|
|
|
async setAddress(userId: string, restaurantId: string, addressId: string): Promise<Cart> {
|
|
|
|
|
const cart = await this.findOneOrFail(userId, restaurantId);
|
|
|
|
|
|
|
|
|
|
const deliveryMethodId = this.validationService.requireDeliveryMethodId(
|
|
|
|
@@ -314,7 +288,7 @@ export class CartService {
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Find and validate address belongs to user
|
|
|
|
|
const address = await this.validationService.getUserAddressOrFail(setAddressDto.addressId);
|
|
|
|
|
const address = await this.validationService.getUserAddressOrFail(addressId);
|
|
|
|
|
|
|
|
|
|
// Verify address belongs to the user
|
|
|
|
|
this.validationService.validateAddressOwnership(address, userId);
|
|
|
|
@@ -346,18 +320,18 @@ export class CartService {
|
|
|
|
|
async setPaymentMethod(
|
|
|
|
|
userId: string,
|
|
|
|
|
restaurantId: string,
|
|
|
|
|
setPaymentMethodDto: SetPaymentMethodDto,
|
|
|
|
|
paymentMethodId: string,
|
|
|
|
|
): Promise<Cart> {
|
|
|
|
|
const cart = await this.findOneOrFail(userId, restaurantId);
|
|
|
|
|
|
|
|
|
|
const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail(
|
|
|
|
|
restaurantId,
|
|
|
|
|
setPaymentMethodDto.paymentMethodId,
|
|
|
|
|
paymentMethodId,
|
|
|
|
|
);
|
|
|
|
|
if (paymentMethod.method === PaymentMethodEnum.Wallet) {
|
|
|
|
|
await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total);
|
|
|
|
|
}
|
|
|
|
|
cart.paymentMethodId = setPaymentMethodDto.paymentMethodId;
|
|
|
|
|
cart.paymentMethodId = paymentMethodId;
|
|
|
|
|
return this.recalculateAndSaveCart(cart);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -367,16 +341,16 @@ export class CartService {
|
|
|
|
|
async setDeliveryMethod(
|
|
|
|
|
userId: string,
|
|
|
|
|
restaurantId: string,
|
|
|
|
|
setDeliveryMethodDto: SetDeliveryMethodDto,
|
|
|
|
|
deliveryMethodId: string,
|
|
|
|
|
): Promise<Cart> {
|
|
|
|
|
const cart = await this.findOneOrFail(userId, restaurantId);
|
|
|
|
|
|
|
|
|
|
const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail(
|
|
|
|
|
restaurantId,
|
|
|
|
|
setDeliveryMethodDto.deliveryMethodId,
|
|
|
|
|
deliveryMethodId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
cart.deliveryMethodId = setDeliveryMethodDto.deliveryMethodId;
|
|
|
|
|
cart.deliveryMethodId = deliveryMethodId;
|
|
|
|
|
this.clearFieldsIncompatibleWithDeliveryMethod(cart, deliveryMethod.method);
|
|
|
|
|
|
|
|
|
|
return this.recalculateAndSaveCart(cart);
|
|
|
|
@@ -385,17 +359,17 @@ export class CartService {
|
|
|
|
|
/**
|
|
|
|
|
* Set description for cart
|
|
|
|
|
*/
|
|
|
|
|
async setDescription(userId: string, restaurantId: string, setDescriptionDto: SetDescriptionDto): Promise<Cart> {
|
|
|
|
|
async setDescription(userId: string, restaurantId: string, description: string): Promise<Cart> {
|
|
|
|
|
const cart = await this.findOneOrFail(userId, restaurantId);
|
|
|
|
|
|
|
|
|
|
cart.description = setDescriptionDto.description;
|
|
|
|
|
cart.description = description;
|
|
|
|
|
return this.saveTouchedCart(cart);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set table number for cart
|
|
|
|
|
*/
|
|
|
|
|
async setTableNumber(userId: string, restaurantId: string, setTableNumberDto: SetTableNumberDto): Promise<Cart> {
|
|
|
|
|
async setTableNumber(userId: string, restaurantId: string, tableNumber: string): Promise<Cart> {
|
|
|
|
|
const cart = await this.findOneOrFail(userId, restaurantId);
|
|
|
|
|
|
|
|
|
|
const deliveryMethodId = this.validationService.requireDeliveryMethodId(
|
|
|
|
@@ -413,7 +387,7 @@ export class CartService {
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn);
|
|
|
|
|
cart.tableNumber = setTableNumberDto.tableNumber;
|
|
|
|
|
cart.tableNumber = tableNumber;
|
|
|
|
|
return this.saveTouchedCart(cart);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|