cart refactored
This commit is contained in:
@@ -20,6 +20,10 @@ import { CartCalculationService } from './providers/cart-calculation.service';
|
|||||||
import { CartItemService } from './providers/cart-item.service';
|
import { CartItemService } from './providers/cart-item.service';
|
||||||
import { PointTransaction } from '../users/entities/point-transaction.entity';
|
import { PointTransaction } from '../users/entities/point-transaction.entity';
|
||||||
import { WalletTransaction } from '../users/entities/wallet-transaction.entity';
|
import { WalletTransaction } from '../users/entities/wallet-transaction.entity';
|
||||||
|
import { ShopsModule } from '../shops/shops.module';
|
||||||
|
import { DeliveryModule } from '../delivery/delivery.module';
|
||||||
|
import { ProductModule } from '../products/product.module';
|
||||||
|
import { PaymentsModule } from '../payments/payments.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -39,6 +43,10 @@ import { WalletTransaction } from '../users/entities/wallet-transaction.entity';
|
|||||||
JwtModule,
|
JwtModule,
|
||||||
UtilsModule,
|
UtilsModule,
|
||||||
CouponModule,
|
CouponModule,
|
||||||
|
ShopsModule,
|
||||||
|
DeliveryModule,
|
||||||
|
ProductModule,
|
||||||
|
PaymentsModule
|
||||||
],
|
],
|
||||||
controllers: [CartController],
|
controllers: [CartController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { OrderCouponDetail, OrderUserAddress } from 'src/modules/orders/interface/order.interface';
|
import type { OrderCouponDetail, OrderUserAddress } from 'src/modules/orders/interface/order.interface';
|
||||||
|
|
||||||
export interface CartItem {
|
export interface CartItem {
|
||||||
foodId: string;
|
productId: string;
|
||||||
foodTitle?: string;
|
productTitle?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
price: number;
|
price: number;
|
||||||
discount: number;
|
discount: number;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export class CartCalculationService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
private readonly couponService: CouponService,
|
private readonly couponService: CouponService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate total price for a cart item
|
* Calculate total price for a cart item
|
||||||
@@ -71,7 +71,7 @@ export class CartCalculationService {
|
|||||||
const foodMap = await this.getFoodsInCartWithCategories(cart);
|
const foodMap = await this.getFoodsInCartWithCategories(cart);
|
||||||
|
|
||||||
for (const item of cart.items) {
|
for (const item of cart.items) {
|
||||||
const product = foodMap.get(item.foodId);
|
const product = foodMap.get(item.productId);
|
||||||
if (!product) continue;
|
if (!product) continue;
|
||||||
|
|
||||||
const matchesCategory =
|
const matchesCategory =
|
||||||
@@ -227,7 +227,7 @@ export class CartCalculationService {
|
|||||||
* Get products in cart with categories
|
* Get products in cart with categories
|
||||||
*/
|
*/
|
||||||
private async getFoodsInCartWithCategories(cart: Cart): Promise<Map<string, Product>> {
|
private async getFoodsInCartWithCategories(cart: Cart): Promise<Map<string, Product>> {
|
||||||
const foodIds = cart.items.map(item => item.foodId);
|
const foodIds = cart.items.map(item => item.productId);
|
||||||
if (foodIds.length === 0) return new Map();
|
if (foodIds.length === 0) return new Map();
|
||||||
|
|
||||||
const products = await this.em.find(Product, { id: { $in: foodIds } }, { populate: ['category'] });
|
const products = await this.em.find(Product, { id: { $in: foodIds } }, { populate: ['category'] });
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import { Cart, CartItem } from '../interfaces/cart.interface';
|
|||||||
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 { CartMessage } from 'src/common/enums/message.enum';
|
import { CartMessage } from 'src/common/enums/message.enum';
|
||||||
|
import { ProductService } from 'src/modules/products/providers/product.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CartItemService {
|
export class CartItemService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly validationService: CartValidationService,
|
private readonly validationService: CartValidationService,
|
||||||
private readonly calculationService: CartCalculationService,
|
private readonly calculationService: CartCalculationService,
|
||||||
) {}
|
private readonly productService: ProductService,
|
||||||
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new cart item from product
|
* Create a new cart item from product
|
||||||
@@ -20,8 +22,8 @@ export class CartItemService {
|
|||||||
const itemDiscount = product.discount || 0;
|
const itemDiscount = product.discount || 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
foodId: product.id,
|
productId: product.id,
|
||||||
foodTitle: product.title,
|
productTitle: product.title,
|
||||||
quantity,
|
quantity,
|
||||||
price: itemPrice,
|
price: itemPrice,
|
||||||
discount: itemDiscount,
|
discount: itemDiscount,
|
||||||
@@ -37,8 +39,8 @@ export class CartItemService {
|
|||||||
const itemDiscount = product.discount || 0;
|
const itemDiscount = product.discount || 0;
|
||||||
return {
|
return {
|
||||||
...(previous ?? ({} as CartItem)),
|
...(previous ?? ({} as CartItem)),
|
||||||
foodId: product.id,
|
productId: product.id,
|
||||||
foodTitle: product.title,
|
productTitle: product.title,
|
||||||
quantity,
|
quantity,
|
||||||
price: itemPrice,
|
price: itemPrice,
|
||||||
discount: itemDiscount,
|
discount: itemDiscount,
|
||||||
@@ -49,16 +51,16 @@ export class CartItemService {
|
|||||||
/**
|
/**
|
||||||
* Get item index in cart
|
* Get item index in cart
|
||||||
*/
|
*/
|
||||||
getItemIndex(cart: Cart, foodId: string): number {
|
getItemIndex(cart: Cart, shopId: string): number {
|
||||||
return cart.items.findIndex(item => item.foodId === foodId);
|
return cart.items.findIndex(item => item.productId === shopId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add or increment item in cart
|
* Add or increment item in cart
|
||||||
*/
|
*/
|
||||||
async addOrIncrementItem(cart: Cart, foodId: string, restaurantId: string, quantityToAdd: number): Promise<void> {
|
async addOrIncrementItem(cart: Cart, shopId: string, restaurantId: string, quantityToAdd: number): Promise<void> {
|
||||||
const product = await this.validationService.validateAndGetFood(foodId, restaurantId, quantityToAdd);
|
const product = await this.validationService.validateAndGetFood(shopId, restaurantId, quantityToAdd);
|
||||||
|
|
||||||
const index = this.getItemIndex(cart, product.id);
|
const index = this.getItemIndex(cart, product.id);
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
@@ -68,15 +70,15 @@ export class CartItemService {
|
|||||||
|
|
||||||
const existingItem = cart.items[index];
|
const existingItem = cart.items[index];
|
||||||
const newQuantity = existingItem.quantity + quantityToAdd;
|
const newQuantity = existingItem.quantity + quantityToAdd;
|
||||||
this.validationService.validateStock(product, newQuantity);
|
|
||||||
cart.items[index] = this.buildCartItemFromFood(product, newQuantity, existingItem);
|
cart.items[index] = this.buildCartItemFromFood(product, newQuantity, existingItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove item from cart
|
* Remove item from cart
|
||||||
*/
|
*/
|
||||||
removeItemOrFail(cart: Cart, foodId: string): void {
|
removeItemOrFail(cart: Cart, shopId: string): void {
|
||||||
const itemIndex = this.getItemIndex(cart, foodId);
|
const itemIndex = this.getItemIndex(cart, shopId);
|
||||||
if (itemIndex < 0) {
|
if (itemIndex < 0) {
|
||||||
throw new NotFoundException(CartMessage.ITEM_NOT_FOUND);
|
throw new NotFoundException(CartMessage.ITEM_NOT_FOUND);
|
||||||
}
|
}
|
||||||
@@ -86,8 +88,8 @@ export class CartItemService {
|
|||||||
/**
|
/**
|
||||||
* Decrement item quantity or remove if quantity reaches 0
|
* Decrement item quantity or remove if quantity reaches 0
|
||||||
*/
|
*/
|
||||||
async decrementOrRemoveItem(cart: Cart, foodId: string): Promise<void> {
|
async decrementOrRemoveItem(cart: Cart, shopId: string): Promise<void> {
|
||||||
const itemIndex = this.getItemIndex(cart, foodId);
|
const itemIndex = this.getItemIndex(cart, shopId);
|
||||||
if (itemIndex < 0) {
|
if (itemIndex < 0) {
|
||||||
throw new NotFoundException(CartMessage.ITEM_NOT_FOUND);
|
throw new NotFoundException(CartMessage.ITEM_NOT_FOUND);
|
||||||
}
|
}
|
||||||
@@ -100,7 +102,7 @@ export class CartItemService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const product = await this.validationService.getFoodOrFail(foodId);
|
const product = await this.productService.findOrFail(shopId);
|
||||||
cart.items[itemIndex] = this.buildCartItemFromFood(product, newQuantity, existingItem);
|
cart.items[itemIndex] = this.buildCartItemFromFood(product, newQuantity, existingItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,40 @@
|
|||||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
import { EntityManager } from '@mikro-orm/postgresql';
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
import { Product } from 'src/modules/products/entities/product.entity';
|
import { Product } from 'src/modules/products/entities/product.entity';
|
||||||
import { Shop } from 'src/modules/shops/entities/shop.entity';
|
|
||||||
import { UserAddress } from 'src/modules/users/entities/user-address.entity';
|
import { UserAddress } from 'src/modules/users/entities/user-address.entity';
|
||||||
import { User } from 'src/modules/users/entities/user.entity';
|
|
||||||
import { PaymentMethod } from 'src/modules/payments/entities/payment-method.entity';
|
import { PaymentMethod } from 'src/modules/payments/entities/payment-method.entity';
|
||||||
import { Delivery } from 'src/modules/delivery/entities/delivery.entity';
|
import { Delivery } from 'src/modules/delivery/entities/delivery.entity';
|
||||||
import { DeliveryMethodEnum } from 'src/modules/delivery/interface/delivery';
|
import { DeliveryMethodEnum } from 'src/modules/delivery/interface/delivery';
|
||||||
import { PointTransaction } from 'src/modules/users/entities/point-transaction.entity';
|
|
||||||
import { Order } from 'src/modules/orders/entities/order.entity';
|
import { Order } from 'src/modules/orders/entities/order.entity';
|
||||||
import { OrderStatus } from 'src/modules/orders/interface/order.interface';
|
import { OrderStatus } from 'src/modules/orders/interface/order.interface';
|
||||||
import { Cart } from '../interfaces/cart.interface';
|
import { Cart } from '../interfaces/cart.interface';
|
||||||
import { GeographicUtils } from '../utils/geographic.utils';
|
import { GeographicUtils } from '../utils/geographic.utils';
|
||||||
import { CartMessage } from 'src/common/enums/message.enum';
|
import { CartMessage } from 'src/common/enums/message.enum';
|
||||||
import { WalletTransactionRepository } from 'src/modules/users/repositories/wallet-transaction.repository';
|
import { WalletTransactionRepository } from 'src/modules/users/repositories/wallet-transaction.repository';
|
||||||
|
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()
|
@Injectable()
|
||||||
export class CartValidationService {
|
export class CartValidationService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
private readonly walletTransactionRepository: WalletTransactionRepository,
|
private readonly walletTransactionRepository: WalletTransactionRepository,
|
||||||
|
private readonly productService: ProductService,
|
||||||
|
private readonly shopService: ShopService,
|
||||||
|
private readonly deliveryService: DeliveryService,
|
||||||
|
private readonly paymentMethodService: PaymentMethodService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate product exists and belongs to shop
|
* Validate product exists and belongs to shop
|
||||||
*/
|
*/
|
||||||
async validateAndGetFood(foodId: string, restaurantId: string, quantity: number): Promise<Product> {
|
async validateAndGetFood(productId: string, shopId: string, quantity: number): Promise<Product> {
|
||||||
const product = await this.em.findOne(Product, { id: foodId }, { populate: ['shop'] });
|
const product = await this.productService.findOrFail(productId)
|
||||||
if (!product) {
|
|
||||||
throw new NotFoundException(CartMessage.PRODUCT_NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (product.shop.id !== restaurantId) {
|
if (product.shop.id !== shopId) {
|
||||||
throw new BadRequestException(CartMessage.PRODUCT_NOT_BELONGS_TO_SHOP);
|
throw new BadRequestException(CartMessage.PRODUCT_NOT_BELONGS_TO_SHOP);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,50 +42,6 @@ export class CartValidationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user or throw if not found
|
|
||||||
*/
|
|
||||||
async getUserOrFail(userId: string): Promise<User> {
|
|
||||||
const user = await this.em.findOne(User, { id: userId });
|
|
||||||
if (!user) {
|
|
||||||
throw new NotFoundException(CartMessage.USER_NOT_FOUND);
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user address or throw if not found
|
|
||||||
*/
|
|
||||||
async getUserAddressOrFail(addressId: string): Promise<UserAddress> {
|
|
||||||
const address = await this.em.findOne(UserAddress, { id: addressId }, { populate: ['user'] });
|
|
||||||
if (!address) {
|
|
||||||
throw new NotFoundException(CartMessage.ADDRESS_NOT_FOUND);
|
|
||||||
}
|
|
||||||
return address;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get product or throw if not found
|
|
||||||
*/
|
|
||||||
async getFoodOrFail(foodId: string): Promise<Product> {
|
|
||||||
const product = await this.em.findOne(Product, { id: foodId });
|
|
||||||
if (!product) {
|
|
||||||
throw new NotFoundException(CartMessage.PRODUCT_NOT_FOUND);
|
|
||||||
}
|
|
||||||
return product;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get shop or throw if not found
|
|
||||||
*/
|
|
||||||
async getRestaurantOrFail(restaurantId: string): Promise<Shop> {
|
|
||||||
const shop = await this.em.findOne(Shop, { id: restaurantId });
|
|
||||||
if (!shop) {
|
|
||||||
throw new NotFoundException(CartMessage.SHOP_NOT_FOUND);
|
|
||||||
}
|
|
||||||
return shop;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate address belongs to user
|
* Validate address belongs to user
|
||||||
*/
|
*/
|
||||||
@@ -96,7 +55,7 @@ export class CartValidationService {
|
|||||||
* Assert address is inside shop service area
|
* Assert address is inside shop service area
|
||||||
*/
|
*/
|
||||||
async assertAddressInsideServiceArea(
|
async assertAddressInsideServiceArea(
|
||||||
restaurantId: string,
|
shopId: string,
|
||||||
latitude?: number | null,
|
latitude?: number | null,
|
||||||
longitude?: number | null,
|
longitude?: number | null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -104,7 +63,7 @@ export class CartValidationService {
|
|||||||
throw new BadRequestException(CartMessage.ADDRESS_NO_COORDINATES);
|
throw new BadRequestException(CartMessage.ADDRESS_NO_COORDINATES);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shop = await this.getRestaurantOrFail(restaurantId);
|
const shop = await this.shopService.findOrFail(shopId);
|
||||||
|
|
||||||
const serviceArea = shop.serviceArea;
|
const serviceArea = shop.serviceArea;
|
||||||
// If no service area is defined, assume service is available everywhere
|
// If no service area is defined, assume service is available everywhere
|
||||||
@@ -130,34 +89,13 @@ export class CartValidationService {
|
|||||||
/**
|
/**
|
||||||
* Get delivery method for shop or throw if not found
|
* Get delivery method for shop or throw if not found
|
||||||
*/
|
*/
|
||||||
async getDeliveryMethodForRestaurantOrFail(
|
|
||||||
restaurantId: string,
|
|
||||||
deliveryMethodId: string,
|
|
||||||
): Promise<Delivery> {
|
|
||||||
const deliveryMethod = await this.em.findOne(Delivery, {
|
|
||||||
id: deliveryMethodId,
|
|
||||||
shop: { id: restaurantId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!deliveryMethod) {
|
|
||||||
throw new NotFoundException(CartMessage.DELIVERY_METHOD_NOT_FOUND_FOR_SHOP);
|
|
||||||
}
|
|
||||||
|
|
||||||
return deliveryMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get enabled delivery method or throw if not found or disabled
|
* Get enabled delivery method or throw if not found or disabled
|
||||||
*/
|
*/
|
||||||
async getEnabledDeliveryMethodOrFail(restaurantId: string, deliveryMethodId: string): Promise<Delivery> {
|
async getEnabledDeliveryMethodOrFail(shopId: string, deliveryMethodId: string): Promise<Delivery> {
|
||||||
const deliveryMethod = await this.em.findOne(
|
const deliveryMethod = await this.deliveryService.findOrFail(shopId, deliveryMethodId)
|
||||||
Delivery,
|
|
||||||
{ id: deliveryMethodId, shop: { id: restaurantId } },
|
|
||||||
{ populate: ['shop'] },
|
|
||||||
);
|
|
||||||
if (!deliveryMethod) {
|
|
||||||
throw new NotFoundException(CartMessage.DELIVERY_METHOD_NOT_FOUND);
|
|
||||||
}
|
|
||||||
if (!deliveryMethod.enabled) {
|
if (!deliveryMethod.enabled) {
|
||||||
throw new BadRequestException(CartMessage.DELIVERY_METHOD_NOT_ENABLED);
|
throw new BadRequestException(CartMessage.DELIVERY_METHOD_NOT_ENABLED);
|
||||||
}
|
}
|
||||||
@@ -186,14 +124,10 @@ export class CartValidationService {
|
|||||||
/**
|
/**
|
||||||
* Get enabled payment method or throw if not found or disabled
|
* Get enabled payment method or throw if not found or disabled
|
||||||
*/
|
*/
|
||||||
async getEnabledPaymentMethodOrFail(restaurantId: string, paymentMethodId: string): Promise<PaymentMethod> {
|
async getEnabledPaymentMethodOrFail(shopId: string, paymentMethodId: string): Promise<PaymentMethod> {
|
||||||
const paymentMethod = await this.em.findOne(
|
const paymentMethod = await this.paymentMethodService.findOneOrFail(paymentMethodId)
|
||||||
PaymentMethod,
|
if (paymentMethod.shop.id !== shopId) {
|
||||||
{ id: paymentMethodId, shop: { id: restaurantId } },
|
throw new BadRequestException('CartMessage.PAYMENT_METHOD_NOT_BELONGS_TO_SHOP');
|
||||||
{ populate: ['shop'] },
|
|
||||||
);
|
|
||||||
if (!paymentMethod) {
|
|
||||||
throw new NotFoundException(CartMessage.PAYMENT_METHOD_NOT_FOUND);
|
|
||||||
}
|
}
|
||||||
if (!paymentMethod.enabled) {
|
if (!paymentMethod.enabled) {
|
||||||
throw new BadRequestException(CartMessage.PAYMENT_METHOD_NOT_ENABLED);
|
throw new BadRequestException(CartMessage.PAYMENT_METHOD_NOT_ENABLED);
|
||||||
@@ -204,8 +138,8 @@ export class CartValidationService {
|
|||||||
/**
|
/**
|
||||||
* Assert wallet has enough balance
|
* Assert wallet has enough balance
|
||||||
*/
|
*/
|
||||||
async assertWalletHasEnoughBalance(userId: string, restaurantId: string, amount: number): Promise<void> {
|
async assertWalletHasEnoughBalance(userId: string, shopId: string, amount: number): Promise<void> {
|
||||||
const balance = await this.walletTransactionRepository.getCurrentWalletBalance(userId, restaurantId);
|
const balance = await this.walletTransactionRepository.getCurrentWalletBalance(userId, shopId);
|
||||||
|
|
||||||
if (balance < amount) {
|
if (balance < amount) {
|
||||||
throw new BadRequestException(CartMessage.WALLET_INSUFFICIENT);
|
throw new BadRequestException(CartMessage.WALLET_INSUFFICIENT);
|
||||||
@@ -247,7 +181,7 @@ export class CartValidationService {
|
|||||||
const foodMap = await this.getFoodsInCartWithCategories(cart);
|
const foodMap = await this.getFoodsInCartWithCategories(cart);
|
||||||
|
|
||||||
for (const item of cart.items) {
|
for (const item of cart.items) {
|
||||||
const product = foodMap.get(item.foodId);
|
const product = foodMap.get(item.productId);
|
||||||
if (!product) continue;
|
if (!product) continue;
|
||||||
|
|
||||||
const matchesCategory = hasCategoryRestriction && product.category && categoryRestriction.includes(product.category.id);
|
const matchesCategory = hasCategoryRestriction && product.category && categoryRestriction.includes(product.category.id);
|
||||||
@@ -262,19 +196,14 @@ export class CartValidationService {
|
|||||||
* Get products in cart with categories
|
* Get products in cart with categories
|
||||||
*/
|
*/
|
||||||
async getFoodsInCartWithCategories(cart: Cart): Promise<Map<string, Product>> {
|
async getFoodsInCartWithCategories(cart: Cart): Promise<Map<string, Product>> {
|
||||||
const foodIds = cart.items.map(item => item.foodId);
|
const productIds = cart.items.map(item => item.productId);
|
||||||
if (foodIds.length === 0) return new Map();
|
if (productIds.length === 0) return new Map();
|
||||||
|
|
||||||
const products = await this.em.find(Product, { id: { $in: foodIds } }, { populate: ['category'] });
|
const products = await this.em.find(Product, { id: { $in: productIds } }, { populate: ['category'] });
|
||||||
return new Map(products.map(f => [f.id, f]));
|
return new Map(products.map(f => [f.id, f]));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate stock for a product
|
|
||||||
*/
|
|
||||||
validateStock(product: Product, quantity: number): void {
|
|
||||||
// TODO: Implement stock validation logic
|
|
||||||
// For now, assume unlimited stock
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ 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 { CartMessage } from 'src/common/enums/message.enum';
|
import { CartMessage } from 'src/common/enums/message.enum';
|
||||||
|
import { UserService } from 'src/modules/users/providers/user.service';
|
||||||
|
import { ShopService } from 'src/modules/shops/providers/shops.service';
|
||||||
|
import { DeliveryService } from 'src/modules/delivery/providers/delivery.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CartService {
|
export class CartService {
|
||||||
@@ -21,6 +24,9 @@ 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,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly shopService: ShopService,
|
||||||
|
private readonly deliveryService: DeliveryService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,7 +60,7 @@ export class CartService {
|
|||||||
throw new BadRequestException(CartMessage.ADDRESS_REQUIRED);
|
throw new BadRequestException(CartMessage.ADDRESS_REQUIRED);
|
||||||
}
|
}
|
||||||
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
|
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
|
||||||
const address = await this.validationService.getUserAddressOrFail(addressId);
|
const address = await this.userService.getUserAddressOrFail(addressId);
|
||||||
this.validationService.validateAddressOwnership(address, userId);
|
this.validationService.validateAddressOwnership(address, userId);
|
||||||
|
|
||||||
// ensure address is within shop service area
|
// ensure address is within shop service area
|
||||||
@@ -111,7 +117,7 @@ export class CartService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find shop
|
// Find shop
|
||||||
const shop = await this.validationService.getRestaurantOrFail(restaurantId);
|
const shop = await this.shopService.findOrFail(restaurantId);
|
||||||
|
|
||||||
// Create new cart
|
// Create new cart
|
||||||
const now = this.nowIso();
|
const now = this.nowIso();
|
||||||
@@ -194,7 +200,7 @@ export class CartService {
|
|||||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||||
|
|
||||||
const orderAmount = cart.subTotal - cart.itemsDiscount;
|
const orderAmount = cart.subTotal - cart.itemsDiscount;
|
||||||
const user = await this.validationService.getUserOrFail(userId);
|
const user = await this.userService.findOneOrFail(userId);
|
||||||
// check if coupon is valid and belong to the shop
|
// check if coupon is valid and belong to the shop
|
||||||
const { valid, coupon, message } = await this.couponService.validateCoupon(
|
const { valid, coupon, message } = await this.couponService.validateCoupon(
|
||||||
applyCouponDto.code,
|
applyCouponDto.code,
|
||||||
@@ -252,10 +258,8 @@ export class CartService {
|
|||||||
cart,
|
cart,
|
||||||
'Delivery method must be set before setting address',
|
'Delivery method must be set before setting address',
|
||||||
);
|
);
|
||||||
const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
|
const deliveryMethod = await this.deliveryService.findOrFail(restaurantId, deliveryMethodId);
|
||||||
restaurantId,
|
|
||||||
deliveryMethodId,
|
|
||||||
);
|
|
||||||
this.validationService.assertDeliveryMethod(
|
this.validationService.assertDeliveryMethod(
|
||||||
deliveryMethod.method,
|
deliveryMethod.method,
|
||||||
DeliveryMethodEnum.DeliveryCourier,
|
DeliveryMethodEnum.DeliveryCourier,
|
||||||
@@ -263,7 +267,7 @@ export class CartService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Find and validate address belongs to user
|
// Find and validate address belongs to user
|
||||||
const address = await this.validationService.getUserAddressOrFail(addressId);
|
const address = await this.userService.getUserAddressOrFail(addressId);
|
||||||
|
|
||||||
// Verify address belongs to the user
|
// Verify address belongs to the user
|
||||||
this.validationService.validateAddressOwnership(address, userId);
|
this.validationService.validateAddressOwnership(address, userId);
|
||||||
@@ -354,7 +358,6 @@ export class CartService {
|
|||||||
*/
|
*/
|
||||||
private clearFieldsIncompatibleWithDeliveryMethod(cart: Cart, method: DeliveryMethodEnum): void {
|
private clearFieldsIncompatibleWithDeliveryMethod(cart: Cart, method: DeliveryMethodEnum): void {
|
||||||
|
|
||||||
|
|
||||||
if (method === DeliveryMethodEnum.CustomerPickup) {
|
if (method === DeliveryMethodEnum.CustomerPickup) {
|
||||||
cart.userAddress = null;
|
cart.userAddress = null;
|
||||||
cart.tableNumber = undefined;
|
cart.tableNumber = undefined;
|
||||||
@@ -366,7 +369,6 @@ export class CartService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { DeliveryModule } from '../delivery/delivery.module';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
MikroOrmModule.forFeature([Order, OrderItem, User, Shop, Product, Variant, UserAddress, PaymentMethod]),
|
MikroOrmModule.forFeature([Order, OrderItem, User, Shop, Product, Variant, UserAddress, PaymentMethod]),
|
||||||
CartModule,
|
forwardRef(() => CartModule),
|
||||||
UtilsModule,
|
UtilsModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
forwardRef(() => PaymentsModule),
|
forwardRef(() => PaymentsModule),
|
||||||
|
|||||||
@@ -366,7 +366,7 @@ export class OrdersService {
|
|||||||
const orderItemsData: OrderItemData[] = [];
|
const orderItemsData: OrderItemData[] = [];
|
||||||
|
|
||||||
for (const cartItem of cart.items) {
|
for (const cartItem of cart.items) {
|
||||||
const variant = await this.em.findOne(Variant, { id: cartItem.foodId }, { populate: ['product', 'product.shop'] });
|
const variant = await this.em.findOne(Variant, { id: cartItem.productId }, { populate: ['product', 'product.shop'] });
|
||||||
if (!variant) throw new NotFoundException(OrderMessage.PRODUCT_NOT_FOUND);
|
if (!variant) throw new NotFoundException(OrderMessage.PRODUCT_NOT_FOUND);
|
||||||
|
|
||||||
if (variant.product.shop.id !== shopId) {
|
if (variant.product.shop.id !== shopId) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { PaymentMessage, OrderMessage } from 'src/common/enums/message.enum';
|
|||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { onlinePaymentSucceedEvent, paymentSucceedEvent } from '../events/payment.events';
|
import { onlinePaymentSucceedEvent, paymentSucceedEvent } from '../events/payment.events';
|
||||||
import { WalletTransactionReason, WalletTransactionType } from 'src/modules/users/interface/wallet';
|
import { WalletTransactionReason, WalletTransactionType } from 'src/modules/users/interface/wallet';
|
||||||
|
import { PaymentRepository } from '../repositories/payment.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PaymentsService {
|
export class PaymentsService {
|
||||||
@@ -22,6 +23,7 @@ export class PaymentsService {
|
|||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
private readonly gatewayManager: GatewayManager,
|
private readonly gatewayManager: GatewayManager,
|
||||||
private readonly eventEmitter: EventEmitter2,
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
private readonly paymentRepository: PaymentRepository,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async payOrder(orderId: string): Promise<{ paymentUrl: string | null }> {
|
async payOrder(orderId: string): Promise<{ paymentUrl: string | null }> {
|
||||||
@@ -141,7 +143,7 @@ export class PaymentsService {
|
|||||||
payment.paidAt = new Date();
|
payment.paidAt = new Date();
|
||||||
order.status = OrderStatus.PAID;
|
order.status = OrderStatus.PAID;
|
||||||
|
|
||||||
em.persist([ payment, order, newWalletTransaction]);
|
em.persist([payment, order, newWalletTransaction]);
|
||||||
await em.flush();
|
await em.flush();
|
||||||
});
|
});
|
||||||
this.eventEmitter.emit(
|
this.eventEmitter.emit(
|
||||||
@@ -278,6 +280,13 @@ export class PaymentsService {
|
|||||||
payment.order.status = OrderStatus.CANCELED;
|
payment.order.status = OrderStatus.CANCELED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findOneOrFail(paymentId: string): Promise<Payment> {
|
||||||
|
const payment=await this.paymentRepository.findOne({ id: paymentId });
|
||||||
|
if (!payment) {
|
||||||
|
throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
|
|
||||||
private async getOrCreateLatestPendingPayment(
|
private async getOrCreateLatestPendingPayment(
|
||||||
orderId: string,
|
orderId: string,
|
||||||
|
|||||||
@@ -25,6 +25,6 @@ import { Favorite } from './entities/favorite.entity';
|
|||||||
],
|
],
|
||||||
controllers: [FoodController, CategoryController],
|
controllers: [FoodController, CategoryController],
|
||||||
providers: [ProductService, CategoryService, ProductRepository, CategoryRepository],
|
providers: [ProductService, CategoryService, ProductRepository, CategoryRepository],
|
||||||
exports: [ProductRepository, CategoryRepository],
|
exports: [ProductRepository, CategoryRepository, ProductService],
|
||||||
})
|
})
|
||||||
export class ProductModule {}
|
export class ProductModule {}
|
||||||
|
|||||||
@@ -392,4 +392,12 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserAddressOrFail(addressId: string): Promise<UserAddress> {
|
||||||
|
const address = await this.em.findOne(UserAddress, { id: addressId }, { populate: ['user'] });
|
||||||
|
if (!address) {
|
||||||
|
throw new NotFoundException("User address not found.");
|
||||||
|
}
|
||||||
|
return address;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||||
|
import { UserAddress } from '../entities/user-address.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AddressRepository extends EntityRepository<UserAddress> {
|
||||||
|
constructor(readonly em: EntityManager) {
|
||||||
|
super(em, UserAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,21 +6,24 @@ import { User } from './entities/user.entity';
|
|||||||
import { UserAddress } from './entities/user-address.entity';
|
import { UserAddress } from './entities/user-address.entity';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { UserRepository } from './repositories/user.repository';
|
import { UserRepository } from './repositories/user.repository';
|
||||||
import { WalletTransactionRepository } from './repositories/wallet-transaction.repository';
|
import { WalletTransactionRepository } from './repositories/wallet-transaction.repository';
|
||||||
import { WalletService } from './providers/wallet.service';
|
import { WalletService } from './providers/wallet.service';
|
||||||
import { WalletTransaction } from './entities/wallet-transaction.entity';
|
import { WalletTransaction } from './entities/wallet-transaction.entity';
|
||||||
import { PointTransaction } from './entities/point-transaction.entity';
|
import { PointTransaction } from './entities/point-transaction.entity';
|
||||||
import { PointTransactionRepository } from './repositories/point-transaction.repository';
|
import { PointTransactionRepository } from './repositories/point-transaction.repository';
|
||||||
import { ShopsModule } from '../shops/shops.module';
|
import { ShopsModule } from '../shops/shops.module';
|
||||||
|
import { AddressRepository } from './repositories/address.repository';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [UserService, WalletService, UserRepository, WalletTransactionRepository, PointTransactionRepository],
|
providers: [UserService, WalletService, UserRepository,
|
||||||
|
WalletTransactionRepository, PointTransactionRepository, AddressRepository],
|
||||||
controllers: [UsersController],
|
controllers: [UsersController],
|
||||||
imports: [
|
imports: [
|
||||||
MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction]),
|
MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction]),
|
||||||
JwtModule,
|
JwtModule,
|
||||||
ShopsModule
|
ShopsModule
|
||||||
],
|
],
|
||||||
exports: [UserService, WalletService, UserRepository, WalletTransactionRepository, PointTransactionRepository],
|
exports: [UserService, WalletService, UserRepository,
|
||||||
|
WalletTransactionRepository, PointTransactionRepository, AddressRepository],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule { }
|
||||||
|
|||||||
Reference in New Issue
Block a user