first
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { Expose, Type, plainToInstance } from "class-transformer";
|
||||
|
||||
import { IAddress } from "../models/Abstraction/IAddress";
|
||||
|
||||
export class CityDTO {
|
||||
@Expose()
|
||||
_id: number;
|
||||
|
||||
@Expose()
|
||||
name: string;
|
||||
|
||||
@Expose()
|
||||
province: number;
|
||||
}
|
||||
|
||||
export class ProvinceDTO {
|
||||
@Expose()
|
||||
_id: number;
|
||||
|
||||
@Expose()
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class AddressDTO {
|
||||
@Expose()
|
||||
_id: string;
|
||||
|
||||
@Expose()
|
||||
address: string;
|
||||
|
||||
@Expose()
|
||||
@Type(() => CityDTO)
|
||||
city: CityDTO;
|
||||
|
||||
@Expose()
|
||||
@Type(() => ProvinceDTO)
|
||||
province: ProvinceDTO;
|
||||
|
||||
@Expose()
|
||||
postalCode: string;
|
||||
|
||||
@Expose()
|
||||
plaque: string;
|
||||
|
||||
@Expose()
|
||||
lat: string;
|
||||
|
||||
@Expose()
|
||||
lon: string;
|
||||
|
||||
public static transformAddress(data: IAddress): AddressDTO {
|
||||
const addressDTO = plainToInstance(AddressDTO, data, {
|
||||
excludeExtraneousValues: true,
|
||||
enableImplicitConversion: true,
|
||||
});
|
||||
|
||||
return addressDTO;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsNotEmpty, IsNumber, IsNumberString, IsOptional, IsString, Length } from "class-validator";
|
||||
|
||||
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||
import { CommonMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class SaveUserAddressDTO {
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ type: "string", description: "province", example: "34.406485" })
|
||||
lat: string;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ type: "string", description: "province", example: "49.159527" })
|
||||
lon: string;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", description: "province", example: 25 })
|
||||
provinceId: number;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", description: "city", example: 569 })
|
||||
cityId: number;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsNumberString({ no_symbols: true }, { message: CommonMessage.PostalIncorrect })
|
||||
@Length(10, 10, { message: CommonMessage.PostalIncorrect })
|
||||
@ApiProperty({ type: "string", description: "iran postal code format (10 char)", example: "5869568592" })
|
||||
postalCode: string;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ type: "string", description: "address of user ", example: "خ ملک" })
|
||||
address: string;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ type: "string", description: "plaque ", example: "25" })
|
||||
plaque: string;
|
||||
}
|
||||
|
||||
export class SaveSellerAddressDTO {
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ type: "string", description: "province", example: "34.406485" })
|
||||
lat: string;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ type: "string", description: "province", example: "49.159527" })
|
||||
lon: string;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", description: "province", example: 25 })
|
||||
provinceId: number;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@ApiProperty({ type: "number", description: "city", example: 569 })
|
||||
cityId: number;
|
||||
|
||||
// @Expose()
|
||||
// @IsNotEmpty()
|
||||
// @IsNumberString({ no_symbols: true }, { message: CommonMessage.PostalIncorrect })
|
||||
// @Length(10, 10, { message: CommonMessage.PostalIncorrect })
|
||||
// @ApiProperty({ type: "string", description: "iran postal code format (10 char)", example: "5869568592" })
|
||||
// postalCode: string;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ type: "string", description: "address of user ", example: "خ ملک" })
|
||||
address: string;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ type: "string", description: "plaque ", example: "25" })
|
||||
plaque: string;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Request } from "express";
|
||||
import { inject } from "inversify";
|
||||
import { controller, httpGet, httpPost, queryParam, request, requestBody } from "inversify-express-utils";
|
||||
|
||||
import { AddressService } from "./address.service";
|
||||
import { HttpStatus } from "../../common";
|
||||
import { SaveSellerAddressDTO, SaveUserAddressDTO } from "./DTO/userAddress.dto";
|
||||
import { BaseController } from "../../common/base/controller";
|
||||
import { ApiAuth, ApiModel, ApiOperation, ApiQuery, ApiResponse, ApiTags } from "../../common/decorator/swggerDocs";
|
||||
import { Guard } from "../../core/middlewares/guard.middleware";
|
||||
import { ValidationMiddleware } from "../../core/middlewares/validator.middleware";
|
||||
import { IOCTYPES } from "../../IOC/ioc.types";
|
||||
import { ISeller } from "../seller/models/Abstraction/ISeller";
|
||||
import { IUser } from "../user/models/Abstraction/IUser";
|
||||
|
||||
@controller("/address")
|
||||
@ApiTags("Address")
|
||||
class AddressController extends BaseController {
|
||||
@inject(IOCTYPES.AddressService) addressService: AddressService;
|
||||
|
||||
@ApiOperation("get user location with geo coordinate")
|
||||
@ApiResponse("successful", HttpStatus.Ok)
|
||||
@ApiQuery("lat", "latitude", true)
|
||||
@ApiQuery("lon", "longitude", true)
|
||||
@httpGet("/location/reverse")
|
||||
public async getLocationWithGeo(@queryParam("lat") lat: string, @queryParam("lon") lon: string) {
|
||||
const data = await this.addressService.getLocationWithGeo({ lat, lon });
|
||||
return this.response(data);
|
||||
}
|
||||
|
||||
@ApiOperation("set user address of current session user ==> login as user")
|
||||
@ApiResponse("successful", HttpStatus.Created)
|
||||
@ApiModel(SaveUserAddressDTO)
|
||||
@ApiAuth()
|
||||
@httpPost("/user/save", Guard.authUser(), ValidationMiddleware.validateInput(SaveUserAddressDTO))
|
||||
public async saveUserAddress(@requestBody() addressDto: SaveUserAddressDTO, @request() req: Request) {
|
||||
const user = req.user as IUser;
|
||||
const data = await this.addressService.setUserAddress(user._id.toString(), addressDto);
|
||||
return this.response(data, HttpStatus.Created);
|
||||
}
|
||||
|
||||
@ApiOperation("set shop address of current session seller ==> login as seller")
|
||||
@ApiResponse("successful", HttpStatus.Created)
|
||||
@ApiModel(SaveSellerAddressDTO)
|
||||
@ApiAuth()
|
||||
@httpPost("/seller/save", Guard.authSeller(), ValidationMiddleware.validateInput(SaveSellerAddressDTO))
|
||||
public async saveSellerShopAddress(@requestBody() addressDto: SaveSellerAddressDTO, @request() req: Request) {
|
||||
const seller = req.user as ISeller;
|
||||
const data = await this.addressService.setSellerShopAddress(seller._id.toString(), addressDto);
|
||||
return this.response(data, HttpStatus.Created);
|
||||
}
|
||||
}
|
||||
|
||||
export { AddressController };
|
||||
@@ -0,0 +1,35 @@
|
||||
import { IAddress } from "./models/Abstraction/IAddress";
|
||||
import { AddressModel } from "./models/address.model";
|
||||
import { CityModel, ICity } from "./models/city.model";
|
||||
import { IProvince, ProvinceModel } from "./models/province.model";
|
||||
import { BaseRepository } from "../../common/base/repository";
|
||||
|
||||
export class AddressRepo extends BaseRepository<IAddress> {
|
||||
constructor() {
|
||||
super(AddressModel);
|
||||
}
|
||||
}
|
||||
|
||||
export class CityRepo extends BaseRepository<ICity> {
|
||||
constructor() {
|
||||
super(CityModel);
|
||||
}
|
||||
}
|
||||
|
||||
export class ProvinceRepo extends BaseRepository<IProvince> {
|
||||
constructor() {
|
||||
super(ProvinceModel);
|
||||
}
|
||||
}
|
||||
|
||||
export function createAddressRepo(): AddressRepo {
|
||||
return new AddressRepo();
|
||||
}
|
||||
|
||||
export function createCityRepo(): CityRepo {
|
||||
return new CityRepo();
|
||||
}
|
||||
|
||||
export function createProvinceRepo(): ProvinceRepo {
|
||||
return new ProvinceRepo();
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import axios from "axios";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { startSession } from "mongoose";
|
||||
|
||||
import { AddressRepo, CityRepo, ProvinceRepo } from "./address.repository";
|
||||
import { SaveSellerAddressDTO, SaveUserAddressDTO } from "./DTO/userAddress.dto";
|
||||
import { INominatimResponse } from "./interfaces/ILocation";
|
||||
import { AddressMessage, CommonMessage, ShopMessage, UserMessage } from "../../common/enums/message.enum";
|
||||
import { BadRequestError, InternalError } from "../../core/app/app.errors";
|
||||
import { IOCTYPES } from "../../IOC/ioc.types";
|
||||
import { UserRepository } from "../user/user.repository";
|
||||
import { ICity } from "./models/city.model";
|
||||
import { IProvince } from "./models/province.model";
|
||||
import { OwnerRef } from "../shop/models/Abstraction/IShop";
|
||||
import { ShopRepo } from "../shop/shop.repository";
|
||||
|
||||
@injectable()
|
||||
class AddressService {
|
||||
private NOMINATIM_URL = "https://nominatim.openstreetmap.org/reverse";
|
||||
// https://nominatim.openstreetmap.org/reverse?lat=34.198770&lon=49.663600&format=json&accept-language=fa
|
||||
// private MAP_IR_URL = "https://map.ir/reverse";
|
||||
// private MAP_IR_API_KEY = process.env.MAP_API_KEY;
|
||||
// private NEHSNAN_URL = "https://api.neshan.org/v5/reverse";
|
||||
// private NESHAN_API_KEY = process.env.NESHAN_API_KEY;
|
||||
@inject(IOCTYPES.UserRepository) private userRepository: UserRepository;
|
||||
@inject(IOCTYPES.AddressRepo) addressRepo: AddressRepo;
|
||||
@inject(IOCTYPES.CityRepo) private cityRepo: CityRepo;
|
||||
@inject(IOCTYPES.ProvinceRepo) private provinceRepo: ProvinceRepo;
|
||||
@inject(IOCTYPES.ShopRepo) shopRepo: ShopRepo;
|
||||
|
||||
//#############################################################
|
||||
//#############################################################
|
||||
async getLocationWithGeo(geo: { lat: string; lon: string }) {
|
||||
const location = await this.getAddressFromCoordinates(geo.lat, geo.lon);
|
||||
let province: IProvince | null;
|
||||
let city: ICity | null;
|
||||
|
||||
//TODO:fix this
|
||||
const provinceName =
|
||||
location?.address?.province?.replace(/^استان\s*/, "").trim() ?? location?.address?.state?.replace(/^استان\s*/, "").trim();
|
||||
const cityName = location.address?.town ? location.address?.town?.replace(/^شهر\s*/, "").trim() : location?.address?.city;
|
||||
|
||||
if (!provinceName || !cityName) throw new BadRequestError(AddressMessage.InvalidLocation);
|
||||
|
||||
province = await this.provinceRepo.model.findOne({ name: provinceName });
|
||||
if (!province) province = await this.provinceRepo.model.create({ name: provinceName });
|
||||
|
||||
city = await this.cityRepo.model.findOne({ name: cityName });
|
||||
if (!city) city = await this.cityRepo.model.create({ name: cityName, province: province._id });
|
||||
|
||||
const displayNameArray = location.display_name.split(",").map((item) => item.trim());
|
||||
|
||||
const filteredDisplayNameArray = location.address.postcode
|
||||
? displayNameArray.filter((item) => !item.includes(location.address.postcode as string))
|
||||
: displayNameArray;
|
||||
|
||||
const address = filteredDisplayNameArray.reverse().join(",").trim();
|
||||
|
||||
return {
|
||||
province: provinceName,
|
||||
provinceId: province._id,
|
||||
city: city.name,
|
||||
cityId: city._id,
|
||||
address,
|
||||
region: location.address.suburb,
|
||||
};
|
||||
}
|
||||
//#############################################################
|
||||
//#############################################################
|
||||
|
||||
async getUserAddress(addressId: string) {
|
||||
const userAddress = await this.addressRepo.findById(addressId);
|
||||
if (!userAddress) throw new BadRequestError([AddressMessage.UserShouldHaveAddress, AddressMessage.NotFound]);
|
||||
return userAddress;
|
||||
}
|
||||
|
||||
//#############################################################
|
||||
//#############################################################
|
||||
|
||||
async setUserAddress(userId: string, addressDto: SaveUserAddressDTO) {
|
||||
const session = await startSession();
|
||||
session.startTransaction();
|
||||
try {
|
||||
const address = await this.addressRepo.model.create(
|
||||
[
|
||||
{
|
||||
address: addressDto.address,
|
||||
city: addressDto.cityId,
|
||||
province: addressDto.provinceId,
|
||||
plaque: addressDto.plaque,
|
||||
postalCode: addressDto.postalCode,
|
||||
lat: addressDto.lat,
|
||||
lon: addressDto.lon,
|
||||
},
|
||||
],
|
||||
{ session },
|
||||
);
|
||||
|
||||
const updatedUser = await this.userRepository.model.findByIdAndUpdate(userId, { address: address[0]._id }, { session });
|
||||
if (!updatedUser) throw new BadRequestError(CommonMessage.NotValid);
|
||||
|
||||
await session.commitTransaction();
|
||||
|
||||
return {
|
||||
message: UserMessage.UserUpdated,
|
||||
address: address[0],
|
||||
};
|
||||
//
|
||||
} catch (error) {
|
||||
await session.abortTransaction();
|
||||
throw error;
|
||||
//
|
||||
} finally {
|
||||
await session.endSession();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
async setSellerShopAddress(sellerId: string, addressDto: SaveSellerAddressDTO) {
|
||||
const session = await startSession();
|
||||
session.startTransaction();
|
||||
try {
|
||||
const shop = await this.shopRepo.model.findOne({ owner: sellerId, ownerRef: OwnerRef.SELLER }).session(session);
|
||||
if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound);
|
||||
const address = await this.addressRepo.model.create(
|
||||
[
|
||||
{
|
||||
address: addressDto.address,
|
||||
city: addressDto.cityId,
|
||||
province: addressDto.provinceId,
|
||||
plaque: addressDto.plaque,
|
||||
// postalCode: addressDto.postalCode,
|
||||
lat: addressDto.lat,
|
||||
lon: addressDto.lon,
|
||||
},
|
||||
],
|
||||
{ session },
|
||||
);
|
||||
shop.address = address[0]._id;
|
||||
await shop.save({ session });
|
||||
|
||||
await session.commitTransaction();
|
||||
|
||||
return {
|
||||
message: AddressMessage.Updated,
|
||||
address: address[0],
|
||||
};
|
||||
} catch (error) {
|
||||
await session.abortTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await session.endSession();
|
||||
}
|
||||
}
|
||||
|
||||
//helpers
|
||||
//##########################
|
||||
private async getAddressFromCoordinates(lat: string, lon: string) {
|
||||
try {
|
||||
const response = await axios.get(this.NOMINATIM_URL, {
|
||||
params: { lat, lon, format: "json", "accept-language": "fa" },
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
},
|
||||
});
|
||||
|
||||
const location = response.data as INominatimResponse;
|
||||
|
||||
if (!location) throw new BadRequestError("Address not found for the given coordinates.");
|
||||
|
||||
return location;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new InternalError([`Failed to get address from coordinates: ${error}`]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { AddressService };
|
||||
@@ -0,0 +1,75 @@
|
||||
export interface IMapIrResponse {
|
||||
address: string;
|
||||
postal_address: string;
|
||||
address_compact: string;
|
||||
primary: string;
|
||||
name: string;
|
||||
poi: string;
|
||||
penult: string;
|
||||
country: string;
|
||||
province: string;
|
||||
county: string;
|
||||
district: string;
|
||||
rural_district: string;
|
||||
city: string;
|
||||
village: string;
|
||||
region: string;
|
||||
neighbourhood: string;
|
||||
last: string;
|
||||
plaque: string;
|
||||
postal_code: string;
|
||||
geom: {
|
||||
type: string;
|
||||
coordinates: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface INeshanResponse {
|
||||
status: string;
|
||||
formatted_address: string;
|
||||
route_name: string;
|
||||
route_type: string;
|
||||
neighbourhood: string;
|
||||
city: string;
|
||||
state: string;
|
||||
place: null;
|
||||
municipality_zone: string;
|
||||
in_traffic_zone: string;
|
||||
in_odd_even_zone: string;
|
||||
village: string;
|
||||
county: string;
|
||||
district: string;
|
||||
}
|
||||
|
||||
export interface INominatimResponse {
|
||||
place_id: number;
|
||||
licence: string;
|
||||
osm_type: string;
|
||||
osm_id: number;
|
||||
lat: string;
|
||||
lon: string;
|
||||
class: string;
|
||||
type: string;
|
||||
place_rank: number;
|
||||
importance: number;
|
||||
addresstype: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
address: {
|
||||
road?: string;
|
||||
province?: string;
|
||||
village?: string;
|
||||
neighbourhood?: string;
|
||||
suburb?: string;
|
||||
town?: string;
|
||||
city: string;
|
||||
district: string;
|
||||
county: string;
|
||||
state: string;
|
||||
"ISO3166-2-lvl4": string;
|
||||
postcode?: string;
|
||||
country: string;
|
||||
country_code: string;
|
||||
};
|
||||
boundingbox: string[];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface IAddress {
|
||||
address: string;
|
||||
city: number;
|
||||
province: number;
|
||||
postalCode: string;
|
||||
plaque: string;
|
||||
lat: string;
|
||||
lon: string;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Schema, model } from "mongoose";
|
||||
|
||||
import { IAddress } from "./Abstraction/IAddress";
|
||||
|
||||
const AddressSchema = new Schema<IAddress>(
|
||||
{
|
||||
address: { type: String, required: true },
|
||||
city: { type: Number, ref: "City", required: true },
|
||||
province: { type: Number, ref: "Province", required: true },
|
||||
postalCode: { type: String, default: null },
|
||||
plaque: { type: String, default: null },
|
||||
lat: { type: String, default: null },
|
||||
lon: { type: String, default: null },
|
||||
},
|
||||
{ timestamps: true, toJSON: { virtuals: true, versionKey: false }, id: false },
|
||||
);
|
||||
|
||||
AddressSchema.pre(["find", "findOne"], function (next) {
|
||||
this.populate([{ path: "city" }, { path: "province" }]);
|
||||
next();
|
||||
});
|
||||
const AddressModel = model<IAddress>("Address", AddressSchema);
|
||||
|
||||
export { AddressModel };
|
||||
@@ -0,0 +1,24 @@
|
||||
import mongoose, { Schema, model } from "mongoose";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
|
||||
const AutoIncrement = require("mongoose-sequence")(mongoose);
|
||||
|
||||
export interface ICity {
|
||||
_id: number;
|
||||
name: string;
|
||||
province: number;
|
||||
}
|
||||
|
||||
const citySchema = new Schema<ICity>(
|
||||
{
|
||||
_id: Number,
|
||||
name: { type: String, required: true, index: true },
|
||||
province: { type: Number, ref: "Province", required: true },
|
||||
},
|
||||
{ timestamps: true, toJSON: { versionKey: false, virtuals: true }, _id: false, id: false },
|
||||
);
|
||||
|
||||
citySchema.plugin(AutoIncrement, { id: "city_id", inc_field: "_id" });
|
||||
const CityModel = model<ICity>("City", citySchema);
|
||||
|
||||
export { CityModel };
|
||||
@@ -0,0 +1,22 @@
|
||||
import mongoose, { Schema, model } from "mongoose";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
|
||||
const AutoIncrement = require("mongoose-sequence")(mongoose);
|
||||
|
||||
export interface IProvince {
|
||||
_id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const ProvinceSchema = new Schema<IProvince>(
|
||||
{
|
||||
_id: Number,
|
||||
name: { type: String, required: true, unique: true, index: true },
|
||||
},
|
||||
{ timestamps: true, toJSON: { versionKey: false, virtuals: true }, _id: false, id: false },
|
||||
);
|
||||
|
||||
ProvinceSchema.plugin(AutoIncrement, { id: "province_id", inc_field: "_id" });
|
||||
const ProvinceModel = model<IProvince>("Province", ProvinceSchema);
|
||||
|
||||
export { ProvinceModel };
|
||||
Reference in New Issue
Block a user