@@ -97,8 +97,11 @@ export class CartService {
this . validateStock ( food , newQuantity ) ;
cart . items [ existingItemIndex ] = {
. . . existingItem ,
foodTitle : food.title ,
price : food.price || 0 ,
discount : food.discount || 0 ,
quantity : newQuantity ,
totalPrice : this.calculateItemTotalPrice ( food , newQuantity ) ,
totalPrice : this.calculateItemTotalPrice ( food . price || 0 , food . discount || 0 , newQuantity ) ,
} ;
} else {
cart . items . push ( this . createCartItem ( food , quantity ) ) ;
@@ -135,8 +138,11 @@ export class CartService {
cart . items [ itemIndex ] = {
. . . existingItem ,
foodTitle : food.title ,
price : food.price || 0 ,
discount : food.discount || 0 ,
quantity : newQuantity ,
totalPrice : this.calculateItemTotalPrice ( food , newQuantity ) ,
totalPrice : this.calculateItemTotalPrice ( food . price || 0 , food . discount || 0 , newQuantity ) ,
} ;
}
@@ -164,8 +170,11 @@ export class CartService {
this . validateStock ( food , newQuantity ) ;
cart . items [ existingItemIndex ] = {
. . . existingItem ,
foodTitle : food.title ,
price : food.price || 0 ,
discount : food.discount || 0 ,
quantity : newQuantity ,
totalPrice : this.calculateItemTotalPrice ( food , newQuantity ) ,
totalPrice : this.calculateItemTotalPrice ( food . price || 0 , food . discount || 0 , newQuantity ) ,
} ;
} else {
cart . items . push ( this . createCartItem ( food , addItemDto . quantity ) ) ;
@@ -323,6 +332,19 @@ export class CartService {
const cart = await this . findOneOrFail ( userId , restaurantId ) ;
if ( ! cart . deliveryMethodId ) throw new BadRequestException ( 'Delivery method must be set before setting address' ) ;
const deliveryMethod = await this . em . findOne ( Delivery , {
id : cart.deliveryMethodId ,
restaurant : { id : restaurantId } ,
} ) ;
if ( ! deliveryMethod ) {
throw new NotFoundException (
` Delivery method with ID ${ cart . deliveryMethodId } not found for restaurant ${ restaurantId } ` ,
) ;
}
if ( deliveryMethod . method !== DeliveryMethodEnum . DeliveryCourier ) {
throw new BadRequestException ( 'Delivery method must be DeliveryCourier' ) ;
}
// Find and validate address belongs to user
const address = await this . em . findOne ( UserAddress , { id : setAddressDto.addressId } , { populate : [ 'user' ] } ) ;
if ( ! address ) {
@@ -341,9 +363,12 @@ export class CartService {
city : address.city ,
province : address.province || '' ,
postalCode : address.postalCode || undefined ,
fullName : address.user.firstName + ' ' + address . user . lastName || '' ,
fullName : ` ${ address . user . firstName || '' } ${ address . user . lastName || '' } ` . trim ( ) ,
phone : address.user.phone ,
} ;
// DeliveryCourier implies: no car + no dine-in table
cart . carAddress = null ;
cart . tableNumber = undefined ;
cart . updatedAt = new Date ( ) . toISOString ( ) ;
await this . saveCart ( cart ) ;
@@ -423,6 +448,7 @@ export class CartService {
}
cart . deliveryMethodId = setDeliveryMethodDto . deliveryMethodId ;
this . clearFieldsIncompatibleWithDeliveryMethod ( cart , deliveryMethod . method ) ;
// Recalculate cart totals (this will update deliveryFee based on delivery method)
await this . recalculateCartTotals ( cart ) ;
@@ -468,6 +494,9 @@ export class CartService {
}
cart . tableNumber = setTableNumberDto . tableNumber ;
// DineIn implies: no delivery addresses
cart . userAddress = null ;
cart . carAddress = null ;
cart . updatedAt = new Date ( ) . toISOString ( ) ;
await this . saveCart ( cart ) ;
@@ -507,6 +536,9 @@ export class CartService {
plateNumber : setCarDeliveryDto.plateNumber ,
phone : user.phone ,
} ;
// DeliveryCar implies: no courier address + no dine-in table
cart . userAddress = null ;
cart . tableNumber = undefined ;
cart . updatedAt = new Date ( ) . toISOString ( ) ;
await this . saveCart ( cart ) ;
@@ -523,11 +555,16 @@ export class CartService {
let totalItems = 0 ;
for ( const item of cart . items ) {
const itemPrice = item . price * item . quantity ;
const itemDiscount = item . discount * item . quantity ;
const unitPrice = Number ( item . price ) || 0 ;
const unitDiscount = Math . min ( Number ( item . discount ) || 0 , unitPrice ) ;
const itemPrice = unitPrice * item . quantity ;
const itemDiscount = unitDiscount * item . quantity ;
subTotal += itemPrice ;
itemsDiscount += itemDiscount ;
totalItems += item . quantity ;
// Keep item.totalPrice consistent with price/discount/quantity
item . totalPrice = itemPrice - itemDiscount ;
}
cart . subTotal = subTotal ;
@@ -537,58 +574,106 @@ export class CartService {
let couponDiscount = 0 ;
if ( cart . coupon ) {
// Get the full coupon entity to check foodCategories and foods
const coupon = await this . couponService . findById ( cart . coupon . couponId ) ;
// Determine which items are eligible for the coupon
let eligibleItemsTotal = 0 ;
let eligibleItemsDiscount = 0 ;
// If coupon has foodCategories or foods restrictions, filter items
const hasCategoryRestriction = coupon . foodCategories && coupon . foodCategories . length > 0 ;
const hasFoodRestriction = coupon . foods && coupon . foods . length > 0 ;
if ( hasCategoryRestriction || hasFoodRestriction ) {
// Fetch all foods in cart with their categories
const foodIds = cart . items . map ( item = > item . foodId ) ;
const foods = await this . em . find ( Food , { id : { $in : foodIds } } , { populate : [ 'category' ] } ) ;
const foodMap = new Map ( foods . map ( f = > [ f . id , f ] ) ) ;
for ( const item of cart . items ) {
const food = foodMap . get ( item . foodId ) ;
if ( ! food ) continue ;
// Check if item matches coupon criteria
const matchesCategory =
hasCategoryRestriction && food . category && coupon . foodCategories ! . includes ( food . category . id ) ;
const matchesFood = hasFoodRestriction && coupon . foods ! . includes ( food . id ) ;
// Item is eligible if it matches category OR food criteria
if ( matchesCategory || matchesFood ) {
const itemPrice = item . price * item . quantity ;
const itemDiscount = item . discount * item . quantity ;
eligibleItemsTotal += itemPrice ;
eligibleItemsDiscount += itemDiscount ;
}
// Get the full coupon entity to check restrictions.
// If coupon was deleted/disabled, fail gracefully by removing it from cart.
let coupon :
| {
id : string ;
type : CouponType ;
value : number ;
maxDiscount? : number ;
isActive? : boolean ;
startDate? : Date ;
endDate? : Date ;
foodCategories? : string [ ] ;
foods? : string [ ] ;
}
} else {
// No restrictions, apply to all items
eligibleItemsTotal = subTotal ;
eligibleItemsDiscount = itemsDiscount ;
| undefined ;
try {
const c = await this . couponService . findById ( cart . coupon . couponId ) ;
coupon = {
id : c.id ,
type : c . type ,
value : c.value ,
maxDiscount : c.maxDiscount ,
isActive : c.isActive ,
startDate : c.startDate ,
endDate : c.endDate ,
foodCategories : c.foodCategories ,
foods : c.foods ,
} ;
} catch {
cart . coupon = undefined ;
}
const priceAfterItemDiscount = eligibleItemsTotal - eligibleItemsDiscount ;
if ( cart . coupon . type === CouponType . PERCENTAGE ) {
let discount = ( priceAfterItemDiscount * cart . coupon . value ) / 100 ;
// Apply maxDiscount limit if set
if ( cart . coupon . maxDiscount && discount > cart . coupon . maxDiscount ) {
discount = cart . coupon . maxDiscount ;
if ( coupon ) {
// Basic validity checks (applyCoupon already does full validation)
const now = new Date ( ) ;
if ( coupon . isActive === false ) {
cart . coupon = undefined ;
}
if ( coupon . startDate && now < coupon . startDate ) {
cart . coupon = undefined ;
}
if ( coupon . endDate && now > coupon . endDate ) {
cart . coupon = undefined ;
}
}
if ( cart . coupon ) {
// Determine which items are eligible for the coupon
let eligibleItemsTotal = 0 ;
let eligibleItemsDiscount = 0 ;
// If coupon has foodCategories or foods restrictions, filter items
const hasCategoryRestriction = coupon ! . foodCategories && coupon ! . foodCategories . length > 0 ;
const hasFoodRestriction = coupon ! . foods && coupon ! . foods . length > 0 ;
if ( hasCategoryRestriction || hasFoodRestriction ) {
// Fetch all foods in cart with their categories
const foodIds = cart . items . map ( item = > item . foodId ) ;
const foods = await this . em . find ( Food , { id : { $in : foodIds } } , { populate : [ 'category' ] } ) ;
const foodMap = new Map ( foods . map ( f = > [ f . id , f ] ) ) ;
for ( const item of cart . items ) {
const food = foodMap . get ( item . foodId ) ;
if ( ! food ) continue ;
// Check if item matches coupon criteria
const matchesCategory =
hasCategoryRestriction && food . category && coupon ! . foodCategories ! . includes ( food . category . id ) ;
const matchesFood = hasFoodRestriction && coupon ! . foods ! . includes ( food . id ) ;
// Item is eligible if it matches category OR food criteria
if ( matchesCategory || matchesFood ) {
const unitPrice = Number ( item . price ) || 0 ;
const unitDiscount = Math . min ( Number ( item . discount ) || 0 , unitPrice ) ;
const itemPrice = unitPrice * item . quantity ;
const itemDiscount = unitDiscount * item . quantity ;
eligibleItemsTotal += itemPrice ;
eligibleItemsDiscount += itemDiscount ;
}
}
} else {
// No restrictions, apply to all items
eligibleItemsTotal = subTotal ;
eligibleItemsDiscount = itemsDiscount ;
}
const priceAfterItemDiscount = Math . max ( 0 , eligibleItemsTotal - eligibleItemsDiscount ) ;
if ( cart . coupon . type === CouponType . PERCENTAGE ) {
let discount = ( priceAfterItemDiscount * cart . coupon . value ) / 100 ;
// Apply maxDiscount limit if set
if ( cart . coupon . maxDiscount && discount > cart . coupon . maxDiscount ) {
discount = cart . coupon . maxDiscount ;
}
couponDiscount = discount ;
} else {
// FIXED amount discount
couponDiscount = Math . min ( cart . coupon . value , priceAfterItemDiscount ) ;
}
couponDiscount = discount ;
} else {
// FIXED amount discount
couponDiscount = Math . min ( cart . coupon . value , priceAfterItemDiscount ) ;
}
}
@@ -601,7 +686,7 @@ export class CartService {
const restaurant = await this . em . findOne ( Restaurant , { id : cart.restaurantId } ) ;
if ( restaurant && restaurant . vat && restaurant . vat > 0 ) {
// Tax is calculated on the price after discounts
const priceAfterDiscounts = subTotal - cart . totalDiscount ;
const priceAfterDiscounts = Math . max ( 0 , subTotal - cart . totalDiscount ) ;
tax = ( priceAfterDiscounts * Number ( restaurant . vat ) ) / 100 ;
}
@@ -618,11 +703,41 @@ export class CartService {
cart . deliveryFee = deliveryFee ;
// Calculate total: total = subtotal – totalDiscount + tax + deliveryFee
cart . total = subTotal - cart . totalDiscount + tax + cart . deliveryFee ;
cart . total = Math . max ( 0 , subTotal - cart . totalDiscount ) + tax + cart . deliveryFee ;
cart . totalItems = totalItems ;
cart . updatedAt = new Date ( ) . toISOString ( ) ;
}
/**
* Clears cart fields that are not applicable for a given delivery method.
*/
private clearFieldsIncompatibleWithDeliveryMethod ( cart : Cart , method : DeliveryMethodEnum ) : void {
if ( method === DeliveryMethodEnum . DineIn ) {
cart . userAddress = null ;
cart . carAddress = null ;
return ;
}
if ( method === DeliveryMethodEnum . CustomerPickup ) {
cart . userAddress = null ;
cart . carAddress = null ;
cart . tableNumber = undefined ;
return ;
}
if ( method === DeliveryMethodEnum . DeliveryCourier ) {
cart . carAddress = null ;
cart . tableNumber = undefined ;
return ;
}
if ( method === DeliveryMethodEnum . DeliveryCar ) {
cart . userAddress = null ;
cart . tableNumber = undefined ;
return ;
}
}
/**
* Type guard to check if an object is a Cart
*/
@@ -726,11 +841,11 @@ export class CartService {
/**
* Calculate total price for a cart item
*/
private calculateItemTotalPrice ( food : Food , quantity : number ) : number {
const itemPrice = food . price || 0 ;
const itemDiscount = food . discount || 0 ;
const itemTotalPrice = itemPrice * quantity ;
const itemTotalDiscount = itemDiscount * quantity ;
private calculateItemTotalPrice ( unitPrice : number , unitDiscount : number , quantity : number ) : number {
const safeUnitPrice = Number ( unitPrice ) || 0 ;
const safeUnitDiscount = Math . min ( Number ( unitDiscount ) || 0 , safeUnitPrice ) ;
const itemTotalPrice = safeUnitPrice * quantity ;
const itemTotalDiscount = safeUnitDiscount * quantity ;
return itemTotalPrice - itemTotalDiscount ;
}
@@ -747,7 +862,7 @@ export class CartService {
quantity ,
price : itemPrice ,
discount : itemDiscount ,
totalPrice : this.calculateItemTotalPrice ( food , quantity ) ,
totalPrice : this.calculateItemTotalPrice ( itemPrice , itemDiscount , quantity ) ,
} ;
}
}