pdf task-h
This commit is contained in:
@@ -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
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user