Files
alla-allaos-fullstack/src/db/seeds/foundation.seed.ts
phaichayon b8f13e36b3 task-e
2026-06-16 08:43:19 +07:00

344 lines
11 KiB
TypeScript

const fs = require('node:fs');
const path = require('node:path');
const postgres = require('postgres');
type SqlClient = ReturnType<typeof postgres>;
type SeedOption = {
code: string;
label: string;
value: string;
sortOrder: number;
};
type BranchSeedRow = {
id: string;
category: string;
code: string;
label: string;
};
function loadEnvFile(filePath: string) {
if (!fs.existsSync(filePath)) {
return;
}
const content = fs.readFileSync(filePath, 'utf8');
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) {
continue;
}
const separatorIndex = line.indexOf('=');
if (separatorIndex === -1) {
continue;
}
const key = line.slice(0, separatorIndex).trim();
let value = line.slice(separatorIndex + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
if (!(key in process.env)) {
process.env[key] = value;
}
}
}
function loadLocalEnv() {
loadEnvFile(path.resolve(process.cwd(), '.env.local'));
loadEnvFile(path.resolve(process.cwd(), '.env'));
}
function requireEnv(name: string) {
const value = process.env[name]?.trim();
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
function getCurrentPeriod(date = new Date()) {
const year = String(date.getFullYear()).slice(-2);
const month = String(date.getMonth() + 1).padStart(2, '0');
return `${year}${month}`;
}
const FOUNDATION_OPTIONS = {
crm_branch: [
{ code: 'head_office', label: 'สำนักงานใหญ่', value: 'head_office', sortOrder: 1 },
{ code: 'factory', label: 'Factory', value: 'factory', sortOrder: 2 },
{ code: 'service', label: 'Service', value: 'service', sortOrder: 3 }
],
crm_customer_status: [
{ code: 'active', label: 'Active', value: 'active', sortOrder: 1 },
{ code: 'inactive', label: 'Inactive', value: 'inactive', sortOrder: 2 },
{ code: 'suspended', label: 'Suspended', value: 'suspended', sortOrder: 3 }
],
crm_customer_type: [
{ code: 'company', label: 'Company', value: 'company', sortOrder: 1 },
{ code: 'individual', label: 'Individual', value: 'individual', sortOrder: 2 }
],
crm_customer_group: [
{ code: 'strategic', label: 'Strategic', value: 'strategic', sortOrder: 1 },
{ code: 'standard', label: 'Standard', value: 'standard', sortOrder: 2 },
{ code: 'project', label: 'Project', value: 'project', sortOrder: 3 }
],
crm_enquiry_status: [
{ code: 'new', label: 'New', value: 'new', sortOrder: 1 },
{ code: 'qualifying', label: 'Qualifying', value: 'qualifying', sortOrder: 2 },
{ code: 'requirement', label: 'Requirement', value: 'requirement', sortOrder: 3 },
{ code: 'follow_up', label: 'Follow Up', value: 'follow_up', sortOrder: 4 },
{ code: 'converted', label: 'Converted', value: 'converted', sortOrder: 5 },
{ code: 'closed_lost', label: 'Closed Lost', value: 'closed_lost', sortOrder: 6 },
{ code: 'cancelled', label: 'Cancelled', value: 'cancelled', sortOrder: 7 }
],
crm_quotation_status: [
{ code: 'draft', label: 'Draft', value: 'draft', sortOrder: 1 },
{
code: 'pending_approval',
label: 'Pending Approval',
value: 'pending_approval',
sortOrder: 2
},
{ code: 'approved', label: 'Approved', value: 'approved', sortOrder: 3 },
{ code: 'rejected', label: 'Rejected', value: 'rejected', sortOrder: 4 },
{ code: 'sent', label: 'Sent', value: 'sent', sortOrder: 5 },
{ code: 'accepted', label: 'Accepted', value: 'accepted', sortOrder: 6 },
{ code: 'lost', label: 'Lost', value: 'lost', sortOrder: 7 },
{ code: 'cancelled', label: 'Cancelled', value: 'cancelled', sortOrder: 8 },
{ code: 'revised', label: 'Revised', value: 'revised', sortOrder: 9 }
],
crm_quotation_type: [
{ code: 'crane', label: 'Crane', value: 'crane', sortOrder: 1 },
{ code: 'dockdoor', label: 'Dock Door', value: 'dockdoor', sortOrder: 2 },
{ code: 'solarcell', label: 'Solar Cell', value: 'solarcell', sortOrder: 3 },
{ code: 'service', label: 'Service', value: 'service', sortOrder: 4 },
{ code: 'other', label: 'Other', value: 'other', sortOrder: 5 }
],
crm_product_type: [
{ code: 'crane', label: 'Crane', value: 'crane', sortOrder: 1 },
{ code: 'dockdoor', label: 'Dock Door', value: 'dockdoor', sortOrder: 2 },
{ code: 'solarcell', label: 'Solar Cell', value: 'solarcell', sortOrder: 3 }
],
crm_currency: [
{ code: 'THB', label: 'THB', value: 'THB', sortOrder: 1 },
{ code: 'USD', label: 'USD', value: 'USD', sortOrder: 2 },
{ code: 'EUR', label: 'EUR', value: 'EUR', sortOrder: 3 }
],
crm_discount_type: [
{ code: 'fixed', label: 'Fixed', value: 'fixed', sortOrder: 1 },
{ code: 'percentage', label: 'Percentage', value: 'percentage', sortOrder: 2 }
],
crm_unit: [
{ code: 'pcs', label: 'PCS', value: 'pcs', sortOrder: 1 },
{ code: 'set', label: 'Set', value: 'set', sortOrder: 2 },
{ code: 'lot', label: 'Lot', value: 'lot', sortOrder: 3 },
{ code: 'job', label: 'Job', value: 'job', sortOrder: 4 }
],
crm_sent_via: [
{ code: 'email', label: 'Email', value: 'email', sortOrder: 1 },
{ code: 'manual', label: 'Manual', value: 'manual', sortOrder: 2 },
{ code: 'system', label: 'System', value: 'system', sortOrder: 3 }
],
crm_quotation_customer_role: [
{ code: 'owner', label: 'Owner', value: 'owner', sortOrder: 1 },
{ code: 'consultant', label: 'Consultant', value: 'consultant', sortOrder: 2 },
{ code: 'contractor', label: 'Contractor', value: 'contractor', sortOrder: 3 },
{ code: 'billing', label: 'Billing', value: 'billing', sortOrder: 4 }
],
crm_quotation_topic_type: [
{ code: 'scope', label: 'Scope', value: 'scope', sortOrder: 1 },
{ code: 'exclusion', label: 'Exclusion', value: 'exclusion', sortOrder: 2 },
{ code: 'payment', label: 'Payment', value: 'payment', sortOrder: 3 }
],
crm_payment_term: [
{ code: 'cash', label: 'Cash', value: 'cash', sortOrder: 1 },
{ code: 'credit_30', label: 'Credit 30', value: 'credit_30', sortOrder: 2 },
{ code: 'credit_60', label: 'Credit 60', value: 'credit_60', sortOrder: 3 }
],
crm_priority: [
{ code: 'low', label: 'Low', value: 'low', sortOrder: 1 },
{ code: 'normal', label: 'Normal', value: 'normal', sortOrder: 2 },
{ code: 'high', label: 'High', value: 'high', sortOrder: 3 },
{ code: 'urgent', label: 'Urgent', value: 'urgent', sortOrder: 4 }
],
crm_followup_type: [
{ code: 'call', label: 'Call', value: 'call', sortOrder: 1 },
{ code: 'meeting', label: 'Meeting', value: 'meeting', sortOrder: 2 },
{ code: 'site_visit', label: 'Site Visit', value: 'site_visit', sortOrder: 3 },
{ code: 'email', label: 'Email', value: 'email', sortOrder: 4 },
{ code: 'line', label: 'LINE', value: 'line', sortOrder: 5 }
],
crm_lead_channel: [
{ code: 'website', label: 'Website', value: 'website', sortOrder: 1 },
{ code: 'referral', label: 'Referral', value: 'referral', sortOrder: 2 },
{ code: 'sales', label: 'Sales', value: 'sales', sortOrder: 3 },
{ code: 'facebook', label: 'Facebook', value: 'facebook', sortOrder: 4 },
{ code: 'line', label: 'LINE', value: 'line', sortOrder: 5 },
{ code: 'other', label: 'Other', value: 'other', sortOrder: 6 }
]
};
const DOCUMENT_SEQUENCES = [
{ documentType: 'customer', prefix: 'CUS' },
{ documentType: 'contact', prefix: 'CON' },
{ documentType: 'enquiry', prefix: 'ENQ' },
{ documentType: 'quotation', prefix: 'QT' },
{ documentType: 'approval', prefix: 'APV' }
];
async function upsertMasterOptionsForOrganization(
sql: SqlClient,
organizationId: string
): Promise<BranchSeedRow[]> {
const branchRows: BranchSeedRow[] = [];
for (const [category, options] of Object.entries(FOUNDATION_OPTIONS) as Array<
[string, SeedOption[]]
>) {
for (const option of options) {
const [row] = await sql`
insert into ms_options (
id,
organization_id,
category,
code,
label,
value,
parent_id,
sort_order,
is_active,
metadata,
deleted_at
) values (
${crypto.randomUUID()},
${organizationId},
${category},
${option.code},
${option.label},
${option.value},
${null},
${option.sortOrder},
${true},
${null},
${null}
)
on conflict (organization_id, category, code) do update
set
label = excluded.label,
value = excluded.value,
parent_id = excluded.parent_id,
sort_order = excluded.sort_order,
is_active = excluded.is_active,
metadata = excluded.metadata,
deleted_at = excluded.deleted_at,
updated_at = now()
returning id, category, code, label
`;
if (category === 'crm_branch') {
branchRows.push(row);
}
}
}
return branchRows;
}
async function upsertDocumentSequencesForOrganization(
sql: SqlClient,
organizationId: string,
branchRows: BranchSeedRow[]
) {
if (!branchRows.length) {
throw new Error(
`No crm_branch options were found for organization ${organizationId}. Foundation branch seed must run first.`
);
}
const period = getCurrentPeriod();
for (const branch of branchRows) {
for (const sequence of DOCUMENT_SEQUENCES) {
await sql`
insert into document_sequences (
id,
organization_id,
branch_id,
document_type,
prefix,
period,
current_number,
padding_length,
is_active
) values (
${crypto.randomUUID()},
${organizationId},
${branch.id},
${sequence.documentType},
${sequence.prefix},
${period},
${0},
${3},
${true}
)
on conflict (organization_id, document_type, period, branch_id) do update
set
prefix = excluded.prefix,
padding_length = excluded.padding_length,
is_active = excluded.is_active,
updated_at = now()
`;
}
}
}
async function main() {
loadLocalEnv();
const databaseUrl = requireEnv('DATABASE_URL');
const sql = postgres(databaseUrl, { prepare: false });
try {
const organizations = await sql`
select id, name
from organizations
order by name asc
`;
if (organizations.length === 0) {
throw new Error(
'No organizations found. Create at least one organization before running foundation seed.'
);
}
for (const organization of organizations) {
const branchRows = await upsertMasterOptionsForOrganization(sql, organization.id);
await upsertDocumentSequencesForOrganization(sql, organization.id, branchRows);
console.log(`[seed-foundation] Seeded foundation data for ${organization.name}`);
}
} finally {
await sql.end();
}
}
if (require.main === module) {
main().catch((error) => {
console.error('[seed-foundation] Failed:', error.message);
process.exitCode = 1;
});
}