module provinces
This commit is contained in:
@@ -16,6 +16,7 @@ import { StylistsModule } from './stylists/stylists.module';
|
||||
import { SubscriptionsModule } from './subscriptions/subscriptions.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { PaymentsModule } from './payments/payments.module';
|
||||
import { ProvincesModule } from './provinces/provinces.module';
|
||||
import { UploaderModule } from './uploader/uploader.module';
|
||||
|
||||
@Module({
|
||||
@@ -38,6 +39,7 @@ import { UploaderModule } from './uploader/uploader.module';
|
||||
SentMessagesModule,
|
||||
SubscriptionsModule,
|
||||
PaymentsModule,
|
||||
ProvincesModule,
|
||||
UploaderModule,
|
||||
],
|
||||
controllers: [],
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { AuthorizationModule } from '../authorization/authorization.module';
|
||||
import { Customer } from '../customers/entities/customer.entity';
|
||||
import { Salon } from '../salons/entities/salon.entity';
|
||||
import { ProvincesModule } from '../provinces/provinces.module';
|
||||
import { SmsModule } from '../sms/sms.module';
|
||||
import { SubscriptionsModule } from '../subscriptions/subscriptions.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
@@ -36,6 +37,7 @@ import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
SmsModule,
|
||||
SubscriptionsModule,
|
||||
AuthorizationModule,
|
||||
ProvincesModule,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
|
||||
@@ -14,6 +14,7 @@ import * as bcrypt from 'bcrypt';
|
||||
import { Repository, IsNull } from 'typeorm';
|
||||
import { Customer } from '../customers/entities/customer.entity';
|
||||
import { Salon } from '../salons/entities/salon.entity';
|
||||
import { ProvincesService } from '../provinces/provinces.service';
|
||||
import { SmsService } from '../sms/sms.service';
|
||||
import { SalonSubscription } from '../subscriptions/entities/salon-subscription.entity';
|
||||
import { SalonSubscriptionsService } from '../subscriptions/salon-subscriptions.service';
|
||||
@@ -69,6 +70,7 @@ export class AuthService {
|
||||
private readonly customersRepository: Repository<Customer>,
|
||||
@InjectRepository(Salon)
|
||||
private readonly salonsRepository: Repository<Salon>,
|
||||
private readonly provincesService: ProvincesService,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly salonSubscriptionsService: SalonSubscriptionsService,
|
||||
private readonly smsService: SmsService,
|
||||
@@ -274,6 +276,7 @@ export class AuthService {
|
||||
dto: VerifySalonOtpDto,
|
||||
): Promise<SalonAuthResponse> {
|
||||
this.validateSalonSignupFields(dto);
|
||||
await this.validateSalonLocation(dto.salonProvinceId!, dto.salonCityId!);
|
||||
|
||||
const user = await this.usersService.create({
|
||||
firstName: dto.firstName!,
|
||||
@@ -287,6 +290,8 @@ export class AuthService {
|
||||
description: dto.salonDescription ?? null,
|
||||
address: dto.salonAddress!,
|
||||
phone: dto.salonPhone!,
|
||||
provinceId: dto.salonProvinceId!,
|
||||
cityId: dto.salonCityId!,
|
||||
ownerId: user.id,
|
||||
});
|
||||
|
||||
@@ -301,6 +306,8 @@ export class AuthService {
|
||||
'salonName',
|
||||
'salonAddress',
|
||||
'salonPhone',
|
||||
'salonProvinceId',
|
||||
'salonCityId',
|
||||
];
|
||||
|
||||
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> {
|
||||
await this.consumeOtp(dto.mobile, dto.code);
|
||||
|
||||
@@ -390,6 +407,7 @@ export class AuthService {
|
||||
if (user.role === UserRole.SALON) {
|
||||
response.salons = await this.salonsRepository.find({
|
||||
where: { ownerId: user.id },
|
||||
relations: { province: true, city: true },
|
||||
order: { createdAt: 'ASC' },
|
||||
});
|
||||
response.activeSubscription =
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Length,
|
||||
Matches,
|
||||
} from 'class-validator';
|
||||
@@ -40,4 +41,12 @@ export class VerifySalonOtpDto {
|
||||
@IsOptional()
|
||||
@Matches(/^09\d{9}$/)
|
||||
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 { UserPermission } from './entities/user-permission.entity';
|
||||
import { CustomerPolicy } from './policies/customer.policy';
|
||||
import { ProvincePolicy } from './policies/province.policy';
|
||||
import { ReservePolicy } from './policies/reserve.policy';
|
||||
import { SalonPolicy } from './policies/salon.policy';
|
||||
import { ServicePolicy } from './policies/service.policy';
|
||||
@@ -17,6 +18,7 @@ import { SubscriptionPolicy } from './policies/subscription.policy';
|
||||
AuthorizationService,
|
||||
SalonPolicy,
|
||||
ServicePolicy,
|
||||
ProvincePolicy,
|
||||
CustomerPolicy,
|
||||
StylistPolicy,
|
||||
ReservePolicy,
|
||||
@@ -26,6 +28,7 @@ import { SubscriptionPolicy } from './policies/subscription.policy';
|
||||
AuthorizationService,
|
||||
SalonPolicy,
|
||||
ServicePolicy,
|
||||
ProvincePolicy,
|
||||
CustomerPolicy,
|
||||
StylistPolicy,
|
||||
ReservePolicy,
|
||||
|
||||
@@ -16,6 +16,9 @@ export enum PermissionCode {
|
||||
SERVICES_READ = 'services:read',
|
||||
SERVICES_WRITE = 'services:write',
|
||||
SERVICES_DELETE = 'services:delete',
|
||||
PROVINCES_READ = 'provinces:read',
|
||||
PROVINCES_WRITE = 'provinces:write',
|
||||
PROVINCES_DELETE = 'provinces:delete',
|
||||
RESERVES_READ = 'reserves:read',
|
||||
RESERVES_WRITE = 'reserves:write',
|
||||
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 { TypeOrmModule } from '@nestjs/typeorm';
|
||||
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 { SeedService } from './seed.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User, Customer])],
|
||||
imports: [TypeOrmModule.forFeature([User, Customer, Province, City])],
|
||||
providers: [SeedService],
|
||||
})
|
||||
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 { InjectRepository } from '@nestjs/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 { User } from '../users/entities/user.entity';
|
||||
import { UserRole } from '../users/enums/user-role.enum';
|
||||
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()
|
||||
export class SeedService implements OnModuleInit {
|
||||
@@ -15,10 +39,15 @@ export class SeedService implements OnModuleInit {
|
||||
private readonly usersRepository: Repository<User>,
|
||||
@InjectRepository(Customer)
|
||||
private readonly customersRepository: Repository<Customer>,
|
||||
@InjectRepository(Province)
|
||||
private readonly provincesRepository: Repository<Province>,
|
||||
@InjectRepository(City)
|
||||
private readonly citiesRepository: Repository<City>,
|
||||
) {}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
await this.seedAdminUsers();
|
||||
await this.seedIranLocations();
|
||||
}
|
||||
|
||||
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}$/)
|
||||
phone: string;
|
||||
|
||||
@IsUUID()
|
||||
provinceId: string;
|
||||
|
||||
@IsUUID()
|
||||
cityId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
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 {
|
||||
@IsOptional()
|
||||
@@ -18,4 +18,12 @@ export class UpdateSalonDto {
|
||||
@IsOptional()
|
||||
@Matches(/^09\d{9}$/)
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
provinceId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
cityId?: string;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { City } from '../../provinces/entities/city.entity';
|
||||
import { Province } from '../../provinces/entities/province.entity';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
|
||||
@Entity('salons')
|
||||
@@ -33,6 +35,20 @@ export class Salon {
|
||||
@JoinColumn({ name: 'ownerId' })
|
||||
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()
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthGuardsModule } from '../auth/auth-guards.module';
|
||||
import { AuthorizationModule } from '../authorization/authorization.module';
|
||||
import { ProvincesModule } from '../provinces/provinces.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { Salon } from './entities/salon.entity';
|
||||
import { SalonsController } from './salons.controller';
|
||||
@@ -11,6 +12,7 @@ import { SalonsService } from './salons.service';
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Salon]),
|
||||
UsersModule,
|
||||
ProvincesModule,
|
||||
AuthorizationModule,
|
||||
AuthGuardsModule,
|
||||
],
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { SalonPolicy } from '../authorization/policies/salon.policy';
|
||||
import { ProvincesService } from '../provinces/provinces.service';
|
||||
import { AuthUser } from '../auth/interfaces/auth-user.interface';
|
||||
import { UserRole } from '../users/enums/user-role.enum';
|
||||
import { UsersService } from '../users/users.service';
|
||||
@@ -21,11 +22,16 @@ export class SalonsService {
|
||||
private readonly salonsRepository: Repository<Salon>,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly salonPolicy: SalonPolicy,
|
||||
private readonly provincesService: ProvincesService,
|
||||
) {}
|
||||
|
||||
async create(createSalonDto: CreateSalonDto, user: AuthUser): Promise<Salon> {
|
||||
const ownerId = this.resolveOwnerId(createSalonDto, user);
|
||||
await this.ensureOwnerIsSalonOwner(ownerId);
|
||||
await this.provincesService.findCity(
|
||||
createSalonDto.provinceId,
|
||||
createSalonDto.cityId,
|
||||
);
|
||||
|
||||
const salon = this.salonsRepository.create({
|
||||
...createSalonDto,
|
||||
@@ -38,17 +44,19 @@ export class SalonsService {
|
||||
if (user?.role === UserRole.SALON) {
|
||||
return this.salonsRepository.find({
|
||||
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> {
|
||||
const salon = await this.salonsRepository.findOne({
|
||||
where: { id },
|
||||
relations: { owner: true },
|
||||
relations: { owner: true, province: true, city: true },
|
||||
});
|
||||
if (!salon) {
|
||||
throw new NotFoundException(`Salon #${id} not found`);
|
||||
@@ -65,6 +73,18 @@ export class SalonsService {
|
||||
if (!this.salonPolicy.canUpdate(user, salon)) {
|
||||
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);
|
||||
return this.salonsRepository.save(salon);
|
||||
}
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user