task-b
task-b.1 complere
This commit is contained in:
298
src/db/seeds/foundation.seed.ts
Normal file
298
src/db/seeds/foundation.seed.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
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_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_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_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_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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user