check address is inside service area
This commit is contained in:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user