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
+95 -23
View File
@@ -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<Cart> {
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<Cart> {
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<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
*/
@@ -317,11 +383,7 @@ export class CartService {
return this.recalculateAndSaveCart(cart);
}
private async assertWalletHasEnoughBalance(
userId: string,
restaurantId: string,
amount: number,
): Promise<void> {
private async assertWalletHasEnoughBalance(userId: string, restaurantId: string, amount: number): Promise<void> {
const wallet = await this.em.findOne(UserWallet, {
user: { id: userId },
restaurant: { id: restaurantId },
@@ -346,7 +408,10 @@ export class CartService {
): Promise<Cart> {
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<Delivery> {
private async getDeliveryMethodForRestaurantOrFail(
restaurantId: string,
deliveryMethodId: string,
): Promise<Delivery> {
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');