/** * One-off: attach missing business owners from contactPhone. * Usage: node --import ts-node/register/esm OR npx ts-node -r dotenv/config scripts/backfill-business-owners.ts */ import 'dotenv/config'; import { DataSource } from 'typeorm'; import { join } from 'path'; const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹'; const ARABIC_DIGITS = '٠١٢٣٤٥٦٧٨٩'; const INVISIBLE_CHARS = /[\u200c\u200d\u202a-\u202e\ufeff]/g; const IRAN_MOBILE_REGEX = /^09\d{9}$/; function toAsciiDigits(value: string): string { return value.replace(/[۰-۹٠-٩]/g, (ch) => { const persian = PERSIAN_DIGITS.indexOf(ch); if (persian >= 0) return String(persian); const arabic = ARABIC_DIGITS.indexOf(ch); if (arabic >= 0) return String(arabic); return ch; }); } function normalizeIranMobile(raw: string): string { let mobile = toAsciiDigits(String(raw ?? '').trim()); mobile = mobile.replace(INVISIBLE_CHARS, ''); let digits = mobile.replace(/\D/g, ''); if (!digits) return ''; if (digits.startsWith('0098')) digits = digits.slice(2); if (digits.startsWith('98') && digits.length >= 12) { digits = `0${digits.slice(2)}`; } else if (digits.startsWith('9') && digits.length === 10) { digits = `0${digits}`; } return digits; } function isValidIranMobile(mobile: string): boolean { return IRAN_MOBILE_REGEX.test(mobile); } async function main() { const ds = new DataSource({ type: 'postgres', host: process.env.DB_HOST, port: parseInt(process.env.POSTGRES_PORT || '5432', 10), username: process.env.POSTGRES_USER, password: process.env.POSTGRES_PASSWORD, database: process.env.POSTGRES_DB, entities: [], synchronize: false, }); await ds.initialize(); console.log('Connected to', process.env.POSTGRES_DB); const businesses: Array<{ id: string; title: string; contactPhone: string | null; ownerId: string | null; }> = await ds.query(` SELECT b.id, b.title, b."contactPhone", b."ownerId" FROM businesses b LEFT JOIN users u ON u.id = b."ownerId" WHERE b."contactPhone" IS NOT NULL AND b."contactPhone" <> '' AND (b."ownerId" IS NULL OR u.id IS NULL) ORDER BY b."createdAt" ASC `); console.log(`Found ${businesses.length} business(es) needing owner backfill`); let created = 0; let linked = 0; let skipped = 0; for (const biz of businesses) { const mobile = normalizeIranMobile(biz.contactPhone ?? ''); if (!isValidIranMobile(mobile)) { console.log(`SKIP ${biz.title}: invalid mobile "${biz.contactPhone}"`); skipped += 1; continue; } const defaultName = `مدیر ${biz.title}`; let users: Array<{ id: string; fullName: string | null; role: string }> = await ds.query(`SELECT id, "fullName", role FROM users WHERE mobile = $1 LIMIT 1`, [ mobile, ]); let userId: string; if (!users.length) { const inserted: Array<{ id: string }> = await ds.query( ` INSERT INTO users (id, mobile, "fullName", role, "isActive", "createdAt", "updatedAt") VALUES (gen_random_uuid(), $1, $2, 'business_owner', true, NOW(), NOW()) RETURNING id `, [mobile, defaultName], ); userId = inserted[0].id; created += 1; console.log(`CREATE user ${mobile} (${defaultName}) → ${biz.title}`); } else { userId = users[0].id; const updates: string[] = []; const params: unknown[] = []; if (users[0].role !== 'admin') { params.push('business_owner'); updates.push(`role = $${params.length}`); } if (!users[0].fullName?.trim()) { params.push(defaultName); updates.push(`"fullName" = $${params.length}`); } params.push(true); updates.push(`"isActive" = $${params.length}`); params.push(userId); await ds.query( `UPDATE users SET ${updates.join(', ')}, "updatedAt" = NOW() WHERE id = $${params.length}`, params, ); linked += 1; console.log(`LINK existing user ${mobile} → ${biz.title}`); } await ds.query( `UPDATE businesses SET "ownerId" = $1, "contactPhone" = $2, "updatedAt" = NOW() WHERE id = $3`, [userId, mobile, biz.id], ); } console.log(`Done. created=${created} linked=${linked} skipped=${skipped}`); await ds.destroy(); } main().catch((err) => { console.error(err); process.exit(1); });