This commit is contained in:
mahyargdz
2025-09-14 15:15:03 +03:30
commit be82059172
534 changed files with 51310 additions and 0 deletions
+59
View File
@@ -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;
}
+54
View File
@@ -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 };
+35
View File
@@ -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();
}
+179
View File
@@ -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 };
+24
View File
@@ -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 };