pdf task-h

This commit is contained in:
phaichayon
2026-06-19 15:09:58 +07:00
parent a85d24235c
commit fd01ebf7c7
35 changed files with 3905 additions and 133 deletions

View File

@@ -7,7 +7,10 @@ const TASK_H1_PASSWORD = 'TaskH1Pass!2026';
const FIXTURE_CODES = {
customer: 'CUS-TASK-H1',
approvedQuotation: 'QT-TASK-H1-APPROVED',
draftQuotation: 'QT-TASK-H1-DRAFT'
draftQuotation: 'QT-TASK-H1-DRAFT',
auditQuotation: 'QT-H5-AUDIT',
billingCustomer: 'CUS-TASK-H5-BILL',
consultantCustomer: 'CUS-TASK-H5-CONSULT'
};
const FIXTURE_USERS = {
@@ -373,6 +376,88 @@ async function ensureCustomerAndContact(sql, organization, creatorId, optionIds)
};
}
async function ensureRelatedCustomer(sql, organization, creatorId, optionIds, definition) {
const existing =
(
await sql`
select id
from crm_customers
where organization_id = ${organization.id}
and code = ${definition.code}
limit 1
`
)[0] ?? null;
const customerId = existing?.id ?? crypto.randomUUID();
if (existing) {
await sql`
update crm_customers
set
branch_id = ${optionIds.branchId},
name = ${definition.name},
abbr = ${definition.abbr},
tax_id = ${definition.taxId},
customer_type = ${optionIds.customerTypeCompanyId},
customer_status = ${optionIds.customerStatusActiveId},
address = ${definition.address},
phone = ${definition.phone},
email = ${definition.email},
lead_channel = ${optionIds.leadChannelSalesId},
customer_group = ${optionIds.customerGroupStandardId},
notes = ${definition.notes},
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},
${definition.code},
${definition.name},
${definition.abbr},
${definition.taxId},
${optionIds.customerTypeCompanyId},
${optionIds.customerStatusActiveId},
${definition.address},
${definition.phone},
${definition.email},
${optionIds.leadChannelSalesId},
${optionIds.customerGroupStandardId},
${definition.notes},
true,
${creatorId},
${creatorId}
)
`;
}
return customerId;
}
async function upsertQuotation(sql, args) {
const {
organization,
@@ -382,7 +467,9 @@ async function upsertQuotation(sql, args) {
customerId,
contactId,
code,
approved
approved,
fixtureProfile = 'h1',
projectParties = []
} = args;
const existing =
(
@@ -397,9 +484,24 @@ async function upsertQuotation(sql, args) {
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.';
const notes =
fixtureProfile === 'h5'
? 'Approved quotation fixture for Task H.5 PDF mapping and parity audit.'
: approved
? 'Approved quotation fixture for Task H.1 PDF stabilization.'
: 'Draft quotation fixture for Task H.1 negative API validation.';
const projectName =
fixtureProfile === 'h5'
? 'Task H.5 Audit Fixture'
: approved
? 'Task H.1 Approved PDF Fixture'
: 'Task H.1 Draft PDF Fixture';
const reference =
fixtureProfile === 'h5'
? 'TASK-H5-AUDIT'
: approved
? 'TASK-H1-APPROVED'
: 'TASK-H1-DRAFT';
if (existing) {
await sql`
@@ -412,10 +514,10 @@ async function upsertQuotation(sql, args) {
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_name = ${projectName},
project_location = 'Bangkok HQ',
attention = 'Task H.1 Contact',
reference = ${approved ? 'TASK-H1-APPROVED' : 'TASK-H1-DRAFT'},
reference = ${reference},
notes = ${notes},
status = ${statusId},
revision = 0,
@@ -507,10 +609,10 @@ async function upsertQuotation(sql, args) {
${'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'},
${projectName},
'Bangkok HQ',
'Task H.1 Contact',
${approved ? 'TASK-H1-APPROVED' : 'TASK-H1-DRAFT'},
${reference},
${notes},
${statusId},
0,
@@ -572,77 +674,170 @@ async function upsertQuotation(sql, args) {
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
)
`;
const items =
fixtureProfile === 'h5'
? [
{
itemNumber: 1,
description: '1 set overhead crane with installation and commissioning',
quantity: 1,
unitPrice: 125000,
discount: 5000,
totalPrice: 120000,
notes: 'Primary lifting package'
},
{
itemNumber: 2,
description: 'Load testing and safety certification',
quantity: 1,
unitPrice: 25000,
discount: 0,
totalPrice: 25000,
notes: 'Audit fixture support line'
},
{
itemNumber: 3,
description: 'Operator training and turnover documentation',
quantity: 2,
unitPrice: 8000,
discount: 0,
totalPrice: 16000,
notes: 'Audit fixture service line'
}
]
: [
{
itemNumber: 1,
description: '1 set overhead crane with installation and commissioning',
quantity: 1,
unitPrice: 125000,
discount: 5000,
totalPrice: 120000,
notes: 'Smoke-test commercial line'
}
];
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
)
`;
for (const [index, item] of items.entries()) {
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},
${item.itemNumber},
${optionIds.productTypeCraneId},
${item.description},
${item.quantity},
${optionIds.unitSetId},
${item.unitPrice},
${item.discount},
${optionIds.discountPercentageId},
7,
${item.totalPrice},
${item.notes},
${index + 1}
)
`;
}
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']
}
];
const resolvedProjectParties =
projectParties.length > 0
? projectParties
: [
{
customerId,
role: optionIds.customerRoleCustomerId,
isPrimary: true
}
];
for (const [index, party] of resolvedProjectParties.entries()) {
await sql`
insert into crm_quotation_customers (
id,
organization_id,
quotation_id,
customer_id,
role,
is_primary,
remark
) values (
${crypto.randomUUID()},
${organization.id},
${quotationId},
${party.customerId},
${party.role},
${party.isPrimary},
${party.remark ?? `Fixture party ${index + 1}`}
)
`;
}
const topics =
fixtureProfile === 'h5'
? [
{
typeId: optionIds.topicTypeScopeId,
title: 'Scope',
items: [
'Supply crane equipment',
'Install and test the system',
'Perform SAT with project stakeholders'
]
},
{
typeId: optionIds.topicTypeExclusionId,
title: 'Exclusion',
items: ['Civil work by customer', 'Power feeder by customer']
},
{
typeId: optionIds.topicTypePaymentId,
title: 'Payment',
items: ['50% upon PO', '40% before delivery', '10% after commissioning']
},
{
typeId: 'warranty',
title: 'Warranty',
items: ['12 months after commissioning', 'Consumables excluded']
},
{
typeId: 'delivery',
title: 'Delivery',
items: ['Within 60 days after approved drawing', 'Subject to final site readiness']
}
]
: [
{
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();
@@ -851,11 +1046,23 @@ async function main() {
'crm_product_type',
'crane'
),
customerRoleOwnerId: await resolveOptionId(
customerRoleCustomerId: await resolveOptionId(
sql,
organization.id,
'crm_quotation_customer_role',
'owner'
'crm_project_party_role',
'customer'
),
customerRoleBillingCustomerId: await resolveOptionId(
sql,
organization.id,
'crm_project_party_role',
'billing_customer'
),
customerRoleConsultantId: await resolveOptionId(
sql,
organization.id,
'crm_project_party_role',
'consultant'
),
topicTypeScopeId: await resolveOptionId(
sql,
@@ -884,6 +1091,32 @@ async function main() {
creatorId,
optionIds
);
const billingCustomerId = await ensureRelatedCustomer(sql, organization, creatorId, optionIds, {
code: FIXTURE_CODES.billingCustomer,
name: 'Task H.5 Billing Customer',
abbr: 'TASKH5BILL',
taxId: '0105559002233',
address: '88 Audit Park Road, Bangkok 10240',
phone: '02-000-2000',
email: 'billing-customer@local.test',
notes: 'Billing customer for Task H.5 parity fixture'
});
const consultantCustomerId = await ensureRelatedCustomer(
sql,
organization,
creatorId,
optionIds,
{
code: FIXTURE_CODES.consultantCustomer,
name: 'Task H.5 Consultant',
abbr: 'TASKH5CONS',
taxId: '0105559003344',
address: '77 Review Lane, Bangkok 10110',
phone: '02-000-3000',
email: 'consultant-customer@local.test',
notes: 'Consultant customer for Task H.5 parity fixture'
}
);
const users = {
admin: await ensureUser(sql, organization, creatorId, FIXTURE_USERS.admin, 'Task H.1 Admin', {
@@ -930,6 +1163,37 @@ async function main() {
code: FIXTURE_CODES.draftQuotation,
approved: false
});
const auditQuotationId = await upsertQuotation(sql, {
organization,
creatorId: users.admin.id,
workflow,
optionIds,
customerId,
contactId,
code: FIXTURE_CODES.auditQuotation,
approved: true,
fixtureProfile: 'h5',
projectParties: [
{
customerId,
role: optionIds.customerRoleCustomerId,
isPrimary: true,
remark: 'End customer'
},
{
customerId: billingCustomerId,
role: optionIds.customerRoleBillingCustomerId,
isPrimary: false,
remark: 'Billing customer'
},
{
customerId: consultantCustomerId,
role: optionIds.customerRoleConsultantId,
isPrimary: false,
remark: 'Consultant'
}
]
});
console.log(
JSON.stringify(
@@ -939,6 +1203,8 @@ async function main() {
approvedQuotationCode: FIXTURE_CODES.approvedQuotation,
draftQuotationId,
draftQuotationCode: FIXTURE_CODES.draftQuotation,
auditQuotationId,
auditQuotationCode: FIXTURE_CODES.auditQuotation,
users,
password: TASK_H1_PASSWORD
},