task-h.1 complate
This commit is contained in:
955
scripts/seed-task-h1-fixture.js
Normal file
955
scripts/seed-task-h1-fixture.js
Normal file
@@ -0,0 +1,955 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { hash } = require('bcryptjs');
|
||||
const postgres = require('postgres');
|
||||
|
||||
const TASK_H1_PASSWORD = 'TaskH1Pass!2026';
|
||||
const FIXTURE_CODES = {
|
||||
customer: 'CUS-TASK-H1',
|
||||
approvedQuotation: 'QT-TASK-H1-APPROVED',
|
||||
draftQuotation: 'QT-TASK-H1-DRAFT'
|
||||
};
|
||||
|
||||
const FIXTURE_USERS = {
|
||||
admin: 'task-h1-admin@local.test',
|
||||
pdfUser: 'task-h1-pdf-user@local.test',
|
||||
viewer: 'task-h1-viewer@local.test'
|
||||
};
|
||||
|
||||
const PDF_PERMISSIONS = [
|
||||
'crm.quotation.read',
|
||||
'crm.quotation.document.preview',
|
||||
'crm.quotation.pdf.preview',
|
||||
'crm.quotation.pdf.download',
|
||||
'crm.quotation.pdf.generate_approved'
|
||||
];
|
||||
|
||||
function loadEnvFile(filePath) {
|
||||
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) {
|
||||
const value = process.env[name]?.trim();
|
||||
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${name}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async function resolveOptionId(sql, organizationId, category, code) {
|
||||
const [row] = await sql`
|
||||
select id
|
||||
from ms_options
|
||||
where organization_id = ${organizationId}
|
||||
and category = ${category}
|
||||
and code = ${code}
|
||||
and is_active = true
|
||||
and deleted_at is null
|
||||
limit 1
|
||||
`;
|
||||
|
||||
if (!row) {
|
||||
throw new Error(`Missing option ${category}:${code}`);
|
||||
}
|
||||
|
||||
return row.id;
|
||||
}
|
||||
|
||||
async function resolveApprovalWorkflow(sql, organizationId) {
|
||||
const [workflow] = await sql`
|
||||
select id
|
||||
from crm_approval_workflows
|
||||
where organization_id = ${organizationId}
|
||||
and code = 'quotation_standard_approval'
|
||||
and is_active = true
|
||||
and deleted_at is null
|
||||
limit 1
|
||||
`;
|
||||
|
||||
if (!workflow) {
|
||||
throw new Error('Missing quotation_standard_approval workflow');
|
||||
}
|
||||
|
||||
const steps = await sql`
|
||||
select step_number as "stepNumber", role_code as "roleCode", role_name as "roleName"
|
||||
from crm_approval_steps
|
||||
where organization_id = ${organizationId}
|
||||
and workflow_id = ${workflow.id}
|
||||
and deleted_at is null
|
||||
order by step_number asc
|
||||
`;
|
||||
|
||||
if (!steps.length) {
|
||||
throw new Error('Approval workflow has no steps');
|
||||
}
|
||||
|
||||
return {
|
||||
id: workflow.id,
|
||||
steps
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureUser(sql, organization, creatorId, email, name, membership) {
|
||||
const passwordHash = await hash(TASK_H1_PASSWORD, 10);
|
||||
const existing =
|
||||
(
|
||||
await sql`
|
||||
select id
|
||||
from users
|
||||
where email = ${email}
|
||||
limit 1
|
||||
`
|
||||
)[0] ?? null;
|
||||
const userId = existing?.id ?? crypto.randomUUID();
|
||||
|
||||
if (existing) {
|
||||
await sql`
|
||||
update users
|
||||
set
|
||||
name = ${name},
|
||||
password_hash = ${passwordHash},
|
||||
system_role = 'user',
|
||||
active_organization_id = ${organization.id},
|
||||
updated_at = now()
|
||||
where id = ${userId}
|
||||
`;
|
||||
} else {
|
||||
await sql`
|
||||
insert into users (
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
password_hash,
|
||||
system_role,
|
||||
active_organization_id
|
||||
) values (
|
||||
${userId},
|
||||
${name},
|
||||
${email},
|
||||
${passwordHash},
|
||||
'user',
|
||||
${organization.id}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
const membershipRow =
|
||||
(
|
||||
await sql`
|
||||
select id
|
||||
from memberships
|
||||
where user_id = ${userId}
|
||||
and organization_id = ${organization.id}
|
||||
limit 1
|
||||
`
|
||||
)[0] ?? null;
|
||||
|
||||
if (membershipRow) {
|
||||
await sql`
|
||||
update memberships
|
||||
set
|
||||
role = ${membership.role},
|
||||
business_role = ${membership.businessRole},
|
||||
permissions = ${membership.permissions},
|
||||
updated_at = now()
|
||||
where id = ${membershipRow.id}
|
||||
`;
|
||||
} else {
|
||||
await sql`
|
||||
insert into memberships (
|
||||
id,
|
||||
user_id,
|
||||
organization_id,
|
||||
role,
|
||||
business_role,
|
||||
permissions
|
||||
) values (
|
||||
${crypto.randomUUID()},
|
||||
${userId},
|
||||
${organization.id},
|
||||
${membership.role},
|
||||
${membership.businessRole},
|
||||
${membership.permissions}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
return {
|
||||
id: userId,
|
||||
email
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureCustomerAndContact(sql, organization, creatorId, optionIds) {
|
||||
const existingCustomer =
|
||||
(
|
||||
await sql`
|
||||
select id
|
||||
from crm_customers
|
||||
where organization_id = ${organization.id}
|
||||
and code = ${FIXTURE_CODES.customer}
|
||||
limit 1
|
||||
`
|
||||
)[0] ?? null;
|
||||
const customerId = existingCustomer?.id ?? crypto.randomUUID();
|
||||
|
||||
if (existingCustomer) {
|
||||
await sql`
|
||||
update crm_customers
|
||||
set
|
||||
branch_id = ${optionIds.branchId},
|
||||
name = 'Task H.1 Demo Customer',
|
||||
abbr = 'TASKH1',
|
||||
tax_id = '0105559001122',
|
||||
customer_type = ${optionIds.customerTypeCompanyId},
|
||||
customer_status = ${optionIds.customerStatusActiveId},
|
||||
address = '99/9 Rama IX Road, Bangkok 10310',
|
||||
phone = '02-000-1000',
|
||||
email = 'demo-customer@local.test',
|
||||
lead_channel = ${optionIds.leadChannelSalesId},
|
||||
customer_group = ${optionIds.customerGroupStandardId},
|
||||
notes = 'Task H.1 smoke-test customer',
|
||||
is_active = true,
|
||||
deleted_at = null,
|
||||
updated_at = now(),
|
||||
updated_by = ${creatorId}
|
||||
where id = ${customerId}
|
||||
`;
|
||||
} else {
|
||||
await sql`
|
||||
insert into crm_customers (
|
||||
id,
|
||||
organization_id,
|
||||
branch_id,
|
||||
code,
|
||||
name,
|
||||
abbr,
|
||||
tax_id,
|
||||
customer_type,
|
||||
customer_status,
|
||||
address,
|
||||
phone,
|
||||
email,
|
||||
lead_channel,
|
||||
customer_group,
|
||||
notes,
|
||||
is_active,
|
||||
created_by,
|
||||
updated_by
|
||||
) values (
|
||||
${customerId},
|
||||
${organization.id},
|
||||
${optionIds.branchId},
|
||||
${FIXTURE_CODES.customer},
|
||||
'Task H.1 Demo Customer',
|
||||
'TASKH1',
|
||||
'0105559001122',
|
||||
${optionIds.customerTypeCompanyId},
|
||||
${optionIds.customerStatusActiveId},
|
||||
'99/9 Rama IX Road, Bangkok 10310',
|
||||
'02-000-1000',
|
||||
'demo-customer@local.test',
|
||||
${optionIds.leadChannelSalesId},
|
||||
${optionIds.customerGroupStandardId},
|
||||
'Task H.1 smoke-test customer',
|
||||
true,
|
||||
${creatorId},
|
||||
${creatorId}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
const existingContact =
|
||||
(
|
||||
await sql`
|
||||
select id
|
||||
from crm_customer_contacts
|
||||
where organization_id = ${organization.id}
|
||||
and customer_id = ${customerId}
|
||||
and email = 'demo-contact@local.test'
|
||||
limit 1
|
||||
`
|
||||
)[0] ?? null;
|
||||
const contactId = existingContact?.id ?? crypto.randomUUID();
|
||||
|
||||
if (existingContact) {
|
||||
await sql`
|
||||
update crm_customer_contacts
|
||||
set
|
||||
name = 'Task H.1 Contact',
|
||||
position = 'Procurement Manager',
|
||||
department = 'Procurement',
|
||||
phone = '02-000-1001',
|
||||
mobile = '081-000-1001',
|
||||
email = 'demo-contact@local.test',
|
||||
is_primary = true,
|
||||
notes = 'Primary smoke-test contact',
|
||||
is_active = true,
|
||||
deleted_at = null,
|
||||
updated_at = now(),
|
||||
updated_by = ${creatorId}
|
||||
where id = ${contactId}
|
||||
`;
|
||||
} else {
|
||||
await sql`
|
||||
insert into crm_customer_contacts (
|
||||
id,
|
||||
organization_id,
|
||||
customer_id,
|
||||
name,
|
||||
position,
|
||||
department,
|
||||
phone,
|
||||
mobile,
|
||||
email,
|
||||
is_primary,
|
||||
notes,
|
||||
is_active,
|
||||
created_by,
|
||||
updated_by
|
||||
) values (
|
||||
${contactId},
|
||||
${organization.id},
|
||||
${customerId},
|
||||
'Task H.1 Contact',
|
||||
'Procurement Manager',
|
||||
'Procurement',
|
||||
'02-000-1001',
|
||||
'081-000-1001',
|
||||
'demo-contact@local.test',
|
||||
true,
|
||||
'Primary smoke-test contact',
|
||||
true,
|
||||
${creatorId},
|
||||
${creatorId}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
return {
|
||||
customerId,
|
||||
contactId
|
||||
};
|
||||
}
|
||||
|
||||
async function upsertQuotation(sql, args) {
|
||||
const {
|
||||
organization,
|
||||
creatorId,
|
||||
workflow,
|
||||
optionIds,
|
||||
customerId,
|
||||
contactId,
|
||||
code,
|
||||
approved
|
||||
} = args;
|
||||
const existing =
|
||||
(
|
||||
await sql`
|
||||
select id
|
||||
from crm_quotations
|
||||
where organization_id = ${organization.id}
|
||||
and code = ${code}
|
||||
limit 1
|
||||
`
|
||||
)[0] ?? null;
|
||||
const quotationId = existing?.id ?? crypto.randomUUID();
|
||||
const statusId = approved ? optionIds.quotationStatusApprovedId : optionIds.quotationStatusDraftId;
|
||||
const approvalNow = approved ? new Date().toISOString() : null;
|
||||
const notes = approved
|
||||
? 'Approved quotation fixture for Task H.1 PDF stabilization.'
|
||||
: 'Draft quotation fixture for Task H.1 negative API validation.';
|
||||
|
||||
if (existing) {
|
||||
await sql`
|
||||
update crm_quotations
|
||||
set
|
||||
branch_id = ${optionIds.branchId},
|
||||
enquiry_id = null,
|
||||
customer_id = ${customerId},
|
||||
contact_id = ${contactId},
|
||||
quotation_date = ${'2026-06-16T00:00:00.000Z'},
|
||||
valid_until = ${'2026-07-16T00:00:00.000Z'},
|
||||
quotation_type = ${optionIds.quotationTypeServiceId},
|
||||
project_name = ${approved ? 'Task H.1 Approved PDF Fixture' : 'Task H.1 Draft PDF Fixture'},
|
||||
project_location = 'Bangkok HQ',
|
||||
attention = 'Task H.1 Contact',
|
||||
reference = ${approved ? 'TASK-H1-APPROVED' : 'TASK-H1-DRAFT'},
|
||||
notes = ${notes},
|
||||
status = ${statusId},
|
||||
revision = 0,
|
||||
parent_quotation_id = null,
|
||||
revision_remark = null,
|
||||
currency = ${optionIds.currencyThbId},
|
||||
exchange_rate = 1,
|
||||
subtotal = 125000,
|
||||
discount = 5000,
|
||||
discount_type = ${optionIds.discountPercentageId},
|
||||
tax_rate = 7,
|
||||
tax_amount = 8400,
|
||||
total_amount = 128400,
|
||||
chance_percent = 90,
|
||||
is_hot_project = true,
|
||||
competitor = 'Task H.1 Competitor',
|
||||
salesman_id = ${creatorId},
|
||||
is_sent = ${approved},
|
||||
sent_at = ${approved ? '2026-06-16T01:00:00.000Z' : null},
|
||||
sent_via = ${optionIds.sentViaEmailId},
|
||||
approved_at = ${approvalNow},
|
||||
approved_pdf_url = null,
|
||||
approved_snapshot = null,
|
||||
approved_template_version_id = null,
|
||||
accepted_at = null,
|
||||
rejected_at = null,
|
||||
rejection_reason = null,
|
||||
is_active = true,
|
||||
deleted_at = null,
|
||||
updated_at = now(),
|
||||
updated_by = ${creatorId}
|
||||
where id = ${quotationId}
|
||||
`;
|
||||
} else {
|
||||
await sql`
|
||||
insert into crm_quotations (
|
||||
id,
|
||||
organization_id,
|
||||
branch_id,
|
||||
code,
|
||||
enquiry_id,
|
||||
customer_id,
|
||||
contact_id,
|
||||
quotation_date,
|
||||
valid_until,
|
||||
quotation_type,
|
||||
project_name,
|
||||
project_location,
|
||||
attention,
|
||||
reference,
|
||||
notes,
|
||||
status,
|
||||
revision,
|
||||
parent_quotation_id,
|
||||
revision_remark,
|
||||
currency,
|
||||
exchange_rate,
|
||||
subtotal,
|
||||
discount,
|
||||
discount_type,
|
||||
tax_rate,
|
||||
tax_amount,
|
||||
total_amount,
|
||||
chance_percent,
|
||||
is_hot_project,
|
||||
competitor,
|
||||
salesman_id,
|
||||
is_sent,
|
||||
sent_at,
|
||||
sent_via,
|
||||
approved_at,
|
||||
approved_pdf_url,
|
||||
approved_snapshot,
|
||||
approved_template_version_id,
|
||||
accepted_at,
|
||||
rejected_at,
|
||||
rejection_reason,
|
||||
is_active,
|
||||
created_by,
|
||||
updated_by
|
||||
) values (
|
||||
${quotationId},
|
||||
${organization.id},
|
||||
${optionIds.branchId},
|
||||
${code},
|
||||
null,
|
||||
${customerId},
|
||||
${contactId},
|
||||
${'2026-06-16T00:00:00.000Z'},
|
||||
${'2026-07-16T00:00:00.000Z'},
|
||||
${optionIds.quotationTypeServiceId},
|
||||
${approved ? 'Task H.1 Approved PDF Fixture' : 'Task H.1 Draft PDF Fixture'},
|
||||
'Bangkok HQ',
|
||||
'Task H.1 Contact',
|
||||
${approved ? 'TASK-H1-APPROVED' : 'TASK-H1-DRAFT'},
|
||||
${notes},
|
||||
${statusId},
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
${optionIds.currencyThbId},
|
||||
1,
|
||||
125000,
|
||||
5000,
|
||||
${optionIds.discountPercentageId},
|
||||
7,
|
||||
8400,
|
||||
128400,
|
||||
90,
|
||||
true,
|
||||
'Task H.1 Competitor',
|
||||
${creatorId},
|
||||
${approved},
|
||||
${approved ? '2026-06-16T01:00:00.000Z' : null},
|
||||
${optionIds.sentViaEmailId},
|
||||
${approvalNow},
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
${creatorId},
|
||||
${creatorId}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
await sql`delete from crm_quotation_attachments where quotation_id = ${quotationId}`;
|
||||
await sql`
|
||||
delete from crm_approval_actions
|
||||
where approval_request_id in (
|
||||
select id
|
||||
from crm_approval_requests
|
||||
where entity_type = 'quotation'
|
||||
and entity_id = ${quotationId}
|
||||
)
|
||||
`;
|
||||
await sql`
|
||||
delete from crm_approval_requests
|
||||
where entity_type = 'quotation'
|
||||
and entity_id = ${quotationId}
|
||||
`;
|
||||
await sql`delete from crm_quotation_customers where quotation_id = ${quotationId}`;
|
||||
await sql`
|
||||
delete from crm_quotation_topic_items
|
||||
where topic_id in (
|
||||
select id
|
||||
from crm_quotation_topics
|
||||
where quotation_id = ${quotationId}
|
||||
)
|
||||
`;
|
||||
await sql`delete from crm_quotation_topics where quotation_id = ${quotationId}`;
|
||||
await sql`delete from crm_quotation_items where quotation_id = ${quotationId}`;
|
||||
|
||||
await sql`
|
||||
insert into crm_quotation_items (
|
||||
id,
|
||||
organization_id,
|
||||
quotation_id,
|
||||
item_number,
|
||||
product_type,
|
||||
description,
|
||||
quantity,
|
||||
unit,
|
||||
unit_price,
|
||||
discount,
|
||||
discount_type,
|
||||
tax_rate,
|
||||
total_price,
|
||||
notes,
|
||||
sort_order
|
||||
) values (
|
||||
${crypto.randomUUID()},
|
||||
${organization.id},
|
||||
${quotationId},
|
||||
1,
|
||||
${optionIds.productTypeCraneId},
|
||||
'1 set overhead crane with installation and commissioning',
|
||||
1,
|
||||
${optionIds.unitSetId},
|
||||
125000,
|
||||
5000,
|
||||
${optionIds.discountPercentageId},
|
||||
7,
|
||||
120000,
|
||||
'Smoke-test commercial line',
|
||||
1
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`
|
||||
insert into crm_quotation_customers (
|
||||
id,
|
||||
organization_id,
|
||||
quotation_id,
|
||||
customer_id,
|
||||
role,
|
||||
is_primary
|
||||
) values (
|
||||
${crypto.randomUUID()},
|
||||
${organization.id},
|
||||
${quotationId},
|
||||
${customerId},
|
||||
${optionIds.customerRoleOwnerId},
|
||||
true
|
||||
)
|
||||
`;
|
||||
|
||||
const topics = [
|
||||
{
|
||||
typeId: optionIds.topicTypeScopeId,
|
||||
title: 'Scope of work',
|
||||
items: ['Supply crane equipment', 'Install and test the system']
|
||||
},
|
||||
{
|
||||
typeId: optionIds.topicTypeExclusionId,
|
||||
title: 'Exclusions',
|
||||
items: ['Civil work by customer', 'Power feeder by customer']
|
||||
},
|
||||
{
|
||||
typeId: optionIds.topicTypePaymentId,
|
||||
title: 'Payment terms',
|
||||
items: ['50% upon PO', '40% before delivery', '10% after commissioning']
|
||||
}
|
||||
];
|
||||
|
||||
for (const [topicIndex, topic] of topics.entries()) {
|
||||
const topicId = crypto.randomUUID();
|
||||
|
||||
await sql`
|
||||
insert into crm_quotation_topics (
|
||||
id,
|
||||
organization_id,
|
||||
quotation_id,
|
||||
topic_type,
|
||||
title,
|
||||
sort_order
|
||||
) values (
|
||||
${topicId},
|
||||
${organization.id},
|
||||
${quotationId},
|
||||
${topic.typeId},
|
||||
${topic.title},
|
||||
${topicIndex + 1}
|
||||
)
|
||||
`;
|
||||
|
||||
for (const [itemIndex, content] of topic.items.entries()) {
|
||||
await sql`
|
||||
insert into crm_quotation_topic_items (
|
||||
id,
|
||||
organization_id,
|
||||
topic_id,
|
||||
content,
|
||||
sort_order
|
||||
) values (
|
||||
${crypto.randomUUID()},
|
||||
${organization.id},
|
||||
${topicId},
|
||||
${content},
|
||||
${itemIndex + 1}
|
||||
)
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
if (approved) {
|
||||
const approvalRequestId = crypto.randomUUID();
|
||||
|
||||
await sql`
|
||||
insert into crm_approval_requests (
|
||||
id,
|
||||
organization_id,
|
||||
workflow_id,
|
||||
entity_type,
|
||||
entity_id,
|
||||
current_step,
|
||||
status,
|
||||
requested_by,
|
||||
requested_at,
|
||||
completed_at
|
||||
) values (
|
||||
${approvalRequestId},
|
||||
${organization.id},
|
||||
${workflow.id},
|
||||
'quotation',
|
||||
${quotationId},
|
||||
${workflow.steps[workflow.steps.length - 1].stepNumber},
|
||||
'approved',
|
||||
${creatorId},
|
||||
${'2026-06-16T00:30:00.000Z'},
|
||||
${'2026-06-16T01:30:00.000Z'}
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`
|
||||
insert into crm_approval_actions (
|
||||
id,
|
||||
organization_id,
|
||||
approval_request_id,
|
||||
step_number,
|
||||
action,
|
||||
remark,
|
||||
acted_by,
|
||||
acted_at
|
||||
) values (
|
||||
${crypto.randomUUID()},
|
||||
${organization.id},
|
||||
${approvalRequestId},
|
||||
0,
|
||||
'submit',
|
||||
'Task H.1 fixture submission',
|
||||
${creatorId},
|
||||
${'2026-06-16T00:30:00.000Z'}
|
||||
)
|
||||
`;
|
||||
|
||||
for (const [index, step] of workflow.steps.entries()) {
|
||||
await sql`
|
||||
insert into crm_approval_actions (
|
||||
id,
|
||||
organization_id,
|
||||
approval_request_id,
|
||||
step_number,
|
||||
action,
|
||||
remark,
|
||||
acted_by,
|
||||
acted_at
|
||||
) values (
|
||||
${crypto.randomUUID()},
|
||||
${organization.id},
|
||||
${approvalRequestId},
|
||||
${step.stepNumber},
|
||||
'approve',
|
||||
${`Task H.1 fixture ${step.roleCode} approval`},
|
||||
${creatorId},
|
||||
${new Date(Date.UTC(2026, 5, 16, 1, index * 10, 0)).toISOString()}
|
||||
)
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
return quotationId;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
loadLocalEnv();
|
||||
|
||||
const sql = postgres(requireEnv('DATABASE_URL'), { prepare: false });
|
||||
|
||||
try {
|
||||
const organization =
|
||||
(
|
||||
await sql`
|
||||
select id, name, slug, created_by as "createdBy"
|
||||
from organizations
|
||||
order by created_at asc
|
||||
limit 1
|
||||
`
|
||||
)[0] ?? null;
|
||||
|
||||
if (!organization) {
|
||||
throw new Error('No organization found for Task H.1 fixture');
|
||||
}
|
||||
|
||||
const creatorId =
|
||||
(
|
||||
await sql`
|
||||
select id
|
||||
from users
|
||||
where system_role = 'super_admin'
|
||||
order by created_at asc
|
||||
limit 1
|
||||
`
|
||||
)[0]?.id ?? organization.createdBy;
|
||||
|
||||
if (!creatorId) {
|
||||
throw new Error('No creator user found for Task H.1 fixture');
|
||||
}
|
||||
|
||||
const optionIds = {
|
||||
branchId: await resolveOptionId(sql, organization.id, 'crm_branch', 'head_office'),
|
||||
customerTypeCompanyId: await resolveOptionId(
|
||||
sql,
|
||||
organization.id,
|
||||
'crm_customer_type',
|
||||
'company'
|
||||
),
|
||||
customerStatusActiveId: await resolveOptionId(
|
||||
sql,
|
||||
organization.id,
|
||||
'crm_customer_status',
|
||||
'active'
|
||||
),
|
||||
leadChannelSalesId: await resolveOptionId(sql, organization.id, 'crm_lead_channel', 'sales'),
|
||||
customerGroupStandardId: await resolveOptionId(
|
||||
sql,
|
||||
organization.id,
|
||||
'crm_customer_group',
|
||||
'standard'
|
||||
),
|
||||
quotationStatusDraftId: await resolveOptionId(
|
||||
sql,
|
||||
organization.id,
|
||||
'crm_quotation_status',
|
||||
'draft'
|
||||
),
|
||||
quotationStatusApprovedId: await resolveOptionId(
|
||||
sql,
|
||||
organization.id,
|
||||
'crm_quotation_status',
|
||||
'approved'
|
||||
),
|
||||
quotationTypeServiceId: await resolveOptionId(
|
||||
sql,
|
||||
organization.id,
|
||||
'crm_quotation_type',
|
||||
'service'
|
||||
),
|
||||
currencyThbId: await resolveOptionId(sql, organization.id, 'crm_currency', 'THB'),
|
||||
discountPercentageId: await resolveOptionId(
|
||||
sql,
|
||||
organization.id,
|
||||
'crm_discount_type',
|
||||
'percentage'
|
||||
),
|
||||
unitSetId: await resolveOptionId(sql, organization.id, 'crm_unit', 'set'),
|
||||
productTypeCraneId: await resolveOptionId(
|
||||
sql,
|
||||
organization.id,
|
||||
'crm_product_type',
|
||||
'crane'
|
||||
),
|
||||
customerRoleOwnerId: await resolveOptionId(
|
||||
sql,
|
||||
organization.id,
|
||||
'crm_quotation_customer_role',
|
||||
'owner'
|
||||
),
|
||||
topicTypeScopeId: await resolveOptionId(
|
||||
sql,
|
||||
organization.id,
|
||||
'crm_quotation_topic_type',
|
||||
'scope'
|
||||
),
|
||||
topicTypeExclusionId: await resolveOptionId(
|
||||
sql,
|
||||
organization.id,
|
||||
'crm_quotation_topic_type',
|
||||
'exclusion'
|
||||
),
|
||||
topicTypePaymentId: await resolveOptionId(
|
||||
sql,
|
||||
organization.id,
|
||||
'crm_quotation_topic_type',
|
||||
'payment'
|
||||
),
|
||||
sentViaEmailId: await resolveOptionId(sql, organization.id, 'crm_sent_via', 'email')
|
||||
};
|
||||
const workflow = await resolveApprovalWorkflow(sql, organization.id);
|
||||
const { customerId, contactId } = await ensureCustomerAndContact(
|
||||
sql,
|
||||
organization,
|
||||
creatorId,
|
||||
optionIds
|
||||
);
|
||||
|
||||
const users = {
|
||||
admin: await ensureUser(sql, organization, creatorId, FIXTURE_USERS.admin, 'Task H.1 Admin', {
|
||||
role: 'admin',
|
||||
businessRole: 'it_admin',
|
||||
permissions: [...PDF_PERMISSIONS, 'users:manage']
|
||||
}),
|
||||
pdfUser: await ensureUser(
|
||||
sql,
|
||||
organization,
|
||||
creatorId,
|
||||
FIXTURE_USERS.pdfUser,
|
||||
'Task H.1 PDF User',
|
||||
{
|
||||
role: 'user',
|
||||
businessRole: 'viewer',
|
||||
permissions: PDF_PERMISSIONS
|
||||
}
|
||||
),
|
||||
viewer: await ensureUser(sql, organization, creatorId, FIXTURE_USERS.viewer, 'Task H.1 Viewer', {
|
||||
role: 'user',
|
||||
businessRole: 'viewer',
|
||||
permissions: ['crm.quotation.read']
|
||||
})
|
||||
};
|
||||
|
||||
const approvedQuotationId = await upsertQuotation(sql, {
|
||||
organization,
|
||||
creatorId,
|
||||
workflow,
|
||||
optionIds,
|
||||
customerId,
|
||||
contactId,
|
||||
code: FIXTURE_CODES.approvedQuotation,
|
||||
approved: true
|
||||
});
|
||||
const draftQuotationId = await upsertQuotation(sql, {
|
||||
organization,
|
||||
creatorId,
|
||||
workflow,
|
||||
optionIds,
|
||||
customerId,
|
||||
contactId,
|
||||
code: FIXTURE_CODES.draftQuotation,
|
||||
approved: false
|
||||
});
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
organizationId: organization.id,
|
||||
approvedQuotationId,
|
||||
approvedQuotationCode: FIXTURE_CODES.approvedQuotation,
|
||||
draftQuotationId,
|
||||
draftQuotationCode: FIXTURE_CODES.draftQuotation,
|
||||
users,
|
||||
password: TASK_H1_PASSWORD
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('[seed-task-h1-fixture] Failed:', error.message);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user