This commit is contained in:
2026-02-09 15:47:17 +03:30
parent 65b937d662
commit 8c9e7053f5
10 changed files with 90 additions and 123 deletions
+26 -27
View File
@@ -19,66 +19,65 @@ export class CartController {
@Get()
@ApiOperation({ summary: 'Get cart for current user and shop' })
@ApiHeader(API_HEADER_SLUG)
async findOne(@UserId() userId: string, @ShopId() restaurantId: string) {
return this.cartService.getOrCreateCart(userId, restaurantId);
async findOne(@UserId() userId: string, @ShopId() shopId: string) {
return this.cartService.getOrCreateCart(userId, shopId);
}
@Post('items/:foodId/increment')
@Post('product/:productId/increment')
@ApiOperation({ summary: 'Increment item quantity in cart by 1' })
@ApiHeader(API_HEADER_SLUG)
@ApiParam({ name: 'foodId', description: 'Product ID to increment in cart' })
async incrementItem(@UserId() userId: string, @ShopId() restaurantId: string, @Param('foodId') foodId: string) {
return this.cartService.incrementItem(userId, restaurantId, foodId, 1);
@ApiParam({ name: 'productId', description: 'Product ID to increment in cart' })
async incrementItem(@UserId() userId: string, @ShopId() shopId: string, @Param('productId') productId: string) {
return this.cartService.incrementItem(userId, shopId, productId, 1);
}
@Post('items/:foodId/decrement')
@Post('product/:productId/decrement')
@ApiOperation({ summary: 'Decrement item quantity in cart by 1 (removes item if quantity reaches 0)' })
@ApiHeader(API_HEADER_SLUG)
@ApiParam({ name: 'foodId', description: 'Product ID to decrement in cart' })
async decrementItem(@UserId() userId: string, @ShopId() restaurantId: string, @Param('foodId') foodId: string) {
return this.cartService.decrementItem(userId, restaurantId, foodId);
@ApiParam({ name: 'productId', description: 'Product ID to decrement in cart' })
async decrementItem(@UserId() userId: string, @ShopId() shopId: string, @Param('productId') productId: string) {
return this.cartService.decrementItem(userId, shopId, productId);
}
@Post('items/bulk')
@Post('product/bulk')
@ApiOperation({ summary: 'Bulk add items to cart (increments quantity if items exist)' })
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: BulkAddItemsToCartDto })
bulkAddItems(
@UserId() userId: string,
@ShopId() restaurantId: string,
@ShopId() shopId: string,
@Body() bulkAddItemsDto: BulkAddItemsToCartDto,
) {
return this.cartService.bulkAddItems(userId, restaurantId, bulkAddItemsDto);
return this.cartService.bulkAddItems(userId, shopId, bulkAddItemsDto);
}
@Delete('items/:foodId')
@Delete('product/:productId')
@ApiOperation({ summary: 'Remove item from cart' })
@ApiHeader(API_HEADER_SLUG)
@ApiParam({ name: 'foodId', description: 'Product ID in the cart' })
async removeItem(@UserId() userId: string, @ShopId() restaurantId: string, @Param('foodId') foodId: string) {
return this.cartService.removeItem(userId, restaurantId, foodId);
@ApiParam({ name: 'productId', description: 'Product ID in the cart' })
async removeItem(@UserId() userId: string, @ShopId() shopId: string, @Param('productId') productId: string) {
return this.cartService.removeItem(userId, shopId, productId);
}
@Delete()
@ApiOperation({ summary: 'Clear entire cart' })
@ApiHeader(API_HEADER_SLUG)
async clearCart(@UserId() userId: string, @ShopId() restaurantId: string) {
return this.cartService.clearCart(userId, restaurantId);
async clearCart(@UserId() userId: string, @ShopId() shopId: string) {
return this.cartService.clearCart(userId, shopId);
}
@Post('apply-coupon')
@Post('coupon')
@ApiOperation({ summary: 'Apply coupon to cart' })
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: ApplyCouponDto })
async applyCoupon(@UserId() userId: string, @ShopId() restaurantId: string, @Body() applyCouponDto: ApplyCouponDto) {
return this.cartService.applyCoupon(userId, restaurantId, applyCouponDto);
async applyCoupon(@UserId() userId: string, @ShopId() shopId: string, @Body() applyCouponDto: ApplyCouponDto) {
return this.cartService.applyCoupon(userId, shopId, applyCouponDto);
}
@Delete('coupon')
@ApiOperation({ summary: 'Remove coupon from cart' })
@ApiHeader(API_HEADER_SLUG)
async removeCoupon(@UserId() userId: string, @ShopId() restaurantId: string) {
return this.cartService.removeCoupon(userId, restaurantId);
async removeCoupon(@UserId() userId: string, @ShopId() shopId: string) {
return this.cartService.removeCoupon(userId, shopId);
}
@@ -86,7 +85,7 @@ export class CartController {
@ApiOperation({ summary: 'Set all cart params' })
@ApiHeader(API_HEADER_SLUG)
@ApiBody({ type: SetAllCartParmsDto })
setAllCartParams(@UserId() userId: string, @ShopId() restaurantId: string, @Body() dto: SetAllCartParmsDto) {
return this.cartService.setAllCartParams(userId, restaurantId, dto);
setAllCartParams(@UserId() userId: string, @ShopId() shopId: string, @Body() dto: SetAllCartParmsDto) {
return this.cartService.setAllCartParams(userId, shopId, dto);
}
}
+3 -3
View File
@@ -13,7 +13,7 @@ class AddItemToCartDto {
@ApiProperty({ description: 'Product ID' })
@IsNotEmpty()
@IsString()
foodId!: string;
productId!: string;
}
export class BulkAddItemsToCartDto {
@@ -21,8 +21,8 @@ export class BulkAddItemsToCartDto {
description: 'Array of items to add to cart',
type: [AddItemToCartDto],
example: [
{ foodId: 'product-123', quantity: 2 },
{ foodId: 'product-456', quantity: 1 },
{ productId: 'product-123', quantity: 2 },
{ productId: 'product-456', quantity: 1 },
],
})
@IsNotEmpty()
@@ -1,8 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
import { SetCarDeliveryDto } from './set-car-delivery.dto';
export class SetAllCartParmsDto {
@ApiProperty({
description: 'Cart description or notes',
@@ -27,14 +25,4 @@ export class SetAllCartParmsDto {
@IsNotEmpty()
@IsString()
paymentMethodId!: string;
@ApiProperty({ description: 'Table number for dine-in orders', example: '5', required: false })
@IsOptional()
@IsString()
tableNumber?: string;
@ApiProperty()
@IsOptional()
@IsObject()
carAddress?: SetCarDeliveryDto;
}
@@ -1,19 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class SetCarDeliveryDto {
@ApiProperty({ description: 'Car model', example: 'Toyota Camry' })
@IsNotEmpty()
@IsString()
carModel!: string;
@ApiProperty({ description: 'Car color', example: 'White' })
@IsNotEmpty()
@IsString()
carColor!: string;
@ApiProperty({ description: 'License plate number', example: '12ABC345' })
@IsNotEmpty()
@IsString()
plateNumber!: string;
}
@@ -11,8 +11,8 @@ export interface CartItem {
export interface Cart {
userId: string;
restaurantId: string;
restaurantName?: string;
shopId: string;
shopName?: string;
items: CartItem[];
coupon?: OrderCouponDetail;
@@ -173,7 +173,7 @@ export class CartCalculationService {
cart.couponDiscount = couponDiscount;
cart.totalDiscount = couponDiscount + itemsDiscount;
cart.tax = await this.calculateTax(cart.restaurantId, 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);
// total = subtotal totalDiscount + tax + deliveryFee
@@ -58,8 +58,8 @@ export class CartItemService {
/**
* Add or increment item in cart
*/
async addOrIncrementItem(cart: Cart, shopId: string, restaurantId: string, quantityToAdd: number): Promise<void> {
const product = await this.validationService.validateAndGetFood(shopId, restaurantId, quantityToAdd);
async addOrIncrementItem(cart: Cart, productId: string, shopId: string, quantityToAdd: number): Promise<void> {
const product = await this.validationService.validateAndGetFood(productId, shopId, quantityToAdd);
const index = this.getItemIndex(cart, product.id);
@@ -14,7 +14,6 @@ import { WalletTransactionRepository } from 'src/modules/users/repositories/wall
import { ProductService } from 'src/modules/products/providers/product.service';
import { ShopService } from 'src/modules/shops/providers/shops.service';
import { DeliveryService } from 'src/modules/delivery/providers/delivery.service';
import { PaymentsService } from 'src/modules/payments/services/payments.service';
import { PaymentMethodService } from 'src/modules/payments/services/payment-method.service';
@Injectable()
+45 -45
View File
@@ -5,7 +5,7 @@ import { OrderCouponDetail } from 'src/modules/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 { SetAllCartParmsDto } from '../dto/set-all-cart-params.dto';
import { SetAllCartParmsDto } from '../dto/set-all-cart-params.dto';
import { CouponService } from 'src/modules/coupons/providers/coupon.service';
import { CartRepository } from '../repositories/cart.repository';
import { CartValidationService } from './cart-validation.service';
@@ -32,15 +32,15 @@ export class CartService {
/**
* Set all cart parameters at once
*/
async setAllCartParams(userId: string, restaurantId: string, params: SetAllCartParmsDto): Promise<Cart> {
const { deliveryMethodId, paymentMethodId, addressId, carAddress, description, tableNumber } = params;
async setAllCartParams(userId: string, shopId: string, params: SetAllCartParmsDto): Promise<Cart> {
const { deliveryMethodId, paymentMethodId, addressId, description } = params;
// get existing cart (or throw)
const cart = await this.findOneOrFail(userId, restaurantId);
const cart = await this.findOneOrFail(userId, shopId);
// Validate and get delivery method
const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail(
restaurantId,
shopId,
deliveryMethodId,
);
cart.deliveryMethodId = deliveryMethodId;
@@ -65,7 +65,7 @@ export class CartService {
// ensure address is within shop service area
await this.validationService.assertAddressInsideServiceArea(
restaurantId,
shopId,
address.latitude,
address.longitude,
);
@@ -85,7 +85,7 @@ export class CartService {
// Validate and set payment method
const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail(
restaurantId,
shopId,
paymentMethodId,
);
@@ -93,7 +93,7 @@ export class CartService {
await this.calculationService.recalculateCartTotals(cart);
if (paymentMethod.method === PaymentMethodEnum.Wallet) {
await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total);
await this.validationService.assertWalletHasEnoughBalance(userId, shopId, cart.total);
}
cart.paymentMethodId = paymentMethodId;
@@ -110,21 +110,21 @@ export class CartService {
/**
* Get or create cart for user and shop
*/
async getOrCreateCart(userId: string, restaurantId: string): Promise<Cart> {
const existingCart = await this.cartRepository.findByUserAndRestaurant(userId, restaurantId);
async getOrCreateCart(userId: string, shopId: string): Promise<Cart> {
const existingCart = await this.cartRepository.findByUserAndShop(userId, shopId);
if (existingCart) {
return existingCart;
}
// Find shop
const shop = await this.shopService.findOrFail(restaurantId);
const shop = await this.shopService.findOrFail(shopId);
// Create new cart
const now = this.nowIso();
const cart: Cart = {
userId,
restaurantId,
restaurantName: shop.name,
shopId: shop.id,
shopName: shop.name,
items: [],
deliveryFee: 0,
subTotal: 0,
@@ -145,8 +145,8 @@ export class CartService {
/**
* Find cart by user and shop
*/
async findOneOrFail(userId: string, restaurantId: string): Promise<Cart> {
const cart = await this.cartRepository.findByUserAndRestaurant(userId, restaurantId);
async findOneOrFail(userId: string, shopId: string): Promise<Cart> {
const cart = await this.cartRepository.findByUserAndShop(userId, shopId);
if (!cart) {
throw new NotFoundException(CartMessage.NOT_FOUND);
}
@@ -156,17 +156,17 @@ export class CartService {
/**
* Increment item quantity in cart
*/
async incrementItem(userId: string, restaurantId: string, foodId: string, quantity: number): Promise<Cart> {
const cart = await this.getOrCreateCart(userId, restaurantId);
await this.itemService.addOrIncrementItem(cart, foodId, restaurantId, quantity);
async incrementItem(userId: string, shopId: string, foodId: string, quantity: number): Promise<Cart> {
const cart = await this.getOrCreateCart(userId, shopId);
await this.itemService.addOrIncrementItem(cart, foodId, shopId, quantity);
return this.recalculateAndSaveCart(cart);
}
/**
* Decrement item quantity in cart by 1 (removes item if quantity reaches 0)
*/
async decrementItem(userId: string, restaurantId: string, foodId: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
async decrementItem(userId: string, shopId: string, foodId: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, shopId);
await this.itemService.decrementOrRemoveItem(cart, foodId);
return this.recalculateAndSaveCart(cart);
}
@@ -174,11 +174,11 @@ export class CartService {
/**
* Bulk add items to cart (increments quantity if items exist)
*/
async bulkAddItems(userId: string, restaurantId: string, bulkAddItemsDto: BulkAddItemsToCartDto): Promise<Cart> {
const cart = await this.getOrCreateCart(userId, restaurantId);
async bulkAddItems(userId: string, shopId: string, bulkAddItemsDto: BulkAddItemsToCartDto): Promise<Cart> {
const cart = await this.getOrCreateCart(userId, shopId);
for (const addItemDto of bulkAddItemsDto.items) {
await this.itemService.addOrIncrementItem(cart, addItemDto.foodId, restaurantId, addItemDto.quantity);
await this.itemService.addOrIncrementItem(cart, addItemDto.productId, shopId, addItemDto.quantity);
}
return this.recalculateAndSaveCart(cart);
@@ -187,8 +187,8 @@ export class CartService {
/**
* Remove item from cart
*/
async removeItem(userId: string, restaurantId: string, foodId: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
async removeItem(userId: string, shopId: string, foodId: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, shopId);
this.itemService.removeItemOrFail(cart, foodId);
return this.recalculateAndSaveCart(cart);
}
@@ -196,15 +196,15 @@ export class CartService {
/**
* Apply coupon to cart
*/
async applyCoupon(userId: string, restaurantId: string, applyCouponDto: ApplyCouponDto): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
async applyCoupon(userId: string, shopId: string, applyCouponDto: ApplyCouponDto): Promise<Cart> {
const cart = await this.findOneOrFail(userId, shopId);
const orderAmount = cart.subTotal - cart.itemsDiscount;
const user = await this.userService.findOneOrFail(userId);
// check if coupon is valid and belong to the shop
const { valid, coupon, message } = await this.couponService.validateCoupon(
applyCouponDto.code,
restaurantId,
shopId,
Number(orderAmount),
user.phone,
);
@@ -241,8 +241,8 @@ export class CartService {
/**
* Remove coupon from cart
*/
async removeCoupon(userId: string, restaurantId: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
async removeCoupon(userId: string, shopId: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, shopId);
cart.coupon = undefined;
return this.recalculateAndSaveCart(cart);
@@ -251,14 +251,14 @@ export class CartService {
/**
* Set address for cart
*/
async setAddress(userId: string, restaurantId: string, addressId: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
async setAddress(userId: string, shopId: string, addressId: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, shopId);
const deliveryMethodId = this.validationService.requireDeliveryMethodId(
cart,
'Delivery method must be set before setting address',
);
const deliveryMethod = await this.deliveryService.findOrFail(restaurantId, deliveryMethodId);
const deliveryMethod = await this.deliveryService.findOrFail(shopId, deliveryMethodId);
this.validationService.assertDeliveryMethod(
deliveryMethod.method,
@@ -274,7 +274,7 @@ export class CartService {
// ensure address is within shop service area
await this.validationService.assertAddressInsideServiceArea(
restaurantId,
shopId,
address.latitude,
address.longitude,
);
@@ -298,17 +298,17 @@ export class CartService {
*/
async setPaymentMethod(
userId: string,
restaurantId: string,
shopId: string,
paymentMethodId: string,
): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
const cart = await this.findOneOrFail(userId, shopId);
const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail(
restaurantId,
shopId,
paymentMethodId,
);
if (paymentMethod.method === PaymentMethodEnum.Wallet) {
await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total);
await this.validationService.assertWalletHasEnoughBalance(userId, shopId, cart.total);
}
cart.paymentMethodId = paymentMethodId;
return this.recalculateAndSaveCart(cart);
@@ -319,13 +319,13 @@ export class CartService {
*/
async setDeliveryMethod(
userId: string,
restaurantId: string,
shopId: string,
deliveryMethodId: string,
): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
const cart = await this.findOneOrFail(userId, shopId);
const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail(
restaurantId,
shopId,
deliveryMethodId,
);
@@ -338,8 +338,8 @@ export class CartService {
/**
* Set description for cart
*/
async setDescription(userId: string, restaurantId: string, description: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId);
async setDescription(userId: string, shopId: string, description: string): Promise<Cart> {
const cart = await this.findOneOrFail(userId, shopId);
cart.description = description;
return this.saveTouchedCart(cart);
@@ -349,8 +349,8 @@ export class CartService {
/**
* Clear cart from cache
*/
async clearCart(userId: string, restaurantId: string): Promise<void> {
return this.cartRepository.delete(userId, restaurantId);
async clearCart(userId: string, shopId: string): Promise<void> {
return this.cartRepository.delete(userId, shopId);
}
/**
@@ -7,19 +7,19 @@ export class CartRepository {
private readonly CART_TTL = 3600; // 1 hour in seconds
private readonly CART_KEY_PREFIX = 'cart';
constructor(private readonly cacheService: CacheService) {}
constructor(private readonly cacheService: CacheService) { }
/**
* Get cart by user and shop
*/
async findByUserAndRestaurant(userId: string, restaurantId: string): Promise<Cart | null> {
const cacheKey = this.getCacheKey(userId, restaurantId);
async findByUserAndShop(userId: string, shopId: string): Promise<Cart | null> {
const cacheKey = this.getCacheKey(userId, shopId);
const cachedCart = await this.cacheService.get<string>(cacheKey);
if (cachedCart) {
try {
const parsed: unknown = JSON.parse(cachedCart);
if (this.isCart(parsed) && parsed.userId === userId && parsed.restaurantId === restaurantId) {
if (this.isCart(parsed) && parsed.userId === userId && parsed.shopId === shopId) {
return parsed;
}
} catch {
@@ -34,23 +34,23 @@ export class CartRepository {
* Save cart to cache
*/
async save(cart: Cart): Promise<void> {
const cacheKey = this.getCacheKey(cart.userId, cart.restaurantId);
const cacheKey = this.getCacheKey(cart.userId, cart.shopId);
await this.cacheService.set(cacheKey, JSON.stringify(cart), this.CART_TTL);
}
/**
* Delete cart from cache
*/
async delete(userId: string, restaurantId: string): Promise<void> {
const cacheKey = this.getCacheKey(userId, restaurantId);
async delete(userId: string, shopId: string): Promise<void> {
const cacheKey = this.getCacheKey(userId, shopId);
await this.cacheService.del(cacheKey);
}
/**
* Generate cache key for cart
*/
private getCacheKey(userId: string, restaurantId: string): string {
return `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`;
private getCacheKey(userId: string, shopId: string): string {
return `${this.CART_KEY_PREFIX}:${userId}:${shopId}`;
}
/**
@@ -63,7 +63,7 @@ export class CartRepository {
const cart = obj as Record<string, unknown>;
return (
typeof cart.userId === 'string' &&
typeof cart.restaurantId === 'string' &&
typeof cart.shopId === 'string' &&
Array.isArray(cart.items) &&
typeof cart.subTotal === 'number' &&
typeof cart.itemsDiscount === 'number' &&