cart refactor

This commit is contained in:
2025-12-21 10:42:08 +03:30
parent 92377eb265
commit c9a182784f
8 changed files with 97 additions and 174 deletions
@@ -2,12 +2,6 @@ import { Controller, Get, Post, Body, Patch, Delete, UseGuards, Param } from '@n
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';
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 { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiParam, ApiHeader } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiParam, ApiHeader } from '@nestjs/swagger';
import { AuthGuard } from '../../auth/guards/auth.guard'; import { AuthGuard } from '../../auth/guards/auth.guard';
import { UserId } from 'src/common/decorators/user-id.decorator'; import { UserId } from 'src/common/decorators/user-id.decorator';
-9
View File
@@ -1,9 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class SetAddressDto {
@ApiProperty({ description: 'User address ID', example: '01ARZ3NDEKTSV4RRFFQ69G5FAV' })
@IsNotEmpty()
@IsString()
addressId!: string;
}
@@ -28,6 +28,10 @@ export class SetAllCartParmsDto {
@IsString() @IsString()
paymentMethodId!: string; paymentMethodId!: string;
@ApiProperty({ description: 'Table number for dine-in orders', example: '5', required: false })
@IsOptional()
@IsString()
tableNumber?: string;
@ApiProperty() @ApiProperty()
@IsOptional() @IsOptional()
@@ -1,9 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class SetDeliveryMethodDto {
@ApiProperty({ description: 'Delivery method ID', example: '01ARZ3NDEKTSV4RRFFQ69G5FAV' })
@IsNotEmpty()
@IsString()
deliveryMethodId!: string;
}
@@ -1,13 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
export class SetDescriptionDto {
@ApiProperty({
description: 'Cart description or notes',
required: false,
example: 'Please deliver to the back door',
})
@IsOptional()
@IsString()
description?: string;
}
@@ -1,9 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class SetPaymentMethodDto {
@ApiProperty({ description: 'Payment method ID', example: '01ARZ3NDEKTSV4RRFFQ69G5FAV' })
@IsNotEmpty()
@IsString()
paymentMethodId!: string;
}
@@ -1,9 +0,0 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsString, ValidateIf } from 'class-validator';
export class SetTableNumberDto {
@ApiPropertyOptional({ description: 'Table number', example: '5' })
@ValidateIf((o, value) => value !== null && value !== undefined)
@IsString()
tableNumber?: string;
}
+56 -82
View File
@@ -5,18 +5,13 @@ import { OrderCouponDetail } from '../../orders/interface/order.interface';
import { Cart } from '../interfaces/cart.interface'; import { Cart } from '../interfaces/cart.interface';
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';
import { SetAddressDto } from '../dto/set-address.dto'; import { SetAllCartParmsDto } from '../dto/set-all-cart-params.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 { CouponService } from '../../coupons/providers/coupon.service'; import { CouponService } from '../../coupons/providers/coupon.service';
import { CartRepository } from '../repositories/cart.repository'; import { CartRepository } from '../repositories/cart.repository';
import { CartValidationService } from './cart-validation.service'; import { CartValidationService } from './cart-validation.service';
import { CartCalculationService } from './cart-calculation.service'; import { CartCalculationService } from './cart-calculation.service';
import { CartItemService } from './cart-item.service'; import { CartItemService } from './cart-item.service';
import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto';
@Injectable() @Injectable()
export class CartService { export class CartService {
@@ -26,44 +21,61 @@ export class CartService {
private readonly calculationService: CartCalculationService, private readonly calculationService: CartCalculationService,
private readonly itemService: CartItemService, private readonly itemService: CartItemService,
private readonly couponService: CouponService, private readonly couponService: CouponService,
) {} ) { }
/** /**
* Set all cart parameters at once * Set all cart parameters at once
*/ */
async setAllCartParams(userId: string, restaurantId: string, params: SetAllCartParmsDto): Promise<Cart> { 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) // get existing cart (or throw)
const cart = await this.findOneOrFail(userId, restaurantId); const cart = await this.findOneOrFail(userId, restaurantId);
// If deliveryMethodId is provided -> validate and set it first // Validate and get delivery method
if (deliveryMethodId) {
const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail( const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail(
restaurantId, restaurantId,
deliveryMethodId, deliveryMethodId,
); );
cart.deliveryMethodId = deliveryMethodId; cart.deliveryMethodId = deliveryMethodId;
// clear fields incompatible with the chosen delivery method
this.clearFieldsIncompatibleWithDeliveryMethod(cart, deliveryMethod.method);
}
// If addressId is provided -> must have delivery method (either provided above or already on cart) // Handle each delivery method with its specific requirements
if (addressId) { switch (deliveryMethod.method) {
const effectiveDeliveryMethodId = deliveryMethodId ?? cart.deliveryMethodId; case DeliveryMethodEnum.DineIn:
if (!effectiveDeliveryMethodId) { // DineIn requires table number and clears incompatible fields
throw new BadRequestException('Delivery method must be set before setting address'); if (!tableNumber) {
throw new BadRequestException('Table number is required for dine-in orders');
} }
const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail( this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn);
restaurantId, cart.tableNumber = tableNumber;
effectiveDeliveryMethodId, break;
);
this.validationService.assertDeliveryMethod(
deliveryMethod.method,
DeliveryMethodEnum.DeliveryCourier,
'Delivery method must be DeliveryCourier',
);
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); const address = await this.validationService.getUserAddressOrFail(addressId);
this.validationService.validateAddressOwnership(address, userId); this.validationService.validateAddressOwnership(address, userId);
@@ -74,8 +86,6 @@ export class CartService {
address.longitude, address.longitude,
); );
// ensure cart fields are compatible and set address
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
cart.userAddress = { cart.userAddress = {
address: address.address, address: address.address,
latitude: address.latitude, latitude: address.latitude,
@@ -86,38 +96,10 @@ export class CartService {
fullName: `${address.user.firstName || ''} ${address.user.lastName || ''}`.trim(), fullName: `${address.user.firstName || ''} ${address.user.lastName || ''}`.trim(),
phone: address.user.phone, phone: address.user.phone,
}; };
break;
} }
// If carAddress is provided -> must have delivery method DeliveryCar // Validate and set payment method
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',
);
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( const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail(
restaurantId, restaurantId,
paymentMethodId, paymentMethodId,
@@ -131,22 +113,14 @@ export class CartService {
} }
cart.paymentMethodId = paymentMethodId; cart.paymentMethodId = paymentMethodId;
}
// Description (if provided) // Set description (if provided)
if (description !== undefined) { if (description !== undefined) {
cart.description = description; cart.description = description;
} }
// Final validation: if effective delivery method is courier, ensure all foods have pickupServe // Final validation: if effective delivery method is courier, ensure all foods have pickupServe
const effectiveDeliveryMethodId = deliveryMethodId ?? cart.deliveryMethodId; await this.validationService.assertAllFoodsHavePickupServeForCourier(cart, deliveryMethod.method);
if (effectiveDeliveryMethodId) {
const effectiveDeliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
restaurantId,
effectiveDeliveryMethodId,
);
await this.validationService.assertAllFoodsHavePickupServeForCourier(cart, effectiveDeliveryMethod.method);
}
// Final recalculation + save and return cart // Final recalculation + save and return cart
return this.recalculateAndSaveCart(cart); return this.recalculateAndSaveCart(cart);
@@ -296,7 +270,7 @@ export class CartService {
/** /**
* Set address for cart * 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 cart = await this.findOneOrFail(userId, restaurantId);
const deliveryMethodId = this.validationService.requireDeliveryMethodId( const deliveryMethodId = this.validationService.requireDeliveryMethodId(
@@ -314,7 +288,7 @@ export class CartService {
); );
// Find and validate address belongs to user // 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 // Verify address belongs to the user
this.validationService.validateAddressOwnership(address, userId); this.validationService.validateAddressOwnership(address, userId);
@@ -346,18 +320,18 @@ export class CartService {
async setPaymentMethod( async setPaymentMethod(
userId: string, userId: string,
restaurantId: string, restaurantId: string,
setPaymentMethodDto: SetPaymentMethodDto, paymentMethodId: string,
): Promise<Cart> { ): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId); const cart = await this.findOneOrFail(userId, restaurantId);
const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail( const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail(
restaurantId, restaurantId,
setPaymentMethodDto.paymentMethodId, paymentMethodId,
); );
if (paymentMethod.method === PaymentMethodEnum.Wallet) { if (paymentMethod.method === PaymentMethodEnum.Wallet) {
await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total); await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total);
} }
cart.paymentMethodId = setPaymentMethodDto.paymentMethodId; cart.paymentMethodId = paymentMethodId;
return this.recalculateAndSaveCart(cart); return this.recalculateAndSaveCart(cart);
} }
@@ -367,16 +341,16 @@ export class CartService {
async setDeliveryMethod( async setDeliveryMethod(
userId: string, userId: string,
restaurantId: string, restaurantId: string,
setDeliveryMethodDto: SetDeliveryMethodDto, deliveryMethodId: string,
): Promise<Cart> { ): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId); const cart = await this.findOneOrFail(userId, restaurantId);
const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail( const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail(
restaurantId, restaurantId,
setDeliveryMethodDto.deliveryMethodId, deliveryMethodId,
); );
cart.deliveryMethodId = setDeliveryMethodDto.deliveryMethodId; cart.deliveryMethodId = deliveryMethodId;
this.clearFieldsIncompatibleWithDeliveryMethod(cart, deliveryMethod.method); this.clearFieldsIncompatibleWithDeliveryMethod(cart, deliveryMethod.method);
return this.recalculateAndSaveCart(cart); return this.recalculateAndSaveCart(cart);
@@ -385,17 +359,17 @@ export class CartService {
/** /**
* Set description for cart * 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); const cart = await this.findOneOrFail(userId, restaurantId);
cart.description = setDescriptionDto.description; cart.description = description;
return this.saveTouchedCart(cart); return this.saveTouchedCart(cart);
} }
/** /**
* Set table number for 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 cart = await this.findOneOrFail(userId, restaurantId);
const deliveryMethodId = this.validationService.requireDeliveryMethodId( const deliveryMethodId = this.validationService.requireDeliveryMethodId(
@@ -413,7 +387,7 @@ export class CartService {
); );
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn); this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn);
cart.tableNumber = setTableNumberDto.tableNumber; cart.tableNumber = tableNumber;
return this.saveTouchedCart(cart); return this.saveTouchedCart(cart);
} }