add business_owner
This commit is contained in:
@@ -147,6 +147,13 @@ export class BusinessesController {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Post('backfill-owners')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
backfillOwners(): Promise<{ filled: number; skipped: number }> {
|
||||
return this.service.backfillMissingOwners();
|
||||
}
|
||||
|
||||
@Post(':id/assign-owner')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
|
||||
@@ -9,6 +9,10 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { deleteBusinessCascade } from '../../common/utils/business-cascade.util';
|
||||
import { parseHmToMinute } from '../../common/utils/booking-schedule.util';
|
||||
import {
|
||||
isValidIranMobile,
|
||||
normalizeIranMobile,
|
||||
} from '../../common/utils/iran-mobile.util';
|
||||
import { resolveUniqueSlug, normalizeSlugInput } from '../../common/utils/slug.util';
|
||||
import { BusinessAvailabilitySlotEntity } from '../../entities/business-availability-slot.entity';
|
||||
import { BusinessCategoryEntity } from '../../entities/business-category.entity';
|
||||
@@ -114,13 +118,15 @@ export class BusinessesService {
|
||||
|
||||
await this.ensureCategoryExists(payload.categoryId);
|
||||
|
||||
const contactPhone = this.normalizeOptionalContactPhone(payload.contactPhone);
|
||||
|
||||
const entity = this.repo.create({
|
||||
title: payload.title,
|
||||
slug,
|
||||
subtitle: payload.subtitle ?? null,
|
||||
categoryId: payload.categoryId,
|
||||
description: payload.description ?? null,
|
||||
contactPhone: payload.contactPhone ?? null,
|
||||
contactPhone,
|
||||
address: payload.address ?? null,
|
||||
latitude: payload.latitude ?? null,
|
||||
longitude: payload.longitude ?? null,
|
||||
@@ -128,7 +134,8 @@ export class BusinessesService {
|
||||
isActive: payload.isActive ?? true,
|
||||
});
|
||||
|
||||
return this.repo.save(entity);
|
||||
const saved = await this.repo.save(entity);
|
||||
return this.ensureOwnerFromContactPhone(saved);
|
||||
}
|
||||
|
||||
async update(
|
||||
@@ -150,8 +157,13 @@ export class BusinessesService {
|
||||
await this.ensureCategoryExists(payload.categoryId);
|
||||
}
|
||||
|
||||
if (payload.contactPhone !== undefined) {
|
||||
payload.contactPhone = this.normalizeOptionalContactPhone(payload.contactPhone);
|
||||
}
|
||||
|
||||
Object.assign(business, payload);
|
||||
return this.repo.save(business);
|
||||
const saved = await this.repo.save(business);
|
||||
return this.ensureOwnerFromContactPhone(saved);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
@@ -217,13 +229,22 @@ export class BusinessesService {
|
||||
dto: AssignBusinessOwnerDto,
|
||||
): Promise<BusinessEntity> {
|
||||
const business = await this.findOne(id);
|
||||
const mobile = this.normalizeMobile(dto.mobile);
|
||||
const mobile = normalizeIranMobile(dto.mobile);
|
||||
if (!isValidIranMobile(mobile)) {
|
||||
throw new BadRequestException('Invalid mobile number');
|
||||
}
|
||||
|
||||
const defaultName = `مدیر ${business.title}`;
|
||||
const resolvedName =
|
||||
dto.fullName === undefined
|
||||
? undefined
|
||||
: dto.fullName?.trim() || defaultName;
|
||||
|
||||
let user = await this.userRepo.findOne({ where: { mobile } });
|
||||
if (!user) {
|
||||
user = this.userRepo.create({
|
||||
mobile,
|
||||
fullName: dto.fullName ?? null,
|
||||
fullName: resolvedName ?? defaultName,
|
||||
role: UserRole.BUSINESS_OWNER,
|
||||
isActive: true,
|
||||
});
|
||||
@@ -231,17 +252,80 @@ export class BusinessesService {
|
||||
if (user.role !== UserRole.ADMIN) {
|
||||
user.role = UserRole.BUSINESS_OWNER;
|
||||
}
|
||||
if (dto.fullName !== undefined) {
|
||||
user.fullName = dto.fullName;
|
||||
if (resolvedName !== undefined) {
|
||||
user.fullName = resolvedName;
|
||||
} else if (!user.fullName?.trim()) {
|
||||
user.fullName = defaultName;
|
||||
}
|
||||
user.isActive = true;
|
||||
}
|
||||
|
||||
const savedUser = await this.userRepo.save(user);
|
||||
business.ownerId = savedUser.id;
|
||||
if (!business.contactPhone) {
|
||||
business.contactPhone = mobile;
|
||||
}
|
||||
return this.repo.save(business);
|
||||
}
|
||||
|
||||
async backfillMissingOwners(): Promise<{ filled: number; skipped: number }> {
|
||||
const withoutOwner = await this.repo
|
||||
.createQueryBuilder('business')
|
||||
.where('business.ownerId IS NULL')
|
||||
.andWhere('business.contactPhone IS NOT NULL')
|
||||
.andWhere("business.contactPhone <> ''")
|
||||
.getMany();
|
||||
|
||||
let filled = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const business of withoutOwner) {
|
||||
const mobile = normalizeIranMobile(business.contactPhone ?? '');
|
||||
if (!isValidIranMobile(mobile)) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
const existing = await this.userRepo.findOne({ where: { mobile } });
|
||||
await this.assignOwner(business.id, {
|
||||
mobile,
|
||||
...(existing ? {} : { fullName: `مدیر ${business.title}` }),
|
||||
});
|
||||
filled += 1;
|
||||
}
|
||||
|
||||
return { filled, skipped };
|
||||
}
|
||||
|
||||
/** Create/attach owner user from contact phone when the business has none yet. */
|
||||
private async ensureOwnerFromContactPhone(
|
||||
business: BusinessEntity,
|
||||
): Promise<BusinessEntity> {
|
||||
if (business.ownerId || !business.contactPhone) {
|
||||
return business;
|
||||
}
|
||||
|
||||
const mobile = normalizeIranMobile(business.contactPhone);
|
||||
if (!isValidIranMobile(mobile)) {
|
||||
return business;
|
||||
}
|
||||
|
||||
const existing = await this.userRepo.findOne({ where: { mobile } });
|
||||
return this.assignOwner(business.id, {
|
||||
mobile,
|
||||
...(existing ? {} : { fullName: `مدیر ${business.title}` }),
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeOptionalContactPhone(
|
||||
value: string | null | undefined,
|
||||
): string | null {
|
||||
if (value == null) return null;
|
||||
const trimmed = String(value).trim();
|
||||
if (!trimmed) return null;
|
||||
const mobile = normalizeIranMobile(trimmed);
|
||||
return isValidIranMobile(mobile) ? mobile : trimmed;
|
||||
}
|
||||
|
||||
private async attachCoverImages(businesses: BusinessEntity[]): Promise<void> {
|
||||
if (!businesses.length) {
|
||||
return;
|
||||
@@ -395,14 +479,6 @@ export class BusinessesService {
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeMobile(mobile: string): string {
|
||||
const normalized = String(mobile ?? '').trim();
|
||||
if (!/^(\+?\d{10,15}|09\d{9})$/.test(normalized)) {
|
||||
throw new BadRequestException('Invalid mobile number');
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private async attachReviewStats(businesses: BusinessEntity[]): Promise<void> {
|
||||
if (!businesses.length) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user