This commit is contained in:
phaichayon
2026-07-15 05:46:59 +07:00
parent b51bf3e02a
commit df1821982d
10 changed files with 932 additions and 1346 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,72 +1,64 @@
import { and, eq, inArray, isNull } from "drizzle-orm";
import {
crmCustomerContacts,
crmCustomers,
crmQuotations,
organizations,
} from "@/db/schema";
import { db } from "@/lib/db";
import { AuthError } from "@/lib/auth/session";
import { and, eq, inArray, isNull } from 'drizzle-orm';
import { crmCustomerContacts, crmCustomers, crmQuotations, organizations } from '@/db/schema';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import {
listApprovalRequests,
getApprovalRequest,
} from "@/features/foundation/approval/server/service";
getApprovalRequest
} from '@/features/foundation/approval/server/service';
import {
mapDocumentDataToTemplateInput,
resolveTemplateForDocument,
resolveTemplateMappings,
} from "@/features/foundation/document-template/server/service";
import { getActiveOptionsByCategory } from "@/features/foundation/master-options/service";
resolveTemplateMappings
} from '@/features/foundation/document-template/server/service';
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
import {
getQuotationDetail,
listQuotationCustomers,
listQuotationItems,
listQuotationTopics,
} from "@/features/crm/quotations/server/service";
listQuotationTopics
} from '@/features/crm/quotations/server/service';
import type {
ApprovedQuotationSnapshot,
QuotationDocumentApprovalStep,
QuotationDocumentData,
} from "../types";
import type { PdfTopic } from "./pdf-topic.type";
QuotationDocumentData
} from '../types';
import type { PdfTopic } from './pdf-topic.type';
import {
buildPdfPriceTableData,
formatPdfCurrency,
formatPdfDate,
formatTopicItems,
} from "./pdfme-transforms";
import { buildQuotationPdfRuntime } from "./quotation-pdf-runtime";
import { resolveQuotationSignatures } from "./signature-resolver";
import { resolveQuotationTopicTypeMapping } from "./topic-mapping";
formatTopicItems
} from './pdfme-transforms';
import { buildQuotationPdfRuntime } from './quotation-pdf-runtime';
import { resolveQuotationSignatures } from './signature-resolver';
import { resolveQuotationTopicTypeMapping } from './topic-mapping';
const DOCUMENT_OPTION_CATEGORIES = {
branch: "crm_branch",
status: "crm_quotation_status",
quotationType: "crm_quotation_type",
currency: "crm_currency",
discountType: "crm_discount_type",
unit: "crm_unit",
customerRole: "crm_project_party_role",
productType: "crm_product_type",
topicType: "crm_quotation_topic_type",
branch: 'crm_branch',
status: 'crm_quotation_status',
quotationType: 'crm_quotation_type',
currency: 'crm_currency',
discountType: 'crm_discount_type',
unit: 'crm_unit',
customerRole: 'crm_project_party_role',
productType: 'crm_product_type',
topicType: 'crm_quotation_topic_type'
} as const;
const PDFME_DEFAULT_LABELS = {
tel: "Tel",
email: "Email",
att: "Att",
project: "Project",
location: "Location",
tel: 'Tel',
email: 'Email',
att: 'Att',
project: 'Project',
location: 'Location'
} as const;
function toRevisionLabel(revision: number) {
return `R${String(revision).padStart(2, "0")}`;
return `R${String(revision).padStart(2, '0')}`;
}
async function assertQuotationDocumentAccess(
quotationId: string,
organizationId: string,
) {
async function assertQuotationDocumentAccess(quotationId: string, organizationId: string) {
const [quotation] = await db
.select()
.from(crmQuotations)
@@ -74,13 +66,13 @@ async function assertQuotationDocumentAccess(
and(
eq(crmQuotations.id, quotationId),
eq(crmQuotations.organizationId, organizationId),
isNull(crmQuotations.deletedAt),
),
isNull(crmQuotations.deletedAt)
)
)
.limit(1);
if (!quotation) {
throw new AuthError("Quotation not found", 404);
throw new AuthError('Quotation not found', 404);
}
return quotation;
@@ -88,16 +80,14 @@ async function assertQuotationDocumentAccess(
function findOptionLabel(
options: Array<{ id: string; code: string; label: string }>,
id: string | null | undefined,
id: string | null | undefined
) {
return id
? (options.find((option) => option.id === id)?.label ?? null)
: null;
return id ? (options.find((option) => option.id === id)?.label ?? null) : null;
}
function findOptionCode(
options: Array<{ id: string; code: string; label: string }>,
id: string | null | undefined,
id: string | null | undefined
) {
return id ? (options.find((option) => option.id === id)?.code ?? null) : null;
}
@@ -111,10 +101,10 @@ function extractSinglePlaceholder(value: string) {
function getFieldName(field: unknown) {
return field &&
typeof field === "object" &&
typeof (field as { name?: unknown }).name === "string"
? ((field as { name: string }).name ?? "")
: "";
typeof field === 'object' &&
typeof (field as { name?: unknown }).name === 'string'
? ((field as { name: string }).name ?? '')
: '';
}
function normalizeTopicKey(value: string | null | undefined) {
@@ -122,14 +112,11 @@ function normalizeTopicKey(value: string | null | undefined) {
value
?.trim()
.toLowerCase()
.replaceAll(/[\s_-]+/g, "") ?? ""
.replaceAll(/[\s_-]+/g, '') ?? ''
);
}
function matchesTopicAlias(
actualCode: string | null | undefined,
expectedCode: string,
) {
function matchesTopicAlias(actualCode: string | null | undefined, expectedCode: string) {
const actual = normalizeTopicKey(actualCode);
const expected = normalizeTopicKey(expectedCode);
@@ -142,33 +129,30 @@ function matchesTopicAlias(
}
const aliasGroups = [
["scope", "scopeofwork", "dockscopeofwork", "solarcellscopeofwork"],
['scope', 'scopeofwork', 'dockscopeofwork', 'solarcellscopeofwork'],
[
"exclusion",
"exclusionfromscopeofsupply",
"exclusionofsupplyinstallation",
"solarcellexclusionofsupplyinstallation",
'exclusion',
'exclusionfromscopeofsupply',
'exclusionofsupplyinstallation',
'solarcellexclusionofsupplyinstallation'
],
[
"payment",
"paymentconditions",
"termsofpaymentuponprogressofeachitem",
"solarcellpaymentconditions",
'payment',
'paymentconditions',
'termsofpaymentuponprogressofeachitem',
'solarcellpaymentconditions'
],
["delivery", "deliverydate", "solarcelldelivery"],
["warranty", "solarcellwarranty"],
['delivery', 'deliverydate', 'solarcelldelivery'],
['warranty', 'solarcellwarranty']
];
return aliasGroups.some(
(group) => group.includes(actual) && group.includes(expected),
);
return aliasGroups.some((group) => group.includes(actual) && group.includes(expected));
}
function sortBySortOrder<T extends { sortOrder?: number | null }>(items: T[]) {
return [...items].sort((left, right) => {
const delta =
(left.sortOrder ?? Number.MAX_SAFE_INTEGER) -
(right.sortOrder ?? Number.MAX_SAFE_INTEGER);
(left.sortOrder ?? Number.MAX_SAFE_INTEGER) - (right.sortOrder ?? Number.MAX_SAFE_INTEGER);
if (delta !== 0) {
return delta;
@@ -178,12 +162,9 @@ function sortBySortOrder<T extends { sortOrder?: number | null }>(items: T[]) {
});
}
function normalizePdfmeTemplateInput(
schemaJson: unknown,
templateInput: Record<string, unknown>,
) {
function normalizePdfmeTemplateInput(schemaJson: unknown, templateInput: Record<string, unknown>) {
const pages =
schemaJson && typeof schemaJson === "object" && "schemas" in schemaJson
schemaJson && typeof schemaJson === 'object' && 'schemas' in schemaJson
? (schemaJson as { schemas?: unknown }).schemas
: null;
@@ -199,37 +180,28 @@ function normalizePdfmeTemplateInput(
}
for (const field of page) {
if (!field || typeof field !== "object") {
if (!field || typeof field !== 'object') {
continue;
}
const tableField = field as { type?: unknown; content?: unknown };
const fieldName = getFieldName(field);
const placeholderKey =
typeof tableField.content === "string"
typeof tableField.content === 'string'
? extractSinglePlaceholder(tableField.content)
: null;
if (fieldName && placeholderKey && fieldName !== placeholderKey) {
if (
normalized[fieldName] !== undefined &&
normalized[placeholderKey] === undefined
) {
if (normalized[fieldName] !== undefined && normalized[placeholderKey] === undefined) {
normalized[placeholderKey] = normalized[fieldName];
}
if (
normalized[placeholderKey] !== undefined &&
normalized[fieldName] === undefined
) {
if (normalized[placeholderKey] !== undefined && normalized[fieldName] === undefined) {
normalized[fieldName] = normalized[placeholderKey];
}
}
if (
tableField.type !== "table" ||
typeof tableField.content !== "string"
) {
if (tableField.type !== 'table' || typeof tableField.content !== 'string') {
continue;
}
@@ -239,12 +211,12 @@ function normalizePdfmeTemplateInput(
const value = normalized[placeholderKey];
if (typeof value !== "string") {
if (typeof value !== 'string') {
continue;
}
const lines = value
.split("\n")
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
@@ -264,20 +236,14 @@ function logQuotationPdfRuntimeWarnings(issues: Array<{ message: string }>) {
return;
}
console.warn(
"[quotation-pdf-runtime]",
issues.map((issue) => issue.message).join(" | "),
);
console.warn('[quotation-pdf-runtime]', issues.map((issue) => issue.message).join(' | '));
}
export async function buildQuotationDocumentData(
quotationId: string,
organizationId: string,
organizationId: string
): Promise<QuotationDocumentData> {
const quotation = await assertQuotationDocumentAccess(
quotationId,
organizationId,
);
const quotation = await assertQuotationDocumentAccess(quotationId, organizationId);
const [
company,
quotationDetail,
@@ -293,7 +259,7 @@ export async function buildQuotationDocumentData(
customerRoleOptions,
productTypeOptions,
topicTypeOptions,
approvalRequests,
approvalRequests
] = await Promise.all([
db
.select()
@@ -305,41 +271,41 @@ export async function buildQuotationDocumentData(
listQuotationCustomers(quotationId, organizationId),
listQuotationTopics(quotationId, organizationId),
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.branch, {
organizationId,
organizationId
}),
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.status, {
organizationId,
organizationId
}),
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.quotationType, {
organizationId,
organizationId
}),
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.currency, {
organizationId,
organizationId
}),
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.discountType, {
organizationId,
organizationId
}),
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.unit, {
organizationId,
organizationId
}),
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.customerRole, {
organizationId,
organizationId
}),
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.productType, {
organizationId,
organizationId
}),
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.topicType, {
organizationId,
organizationId
}),
listApprovalRequests(organizationId, {
entityType: "quotation",
entityType: 'quotation',
entityId: quotationId,
limit: 20,
}),
limit: 20
})
]);
if (!company) {
throw new AuthError("Organization not found", 404);
throw new AuthError('Organization not found', 404);
}
const [customer, contact, relatedCustomers, signatures] = await Promise.all([
@@ -350,8 +316,8 @@ export async function buildQuotationDocumentData(
and(
eq(crmCustomers.id, quotation.customerId),
eq(crmCustomers.organizationId, organizationId),
isNull(crmCustomers.deletedAt),
),
isNull(crmCustomers.deletedAt)
)
)
.then((rows) => rows[0] ?? null),
quotation.contactId
@@ -362,8 +328,8 @@ export async function buildQuotationDocumentData(
and(
eq(crmCustomerContacts.id, quotation.contactId),
eq(crmCustomerContacts.organizationId, organizationId),
isNull(crmCustomerContacts.deletedAt),
),
isNull(crmCustomerContacts.deletedAt)
)
)
.then((rows) => rows[0] ?? null)
: Promise.resolve(null),
@@ -372,7 +338,7 @@ export async function buildQuotationDocumentData(
.select({
id: crmCustomers.id,
code: crmCustomers.code,
name: crmCustomers.name,
name: crmCustomers.name
})
.from(crmCustomers)
.where(
@@ -380,39 +346,33 @@ export async function buildQuotationDocumentData(
eq(crmCustomers.organizationId, organizationId),
inArray(
crmCustomers.id,
quotationCustomers.map((item) => item.customerId),
quotationCustomers.map((item) => item.customerId)
),
isNull(crmCustomers.deletedAt),
),
isNull(crmCustomers.deletedAt)
)
)
: Promise.resolve([]),
resolveQuotationSignatures(quotationId, organizationId),
resolveQuotationSignatures(quotationId, organizationId)
]);
if (!customer) {
throw new AuthError("Customer not found", 404);
throw new AuthError('Customer not found', 404);
}
const approvalRequest =
approvalRequests.items.find((item) => item.status === "approved") ??
approvalRequests.items.find((item) => item.status === 'approved') ??
approvalRequests.items[0] ??
null;
const approvalDetail = approvalRequest
? await getApprovalRequest(approvalRequest.id, organizationId)
: null;
const branch =
branchOptions.find((option) => option.id === quotation.branchId) ?? null;
const relatedCustomerMap = new Map(
relatedCustomers.map((item) => [item.id, item]),
);
const branch = branchOptions.find((option) => option.id === quotation.branchId) ?? null;
const relatedCustomerMap = new Map(relatedCustomers.map((item) => [item.id, item]));
const statusCode = findOptionCode(statusOptions, quotation.status);
const statusLabel = findOptionLabel(statusOptions, quotation.status);
const topicCodeById = new Map(
topicTypeOptions.map((option) => [option.id, option.code]),
);
const topicCodeById = new Map(topicTypeOptions.map((option) => [option.id, option.code]));
const primaryProductType =
items.find((item) => findOptionCode(productTypeOptions, item.productType))
?.productType ?? null;
items.find((item) => findOptionCode(productTypeOptions, item.productType))?.productType ?? null;
const productTypeCode =
findOptionCode(productTypeOptions, primaryProductType) ??
findOptionCode(quotationTypeOptions, quotationDetail.quotationType) ??
@@ -424,7 +384,7 @@ export async function buildQuotationDocumentData(
findOptionLabel(topicTypeOptions, item.topicType) ??
item.title?.trim() ??
topicCodeById.get(item.topicType) ??
"Topic";
'Topic';
return {
id: item.id,
@@ -435,8 +395,8 @@ export async function buildQuotationDocumentData(
items: sortBySortOrder(item.items).map((child) => ({
id: child.id,
content: child.content,
sortOrder: child.sortOrder,
})),
sortOrder: child.sortOrder
}))
};
});
const pdfTopics: PdfTopic[] = resolvedTopics.map((item) => ({
@@ -445,24 +405,24 @@ export async function buildQuotationDocumentData(
sortOrder: item.sortOrder,
items: item.items.map((child) => ({
id: child.id,
content: child.content?.trim() || "-",
sortOrder: child.sortOrder,
})),
content: child.content?.trim() || '-',
sortOrder: child.sortOrder
}))
}));
const scopeTopics = resolvedTopics.filter((item) =>
matchesTopicAlias(item.topicCode, topicTypeMapping.scope),
matchesTopicAlias(item.topicCode, topicTypeMapping.scope)
);
const exclusionTopics = resolvedTopics.filter((item) =>
matchesTopicAlias(item.topicCode, topicTypeMapping.exclusion),
matchesTopicAlias(item.topicCode, topicTypeMapping.exclusion)
);
const paymentTopics = resolvedTopics.filter((item) =>
matchesTopicAlias(item.topicCode, topicTypeMapping.payment),
matchesTopicAlias(item.topicCode, topicTypeMapping.payment)
);
const warrantyTopics = resolvedTopics.filter((item) =>
matchesTopicAlias(item.topicCode, topicTypeMapping.warranty),
matchesTopicAlias(item.topicCode, topicTypeMapping.warranty)
);
const deliveryTopics = resolvedTopics.filter((item) =>
matchesTopicAlias(item.topicCode, topicTypeMapping.delivery),
matchesTopicAlias(item.topicCode, topicTypeMapping.delivery)
);
const otherTopics = resolvedTopics.filter((item) => {
return ![
@@ -470,7 +430,7 @@ export async function buildQuotationDocumentData(
topicTypeMapping.exclusion,
topicTypeMapping.payment,
topicTypeMapping.warranty,
topicTypeMapping.delivery,
topicTypeMapping.delivery
].some((expectedCode) => matchesTopicAlias(item.topicCode, expectedCode));
});
const exclusionTopic = exclusionTopics[0];
@@ -479,23 +439,22 @@ export async function buildQuotationDocumentData(
const deliveryTopic = deliveryTopics[0];
const approvalApprovers: QuotationDocumentApprovalStep[] =
approvalDetail?.timeline
.filter((item) => ["approve", "reject", "return"].includes(item.action))
.filter((item) => ['approve', 'reject', 'return'].includes(item.action))
.map((item) => {
const step = approvalDetail.steps.find(
(candidate) => candidate.stepNumber === item.stepNumber,
(candidate) => candidate.stepNumber === item.stepNumber
);
return {
stepNumber: item.stepNumber,
roleCode: step?.roleCode ?? "",
roleCode: step?.roleCode ?? '',
roleName: step?.roleName ?? `Step ${item.stepNumber}`,
action: item.action,
actorName: item.actorName,
actedAt: item.actedAt,
remark: item.remark,
remark: item.remark
};
}) ?? [];
const currencyCode =
findOptionCode(currencyOptions, quotationDetail.currency) ?? "THB";
const currencyCode = findOptionCode(currencyOptions, quotationDetail.currency) ?? 'THB';
return {
company: {
@@ -503,14 +462,14 @@ export async function buildQuotationDocumentData(
name: company.name,
slug: company.slug,
imageUrl: company.imageUrl,
plan: company.plan,
plan: company.plan
},
branch: branch
? {
id: branch.id,
code: branch.code,
label: branch.label,
value: branch.value,
value: branch.value
}
: null,
customer: {
@@ -520,7 +479,7 @@ export async function buildQuotationDocumentData(
address: customer.address,
phone: customer.phone,
email: customer.email,
taxId: customer.taxId,
taxId: customer.taxId
},
contact: contact
? {
@@ -528,7 +487,7 @@ export async function buildQuotationDocumentData(
name: contact.name,
email: contact.email,
mobile: contact.mobile,
position: contact.position,
position: contact.position
}
: null,
quotation: {
@@ -544,32 +503,20 @@ export async function buildQuotationDocumentData(
revisionLabel: toRevisionLabel(quotationDetail.revision),
statusCode,
statusLabel,
quotationTypeCode: findOptionCode(
quotationTypeOptions,
quotationDetail.quotationType,
),
quotationTypeLabel: findOptionLabel(
quotationTypeOptions,
quotationDetail.quotationType,
),
quotationTypeCode: findOptionCode(quotationTypeOptions, quotationDetail.quotationType),
quotationTypeLabel: findOptionLabel(quotationTypeOptions, quotationDetail.quotationType),
currency: currencyCode,
currencyCode,
currencyLabel: findOptionLabel(currencyOptions, quotationDetail.currency),
exchangeRate: quotationDetail.exchangeRate,
subtotal: quotationDetail.subtotal,
discount: quotationDetail.discount,
discountTypeCode: findOptionCode(
discountTypeOptions,
quotationDetail.discountType,
),
discountTypeLabel: findOptionLabel(
discountTypeOptions,
quotationDetail.discountType,
),
discountTypeCode: findOptionCode(discountTypeOptions, quotationDetail.discountType),
discountTypeLabel: findOptionLabel(discountTypeOptions, quotationDetail.discountType),
taxRate: quotationDetail.taxRate,
taxAmount: quotationDetail.taxAmount,
totalAmount: quotationDetail.totalAmount,
notes: quotationDetail.notes,
notes: quotationDetail.notes
},
items: items.map((item) => ({
id: item.id,
@@ -585,130 +532,107 @@ export async function buildQuotationDocumentData(
discountTypeCode: findOptionCode(discountTypeOptions, item.discountType),
taxRate: item.taxRate,
totalPrice: item.totalPrice,
notes: item.notes,
notes: item.notes
})),
quotationCustomers: quotationCustomers.map((item) => ({
id: item.id,
customerId: item.customerId,
customerName:
relatedCustomerMap.get(item.customerId)?.name ?? item.customerName,
customerCode:
relatedCustomerMap.get(item.customerId)?.code ?? item.customerCode,
customerName: relatedCustomerMap.get(item.customerId)?.name ?? item.customerName,
customerCode: relatedCustomerMap.get(item.customerId)?.code ?? item.customerCode,
roleCode: item.roleCode,
roleLabel:
item.roleLabel ?? findOptionLabel(customerRoleOptions, item.role),
isPrimary: item.isPrimary,
roleLabel: item.roleLabel ?? findOptionLabel(customerRoleOptions, item.role),
isPrimary: item.isPrimary
})),
topics: {
scope: scopeTopics.map((item) => ({
title: item.title,
items: item.items.map((child) => child.content ?? "-"),
items: item.items.map((child) => child.content ?? '-')
})),
exclusion: exclusionTopics.map((item) => ({
title: item.title,
items: item.items.map((child) => child.content ?? "-"),
items: item.items.map((child) => child.content ?? '-')
})),
payment: paymentTopics.map((item) => ({
title: item.title,
items: item.items.map((child) => child.content ?? "-"),
items: item.items.map((child) => child.content ?? '-')
})),
warranty: warrantyTopics.map((item) => ({
title: item.title,
items: item.items.map((child) => child.content ?? "-"),
items: item.items.map((child) => child.content ?? '-')
})),
delivery: deliveryTopics.map((item) => ({
title: item.title,
items: item.items.map((child) => child.content ?? "-"),
items: item.items.map((child) => child.content ?? '-')
})),
other: otherTopics.map((item) => ({
id: item.id,
title: item.title,
sortOrder: item.sortOrder,
items: item.items.map((child) => child.content ?? "-"),
items: item.items.map((child) => child.content ?? '-')
})),
all: pdfTopics,
all: pdfTopics
},
pdfme: {
labels: PDFME_DEFAULT_LABELS,
quotation_date: formatPdfDate(quotationDetail.quotationDate, "en"),
quotation_price: formatPdfCurrency(
quotationDetail.subtotal,
currencyCode,
),
quotation_price_data: buildPdfPriceTableData(
quotationDetail.subtotal,
currencyCode,
),
quotation_date: formatPdfDate(quotationDetail.quotationDate, 'en'),
quotation_price: formatPdfCurrency(quotationDetail.subtotal, currencyCode),
quotation_price_data: buildPdfPriceTableData(quotationDetail.subtotal, currencyCode),
exclusion_data: formatTopicItems(exclusionTopic),
payment_data: formatTopicItems(paymentTopic),
warranty_data: formatTopicItems(warrantyTopic),
delivery_data: formatTopicItems(deliveryTopic),
topics_data: formatTopicItems(
scopeTopics[0] ?? exclusionTopic ?? paymentTopic,
),
topic_inputs: {},
topics_data: formatTopicItems(scopeTopics[0] ?? exclusionTopic ?? paymentTopic),
topic_inputs: {}
},
approval: {
requestId: approvalDetail?.request.id ?? null,
workflowName: approvalDetail?.workflow.name ?? null,
status: approvalDetail?.request.status ?? null,
approvedAt:
approvalDetail?.request.status === "approved"
? approvalDetail.request.completedAt
: null,
approvalDetail?.request.status === 'approved' ? approvalDetail.request.completedAt : null,
currentStep: approvalDetail?.request.currentStep ?? null,
currentStepRoleName: approvalDetail?.currentStep?.roleName ?? null,
approvers: approvalApprovers,
approvers: approvalApprovers
},
signatures,
watermarkStatus:
statusCode && statusCode !== "approved"
? (statusLabel ?? statusCode)
: null,
watermarkStatus: statusCode && statusCode !== 'approved' ? (statusLabel ?? statusCode) : null
};
}
export async function getQuotationDocumentPreviewData(
quotationId: string,
organizationId: string,
options?: {
templateVariant?: string | null;
}
) {
const documentData = await buildQuotationDocumentData(
quotationId,
organizationId,
);
const documentData = await buildQuotationDocumentData(quotationId, organizationId);
const template = await resolveTemplateForDocument(organizationId, {
documentType: "quotation",
productType: "default",
fileType: "pdfme",
documentType: 'quotation',
productType: 'default',
fileType: 'pdfme',
templateVariant: options?.templateVariant ?? null
});
const mappings = await resolveTemplateMappings(
template.version.id,
organizationId,
);
const mappings = await resolveTemplateMappings(template.version.id, organizationId);
if (!mappings.length) {
throw new AuthError(
"Document template mappings are not configured for this template version",
409,
'Document template mappings are not configured for this template version',
409
);
}
const templateInput = normalizePdfmeTemplateInput(
template.version.schemaJson,
mapDocumentDataToTemplateInput(
documentData as unknown as Record<string, unknown>,
mappings,
),
mapDocumentDataToTemplateInput(documentData as unknown as Record<string, unknown>, mappings)
);
const runtime = await buildQuotationPdfRuntime({
documentData,
template,
mappings,
templateInput,
templateInput
});
const runtimeWarnings = runtime.issues.filter(
(issue) => issue.severity === "warning",
);
const runtimeWarnings = runtime.issues.filter((issue) => issue.severity === 'warning');
logQuotationPdfRuntimeWarnings(runtimeWarnings);
@@ -720,23 +644,20 @@ export async function getQuotationDocumentPreviewData(
...template,
version: {
...template.version,
schemaJson: runtime.assembled.template,
},
schemaJson: runtime.assembled.template
}
},
mappings,
templateInput: runtime.assembled.templateInput,
templateInput: runtime.assembled.templateInput
};
}
export async function prepareApprovedQuotationSnapshot(
quotationId: string,
organizationId: string,
userId: string,
userId: string
): Promise<ApprovedQuotationSnapshot> {
const preview = await getQuotationDocumentPreviewData(
quotationId,
organizationId,
);
const preview = await getQuotationDocumentPreviewData(quotationId, organizationId);
const generatedAt = new Date().toISOString();
return {
@@ -746,6 +667,6 @@ export async function prepareApprovedQuotationSnapshot(
templateVersionId: preview.template.version.id,
templateInput: preview.templateInput,
generatedAt,
generatedBy: userId,
generatedBy: userId
};
}