164 lines
5.3 KiB
TypeScript
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());
|