Files
goodgo-platform/prisma/seed-b2b-accounts.ts
2026-05-07 13:08:20 +07:00

164 lines
5.3 KiB
TypeScript

/**
* One-off seed script: provision 2 B2B demo accounts (DEVELOPER + PARK_OPERATOR)
* and backfill ownership for a subset of existing projects / industrial parks.
* Safe to re-run — uses upsert semantics.
*/
import { createHash } from 'node:crypto';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcrypt';
import pg from 'pg';
const pool = new pg.Pool({ connectionString: process.env['DATABASE_URL'] });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
function getRequiredEnv(name: string): string {
const value = process.env[name]?.trim();
if (!value) {
throw new Error(`${name} must be set before running B2B seed`);
}
return value;
}
function getBcryptRounds(): number {
const raw = process.env['BCRYPT_ROUNDS'] ?? '12';
const rounds = Number.parseInt(raw, 10);
if (!Number.isInteger(rounds) || rounds < 4) {
throw new Error('BCRYPT_ROUNDS must be an integer >= 4');
}
return rounds;
}
const SEED_DEFAULT_PASSWORD = getRequiredEnv('SEED_DEFAULT_PASSWORD');
const BCRYPT_ROUNDS = getBcryptRounds();
// Matches RegisterUserHandler hashing while allowing faster rounds in tests.
async function hashPassword(raw: string): Promise<string> {
return bcrypt.hash(raw, BCRYPT_ROUNDS);
}
function hash(value: string): string {
return createHash('sha256').update(value.toLowerCase().trim()).digest('hex');
}
async function main() {
const passwordHash = await hashPassword(SEED_DEFAULT_PASSWORD);
// ── 1. DEVELOPER: CĐT Vingroup ──
const developerPhone = '+84912000001';
const developerEmail = 'cdt-vingroup@goodgo.vn';
const developer = await prisma.user.upsert({
where: { phoneHash: hash(developerPhone) },
update: { role: 'DEVELOPER', isActive: true },
create: {
id: 'seed-developer-001',
phone: developerPhone,
phoneHash: hash(developerPhone),
email: developerEmail,
emailHash: hash(developerEmail),
passwordHash,
fullName: 'CĐT Vingroup',
role: 'DEVELOPER',
kycStatus: 'VERIFIED',
avatarUrl:
'https://ui-avatars.com/api/?name=CDT+Vingroup&background=7c3aed&color=fff',
isActive: true,
},
});
// Link Vingroup-led projects.
const vingroupProjectIds = [
'seed-project-001', // Vinhomes Grand Park
'seed-project-005', // Vinhomes Central Park
'seed-project-007', // Vinhomes Ocean Park
'seed-project-010', // Vinhomes Smart City
];
const vingroupRes = await prisma.projectDevelopment.updateMany({
where: { id: { in: vingroupProjectIds } },
data: { ownerId: developer.id },
});
// ── 2. DEVELOPER: CĐT Masterise Homes ──
const devMasterPhone = '+84912000003';
const devMasterEmail = 'cdt-masterise@goodgo.vn';
const devMaster = await prisma.user.upsert({
where: { phoneHash: hash(devMasterPhone) },
update: { role: 'DEVELOPER', isActive: true },
create: {
id: 'seed-developer-002',
phone: devMasterPhone,
phoneHash: hash(devMasterPhone),
email: devMasterEmail,
emailHash: hash(devMasterEmail),
passwordHash,
fullName: 'CĐT Masterise Homes',
role: 'DEVELOPER',
kycStatus: 'VERIFIED',
avatarUrl:
'https://ui-avatars.com/api/?name=Masterise&background=6366f1&color=fff',
isActive: true,
},
});
const masterProjectIds = ['seed-project-002', 'seed-project-008']; // Masteri Thảo Điền + The Global City
const masterRes = await prisma.projectDevelopment.updateMany({
where: { id: { in: masterProjectIds } },
data: { ownerId: devMaster.id },
});
// ── 3. PARK_OPERATOR: KCN VSIP ──
const parkPhone = '+84912000002';
const parkEmail = 'kcn-vsip@goodgo.vn';
const parkOp = await prisma.user.upsert({
where: { phoneHash: hash(parkPhone) },
update: { role: 'PARK_OPERATOR', isActive: true },
create: {
id: 'seed-park-operator-001',
phone: parkPhone,
phoneHash: hash(parkPhone),
email: parkEmail,
emailHash: hash(parkEmail),
passwordHash,
fullName: 'Vận hành KCN VSIP',
role: 'PARK_OPERATOR',
kycStatus: 'VERIFIED',
avatarUrl:
'https://ui-avatars.com/api/?name=VSIP&background=0891b2&color=fff',
isActive: true,
},
});
// Link any KCN with "vsip" in the slug or name.
const parks = await prisma.industrialPark.findMany({
where: {
OR: [
{ slug: { contains: 'vsip', mode: 'insensitive' } },
{ name: { contains: 'VSIP', mode: 'insensitive' } },
],
},
select: { id: true, name: true, slug: true, ownerId: true },
});
let parkLinked = 0;
if (parks.length > 0) {
const res = await prisma.industrialPark.updateMany({
where: { id: { in: parks.map((p) => p.id) } },
data: { ownerId: parkOp.id },
});
parkLinked = res.count;
}
console.log('─── B2B seed summary ───');
console.log(`DEVELOPER: ${developer.fullName} (${developerPhone}) — linked ${vingroupRes.count} projects`);
console.log(`DEVELOPER: ${devMaster.fullName} (${devMasterPhone}) — linked ${masterRes.count} projects`);
console.log(`PARK_OPERATOR: ${parkOp.fullName} (${parkPhone}) — linked ${parkLinked} KCN(s)`);
console.log('Password for all: configured via SEED_DEFAULT_PASSWORD');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(() => prisma.$disconnect());