module provinces
This commit is contained in:
@@ -16,6 +16,7 @@ import { StylistsModule } from './stylists/stylists.module';
|
|||||||
import { SubscriptionsModule } from './subscriptions/subscriptions.module';
|
import { SubscriptionsModule } from './subscriptions/subscriptions.module';
|
||||||
import { UsersModule } from './users/users.module';
|
import { UsersModule } from './users/users.module';
|
||||||
import { PaymentsModule } from './payments/payments.module';
|
import { PaymentsModule } from './payments/payments.module';
|
||||||
|
import { ProvincesModule } from './provinces/provinces.module';
|
||||||
import { UploaderModule } from './uploader/uploader.module';
|
import { UploaderModule } from './uploader/uploader.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -38,6 +39,7 @@ import { UploaderModule } from './uploader/uploader.module';
|
|||||||
SentMessagesModule,
|
SentMessagesModule,
|
||||||
SubscriptionsModule,
|
SubscriptionsModule,
|
||||||
PaymentsModule,
|
PaymentsModule,
|
||||||
|
ProvincesModule,
|
||||||
UploaderModule,
|
UploaderModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
|||||||
import { AuthorizationModule } from '../authorization/authorization.module';
|
import { AuthorizationModule } from '../authorization/authorization.module';
|
||||||
import { Customer } from '../customers/entities/customer.entity';
|
import { Customer } from '../customers/entities/customer.entity';
|
||||||
import { Salon } from '../salons/entities/salon.entity';
|
import { Salon } from '../salons/entities/salon.entity';
|
||||||
|
import { ProvincesModule } from '../provinces/provinces.module';
|
||||||
import { SmsModule } from '../sms/sms.module';
|
import { SmsModule } from '../sms/sms.module';
|
||||||
import { SubscriptionsModule } from '../subscriptions/subscriptions.module';
|
import { SubscriptionsModule } from '../subscriptions/subscriptions.module';
|
||||||
import { UsersModule } from '../users/users.module';
|
import { UsersModule } from '../users/users.module';
|
||||||
@@ -36,6 +37,7 @@ import { JwtStrategy } from './strategies/jwt.strategy';
|
|||||||
SmsModule,
|
SmsModule,
|
||||||
SubscriptionsModule,
|
SubscriptionsModule,
|
||||||
AuthorizationModule,
|
AuthorizationModule,
|
||||||
|
ProvincesModule,
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import * as bcrypt from 'bcrypt';
|
|||||||
import { Repository, IsNull } from 'typeorm';
|
import { Repository, IsNull } from 'typeorm';
|
||||||
import { Customer } from '../customers/entities/customer.entity';
|
import { Customer } from '../customers/entities/customer.entity';
|
||||||
import { Salon } from '../salons/entities/salon.entity';
|
import { Salon } from '../salons/entities/salon.entity';
|
||||||
|
import { ProvincesService } from '../provinces/provinces.service';
|
||||||
import { SmsService } from '../sms/sms.service';
|
import { SmsService } from '../sms/sms.service';
|
||||||
import { SalonSubscription } from '../subscriptions/entities/salon-subscription.entity';
|
import { SalonSubscription } from '../subscriptions/entities/salon-subscription.entity';
|
||||||
import { SalonSubscriptionsService } from '../subscriptions/salon-subscriptions.service';
|
import { SalonSubscriptionsService } from '../subscriptions/salon-subscriptions.service';
|
||||||
@@ -69,6 +70,7 @@ export class AuthService {
|
|||||||
private readonly customersRepository: Repository<Customer>,
|
private readonly customersRepository: Repository<Customer>,
|
||||||
@InjectRepository(Salon)
|
@InjectRepository(Salon)
|
||||||
private readonly salonsRepository: Repository<Salon>,
|
private readonly salonsRepository: Repository<Salon>,
|
||||||
|
private readonly provincesService: ProvincesService,
|
||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
private readonly salonSubscriptionsService: SalonSubscriptionsService,
|
private readonly salonSubscriptionsService: SalonSubscriptionsService,
|
||||||
private readonly smsService: SmsService,
|
private readonly smsService: SmsService,
|
||||||
@@ -274,6 +276,7 @@ export class AuthService {
|
|||||||
dto: VerifySalonOtpDto,
|
dto: VerifySalonOtpDto,
|
||||||
): Promise<SalonAuthResponse> {
|
): Promise<SalonAuthResponse> {
|
||||||
this.validateSalonSignupFields(dto);
|
this.validateSalonSignupFields(dto);
|
||||||
|
await this.validateSalonLocation(dto.salonProvinceId!, dto.salonCityId!);
|
||||||
|
|
||||||
const user = await this.usersService.create({
|
const user = await this.usersService.create({
|
||||||
firstName: dto.firstName!,
|
firstName: dto.firstName!,
|
||||||
@@ -287,6 +290,8 @@ export class AuthService {
|
|||||||
description: dto.salonDescription ?? null,
|
description: dto.salonDescription ?? null,
|
||||||
address: dto.salonAddress!,
|
address: dto.salonAddress!,
|
||||||
phone: dto.salonPhone!,
|
phone: dto.salonPhone!,
|
||||||
|
provinceId: dto.salonProvinceId!,
|
||||||
|
cityId: dto.salonCityId!,
|
||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -301,6 +306,8 @@ export class AuthService {
|
|||||||
'salonName',
|
'salonName',
|
||||||
'salonAddress',
|
'salonAddress',
|
||||||
'salonPhone',
|
'salonPhone',
|
||||||
|
'salonProvinceId',
|
||||||
|
'salonCityId',
|
||||||
];
|
];
|
||||||
|
|
||||||
const missingFields = requiredFields.filter((field) => !dto[field]);
|
const missingFields = requiredFields.filter((field) => !dto[field]);
|
||||||
@@ -312,6 +319,16 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async validateSalonLocation(
|
||||||
|
provinceId: string,
|
||||||
|
cityId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const city = await this.provincesService.findCity(provinceId, cityId);
|
||||||
|
if (!city) {
|
||||||
|
throw new BadRequestException('شهر انتخابشده با استان مطابقت ندارد');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async verifyOtp(dto: VerifyOtpDto): Promise<AuthTokensResponse> {
|
async verifyOtp(dto: VerifyOtpDto): Promise<AuthTokensResponse> {
|
||||||
await this.consumeOtp(dto.mobile, dto.code);
|
await this.consumeOtp(dto.mobile, dto.code);
|
||||||
|
|
||||||
@@ -390,6 +407,7 @@ export class AuthService {
|
|||||||
if (user.role === UserRole.SALON) {
|
if (user.role === UserRole.SALON) {
|
||||||
response.salons = await this.salonsRepository.find({
|
response.salons = await this.salonsRepository.find({
|
||||||
where: { ownerId: user.id },
|
where: { ownerId: user.id },
|
||||||
|
relations: { province: true, city: true },
|
||||||
order: { createdAt: 'ASC' },
|
order: { createdAt: 'ASC' },
|
||||||
});
|
});
|
||||||
response.activeSubscription =
|
response.activeSubscription =
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
|
IsUUID,
|
||||||
Length,
|
Length,
|
||||||
Matches,
|
Matches,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
@@ -40,4 +41,12 @@ export class VerifySalonOtpDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Matches(/^09\d{9}$/)
|
@Matches(/^09\d{9}$/)
|
||||||
salonPhone?: string;
|
salonPhone?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
salonProvinceId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
salonCityId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AuthorizationService } from './authorization.service';
|
|||||||
import { Permission } from './entities/permission.entity';
|
import { Permission } from './entities/permission.entity';
|
||||||
import { UserPermission } from './entities/user-permission.entity';
|
import { UserPermission } from './entities/user-permission.entity';
|
||||||
import { CustomerPolicy } from './policies/customer.policy';
|
import { CustomerPolicy } from './policies/customer.policy';
|
||||||
|
import { ProvincePolicy } from './policies/province.policy';
|
||||||
import { ReservePolicy } from './policies/reserve.policy';
|
import { ReservePolicy } from './policies/reserve.policy';
|
||||||
import { SalonPolicy } from './policies/salon.policy';
|
import { SalonPolicy } from './policies/salon.policy';
|
||||||
import { ServicePolicy } from './policies/service.policy';
|
import { ServicePolicy } from './policies/service.policy';
|
||||||
@@ -17,6 +18,7 @@ import { SubscriptionPolicy } from './policies/subscription.policy';
|
|||||||
AuthorizationService,
|
AuthorizationService,
|
||||||
SalonPolicy,
|
SalonPolicy,
|
||||||
ServicePolicy,
|
ServicePolicy,
|
||||||
|
ProvincePolicy,
|
||||||
CustomerPolicy,
|
CustomerPolicy,
|
||||||
StylistPolicy,
|
StylistPolicy,
|
||||||
ReservePolicy,
|
ReservePolicy,
|
||||||
@@ -26,6 +28,7 @@ import { SubscriptionPolicy } from './policies/subscription.policy';
|
|||||||
AuthorizationService,
|
AuthorizationService,
|
||||||
SalonPolicy,
|
SalonPolicy,
|
||||||
ServicePolicy,
|
ServicePolicy,
|
||||||
|
ProvincePolicy,
|
||||||
CustomerPolicy,
|
CustomerPolicy,
|
||||||
StylistPolicy,
|
StylistPolicy,
|
||||||
ReservePolicy,
|
ReservePolicy,
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export enum PermissionCode {
|
|||||||
SERVICES_READ = 'services:read',
|
SERVICES_READ = 'services:read',
|
||||||
SERVICES_WRITE = 'services:write',
|
SERVICES_WRITE = 'services:write',
|
||||||
SERVICES_DELETE = 'services:delete',
|
SERVICES_DELETE = 'services:delete',
|
||||||
|
PROVINCES_READ = 'provinces:read',
|
||||||
|
PROVINCES_WRITE = 'provinces:write',
|
||||||
|
PROVINCES_DELETE = 'provinces:delete',
|
||||||
RESERVES_READ = 'reserves:read',
|
RESERVES_READ = 'reserves:read',
|
||||||
RESERVES_WRITE = 'reserves:write',
|
RESERVES_WRITE = 'reserves:write',
|
||||||
SUBSCRIPTIONS_READ = 'subscriptions:read',
|
SUBSCRIPTIONS_READ = 'subscriptions:read',
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AuthUser } from '../../auth/interfaces/auth-user.interface';
|
||||||
|
import { AuthorizationService } from '../authorization.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProvincePolicy {
|
||||||
|
constructor(private readonly authorizationService: AuthorizationService) {}
|
||||||
|
|
||||||
|
canManage(user: AuthUser): boolean {
|
||||||
|
return this.authorizationService.isAdmin(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Customer } from '../customers/entities/customer.entity';
|
import { Customer } from '../customers/entities/customer.entity';
|
||||||
|
import { City } from '../provinces/entities/city.entity';
|
||||||
|
import { Province } from '../provinces/entities/province.entity';
|
||||||
import { User } from '../users/entities/user.entity';
|
import { User } from '../users/entities/user.entity';
|
||||||
import { SeedService } from './seed.service';
|
import { SeedService } from './seed.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([User, Customer])],
|
imports: [TypeOrmModule.forFeature([User, Customer, Province, City])],
|
||||||
providers: [SeedService],
|
providers: [SeedService],
|
||||||
})
|
})
|
||||||
export class DatabaseModule {}
|
export class DatabaseModule {}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import {
|
||||||
|
MigrationInterface,
|
||||||
|
QueryRunner,
|
||||||
|
Table,
|
||||||
|
TableForeignKey,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
export class AddProvincesAndCities1721000000000 implements MigrationInterface {
|
||||||
|
name = 'AddProvincesAndCities1721000000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: 'provinces',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'uuid',
|
||||||
|
isPrimary: true,
|
||||||
|
generationStrategy: 'uuid',
|
||||||
|
default: 'gen_random_uuid()',
|
||||||
|
},
|
||||||
|
{ name: 'name', type: 'varchar', length: '100', isUnique: true },
|
||||||
|
{ name: 'slug', type: 'varchar', length: '100', isUnique: true },
|
||||||
|
{ name: 'createdAt', type: 'timestamp', default: 'now()' },
|
||||||
|
{ name: 'updatedAt', type: 'timestamp', default: 'now()' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: 'cities',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'uuid',
|
||||||
|
isPrimary: true,
|
||||||
|
generationStrategy: 'uuid',
|
||||||
|
default: 'gen_random_uuid()',
|
||||||
|
},
|
||||||
|
{ name: 'provinceId', type: 'uuid' },
|
||||||
|
{ name: 'name', type: 'varchar', length: '100' },
|
||||||
|
{ name: 'slug', type: 'varchar', length: '100', isUnique: true },
|
||||||
|
{ name: 'createdAt', type: 'timestamp', default: 'now()' },
|
||||||
|
{ name: 'updatedAt', type: 'timestamp', default: 'now()' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'cities',
|
||||||
|
new TableForeignKey({
|
||||||
|
columnNames: ['provinceId'],
|
||||||
|
referencedTableName: 'provinces',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "salons"
|
||||||
|
ADD COLUMN "provinceId" uuid,
|
||||||
|
ADD COLUMN "cityId" uuid
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'salons',
|
||||||
|
new TableForeignKey({
|
||||||
|
columnNames: ['provinceId'],
|
||||||
|
referencedTableName: 'provinces',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'salons',
|
||||||
|
new TableForeignKey({
|
||||||
|
columnNames: ['cityId'],
|
||||||
|
referencedTableName: 'cities',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const salonsTable = await queryRunner.getTable('salons');
|
||||||
|
if (salonsTable) {
|
||||||
|
for (const fk of salonsTable.foreignKeys.filter(
|
||||||
|
(key) =>
|
||||||
|
key.columnNames.includes('provinceId') ||
|
||||||
|
key.columnNames.includes('cityId'),
|
||||||
|
)) {
|
||||||
|
await queryRunner.dropForeignKey('salons', fk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "salons"
|
||||||
|
DROP COLUMN IF EXISTS "provinceId",
|
||||||
|
DROP COLUMN IF EXISTS "cityId"
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.dropTable('cities', true);
|
||||||
|
await queryRunner.dropTable('provinces', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,34 @@
|
|||||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
import { City } from '../provinces/entities/city.entity';
|
||||||
|
import { Province } from '../provinces/entities/province.entity';
|
||||||
import { Customer } from '../customers/entities/customer.entity';
|
import { Customer } from '../customers/entities/customer.entity';
|
||||||
import { User } from '../users/entities/user.entity';
|
import { User } from '../users/entities/user.entity';
|
||||||
import { UserRole } from '../users/enums/user-role.enum';
|
import { UserRole } from '../users/enums/user-role.enum';
|
||||||
import { ADMIN_USERS_SEED } from './seeds/admin-users.seed';
|
import { ADMIN_USERS_SEED } from './seeds/admin-users.seed';
|
||||||
|
import iranCitiesSeed from './seeds/data/iran-cities.json';
|
||||||
|
import iranProvincesSeed from './seeds/data/iran-provinces.json';
|
||||||
|
|
||||||
|
type IranProvinceSeed = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IranCitySeed = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
province_id: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const slugify = (value: string) =>
|
||||||
|
value
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '');
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SeedService implements OnModuleInit {
|
export class SeedService implements OnModuleInit {
|
||||||
@@ -15,10 +39,15 @@ export class SeedService implements OnModuleInit {
|
|||||||
private readonly usersRepository: Repository<User>,
|
private readonly usersRepository: Repository<User>,
|
||||||
@InjectRepository(Customer)
|
@InjectRepository(Customer)
|
||||||
private readonly customersRepository: Repository<Customer>,
|
private readonly customersRepository: Repository<Customer>,
|
||||||
|
@InjectRepository(Province)
|
||||||
|
private readonly provincesRepository: Repository<Province>,
|
||||||
|
@InjectRepository(City)
|
||||||
|
private readonly citiesRepository: Repository<City>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit(): Promise<void> {
|
async onModuleInit(): Promise<void> {
|
||||||
await this.seedAdminUsers();
|
await this.seedAdminUsers();
|
||||||
|
await this.seedIranLocations();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async seedAdminUsers(): Promise<void> {
|
private async seedAdminUsers(): Promise<void> {
|
||||||
@@ -55,4 +84,45 @@ export class SeedService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async seedIranLocations(): Promise<void> {
|
||||||
|
const existingCount = await this.provincesRepository.count();
|
||||||
|
if (existingCount > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provinceIdMap = new Map<number, string>();
|
||||||
|
const usedCitySlugs = new Set<string>();
|
||||||
|
|
||||||
|
for (const provinceSeed of iranProvincesSeed as IranProvinceSeed[]) {
|
||||||
|
const province = await this.provincesRepository.save({
|
||||||
|
name: provinceSeed.title.trim(),
|
||||||
|
slug: slugify(provinceSeed.slug || provinceSeed.title),
|
||||||
|
});
|
||||||
|
provinceIdMap.set(provinceSeed.id, province.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const citySeed of iranCitiesSeed as IranCitySeed[]) {
|
||||||
|
const provinceId = provinceIdMap.get(citySeed.province_id);
|
||||||
|
if (!provinceId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let slug = slugify(citySeed.slug || citySeed.title);
|
||||||
|
if (usedCitySlugs.has(slug)) {
|
||||||
|
slug = `${slug}-${citySeed.id}`;
|
||||||
|
}
|
||||||
|
usedCitySlugs.add(slug);
|
||||||
|
|
||||||
|
await this.citiesRepository.save({
|
||||||
|
provinceId,
|
||||||
|
name: citySeed.title.trim(),
|
||||||
|
slug,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Seeded ${provinceIdMap.size} provinces and ${usedCitySlugs.size} cities`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,219 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "آذربایجان شرقی",
|
||||||
|
"slug": "East Azarbaijan",
|
||||||
|
"latitude": 37.9035733,
|
||||||
|
"longitude": 46.2682109
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"title": "آذربایجان غربی",
|
||||||
|
"slug": "Western Azerbaijan",
|
||||||
|
"latitude": 37.7208556,
|
||||||
|
"longitude": 45.0126837
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 25,
|
||||||
|
"title": "اردبیل",
|
||||||
|
"slug": "Ardabil",
|
||||||
|
"latitude": 38.4853276,
|
||||||
|
"longitude": 47.8911209
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"title": "اصفهان",
|
||||||
|
"slug": "Esfahan",
|
||||||
|
"latitude": 32.6546275,
|
||||||
|
"longitude": 51.6679826
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 31,
|
||||||
|
"title": "البرز",
|
||||||
|
"slug": "Alborz",
|
||||||
|
"latitude": 35.9960467,
|
||||||
|
"longitude": 50.9289246
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"title": "ایلام",
|
||||||
|
"slug": "Ilam",
|
||||||
|
"latitude": 33.2957618,
|
||||||
|
"longitude": 46.670534
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"title": "بوشهر",
|
||||||
|
"slug": "Bushehr",
|
||||||
|
"latitude": 28.9233837,
|
||||||
|
"longitude": 50.820314
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"title": "تهران",
|
||||||
|
"slug": "Tehran",
|
||||||
|
"latitude": 35.696111,
|
||||||
|
"longitude": 51.423056
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 30,
|
||||||
|
"title": "خراسان جنوبی",
|
||||||
|
"slug": "South Khorasan",
|
||||||
|
"latitude": 32.5175643,
|
||||||
|
"longitude": 59.1041758
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"title": "خراسان رضوی",
|
||||||
|
"slug": "Khorasan Razavi",
|
||||||
|
"latitude": 35.5160063,
|
||||||
|
"longitude": 59.2230448
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 29,
|
||||||
|
"title": "خراسان شمالی",
|
||||||
|
"slug": "North Khorasan",
|
||||||
|
"latitude": 37.4710353,
|
||||||
|
"longitude": 57.1013188
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"title": "خوزستان",
|
||||||
|
"slug": "Khuzestan",
|
||||||
|
"latitude": 31.4360149,
|
||||||
|
"longitude": 49.041312
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"title": "زنجان",
|
||||||
|
"slug": "Zanjan",
|
||||||
|
"latitude": 36.5018185,
|
||||||
|
"longitude": 48.3988186
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21,
|
||||||
|
"title": "سمنان",
|
||||||
|
"slug": "Semnan",
|
||||||
|
"latitude": 35.2255585,
|
||||||
|
"longitude": 54.4342138
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"title": "سیستان وبلوچستان",
|
||||||
|
"slug": "Sistan and Baluchistan",
|
||||||
|
"latitude": 27.5299906,
|
||||||
|
"longitude": 60.5820676
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"title": "فارس",
|
||||||
|
"slug": "Fars",
|
||||||
|
"latitude": 29.1043813,
|
||||||
|
"longitude": 53.045893
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 27,
|
||||||
|
"title": "قزوین",
|
||||||
|
"slug": "Qazvin",
|
||||||
|
"latitude": 36.0881317,
|
||||||
|
"longitude": 49.8547266
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 26,
|
||||||
|
"title": "قم",
|
||||||
|
"slug": "Qom",
|
||||||
|
"latitude": 34.6399443,
|
||||||
|
"longitude": 50.8759419
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"title": "کردستان",
|
||||||
|
"slug": "Kurdistan",
|
||||||
|
"latitude": 35.6680062,
|
||||||
|
"longitude": 47.0027204
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"title": "کرمان",
|
||||||
|
"slug": "Kerman",
|
||||||
|
"latitude": 30.2839379,
|
||||||
|
"longitude": 57.0833628
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"title": "کرمانشاه",
|
||||||
|
"slug": "Kermanshah",
|
||||||
|
"latitude": 34.314167,
|
||||||
|
"longitude": 47.065
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"title": "کهگیلویه و بویراحمد",
|
||||||
|
"slug": "Kohgiloyeh Boyerahmad",
|
||||||
|
"latitude": 30.6509479,
|
||||||
|
"longitude": 51.60525
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 28,
|
||||||
|
"title": "گلستان",
|
||||||
|
"slug": "Golestan",
|
||||||
|
"latitude": 37.2898123,
|
||||||
|
"longitude": 55.1375834
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "گیلان",
|
||||||
|
"slug": "Gilan",
|
||||||
|
"latitude": 37.2809,
|
||||||
|
"longitude": 49.5924
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"title": "لرستان",
|
||||||
|
"slug": "Lorestan",
|
||||||
|
"latitude": 33.4529829,
|
||||||
|
"longitude": 48.306995
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "مازندران",
|
||||||
|
"slug": "Mazandaran",
|
||||||
|
"latitude": 36.2262393,
|
||||||
|
"longitude": 52.5318604
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "مرکزی",
|
||||||
|
"slug": "Markazi",
|
||||||
|
"latitude": 34.6123,
|
||||||
|
"longitude": 49.8547
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23,
|
||||||
|
"title": "هرمزگان",
|
||||||
|
"slug": "Hormozgan",
|
||||||
|
"latitude": 27.138723,
|
||||||
|
"longitude": 55.1375834
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"title": "همدان",
|
||||||
|
"slug": "Hamedan",
|
||||||
|
"latitude": 34.9277212,
|
||||||
|
"longitude": 48.6655971
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"title": "چهارمحال و بختیاری",
|
||||||
|
"slug": "Chaharmahal and Bakhtiari ",
|
||||||
|
"latitude": 31.9614348,
|
||||||
|
"longitude": 50.8456323
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"title": "یزد",
|
||||||
|
"slug": "Yazd",
|
||||||
|
"latitude": 32.0025525,
|
||||||
|
"longitude": 54.687334
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { IsNotEmpty, IsString, Length } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateCityDto {
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 100)
|
||||||
|
@IsNotEmpty()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 100)
|
||||||
|
@IsNotEmpty()
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { IsNotEmpty, IsString, Length } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateProvinceDto {
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 100)
|
||||||
|
@IsNotEmpty()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 100)
|
||||||
|
@IsNotEmpty()
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { IsOptional, IsString, Length } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateCityDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 100)
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 100)
|
||||||
|
slug?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { IsOptional, IsString, Length } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateProvinceDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 100)
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 100)
|
||||||
|
slug?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Province } from './province.entity';
|
||||||
|
|
||||||
|
@Entity('cities')
|
||||||
|
export class City {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
provinceId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Province, (province) => province.cities, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'provinceId' })
|
||||||
|
province: Province;
|
||||||
|
|
||||||
|
@Column({ length: 100 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ length: 100, unique: true })
|
||||||
|
slug: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
OneToMany,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { City } from './city.entity';
|
||||||
|
|
||||||
|
@Entity('provinces')
|
||||||
|
export class Province {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ length: 100, unique: true })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ length: 100, unique: true })
|
||||||
|
slug: string;
|
||||||
|
|
||||||
|
@OneToMany(() => City, (city) => city.province)
|
||||||
|
cities: City[];
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
|
import { Public } from '../auth/decorators/public.decorator';
|
||||||
|
import { Permissions } from '../auth/decorators/permissions.decorator';
|
||||||
|
import { Roles } from '../auth/decorators/roles.decorator';
|
||||||
|
import { PermissionsGuard } from '../auth/guards/permissions.guard';
|
||||||
|
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||||
|
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
||||||
|
import { PermissionCode } from '../authorization/enums/permission-code.enum';
|
||||||
|
import { UserRole } from '../users/enums/user-role.enum';
|
||||||
|
import { CreateCityDto } from './dto/create-city.dto';
|
||||||
|
import { CreateProvinceDto } from './dto/create-province.dto';
|
||||||
|
import { UpdateCityDto } from './dto/update-city.dto';
|
||||||
|
import { UpdateProvinceDto } from './dto/update-province.dto';
|
||||||
|
import { City } from './entities/city.entity';
|
||||||
|
import { Province } from './entities/province.entity';
|
||||||
|
import { ProvincesService } from './provinces.service';
|
||||||
|
import { ApiPublicEndpoint } from '../swagger/decorators/swagger-auth.decorator';
|
||||||
|
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
|
||||||
|
import {
|
||||||
|
ApiStandardController,
|
||||||
|
ApiStandardEmptyResponse,
|
||||||
|
ApiStandardOkResponse,
|
||||||
|
} from '../swagger/decorators/swagger-response.decorator';
|
||||||
|
|
||||||
|
@ApiTags('Provinces')
|
||||||
|
@ApiBearerAuth(SWAGGER_JWT_AUTH)
|
||||||
|
@ApiStandardController()
|
||||||
|
@Controller('provinces')
|
||||||
|
@UseGuards(RolesGuard, PermissionsGuard)
|
||||||
|
export class ProvincesController {
|
||||||
|
constructor(private readonly provincesService: ProvincesService) {}
|
||||||
|
|
||||||
|
@Roles(UserRole.ADMIN)
|
||||||
|
@Permissions(PermissionCode.PROVINCES_WRITE)
|
||||||
|
@ApiStandardOkResponse(Province)
|
||||||
|
@Post()
|
||||||
|
create(
|
||||||
|
@Body() createProvinceDto: CreateProvinceDto,
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
) {
|
||||||
|
return this.provincesService.create(createProvinceDto, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@ApiPublicEndpoint('List all provinces')
|
||||||
|
@ApiStandardOkResponse(Province, { isArray: true })
|
||||||
|
@Get()
|
||||||
|
findAll() {
|
||||||
|
return this.provincesService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@ApiPublicEndpoint('Get province by ID')
|
||||||
|
@ApiStandardOkResponse(Province)
|
||||||
|
@Get(':id')
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.provincesService.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles(UserRole.ADMIN)
|
||||||
|
@Permissions(PermissionCode.PROVINCES_WRITE)
|
||||||
|
@ApiStandardOkResponse(Province)
|
||||||
|
@Patch(':id')
|
||||||
|
update(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Body() updateProvinceDto: UpdateProvinceDto,
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
) {
|
||||||
|
return this.provincesService.update(id, updateProvinceDto, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles(UserRole.ADMIN)
|
||||||
|
@Permissions(PermissionCode.PROVINCES_DELETE)
|
||||||
|
@ApiStandardEmptyResponse()
|
||||||
|
@Delete(':id')
|
||||||
|
remove(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
) {
|
||||||
|
return this.provincesService.remove(id, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles(UserRole.ADMIN)
|
||||||
|
@Permissions(PermissionCode.PROVINCES_WRITE)
|
||||||
|
@ApiStandardOkResponse(City)
|
||||||
|
@Post(':provinceId/cities')
|
||||||
|
createCity(
|
||||||
|
@Param('provinceId', ParseUUIDPipe) provinceId: string,
|
||||||
|
@Body() createCityDto: CreateCityDto,
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
) {
|
||||||
|
return this.provincesService.createCity(provinceId, createCityDto, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@ApiPublicEndpoint('List cities for a province')
|
||||||
|
@ApiStandardOkResponse(City, { isArray: true })
|
||||||
|
@Get(':provinceId/cities')
|
||||||
|
findCities(@Param('provinceId', ParseUUIDPipe) provinceId: string) {
|
||||||
|
return this.provincesService.findCities(provinceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@ApiPublicEndpoint('Get a city by ID')
|
||||||
|
@ApiStandardOkResponse(City)
|
||||||
|
@Get(':provinceId/cities/:cityId')
|
||||||
|
findCity(
|
||||||
|
@Param('provinceId', ParseUUIDPipe) provinceId: string,
|
||||||
|
@Param('cityId', ParseUUIDPipe) cityId: string,
|
||||||
|
) {
|
||||||
|
return this.provincesService.findCity(provinceId, cityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles(UserRole.ADMIN)
|
||||||
|
@Permissions(PermissionCode.PROVINCES_WRITE)
|
||||||
|
@ApiStandardOkResponse(City)
|
||||||
|
@Patch(':provinceId/cities/:cityId')
|
||||||
|
updateCity(
|
||||||
|
@Param('provinceId', ParseUUIDPipe) provinceId: string,
|
||||||
|
@Param('cityId', ParseUUIDPipe) cityId: string,
|
||||||
|
@Body() updateCityDto: UpdateCityDto,
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
) {
|
||||||
|
return this.provincesService.updateCity(
|
||||||
|
provinceId,
|
||||||
|
cityId,
|
||||||
|
updateCityDto,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Roles(UserRole.ADMIN)
|
||||||
|
@Permissions(PermissionCode.PROVINCES_DELETE)
|
||||||
|
@ApiStandardEmptyResponse()
|
||||||
|
@Delete(':provinceId/cities/:cityId')
|
||||||
|
removeCity(
|
||||||
|
@Param('provinceId', ParseUUIDPipe) provinceId: string,
|
||||||
|
@Param('cityId', ParseUUIDPipe) cityId: string,
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
) {
|
||||||
|
return this.provincesService.removeCity(provinceId, cityId, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AuthGuardsModule } from '../auth/auth-guards.module';
|
||||||
|
import { AuthorizationModule } from '../authorization/authorization.module';
|
||||||
|
import { City } from './entities/city.entity';
|
||||||
|
import { Province } from './entities/province.entity';
|
||||||
|
import { ProvincesController } from './provinces.controller';
|
||||||
|
import { ProvincesService } from './provinces.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Province, City]),
|
||||||
|
AuthorizationModule,
|
||||||
|
AuthGuardsModule,
|
||||||
|
],
|
||||||
|
controllers: [ProvincesController],
|
||||||
|
providers: [ProvincesService],
|
||||||
|
exports: [ProvincesService],
|
||||||
|
})
|
||||||
|
export class ProvincesModule {}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import {
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { ProvincePolicy } from '../authorization/policies/province.policy';
|
||||||
|
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
||||||
|
import { CreateCityDto } from './dto/create-city.dto';
|
||||||
|
import { CreateProvinceDto } from './dto/create-province.dto';
|
||||||
|
import { UpdateCityDto } from './dto/update-city.dto';
|
||||||
|
import { UpdateProvinceDto } from './dto/update-province.dto';
|
||||||
|
import { City } from './entities/city.entity';
|
||||||
|
import { Province } from './entities/province.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProvincesService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Province)
|
||||||
|
private readonly provincesRepository: Repository<Province>,
|
||||||
|
@InjectRepository(City)
|
||||||
|
private readonly citiesRepository: Repository<City>,
|
||||||
|
private readonly provincePolicy: ProvincePolicy,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
create(
|
||||||
|
createProvinceDto: CreateProvinceDto,
|
||||||
|
user: AuthUser,
|
||||||
|
): Promise<Province> {
|
||||||
|
this.ensureCanManage(user);
|
||||||
|
const province = this.provincesRepository.create(createProvinceDto);
|
||||||
|
return this.provincesRepository
|
||||||
|
.save(province)
|
||||||
|
.then((saved) => this.findOne(saved.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(): Promise<Province[]> {
|
||||||
|
return this.provincesRepository.find({
|
||||||
|
relations: { cities: true },
|
||||||
|
order: { name: 'ASC', cities: { name: 'ASC' } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string): Promise<Province> {
|
||||||
|
const province = await this.provincesRepository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: { cities: true },
|
||||||
|
order: { cities: { name: 'ASC' } },
|
||||||
|
});
|
||||||
|
if (!province) {
|
||||||
|
throw new NotFoundException(`Province #${id} not found`);
|
||||||
|
}
|
||||||
|
return province;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: string,
|
||||||
|
updateProvinceDto: UpdateProvinceDto,
|
||||||
|
user: AuthUser,
|
||||||
|
): Promise<Province> {
|
||||||
|
this.ensureCanManage(user);
|
||||||
|
const province = await this.findOne(id);
|
||||||
|
Object.assign(province, updateProvinceDto);
|
||||||
|
await this.provincesRepository.save(province);
|
||||||
|
return this.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string, user: AuthUser): Promise<void> {
|
||||||
|
this.ensureCanManage(user);
|
||||||
|
const province = await this.findOne(id);
|
||||||
|
await this.provincesRepository.remove(province);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCity(
|
||||||
|
provinceId: string,
|
||||||
|
createCityDto: CreateCityDto,
|
||||||
|
user: AuthUser,
|
||||||
|
): Promise<City> {
|
||||||
|
this.ensureCanManage(user);
|
||||||
|
await this.findOne(provinceId);
|
||||||
|
const city = this.citiesRepository.create({
|
||||||
|
...createCityDto,
|
||||||
|
provinceId,
|
||||||
|
});
|
||||||
|
const saved = await this.citiesRepository.save(city);
|
||||||
|
return this.findCity(provinceId, saved.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findCities(provinceId: string): Promise<City[]> {
|
||||||
|
await this.ensureProvinceExists(provinceId);
|
||||||
|
return this.citiesRepository.find({
|
||||||
|
where: { provinceId },
|
||||||
|
order: { name: 'ASC' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findCity(provinceId: string, cityId: string): Promise<City> {
|
||||||
|
const city = await this.citiesRepository.findOne({
|
||||||
|
where: { id: cityId, provinceId },
|
||||||
|
relations: { province: true },
|
||||||
|
});
|
||||||
|
if (!city) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`City #${cityId} not found for province #${provinceId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return city;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCity(
|
||||||
|
provinceId: string,
|
||||||
|
cityId: string,
|
||||||
|
updateCityDto: UpdateCityDto,
|
||||||
|
user: AuthUser,
|
||||||
|
): Promise<City> {
|
||||||
|
this.ensureCanManage(user);
|
||||||
|
const city = await this.findCity(provinceId, cityId);
|
||||||
|
Object.assign(city, updateCityDto);
|
||||||
|
await this.citiesRepository.save(city);
|
||||||
|
return this.findCity(provinceId, cityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeCity(
|
||||||
|
provinceId: string,
|
||||||
|
cityId: string,
|
||||||
|
user: AuthUser,
|
||||||
|
): Promise<void> {
|
||||||
|
this.ensureCanManage(user);
|
||||||
|
const city = await this.findCity(provinceId, cityId);
|
||||||
|
await this.citiesRepository.remove(city);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureCanManage(user: AuthUser): void {
|
||||||
|
if (!this.provincePolicy.canManage(user)) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureProvinceExists(provinceId: string): Promise<void> {
|
||||||
|
const exists = await this.provincesRepository.existsBy({ id: provinceId });
|
||||||
|
if (!exists) {
|
||||||
|
throw new NotFoundException(`Province #${provinceId} not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,12 @@ export class CreateSalonDto {
|
|||||||
@Matches(/^09\d{9}$/)
|
@Matches(/^09\d{9}$/)
|
||||||
phone: string;
|
phone: string;
|
||||||
|
|
||||||
|
@IsUUID()
|
||||||
|
provinceId: string;
|
||||||
|
|
||||||
|
@IsUUID()
|
||||||
|
cityId: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
ownerId?: string;
|
ownerId?: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsOptional, IsString, Length, Matches } from 'class-validator';
|
import { IsOptional, IsString, IsUUID, Length, Matches } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateSalonDto {
|
export class UpdateSalonDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -18,4 +18,12 @@ export class UpdateSalonDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Matches(/^09\d{9}$/)
|
@Matches(/^09\d{9}$/)
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
provinceId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
cityId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
import { City } from '../../provinces/entities/city.entity';
|
||||||
|
import { Province } from '../../provinces/entities/province.entity';
|
||||||
import { User } from '../../users/entities/user.entity';
|
import { User } from '../../users/entities/user.entity';
|
||||||
|
|
||||||
@Entity('salons')
|
@Entity('salons')
|
||||||
@@ -33,6 +35,20 @@ export class Salon {
|
|||||||
@JoinColumn({ name: 'ownerId' })
|
@JoinColumn({ name: 'ownerId' })
|
||||||
owner: User;
|
owner: User;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
provinceId: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => Province, { onDelete: 'SET NULL', nullable: true })
|
||||||
|
@JoinColumn({ name: 'provinceId' })
|
||||||
|
province: Province | null;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
cityId: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => City, { onDelete: 'SET NULL', nullable: true })
|
||||||
|
@JoinColumn({ name: 'cityId' })
|
||||||
|
city: City | null;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AuthGuardsModule } from '../auth/auth-guards.module';
|
import { AuthGuardsModule } from '../auth/auth-guards.module';
|
||||||
import { AuthorizationModule } from '../authorization/authorization.module';
|
import { AuthorizationModule } from '../authorization/authorization.module';
|
||||||
|
import { ProvincesModule } from '../provinces/provinces.module';
|
||||||
import { UsersModule } from '../users/users.module';
|
import { UsersModule } from '../users/users.module';
|
||||||
import { Salon } from './entities/salon.entity';
|
import { Salon } from './entities/salon.entity';
|
||||||
import { SalonsController } from './salons.controller';
|
import { SalonsController } from './salons.controller';
|
||||||
@@ -11,6 +12,7 @@ import { SalonsService } from './salons.service';
|
|||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Salon]),
|
TypeOrmModule.forFeature([Salon]),
|
||||||
UsersModule,
|
UsersModule,
|
||||||
|
ProvincesModule,
|
||||||
AuthorizationModule,
|
AuthorizationModule,
|
||||||
AuthGuardsModule,
|
AuthGuardsModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { SalonPolicy } from '../authorization/policies/salon.policy';
|
import { SalonPolicy } from '../authorization/policies/salon.policy';
|
||||||
|
import { ProvincesService } from '../provinces/provinces.service';
|
||||||
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
||||||
import { UserRole } from '../users/enums/user-role.enum';
|
import { UserRole } from '../users/enums/user-role.enum';
|
||||||
import { UsersService } from '../users/users.service';
|
import { UsersService } from '../users/users.service';
|
||||||
@@ -21,11 +22,16 @@ export class SalonsService {
|
|||||||
private readonly salonsRepository: Repository<Salon>,
|
private readonly salonsRepository: Repository<Salon>,
|
||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
private readonly salonPolicy: SalonPolicy,
|
private readonly salonPolicy: SalonPolicy,
|
||||||
|
private readonly provincesService: ProvincesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(createSalonDto: CreateSalonDto, user: AuthUser): Promise<Salon> {
|
async create(createSalonDto: CreateSalonDto, user: AuthUser): Promise<Salon> {
|
||||||
const ownerId = this.resolveOwnerId(createSalonDto, user);
|
const ownerId = this.resolveOwnerId(createSalonDto, user);
|
||||||
await this.ensureOwnerIsSalonOwner(ownerId);
|
await this.ensureOwnerIsSalonOwner(ownerId);
|
||||||
|
await this.provincesService.findCity(
|
||||||
|
createSalonDto.provinceId,
|
||||||
|
createSalonDto.cityId,
|
||||||
|
);
|
||||||
|
|
||||||
const salon = this.salonsRepository.create({
|
const salon = this.salonsRepository.create({
|
||||||
...createSalonDto,
|
...createSalonDto,
|
||||||
@@ -38,17 +44,19 @@ export class SalonsService {
|
|||||||
if (user?.role === UserRole.SALON) {
|
if (user?.role === UserRole.SALON) {
|
||||||
return this.salonsRepository.find({
|
return this.salonsRepository.find({
|
||||||
where: { ownerId: user.id },
|
where: { ownerId: user.id },
|
||||||
relations: { owner: true },
|
relations: { owner: true, province: true, city: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.salonsRepository.find({ relations: { owner: true } });
|
return this.salonsRepository.find({
|
||||||
|
relations: { owner: true, province: true, city: true },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: string): Promise<Salon> {
|
async findOne(id: string): Promise<Salon> {
|
||||||
const salon = await this.salonsRepository.findOne({
|
const salon = await this.salonsRepository.findOne({
|
||||||
where: { id },
|
where: { id },
|
||||||
relations: { owner: true },
|
relations: { owner: true, province: true, city: true },
|
||||||
});
|
});
|
||||||
if (!salon) {
|
if (!salon) {
|
||||||
throw new NotFoundException(`Salon #${id} not found`);
|
throw new NotFoundException(`Salon #${id} not found`);
|
||||||
@@ -65,6 +73,18 @@ export class SalonsService {
|
|||||||
if (!this.salonPolicy.canUpdate(user, salon)) {
|
if (!this.salonPolicy.canUpdate(user, salon)) {
|
||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updateSalonDto.provinceId && updateSalonDto.cityId) {
|
||||||
|
await this.provincesService.findCity(
|
||||||
|
updateSalonDto.provinceId,
|
||||||
|
updateSalonDto.cityId,
|
||||||
|
);
|
||||||
|
} else if (updateSalonDto.provinceId || updateSalonDto.cityId) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Both provinceId and cityId are required when updating location',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(salon, updateSalonDto);
|
Object.assign(salon, updateSalonDto);
|
||||||
return this.salonsRepository.save(salon);
|
return this.salonsRepository.save(salon);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -20,6 +20,7 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"noFallthroughCasesInSwitch": false
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
"resolveJsonModule": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user