Files
serverMontime/scripts/backfill-business-owners.ts
2026-07-15 19:14:12 +03:30

144 lines
4.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});