check address is inside service area

This commit is contained in:
2025-12-20 10:52:28 +03:30
parent 18706e03e5
commit 987d7ec346
2 changed files with 102 additions and 30 deletions
+94 -22
View File
@@ -56,13 +56,20 @@ export class CartService {
throw new BadRequestException('Delivery method must be set before setting address'); throw new BadRequestException('Delivery method must be set before setting address');
} }
const deliveryMethod = await this.getDeliveryMethodForRestaurantOrFail(restaurantId, effectiveDeliveryMethodId); 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); const address = await this.getUserAddressOrFail(addressId);
if (address.user.id !== userId) { if (address.user.id !== userId) {
throw new BadRequestException('Address does not belong to the current user'); 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 // ensure cart fields are compatible and set address
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier); this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
cart.userAddress = { cart.userAddress = {
@@ -84,7 +91,11 @@ export class CartService {
throw new BadRequestException('Delivery method must be set before setting car delivery'); throw new BadRequestException('Delivery method must be set before setting car delivery');
} }
const deliveryMethod = await this.getDeliveryMethodForRestaurantOrFail(restaurantId, effectiveDeliveryMethodId); 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); const user = await this.getUserOrFail(userId);
@@ -270,12 +281,13 @@ export class CartService {
async setAddress(userId: string, restaurantId: string, setAddressDto: SetAddressDto): Promise<Cart> { async setAddress(userId: string, restaurantId: string, setAddressDto: SetAddressDto): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId); const cart = await this.findOneOrFail(userId, restaurantId);
const deliveryMethodId = this.requireDeliveryMethodId( const deliveryMethodId = this.requireDeliveryMethodId(cart, 'Delivery method must be set before setting address');
cart,
'Delivery method must be set before setting address',
);
const deliveryMethod = await this.getDeliveryMethodForRestaurantOrFail(restaurantId, deliveryMethodId); 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 // Find and validate address belongs to user
const address = await this.getUserAddressOrFail(setAddressDto.addressId); 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'); 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); this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
cart.userAddress = { cart.userAddress = {
address: address.address, address: address.address,
@@ -299,6 +314,57 @@ export class CartService {
return this.saveTouchedCart(cart); return this.saveTouchedCart(cart);
} }
private async assertAddressInsideServiceArea(
restaurantId: string,
latitude?: number | null,
longitude?: number | null,
): Promise<void> {
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 * Set payment method for cart
*/ */
@@ -317,11 +383,7 @@ export class CartService {
return this.recalculateAndSaveCart(cart); return this.recalculateAndSaveCart(cart);
} }
private async assertWalletHasEnoughBalance( private async assertWalletHasEnoughBalance(userId: string, restaurantId: string, amount: number): Promise<void> {
userId: string,
restaurantId: string,
amount: number,
): Promise<void> {
const wallet = await this.em.findOne(UserWallet, { const wallet = await this.em.findOne(UserWallet, {
user: { id: userId }, user: { id: userId },
restaurant: { id: restaurantId }, restaurant: { id: restaurantId },
@@ -346,7 +408,10 @@ export class CartService {
): Promise<Cart> { ): Promise<Cart> {
const cart = await this.findOneOrFail(userId, restaurantId); 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; cart.deliveryMethodId = setDeliveryMethodDto.deliveryMethodId;
this.clearFieldsIncompatibleWithDeliveryMethod(cart, deliveryMethod.method); this.clearFieldsIncompatibleWithDeliveryMethod(cart, deliveryMethod.method);
@@ -393,7 +458,11 @@ export class CartService {
'Delivery method must be set before setting car delivery', 'Delivery method must be set before setting car delivery',
); );
const deliveryMethod = await this.getDeliveryMethodForRestaurantOrFail(restaurantId, deliveryMethodId); 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); const user = await this.getUserOrFail(userId);
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar); this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar);
@@ -689,24 +758,25 @@ export class CartService {
return cart.deliveryMethodId; return cart.deliveryMethodId;
} }
private async getDeliveryMethodForRestaurantOrFail(restaurantId: string, deliveryMethodId: string): Promise<Delivery> { private async getDeliveryMethodForRestaurantOrFail(
restaurantId: string,
deliveryMethodId: string,
): Promise<Delivery> {
const deliveryMethod = await this.em.findOne(Delivery, { const deliveryMethod = await this.em.findOne(Delivery, {
id: deliveryMethodId, id: deliveryMethodId,
restaurant: { id: restaurantId }, restaurant: { id: restaurantId },
}); });
if (!deliveryMethod) { 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; return deliveryMethod;
} }
private assertDeliveryMethod( private assertDeliveryMethod(actual: DeliveryMethodEnum, expected: DeliveryMethodEnum, message: string): void {
actual: DeliveryMethodEnum,
expected: DeliveryMethodEnum,
message: string,
): void {
if (actual !== expected) { if (actual !== expected) {
throw new BadRequestException(message); throw new BadRequestException(message);
} }
@@ -734,7 +804,9 @@ export class CartService {
{ populate: ['restaurant'] }, { populate: ['restaurant'] },
); );
if (!deliveryMethod) { 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) { if (!deliveryMethod.enabled) {
throw new BadRequestException('Delivery method is not enabled for this restaurant'); throw new BadRequestException('Delivery method is not enabled for this restaurant');
@@ -24,7 +24,7 @@ export class OrderListeners {
async handleOrderCreated(event: paymentSucceedEvent) { async handleOrderCreated(event: paymentSucceedEvent) {
try { try {
this.logger.log( 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 // get admnin os restuaraant that have order permissuins
const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.MANAGE_ORDERS); const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.MANAGE_ORDERS);
@@ -36,7 +36,7 @@ export class OrderListeners {
restaurantId: event.restaurantId, restaurantId: event.restaurantId,
message: { message: {
title: NotifTitleEnum.ORDER_CREATED, title: NotifTitleEnum.ORDER_CREATED,
content: `Order ${event.orderNumber} has been created successfully`, content: `payment order #${event.orderNumber} has been paid successfully`,
sms: { sms: {
templateId: this.orderCreatedSmsTemplateId, templateId: this.orderCreatedSmsTemplateId,
parameters: { parameters: {
@@ -44,12 +44,12 @@ export class OrderListeners {
}, },
}, },
pushNotif: { pushNotif: {
title: `Order ${event.orderNumber} has been created successfully`, title: `Payment Order ${event.orderNumber} has been paid successfully`,
content: `Order ${event.orderNumber} has been created successfully`, content: `Payment Order ${event.orderNumber} has been paid successfully`,
icon: `Order ${event.orderNumber} has been created successfully`, icon: `Payment Order ${event.orderNumber} has been paid successfully`,
action: { action: {
type: `Order ${event.orderNumber} has been created successfully`, type: `Payment Order ${event.orderNumber} has been paid successfully`,
url: `Order ${event.orderNumber} has been created successfully`, url: `Payment Order ${event.orderNumber} has been paid successfully`,
}, },
}, },
}, },