344 lines
11 KiB
TypeScript
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;
|
|
});
|
|
}
|