add business_owner

This commit is contained in:
2026-07-15 19:14:12 +03:30
parent 3844594506
commit aa32dae736
3 changed files with 241 additions and 15 deletions
@@ -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)
+91 -15
View File
@@ -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;