diff --git a/src/modules/cart/providers/cart.service.ts b/src/modules/cart/providers/cart.service.ts index 24a0612..9ef7592 100644 --- a/src/modules/cart/providers/cart.service.ts +++ b/src/modules/cart/providers/cart.service.ts @@ -33,7 +33,7 @@ export class CartService { private readonly em: EntityManager, private readonly cacheService: CacheService, private readonly couponService: CouponService, - ) { } + ) {} async setAllCartParams(userId: string, restaurantId: string, params: SetAllCartParmsDto): Promise { const { deliveryMethodId, paymentMethodId, addressId, carAddress, description } = params; @@ -56,13 +56,20 @@ export class CartService { throw new BadRequestException('Delivery method must be set before setting address'); } const deliveryMethod = await this.getDeliveryMethodForRestaurantOrFail(restaurantId, effectiveDeliveryMethodId); - this.assertDeliveryMethod(deliveryMethod.method, DeliveryMethodEnum.DeliveryCourier, 'Delivery method must be DeliveryCourier'); + this.assertDeliveryMethod( + deliveryMethod.method, + DeliveryMethodEnum.DeliveryCourier, + 'Delivery method must be DeliveryCourier', + ); const address = await this.getUserAddressOrFail(addressId); if (address.user.id !== userId) { throw new BadRequestException('Address does not belong to the current user'); } + // ensure address is within restaurant service area + await this.assertAddressInsideServiceArea(restaurantId, address.latitude, address.longitude); + // ensure cart fields are compatible and set address this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier); cart.userAddress = { @@ -84,7 +91,11 @@ export class CartService { throw new BadRequestException('Delivery method must be set before setting car delivery'); } const deliveryMethod = await this.getDeliveryMethodForRestaurantOrFail(restaurantId, effectiveDeliveryMethodId); - this.assertDeliveryMethod(deliveryMethod.method, DeliveryMethodEnum.DeliveryCar, 'Delivery method must be DeliveryCar'); + this.assertDeliveryMethod( + deliveryMethod.method, + DeliveryMethodEnum.DeliveryCar, + 'Delivery method must be DeliveryCar', + ); const user = await this.getUserOrFail(userId); @@ -270,12 +281,13 @@ export class CartService { async setAddress(userId: string, restaurantId: string, setAddressDto: SetAddressDto): Promise { const cart = await this.findOneOrFail(userId, restaurantId); - const deliveryMethodId = this.requireDeliveryMethodId( - cart, - 'Delivery method must be set before setting address', - ); + const deliveryMethodId = this.requireDeliveryMethodId(cart, 'Delivery method must be set before setting address'); const deliveryMethod = await this.getDeliveryMethodForRestaurantOrFail(restaurantId, deliveryMethodId); - this.assertDeliveryMethod(deliveryMethod.method, DeliveryMethodEnum.DeliveryCourier, 'Delivery method must be DeliveryCourier'); + this.assertDeliveryMethod( + deliveryMethod.method, + DeliveryMethodEnum.DeliveryCourier, + 'Delivery method must be DeliveryCourier', + ); // Find and validate address belongs to user const address = await this.getUserAddressOrFail(setAddressDto.addressId); @@ -285,6 +297,9 @@ export class CartService { throw new BadRequestException('Address does not belong to the current user'); } + // ensure address is within restaurant service area + await this.assertAddressInsideServiceArea(restaurantId, address.latitude, address.longitude); + this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier); cart.userAddress = { address: address.address, @@ -299,6 +314,57 @@ export class CartService { return this.saveTouchedCart(cart); } + private async assertAddressInsideServiceArea( + restaurantId: string, + latitude?: number | null, + longitude?: number | null, + ): Promise { + if (latitude === undefined || latitude === null || longitude === undefined || longitude === null) { + throw new BadRequestException('Address does not have coordinates'); + } + + const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); + if (!restaurant) { + throw new NotFoundException('Restaurant not found'); + } + + const serviceArea = restaurant.serviceArea; + // If no service area is defined, assume service is available everywhere + if (!serviceArea || !serviceArea.coordinates || !Array.isArray(serviceArea.coordinates)) return; + + // GeoJSON coordinates: [ [ [lng, lat], ... ] ] -> take first ring + const rings = serviceArea.coordinates; + if (!rings || rings.length === 0 || !Array.isArray(rings[0])) return; + + const ring = rings[0]; + const point: [number, number] = [Number(longitude), Number(latitude)]; + + // Ensure polygon has the correct tuple typing ([lng, lat] pairs) + const polygon: [number, number][] = ring.map( + coord => [Number(coord[0] ?? 0), Number(coord[1] ?? 0)] as [number, number], + ); + if (!this.isPointInPolygon(point, polygon)) { + throw new BadRequestException('Address is outside the restaurant service area'); + } + } + + // Ray-casting algorithm for point in polygon. Expects polygon as array of [lng, lat] + private isPointInPolygon(point: [number, number], polygon: [number, number][]): boolean { + const x = point[0]; + const y = point[1]; + let inside = false; + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i][0], + yi = polygon[i][1]; + const xj = polygon[j][0], + yj = polygon[j][1]; + + const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi + Number.EPSILON) + xi; + if (intersect) inside = !inside; + } + return inside; + } + /** * Set payment method for cart */ @@ -317,11 +383,7 @@ export class CartService { return this.recalculateAndSaveCart(cart); } - private async assertWalletHasEnoughBalance( - userId: string, - restaurantId: string, - amount: number, - ): Promise { + private async assertWalletHasEnoughBalance(userId: string, restaurantId: string, amount: number): Promise { const wallet = await this.em.findOne(UserWallet, { user: { id: userId }, restaurant: { id: restaurantId }, @@ -346,7 +408,10 @@ export class CartService { ): Promise { const cart = await this.findOneOrFail(userId, restaurantId); - const deliveryMethod = await this.getEnabledDeliveryMethodOrFail(restaurantId, setDeliveryMethodDto.deliveryMethodId); + const deliveryMethod = await this.getEnabledDeliveryMethodOrFail( + restaurantId, + setDeliveryMethodDto.deliveryMethodId, + ); cart.deliveryMethodId = setDeliveryMethodDto.deliveryMethodId; this.clearFieldsIncompatibleWithDeliveryMethod(cart, deliveryMethod.method); @@ -393,7 +458,11 @@ export class CartService { 'Delivery method must be set before setting car delivery', ); const deliveryMethod = await this.getDeliveryMethodForRestaurantOrFail(restaurantId, deliveryMethodId); - this.assertDeliveryMethod(deliveryMethod.method, DeliveryMethodEnum.DeliveryCar, 'Delivery method must be DeliveryCar'); + this.assertDeliveryMethod( + deliveryMethod.method, + DeliveryMethodEnum.DeliveryCar, + 'Delivery method must be DeliveryCar', + ); const user = await this.getUserOrFail(userId); this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar); @@ -689,24 +758,25 @@ export class CartService { return cart.deliveryMethodId; } - private async getDeliveryMethodForRestaurantOrFail(restaurantId: string, deliveryMethodId: string): Promise { + private async getDeliveryMethodForRestaurantOrFail( + restaurantId: string, + deliveryMethodId: string, + ): Promise { const deliveryMethod = await this.em.findOne(Delivery, { id: deliveryMethodId, restaurant: { id: restaurantId }, }); if (!deliveryMethod) { - throw new NotFoundException(`Delivery method with ID ${deliveryMethodId} not found for restaurant ${restaurantId}`); + throw new NotFoundException( + `Delivery method with ID ${deliveryMethodId} not found for restaurant ${restaurantId}`, + ); } return deliveryMethod; } - private assertDeliveryMethod( - actual: DeliveryMethodEnum, - expected: DeliveryMethodEnum, - message: string, - ): void { + private assertDeliveryMethod(actual: DeliveryMethodEnum, expected: DeliveryMethodEnum, message: string): void { if (actual !== expected) { throw new BadRequestException(message); } @@ -734,7 +804,9 @@ export class CartService { { populate: ['restaurant'] }, ); if (!deliveryMethod) { - throw new NotFoundException(`Delivery method with ID ${deliveryMethodId} not found for restaurant ${restaurantId}`); + throw new NotFoundException( + `Delivery method with ID ${deliveryMethodId} not found for restaurant ${restaurantId}`, + ); } if (!deliveryMethod.enabled) { throw new BadRequestException('Delivery method is not enabled for this restaurant'); diff --git a/src/modules/payments/listeners/payment.listeners.ts b/src/modules/payments/listeners/payment.listeners.ts index 2be06d0..a6faf77 100644 --- a/src/modules/payments/listeners/payment.listeners.ts +++ b/src/modules/payments/listeners/payment.listeners.ts @@ -24,7 +24,7 @@ export class OrderListeners { async handleOrderCreated(event: paymentSucceedEvent) { try { this.logger.log( - `Order created event received: ${event.orderId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`, + `Payment paid event received: ${event.orderId} for restaurant: ${event.restaurantId} and order number: ${event.orderNumber}`, ); // get admnin os restuaraant that have order permissuins const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.MANAGE_ORDERS); @@ -36,7 +36,7 @@ export class OrderListeners { restaurantId: event.restaurantId, message: { title: NotifTitleEnum.ORDER_CREATED, - content: `Order ${event.orderNumber} has been created successfully`, + content: `payment order #${event.orderNumber} has been paid successfully`, sms: { templateId: this.orderCreatedSmsTemplateId, parameters: { @@ -44,12 +44,12 @@ export class OrderListeners { }, }, pushNotif: { - title: `Order ${event.orderNumber} has been created successfully`, - content: `Order ${event.orderNumber} has been created successfully`, - icon: `Order ${event.orderNumber} has been created successfully`, + title: `Payment Order ${event.orderNumber} has been paid successfully`, + content: `Payment Order ${event.orderNumber} has been paid successfully`, + icon: `Payment Order ${event.orderNumber} has been paid successfully`, action: { - type: `Order ${event.orderNumber} has been created successfully`, - url: `Order ${event.orderNumber} has been created successfully`, + type: `Payment Order ${event.orderNumber} has been paid successfully`, + url: `Payment Order ${event.orderNumber} has been paid successfully`, }, }, },