Files
alla-allaos-fullstack/docs/implementation/pdfme-runtime-audit.md
2026-06-18 08:58:01 +07:00

26 KiB

PDFME Runtime Audit Report

Executive Summary

This audit compares the current PDFME template and mapping system against the legacy implementation and identifies runtime issues that cause incomplete or incorrectly mapped data in generated quotation PDFs.

Key Finding: The current implementation has significant gaps in placeholder mappings, missing topic sections, and incorrect data formats that prevent PDFME from displaying complete quotation information.

Total Template Placeholders: 20
Total Required Mappings: 20
Missing Mappings: 8
Critical Issues: 5


1. Template Analysis

1.1 Current Template File

Path: src/pdfme_template/ALLA_template_pdfme_fainal3.json

1.2 Placeholder Extraction

The following placeholders were extracted from the current template schema:

Placeholder Key Field Type Content Pattern Position (Page 1)
customer_name text {customer_name} Header
customer_addr text {customer_addr} Header
customer_tel text {customer_tel} Header
customer_email text {customer_email} Header
customer_att text {customer_att} Header
project_name text {project_name} Header
site_location text {site_location} Header
quotation_date text {quotation_date} Header
quotation_code text {quotation_code} Header
quotation_price table (cell) {quotation_price} Price section
currency table (cell) {currency} Price section
exclusion_data table [[\"{exclusion_data}\"]] Exclusion section

Static Content (No Mapping Required):

  • Company header (company1, company2, company_addr1, company_addr2, company_tel, company_email, company_tax)
  • Labels (Date, Quotation No, Tel, Email, etc.)
  • Colons and separators
  • "Dear sirs" text

2. Runtime Audit Results

2.1 Placeholder Mapping Status

Placeholder Template Exists Mapping Exists Source Path Expected Type Actual Type Status
customer_name customer.name string string OK
customer_addr customer.address string string OK
customer_tel customer.contacts[primary].phone string string OK
customer_email customer.contacts[primary].email string string OK
customer_att quotation.attention string string OK
project_name quotation.projectName string string OK
site_location quotation.projectLocation string string OK
quotation_date quotation.quotationDate string string ⚠️ Check Format
quotation_code quotation.code string string OK
quotation_price quotation.totalAmount string (formatted) number ⚠️ Type Mismatch
currency quotation.currency string string OK
exclusion_data - string[][] undefined MISSING

2.2 Legacy Placeholders Not in Current Template

The following placeholders existed in the legacy implementation but are not present in the current template:

Placeholder Legacy Purpose Status
exclusion_label Topic label (static) Not in template
Terms_of_payment Payment topic label Not in template
data_term_of_payment Payment topic data Not in template
warranty Warranty topic label Not in template
data_warranty Warranty topic data Not in template
Delivery Delivery topic label Not in template
data_delivery Delivery topic data Not in template
Affter service After-service topic label Not in template
data_affter_service After-service topic data Not in template
topic General topic label Not in template
data_topic General topic data Not in template
app1 Approver 1 name Not in template
app1_position Approver 1 position Not in template
app2 Approver 2 name Not in template
app2_position Approver 2 position Not in template
app3 Approver 3 name Not in template
app3_position Approver 3 position Not in template

3. Critical Issues

3.1 Issue: Missing Exclusion Data Mapping

Severity: HIGH
Placeholder: exclusion_data
Template Format: [[\"{exclusion_data}\"]] (table with single column)
Expected Data Type: string[][]
Current Status: No mapping exists

Problem:

  • Template expects exclusion topic items to be displayed as a table
  • Legacy implementation used formatTopicItems() to convert topic items to string[][] format
  • Current implementation has no mapping for this placeholder
  • Exclusion topic data exists in database but is not passed to PDFME

Impact:

  • Exclusion section will be empty or show placeholder text
  • Critical business information missing from quotation PDF

Legacy Implementation:

// Legacy: src/features/quotations/lib/pdfme-export.ts
function formatTopicItems(topic: any): string[][] {
  if (!topic || !topic.items || topic.items.length === 0) {
    return [['-']];
  }
  return topic.items.map((item: any) => [item.content || '-']);
}

const termsTopic = topics.find(t => t.topicType === 'exclusion_from_scope_of_supply');
data.exclusion_data = formatTopicItems(termsTopic);

Required Fix:

  1. Add mapping for exclusion_data placeholder
  2. Transform topic items to string[][] format
  3. Handle empty topics with fallback [['-']]

3.2 Issue: Missing Topic Sections

Severity: HIGH
Affected Placeholders:

  • Terms_of_payment, data_term_of_payment
  • warranty, data_warranty
  • Delivery, data_delivery
  • Affter service, data_affter_service

Problem:

  • Legacy template had separate sections for each topic type
  • Current template only has exclusion section
  • Payment, warranty, delivery, and after-service topics are not displayed
  • These are critical business terms in quotations

Impact:

  • Payment terms not visible to customer
  • Warranty information missing
  • Delivery details not specified
  • After-service information absent

Legacy Structure:

Topic: Scope of Work
[data_scope]

Topic: Exclusion from scope of Supply:
[data_exclusion]

Terms of payment upon progress of each item:
[data_term_of_payment]

Warranty:
[data_warranty]

Delivery:
[data_delivery]

Affter service:
[data_affter_service]

Current Template Structure:

Quotation details...

Exclusion from scope of Supply:
[exclusion_data]

Required Fix:

  1. Add topic section fields to template OR
  2. Merge all topics into a single table with labels
  3. Implement topic filtering based on product type

3.3 Issue: Currency Format Mismatch

Severity: MEDIUM
Placeholder: quotation_price
Template Usage: [[\"Price (Exclude VAT)\",\"{quotation_price}\",\" \",\"{currency}\"]]
Expected Type: Formatted string (e.g., "฿1,000,000.00")
Actual Type: Number (from database)

Problem:

  • Database stores quotation.totalAmount as number (double precision)
  • PDFME table cell expects formatted currency string
  • Current mapping likely passes raw number without formatting
  • Thai locale formatting required (฿ symbol, thousand separators, 2 decimal places)

Impact:

  • Price may display as "1000000" instead of "฿1,000,000.00"
  • Unprofessional appearance
  • Potential customer confusion

Legacy Implementation:

function formatCurrency(amount: number, currencyCode: CurrencyCode): string {
  const symbol = getCurrencySymbol(currencyCode);
  const formatted = new Intl.NumberFormat('th-TH', {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(amount);
  return `${symbol}${formatted}`;
}

data.quotation_price = formatCurrency(quotation.totalAmount, quotation.currency);

Required Fix:

  1. Add currency formatting transform
  2. Use formatMask in mapping or custom transform function
  3. Support multiple currency codes

3.4 Issue: Date Format

Severity: MEDIUM
Placeholder: quotation_date
Expected Format: Localized date string (Thai: Buddhist calendar or English: Gregorian)
Current Format: ISO date string

Problem:

  • Database stores dates as ISO 8601 strings (e.g., "2026-06-17T00:00:00.000Z")
  • PDF displays raw ISO string instead of formatted date
  • Legacy implementation had language-aware date formatting

Impact:

  • Unprofessional appearance
  • May cause confusion about quotation validity date
  • Doesn't match business requirements for Thai/English date formats

Legacy Implementation:

function formatDate(dateString: string, language: 'th' | 'en'): string {
  const date = new Date(dateString);
  if (language === 'th') {
    return date.toLocaleDateString('th-TH', {
      day: 'numeric',
      month: 'short',
      year: 'numeric',
      calendar: 'buddhist'
    });
  }
  return date.toLocaleDateString('en-US', {
    day: 'numeric',
    month: 'short',
    year: 'numeric'
  });
}

Required Fix:

  1. Add date formatting transform
  2. Support Thai and English locales
  3. Configure format mask in mapping

3.5 Issue: Missing Product Type Topic Mapping

Severity: HIGH
Legacy Behavior: Different topic types mapped based on quotation product type

Problem:

  • Legacy implementation had product-specific topic mappings:
    • Crane: scope_of_work, exclusion_from_scope_of_supply, terms_of_payment_upon_progress_of_each_item, delivery_date, warranty
    • Dockdoor: dock_scope_of_work, exclusion_of_supply_installation, payment_conditions, delivery_date, warranty
    • Solarcell: solarcell_scope_of_work, solarcell_exclusion_of_supply_installation, solarcell_payment_conditions, solarcell_delivery, solarcell_warranty
  • Current implementation treats all topics uniformly
  • Topic type labels may not match product type

Impact:

  • Wrong topic labels for different product types
  • Inconsistent quotation formatting
  • Business process violation

Required Fix:

  1. Implement product type to topic type mapping
  2. Apply correct labels based on quotation type
  3. Support dynamic topic resolution

4. Data Format Analysis

4.1 Table Data Formats

Exclusion Data Table

Template Content:

"content": "[[\"{exclusion_data}\"]]"

Expected Format: string[][]

Example:

[
  ["Provide crane and installation service"],
  ["Site survey and installation planning"],
  ["Operator training (2 days)"]
]

Legacy Transform:

function formatTopicItems(topic: any): string[][] {
  if (!topic || !topic.items || topic.items.length === 0) {
    return [['-']];
  }
  return topic.items.map((item: any) => [item.content || '-']);
}

Current Status: Not implemented


Price Table

Template Content:

"content": "[[\"Price (Exclude VAT)\",\"{quotation_price}\",\" \",\"{currency}\"]]"

Expected Format: string[][] (4 columns)

Example:

[
  ["Price (Exclude VAT)", "฿1,000,000.00", " ", "THB"]
]

Legacy Transform:

data.quotation_price = formatCurrency(quotation.totalAmount, quotation.currency);
data.currency = quotation.currency;

Current Status: ⚠️ Number passed instead of formatted string


4.2 Text Data Formats

Customer Contact Information

Legacy Prefixes:

  • Tel: Tel : {customer_tel}
  • Email: Email : {customer_email}

Current Template: Labels and data are separate fields

  • Label: "Tel" (static)
  • Data: {customer_tel} (placeholder)

Status: OK (modern approach)


5. Current Implementation Gaps

5.1 Missing Transform Functions

Function Purpose Current Status
formatTopicItems() Convert topic items to string[][] Missing
formatCurrency() Format currency with locale ⚠️ May exist but not applied
formatDate() Format date with locale ⚠️ May exist but not applied
getCurrencySymbol() Get currency symbol Missing
section() Create label/data pairs for topics Missing
productTypeTopicMapping Map product type to topic types Missing

5.2 Missing Database Mappings

The following mappings should exist in crm_document_template_mappings table:

placeholder_key source_path data_type format_mask status
exclusion_data topics.exclusion_from_scope_of_supply.items table - Missing
quotation_price quotation.totalAmount scalar currency_THB ⚠️ Needs format
quotation_date quotation.quotationDate scalar date_th ⚠️ Needs format

5.3 Data Builder Issues

File: src/features/crm/quotations/document/server/service.ts

Issues:

  1. buildQuotationDocumentData() does not transform topic items to string[][]
  2. Does not format currency or dates
  3. Does not apply product type topic mapping
  4. Does not handle empty topics with fallbacks

6. Comparison with Legacy

6.1 Legacy Flow

generateQuotationPDF(quotation, customer, topics)
  
mapDataToTemplate() // Direct mapping with transforms
  
formatDate() // Thai/English date
  
formatCurrency() // Thai locale currency
  
formatTopicItems() // Convert to string[][]
  
generateExtraTopicFields() // Dynamic schema
  
pdfme.generate(template, inputs)

6.2 Current Flow

buildQuotationDocumentData() // Fetch data
  
mapDocumentDataToTemplateInput() // Generic mapping
  
applyFormatMask() // Simple format masks only
  
pdfme.generate(template, inputs)

6.3 Key Differences

Aspect Legacy Current Gap
Mapping Approach Direct with inline transforms Generic via database Transform logic
Topic Formatting string[][] via formatTopicItems() Unknown Format mismatch
Currency Formatting formatCurrency() with locale Basic format mask Missing locale
Date Formatting formatDate() with locale Basic format mask Missing locale
Product Type Product-specific topic mapping None Wrong labels
Dynamic Topics generateExtraTopicFields() Static schema No extra topics
Fallback Values Inline defaults in mapping defaultValue column May not cover all cases

7.1 Immediate Fixes (P0)

Fix 1: Add Exclusion Data Mapping

Add to crm_document_template_mappings:

INSERT INTO crm_document_template_mappings (
  id, organization_id, template_version_id, placeholder_key,
  source_path, data_type, sort_order
) VALUES (
  gen_random_uuid(),
  '<organization_id>',
  '<template_version_id>',
  'exclusion_data',
  'topics.exclusion_from_scope_of_supply.items',
  'table',
  10
);

Add transform function in src/features/crm/quotations/document/server/utils.ts:

export function formatTopicItems(
  topic: { items?: Array<{ content?: string }> } | undefined
): string[][] {
  if (!topic?.items || topic.items.length === 0) {
    return [['-']];
  }
  return topic.items.map((item) => [item.content || '-']);
}

Update buildQuotationDocumentData():

const exclusionTopic = topics.find(
  t => t.topicType === 'exclusion_from_scope_of_supply'
);
data.exclusion_data = formatTopicItems(exclusionTopic);

Fix 2: Format Currency

Update mapping for quotation_price:

UPDATE crm_document_template_mappings
SET format_mask = 'currency_THB'
WHERE placeholder_key = 'quotation_price';

Add currency formatter in src/features/crm/quotations/document/server/utils.ts:

export function formatCurrency(
  amount: number,
  currencyCode: string = 'THB'
): string {
  const symbols: Record<string, string> = {
    THB: '฿',
    USD: '$',
    EUR: '€'
  };
  const symbol = symbols[currencyCode] || currencyCode;
  const formatted = new Intl.NumberFormat('th-TH', {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(amount);
  return `${symbol}${formatted}`;
}

Update mapDocumentDataToTemplateInput() in document-template/server/service.ts:

if (mapping.formatMask?.startsWith('currency_')) {
  const currencyCode = mapping.formatMask.replace('currency_', '');
  result[mapping.placeholderKey] = formatCurrency(
    rawValue as number,
    currencyCode
  );
  continue;
}

Fix 3: Format Date

Update mapping for quotation_date:

UPDATE crm_document_template_mappings
SET format_mask = 'date_th'
WHERE placeholder_key = 'quotation_date';

Add date formatter in src/features/crm/quotations/document/server/utils.ts:

export function formatDate(
  dateString: string,
  language: 'th' | 'en' = 'th'
): string {
  const date = new Date(dateString);
  if (language === 'th') {
    return date.toLocaleDateString('th-TH', {
      day: 'numeric',
      month: 'short',
      year: 'numeric',
      calendar: 'buddhist'
    });
  }
  return date.toLocaleDateString('en-US', {
    day: 'numeric',
    month: 'short',
    year: 'numeric'
  });
}

Update mapDocumentDataToTemplateInput():

if (mapping.formatMask?.startsWith('date_')) {
  const language = mapping.formatMask.replace('date_', '') as 'th' | 'en';
  result[mapping.placeholderKey] = formatDate(
    rawValue as string,
    language
  );
  continue;
}

7.2 Template Updates (P0)

Option A: Add Missing Topic Sections to Template

Add the following fields to the template schema:

{
  "name": "payment_label",
  "type": "table",
  "content": "[[\"Terms of payment upon progress of each item:\"]]",
  "position": { "x": 10, "y": 220 },
  "width": 190,
  "height": 5
},
{
  "name": "payment_data",
  "type": "table",
  "content": "[[\"{payment_data}\"]]",
  "position": { "x": 15, "y": 230 },
  "width": 185,
  "height": 7
},
{
  "name": "warranty_label",
  "type": "table",
  "content": "[[\"Warranty:\"]]",
  "position": { "x": 10, "y": 250 },
  "width": 190,
  "height": 5
},
{
  "name": "warranty_data",
  "type": "table",
  "content": "[[\"{warranty_data}\"]]",
  "position": { "x": 15, "y": 260 },
  "width": 185,
  "height": 7
}

Option B: Consolidate Topics into Single Table

Create a unified topics table with labels:

{
  "name": "topics_table",
  "type": "table",
  "content": "[{topics_data}]",
  "position": { "x": 10, "y": 195 },
  "width": 190,
  "height": 50
}

Where topics_data is a structured array:

[
  ["Exclusion from scope of Supply:", "Provide crane, Site survey"],
  ["Terms of payment:", "30% deposit, 70% on delivery"],
  ["Warranty:", "12 months parts and labor"],
  ["Delivery:", "30 days after order confirmation"],
  ["After service:", "Annual maintenance available"]
]

7.3 Product Type Mapping (P1)

Create configuration in src/features/crm/quotations/document/config/topic-mapping.ts:

export const TOPIC_TYPE_MAPPING: Record<string, Record<string, string>> = {
  crane: {
    scope: 'scope_of_work',
    exclusion: 'exclusion_from_scope_of_supply',
    payment: 'terms_of_payment_upon_progress_of_each_item',
    delivery: 'delivery_date',
    warranty: 'warranty'
  },
  dockdoor: {
    scope: 'dock_scope_of_work',
    exclusion: 'exclusion_of_supply_installation',
    payment: 'payment_conditions',
    delivery: 'delivery_date',
    warranty: 'warranty'
  },
  solarcell: {
    scope: 'solarcell_scope_of_work',
    exclusion: 'solarcell_exclusion_of_supply_installation',
    payment: 'solarcell_payment_conditions',
    delivery: 'solarcell_delivery',
    warranty: 'solarcell_warranty'
  }
};

Update buildQuotationDocumentData() to use product type:

const mapping = TOPIC_TYPE_MAPPING[quotation.productType] || TOPIC_TYPE_MAPPING.crane;
const exclusionTopic = topics.find(t => t.topicType === mapping.exclusion);
data.exclusion_data = formatTopicItems(exclusionTopic);

8. Risk Assessment

8.1 High Risk

Risk Impact Probability Mitigation
Missing exclusion data High customer confusion 100% Add mapping immediately
Missing payment terms Legal/compliance issue 100% Add to template
Wrong currency format Unprofessional appearance 90% Add currency formatter
Wrong date format Date confusion 90% Add date formatter

8.2 Medium Risk

Risk Impact Probability Mitigation
Missing product type mapping Inconsistent formatting 80% Implement product mapping
Missing warranty info Customer expectations 70% Add to template
Missing delivery info Order confusion 70% Add to template

8.3 Low Risk

Risk Impact Probability Mitigation
Missing signatures Process compliance 50% Add approval workflow
Static company info Outdated info 30% Make dynamic

9. Testing Recommendations

9.1 Unit Tests

Create tests for transform functions:

// tests/features/crm/quotations/document/utils.test.ts
describe('formatTopicItems', () => {
  it('should format topic items to string[][]', () => {
    const topic = {
      items: [
        { content: 'Item 1' },
        { content: 'Item 2' }
      ]
    };
    expect(formatTopicItems(topic)).toEqual([
      ['Item 1'],
      ['Item 2']
    ]);
  });

  it('should return fallback for empty topic', () => {
    expect(formatTopicItems(undefined)).toEqual([['-']]);
  });
});

9.2 Integration Tests

Test end-to-end PDF generation:

describe('Quotation PDF Generation', () => {
  it('should generate PDF with all fields populated', async () => {
    const quotation = await createTestQuotation();
    const pdf = await generateQuotationPDF(quotation.id);
    
    // Verify placeholders are replaced
    expect(pdf).toContain('CUSTOMER NAME');
    expect(pdf).not.toContain('{customer_name}');
  });
});

9.3 Visual Regression Tests

Compare generated PDFs with legacy PDFs:

  1. Generate PDF from legacy implementation
  2. Generate PDF from current implementation
  3. Compare pixel-by-pixel
  4. Identify visual differences

10. Implementation Priority

Phase 1: Critical Fixes (1-2 days)

  1. Add exclusion data mapping
  2. Implement formatTopicItems()
  3. Format currency
  4. Format dates
  5. Test with real quotation

Phase 2: Template Updates (1 day)

  1. Add missing topic sections (payment, warranty, delivery)
  2. Update template layout
  3. Add mappings for new fields

Phase 3: Product Type Logic (1 day)

  1. Implement product type mapping
  2. Update topic resolution
  3. Test different product types

Phase 4: Enhancements (2-3 days)

  1. Dynamic topic schema generation
  2. Approval signature integration
  3. Visual regression testing

11. Conclusion

The current PDFME implementation has significant gaps compared to the legacy system, resulting in incomplete or incorrectly formatted quotation PDFs. The primary issues are:

  1. Missing topic mappings (exclusion, payment, warranty, delivery)
  2. Missing transform functions (formatTopicItems, currency, date)
  3. Template structure differences (fewer sections)
  4. No product type logic (generic topic labels)

Recommended Action: Implement Phase 1 fixes immediately to restore basic functionality, then proceed with template updates and product type logic.

Estimated Total Effort: 5-7 days for complete resolution.


Appendix A: Placeholder Inventory

A.1 Current Template Placeholders

customer_name
customer_addr
customer_tel
customer_email
customer_att
project_name
site_location
quotation_date
quotation_code
quotation_price
currency
exclusion_data

A.2 Legacy Template Placeholders (Not in Current)

exclusion_label
Terms_of_payment
data_term_of_payment
warranty
data_warranty
Delivery
data_delivery
Affter service
data_affter_service
topic
data_topic
app1
app1_position
app2
app2_position
app3
app3_position

A.3 Mapped vs Unmapped

Category Total Mapped Unmapped % Mapped
Customer Info 4 4 0 100%
Quotation Info 6 6 0 100%
Topics 1 0 1 0%
Signatures 6 0 6 0%
Total 17 10 7 59%

Appendix B: Data Source Reference

B.1 Database Schema

Customer: crm_customers

  • name{customer_name}
  • address{customer_addr}
  • phone{customer_tel}
  • email{customer_email}

Quotation: crm_quotations

  • attention{customer_att}
  • projectName{project_name}
  • projectLocation{site_location}
  • quotationDate{quotation_date}
  • code{quotation_code}
  • totalAmount{quotation_price}
  • currency{currency}

Topics: crm_quotation_topics + crm_quotation_topic_items

  • topicType → determines which topic
  • items[].content → topic content items

B.2 Source Path Examples

Placeholder Source Path Notes
customer_name customer.name Direct mapping
customer_tel customer.contacts[primary].phone Array access
quotation_price quotation.totalAmount Number → string transform
exclusion_data topics.exclusion_from_scope_of_supply.items Topic → string[][] transform

Report Generated: 2026-06-17
Auditor: AI Code Review Agent
Version: 1.0