Build CoreOrdered learning track

Document Generation and Quote Artifacts

Learn Enterprise CPQ OMS Camunda 7 - Part 033

Mendesain document generation dan quote artifact untuk CPQ/OMS enterprise: quote PDF, proposal, template versioning, immutable artifact, snapshot, storage, audit, security, idempotency, dan workflow integration.

21 min read4026 words
PrevNext
Lesson 3364 lesson track1335 Build Core
#java#microservices#cpq#oms+6 more

Part 033 — Document Generation and Quote Artifacts

Di sistem CPQ enterprise, quote tidak selesai ketika angka total muncul di UI.

Quote baru benar-benar bisa dipakai ketika organisasi dapat menghasilkan dokumen yang:

  • merepresentasikan konfigurasi yang benar;
  • memakai harga yang disetujui;
  • punya term dan condition yang tepat;
  • dapat dikirim ke customer;
  • bisa dibuktikan kembali beberapa bulan atau tahun kemudian;
  • tidak berubah diam-diam setelah customer menerima;
  • dapat dilacak siapa yang membuat, mengirim, menerima, dan membatalkan;
  • bisa dipakai sebagai dasar order tanpa ambiguity.

Dokumen quote adalah artefak legal-komersial. Ia bukan sekadar PDF.

Prinsip utama part ini:

A quote document is a frozen commercial artifact. It must be reproducible as evidence, but not necessarily regenerated from today's system state.

Kesalahan desain paling umum adalah menganggap document generation sebagai fitur template belaka:

Quote data -> template -> PDF -> done

Itu terlalu dangkal untuk enterprise CPQ/OMS.

Desain yang benar memisahkan:

  • commercial snapshot;
  • render input;
  • template version;
  • artifact metadata;
  • binary storage;
  • generation job;
  • approval dependency;
  • customer delivery;
  • audit evidence;
  • invalidation policy.

Dokumen yang terlihat sederhana di permukaan sebenarnya berada di persimpangan antara quote lifecycle, pricing, approval, compliance, notification, storage, dan order conversion.


1. What Is a Quote Artifact?

Kita perlu membedakan istilah sejak awal.

ConceptMeaningAuthority
QuoteCommercial intent being negotiatedQuote Service
Quote RevisionImmutable-ish revision candidateQuote Service
Price ResultFrozen pricing evidence for a revisionPricing/Quote boundary
Approval DecisionPermission to proceed under policyApproval/Workflow boundary
Render InputData structure used to generate documentDocument Service
TemplateVersioned rendering ruleDocument Service
ArtifactGenerated immutable output metadata + binaryDocument Service
DeliverySending artifact to external recipientNotification/Communication Service
AcceptanceCustomer/business acceptance of quote termsQuote Service

Jangan mencampur semua ini dalam satu kolom quote.pdf_url.

quote.pdf_url kelihatannya praktis, tetapi menyembunyikan banyak pertanyaan penting:

  • PDF itu dibuat dari revision berapa?
  • Template version apa?
  • Harga mana yang dipakai?
  • Apakah approval masih fresh waktu dokumen dibuat?
  • Siapa yang generate?
  • Dokumen ini pernah dikirim ke customer?
  • Apakah dokumen ini superseded?
  • Apakah binary-nya berubah?
  • Apakah ada hash untuk membuktikan integritas?
  • Apakah dokumen ini valid untuk acceptance?

Jika jawaban pertanyaan ini tidak ada, artifact model belum enterprise-grade.


2. Document Generation Is a Lifecycle

Document generation bukan single function. Ia lifecycle.

Lifecycle ini penting karena generate document sering gagal karena hal non-domain:

  • template missing;
  • font/render engine error;
  • image/logo tidak tersedia;
  • object storage timeout;
  • malformed render input;
  • unsupported locale;
  • document terlalu besar;
  • external storage conflict;
  • network issue;
  • data dependency belum fresh;
  • approval status berubah saat generate.

Jika sistem hanya punya method generatePdf(quoteId), semua failure ini akan bercampur sebagai 500 Internal Server Error.

Enterprise system membutuhkan artifact lifecycle yang eksplisit.


3. Why Artifact Immutability Matters

Quote artifact harus immutable karena dokumen adalah evidence.

Misalnya:

  1. Sales membuat quote revision 4.
  2. Pricing menghasilkan total USD 129,900.
  3. Manager menyetujui discount 18%.
  4. Document service membuat proposal PDF.
  5. Proposal dikirim ke customer.
  6. Customer menyetujui 5 hari kemudian.
  7. Product catalog berubah sehari setelah dokumen dikirim.
  8. Pricing rule berubah dua hari setelah dokumen dikirim.

Pada saat acceptance, sistem tidak boleh berkata:

“Mari kita generate ulang dari data terbaru.”

Karena yang diterima customer adalah dokumen lama.

Maka artifact harus menyimpan bukti:

  • quote revision identity;
  • price result identity;
  • approval decision identity;
  • template identity;
  • render input hash;
  • binary hash;
  • generated timestamp;
  • generated by;
  • delivery timestamp;
  • recipient;
  • supersession status.

Artifact immutable bukan berarti tidak bisa ada versi baru. Artinya artifact lama tidak diedit. Jika ada perubahan, buat artifact baru dan tandai artifact lama sebagai superseded/withdrawn.


4. Architecture Overview

Document Service bertanggung jawab atas:

  • artifact request;
  • render input validation;
  • template resolution;
  • artifact metadata;
  • binary storage coordination;
  • document lifecycle;
  • document events;
  • document access control surface.

Document Service tidak bertanggung jawab atas:

  • menentukan harga;
  • menyetujui discount;
  • mengubah quote lifecycle;
  • mengirim email secara langsung;
  • menyimpan rule pricing;
  • menjadi source of truth quote.

Ia adalah service spesialis untuk menghasilkan dan mengelola artefak.


5. Core Invariants

Inilah invariants utama.

InvariantWhy it matters
Artifact must point to exactly one quote revisionMencegah dokumen ambigu
Artifact must point to exact render input version/hashMembuktikan data yang dirender
Artifact binary must be immutable after storedMencegah evidence tampering
Artifact must record template versionPerubahan template tidak mengubah dokumen lama
Artifact cannot be valid if quote revision changed after renderMencegah stale document dipakai
Accepted quote must reference the accepted artifact when acceptance is document-basedAcceptance punya evidence
Superseded artifact remains retrievable for auditHistory tidak hilang
Artifact access must be authorization-checked at object levelCommercial data sensitif
Artifact generation must be idempotent for same request keyRetry aman
Artifact delivery must be separated from artifact generationGenerate sukses tidak berarti terkirim

Mental model:

The document service is not a printer. It is an evidence factory.


6. Artifact Types

Tidak semua artifact sama.

Artifact typeExampleMutabilityCustomer-facing?Acceptance relevance
Quote ProposalCustomer proposal PDFimmutableyeshigh
Internal Approval PackApproval summaryimmutablenomedium
Price BreakdownDetailed pricing attachmentimmutablesometimeshigh
Terms AppendixLegal termsversioned/immutable artifactyeshigh
Configuration SummaryProduct/options summaryimmutableyeshigh
Order ConfirmationConfirmation after orderimmutableyesmedium
Fallout ReportOperational exception artifactimmutablenolow
Amendment ComparisonDelta quote comparisonimmutableyeshigh

Artifact type menentukan:

  • template family;
  • required dependencies;
  • access policy;
  • retention policy;
  • delivery policy;
  • acceptance policy;
  • watermark behavior;
  • localization rules;
  • allowed lifecycle transitions.

Jangan memakai satu document_type = 'PDF'.

PDF adalah format. Bukan business artifact type.


7. Quote Snapshot vs Render Input

Quote snapshot dan render input mirip, tetapi tidak sama.

Quote snapshot adalah domain evidence:

{
  "quoteRevisionId": "qr-004",
  "customerId": "cust-889",
  "lineItems": [...],
  "priceResultId": "pr-1209",
  "approvalDecisionId": "ap-771",
  "effectiveCatalogVersion": "cat-2026-07",
  "currency": "USD"
}

Render input adalah view model untuk template:

{
  "proposalNumber": "Q-2026-0001029-R4",
  "customerDisplayName": "Acme Manufacturing",
  "salesContact": {
    "name": "Dina S.",
    "email": "dina@example.internal"
  },
  "sections": [
    {
      "title": "Commercial Summary",
      "rows": [
        {"label": "Subtotal", "value": "USD 158,414.63"},
        {"label": "Discount", "value": "USD -28,514.63"},
        {"label": "Total", "value": "USD 129,900.00"}
      ]
    }
  ],
  "terms": {
    "validUntil": "2026-08-01",
    "paymentTerm": "NET_30"
  }
}

Render input boleh memiliki field formatting dan layout helper. Quote snapshot tidak boleh tercemar oleh kebutuhan template.

Rule:

Domain snapshot is for truth. Render input is for presentation.


8. Required Snapshot Data

Quote proposal biasanya membutuhkan snapshot data berikut.

8.1 Identity

  • tenant id;
  • quote id;
  • quote number;
  • revision number;
  • artifact type;
  • generated by;
  • generation timestamp;
  • validity date;
  • customer identity;
  • sales owner identity.

8.2 Commercial data

  • product offering name;
  • configured options;
  • quantity;
  • contract term;
  • one-time charge;
  • recurring charge;
  • discount;
  • surcharge;
  • currency;
  • tax placeholder or tax disclaimer;
  • total;
  • payment terms.

8.3 Governance data

  • approval status;
  • approval decision id;
  • approver list or approval summary;
  • policy reason;
  • manual override reason;
  • price result id;
  • catalog version;
  • pricing rule version.
  • customer legal name;
  • billing address;
  • service address if relevant;
  • legal entity of seller;
  • terms version;
  • acceptance instructions;
  • validity window;
  • confidentiality notice.

8.5 Traceability data

  • correlation id;
  • request id;
  • artifact id;
  • template version;
  • render input hash;
  • binary hash;
  • generation worker version.

Jika ada field yang tidak boleh dilihat customer, jangan hanya “hide in template”. Jangan masukkan field itu ke render input customer-facing.


9. Template Versioning

Template adalah contract.

Perubahan template bisa mengubah:

  • kalimat legal;
  • layout price table;
  • urutan section;
  • format discount;
  • label product;
  • wording validity;
  • acceptance instruction;
  • logo/entity seller;
  • language;
  • footer regulatory notice.

Maka template harus versioned.

Contoh model:

TemplateFamily: quote-proposal
TemplateVersion: quote-proposal:2026.07.0
Locale: en-US
Market: US
Channel: direct-sales
Format: pdf
Status: ACTIVE
EffectiveFrom: 2026-07-01T00:00:00Z
EffectiveTo: null

Jangan pakai file path sebagai version:

/templates/quote.ftl

Itu bukan identity. Itu implementation detail.

Template resolution policy

Template dipilih berdasarkan:

  • artifact type;
  • tenant;
  • market/country;
  • language/locale;
  • sales channel;
  • product family;
  • customer segment;
  • effective date;
  • explicit override if allowed.

Resolution result harus disimpan di artifact metadata.

{
  "templateFamily": "quote-proposal",
  "templateVersion": "quote-proposal:2026.07.0",
  "locale": "en-US",
  "market": "US",
  "resolvedAt": "2026-07-02T10:15:00Z"
}

Jika template berubah minggu depan, artifact lama tetap mengarah ke template lama.


10. Template Governance

Template governance sering diremehkan. Padahal template adalah production code dengan dampak legal.

Minimal governance:

  • template has owner;
  • template has version;
  • template has review workflow;
  • template has preview tests;
  • template has sample render cases;
  • template has effective date;
  • template has rollback path;
  • template has compatibility contract with render input schema;
  • legal text changes require approval;
  • active template cannot be edited in place.

Template deployment tidak boleh seperti upload file acak ke server.

Template release harus melewati quality gate:

Template change -> lint -> schema compatibility -> sample render -> legal review -> approval -> publish as new version

Jika ada kebutuhan hotfix, buat version baru. Jangan mutate version lama.


11. Render Input Schema

Render input harus punya schema.

Tanpa schema, template engine akan gagal runtime karena field missing.

Contoh simplified schema idea:

{
  "$id": "quote-proposal-render-input.v1",
  "type": "object",
  "required": [
    "proposalNumber",
    "customer",
    "seller",
    "commercialSummary",
    "lineItems",
    "validUntil"
  ],
  "properties": {
    "proposalNumber": {"type": "string"},
    "customer": {
      "type": "object",
      "required": ["displayName"],
      "properties": {
        "displayName": {"type": "string"},
        "legalName": {"type": "string"}
      }
    },
    "lineItems": {
      "type": "array",
      "items": {"$ref": "#/$defs/lineItem"}
    }
  }
}

Template version harus declare render input schema version yang didukung.

quote-proposal:2026.07.0 supports quote-proposal-render-input.v1

Jika render input berubah secara breaking, rilis template family/version yang sesuai.


12. Artifact Identity

Artifact id harus menjadi first-class identity.

Contoh:

artifact_id = art_01J1X8...
artifact_number = Q-2026-0001029-R4-PROP-001

artifact_id adalah internal stable identifier.

artifact_number adalah human-readable identifier untuk audit/customer support.

Artifact metadata minimal:

{
  "artifactId": "art-77a",
  "artifactNumber": "Q-2026-0001029-R4-PROP-001",
  "tenantId": "tnt-123",
  "quoteId": "q-10029",
  "quoteRevisionId": "qr-004",
  "artifactType": "QUOTE_PROPOSAL",
  "status": "AVAILABLE",
  "format": "PDF",
  "templateVersion": "quote-proposal:2026.07.0",
  "renderInputHash": "sha256:...",
  "binaryHash": "sha256:...",
  "storageObjectKey": "tenant/tnt-123/quote/q-10029/artifacts/art-77a.pdf",
  "generatedAt": "2026-07-02T10:15:30Z",
  "generatedBy": "user-991"
}

Artifact identity harus muncul di:

  • audit event;
  • notification payload;
  • customer portal link;
  • quote acceptance record;
  • support screen;
  • object storage metadata;
  • business timeline.

13. Database Model

Simplified PostgreSQL schema:

CREATE TABLE document_artifact (
  artifact_id             uuid PRIMARY KEY,
  tenant_id               text NOT NULL,
  artifact_number         text NOT NULL,
  artifact_type           text NOT NULL,
  status                  text NOT NULL,
  subject_type            text NOT NULL,
  subject_id              uuid NOT NULL,
  subject_revision_id     uuid,
  format                  text NOT NULL,
  template_family         text NOT NULL,
  template_version        text NOT NULL,
  render_input_schema     text NOT NULL,
  render_input_hash       text,
  binary_hash             text,
  storage_bucket          text,
  storage_object_key      text,
  content_type            text,
  content_length          bigint,
  generated_by            text,
  generated_at            timestamptz,
  superseded_by_artifact_id uuid,
  failure_code            text,
  failure_message         text,
  created_at              timestamptz NOT NULL DEFAULT now(),
  updated_at              timestamptz NOT NULL DEFAULT now(),
  version                 bigint NOT NULL DEFAULT 0,

  CONSTRAINT uq_document_artifact_number UNIQUE (tenant_id, artifact_number),
  CONSTRAINT ck_document_artifact_status CHECK (
    status IN (
      'REQUESTED',
      'WAITING_FOR_DEPENDENCIES',
      'RENDERING',
      'AVAILABLE',
      'FAILED',
      'SUPERSEDED',
      'CANCELLED'
    )
  )
);

Generation job:

CREATE TABLE document_generation_job (
  job_id                  uuid PRIMARY KEY,
  tenant_id               text NOT NULL,
  artifact_id             uuid NOT NULL REFERENCES document_artifact(artifact_id),
  idempotency_key          text NOT NULL,
  status                  text NOT NULL,
  attempt_count            int NOT NULL DEFAULT 0,
  next_attempt_at          timestamptz,
  locked_by                text,
  locked_until             timestamptz,
  last_error_code          text,
  last_error_message       text,
  created_at              timestamptz NOT NULL DEFAULT now(),
  updated_at              timestamptz NOT NULL DEFAULT now(),

  CONSTRAINT uq_document_generation_idempotency UNIQUE (tenant_id, idempotency_key)
);

Artifact event/outbox:

CREATE TABLE document_outbox (
  outbox_id               uuid PRIMARY KEY,
  tenant_id               text NOT NULL,
  aggregate_type          text NOT NULL,
  aggregate_id            uuid NOT NULL,
  event_type              text NOT NULL,
  event_version           int NOT NULL,
  payload                 jsonb NOT NULL,
  headers                 jsonb NOT NULL,
  status                  text NOT NULL DEFAULT 'PENDING',
  created_at              timestamptz NOT NULL DEFAULT now(),
  published_at            timestamptz
);

Jangan simpan binary besar di table utama kecuali ada alasan kuat. Simpan metadata di DB, binary di object storage atau storage service yang sesuai.


14. Binary Storage Model

Artifact binary harus disimpan dengan pola yang aman.

Storage metadata:

  • bucket/container;
  • object key;
  • content type;
  • content length;
  • checksum/hash;
  • encryption mode;
  • retention class;
  • storage version id if supported;
  • created timestamp;
  • access policy;
  • legal hold marker if applicable.

Object key jangan bergantung pada nama file customer.

Bad:

Acme Quote Final.pdf

Better:

tenant/tnt-123/quote/q-10029/revision/qr-004/artifact/art-77a/content.pdf

Tetap jangan menganggap object key sebagai authorization. Authorization harus dicek lewat service/API.

Signed URL boundary

Jika memakai signed URL:

  • durasi pendek;
  • scoped ke artifact tertentu;
  • hanya setelah authorization check;
  • jangan log signed URL penuh;
  • jangan kirim signed URL permanen via email;
  • pertimbangkan portal link yang resolve ke short-lived URL.

Signed URL adalah delivery mechanism, bukan access policy.


15. API Design

Document API harus command-shaped.

Request quote proposal generation

POST /quotes/{quoteId}/revisions/{revisionNo}/artifacts/quote-proposal
Idempotency-Key: gen-q-10029-r4-proposal-v1
If-Match: "quote-revision-version-8"

Request body:

{
  "locale": "en-US",
  "format": "PDF",
  "deliveryIntent": "GENERATE_ONLY",
  "requestedBy": "user-991"
}

Response:

{
  "artifactId": "art-77a",
  "artifactNumber": "Q-2026-0001029-R4-PROP-001",
  "status": "REQUESTED",
  "links": {
    "self": "/artifacts/art-77a"
  }
}

Jika generation asynchronous:

202 Accepted

Jangan menjanjikan PDF langsung jika render bisa lebih dari beberapa ratus milidetik, bergantung template dan storage.

Get artifact metadata

GET /artifacts/{artifactId}

Response:

{
  "artifactId": "art-77a",
  "artifactType": "QUOTE_PROPOSAL",
  "status": "AVAILABLE",
  "quoteId": "q-10029",
  "quoteRevisionNo": 4,
  "templateVersion": "quote-proposal:2026.07.0",
  "generatedAt": "2026-07-02T10:15:30Z",
  "content": {
    "contentType": "application/pdf",
    "contentLength": 240181,
    "hash": "sha256:..."
  }
}

Download artifact

GET /artifacts/{artifactId}/content

Server melakukan authorization check lalu streaming binary atau redirect ke short-lived signed URL.

Supersede artifact

POST /artifacts/{artifactId}/supersede

Supersede bukan delete.


16. Synchronous vs Asynchronous Generation

Tidak semua document generation harus async, tetapi enterprise CPQ biasanya lebih aman async.

ModeGood forRisk
Synchronoussmall internal preview, fast templatesrequest timeout, user waits, retry ambiguity
Asynchronouscustomer proposal, large quote, legal packageextra lifecycle complexity
Hybridquick preview sync, final artifact asynctwo paths must not diverge

Recommended model:

  • preview can be sync and watermarked;
  • final customer-facing artifact is async and immutable;
  • final artifact emits event when available;
  • notification service sends when artifact is available and delivery requested.

Preview tidak boleh dipakai sebagai accepted artifact.


17. Idempotency Design

Generation request harus idempotent.

Example idempotency key:

tenant:tnt-123:quote:q-10029:revision:4:type:QUOTE_PROPOSAL:locale:en-US:format:PDF

Tetapi jangan mengandalkan client untuk selalu membentuk key dengan benar. Server juga harus punya natural uniqueness jika business rule menginginkan one active artifact per revision/type.

Possible uniqueness:

CREATE UNIQUE INDEX uq_active_quote_proposal_artifact
ON document_artifact (tenant_id, subject_revision_id, artifact_type, template_version, format)
WHERE status IN ('REQUESTED', 'WAITING_FOR_DEPENDENCIES', 'RENDERING', 'AVAILABLE');

Policy harus jelas:

  • same request returns existing artifact;
  • changed locale creates different artifact;
  • changed template version creates different artifact;
  • quote revision change creates different artifact;
  • superseded artifact can be regenerated as new artifact;
  • failed artifact can be retried without new artifact or can create new artifact depending status policy.

Idempotency bukan “ignore duplicate”. Idempotency adalah “same command produces same observable result”.


18. Freshness Checks

Sebelum final artifact dibuat, system harus memeriksa freshness.

Checklist:

  • quote revision exists;
  • quote revision is in allowed state;
  • configuration snapshot exists;
  • price result exists;
  • price result is valid for revision;
  • approval required? if yes, approval decision exists and is valid;
  • quote is not expired;
  • customer data required for document exists;
  • template resolution succeeds;
  • user has permission to generate;
  • no newer revision already supersedes this revision for final proposal use.

Mermaid dependency gate:

Jangan generate final proposal dari draft yang masih bergerak.


19. Artifact Status vs Quote Status

Artifact status tidak sama dengan quote status.

Quote status:

DRAFT -> CONFIGURED -> PRICED -> APPROVAL_PENDING -> APPROVED -> ACCEPTED

Artifact status:

REQUESTED -> RENDERING -> AVAILABLE -> DELIVERED/SUPERSEDED

Quote bisa APPROVED tetapi artifact generation FAILED.

Artifact bisa AVAILABLE tetapi quote revision sudah SUPERSEDED.

Quote bisa ACCEPTED berdasarkan artifact tertentu.

Jangan gabungkan state seperti:

QUOTE_STATUS = PDF_GENERATED

Itu mencampur domain commercial lifecycle dengan document lifecycle.


20. Document Events

Document service harus publish events.

Examples:

DocumentArtifactRequested
DocumentArtifactRenderingStarted
DocumentArtifactAvailable
DocumentArtifactGenerationFailed
DocumentArtifactSuperseded
DocumentArtifactDownloaded
DocumentArtifactDeliveryRequested

Event payload:

{
  "eventId": "evt-901",
  "eventType": "DocumentArtifactAvailable",
  "eventVersion": 1,
  "occurredAt": "2026-07-02T10:15:30Z",
  "tenantId": "tnt-123",
  "artifactId": "art-77a",
  "artifactType": "QUOTE_PROPOSAL",
  "subjectType": "QUOTE_REVISION",
  "subjectId": "qr-004",
  "quoteId": "q-10029",
  "quoteRevisionNo": 4,
  "artifactNumber": "Q-2026-0001029-R4-PROP-001",
  "templateVersion": "quote-proposal:2026.07.0",
  "binaryHash": "sha256:...",
  "correlationId": "corr-abc"
}

Notification service dapat subscribe ke DocumentArtifactAvailable, tetapi hanya mengirim jika ada delivery intent atau workflow command.

Quote service dapat subscribe untuk update read model, tetapi quote domain truth tidak boleh berubah hanya karena artifact tersedia kecuali ada command/event contract yang jelas.


21. Camunda 7 Boundary

Camunda workflow sering butuh document generation:

  • after quote approval, generate proposal;
  • before sending customer notification, ensure artifact exists;
  • before contract acceptance, verify artifact available;
  • during order confirmation, generate confirmation document.

Namun Camunda process variable tidak boleh menyimpan seluruh render input.

Good variable:

{
  "quoteId": "q-10029",
  "quoteRevisionNo": 4,
  "artifactId": "art-77a",
  "artifactType": "QUOTE_PROPOSAL"
}

Bad variable:

{
  "entireQuote": {...},
  "renderInput": {...},
  "pdfBase64": "..."
}

Workflow harus memanggil Document Service melalui command API, lalu menunggu event/callback jika async.

Pattern:

Workflow waits for business event, not for render thread.


22. Rendering Engine Boundary

Rendering engine bisa memakai teknologi berbeda:

  • HTML-to-PDF;
  • template engine + PDF renderer;
  • DOCX template + PDF conversion;
  • dedicated document composition platform;
  • external vendor.

Yang penting: boundary-nya stabil.

Render worker input:

{
  "artifactId": "art-77a",
  "templateVersion": "quote-proposal:2026.07.0",
  "renderInputRef": "render-input/art-77a.json",
  "format": "PDF",
  "locale": "en-US"
}

Render worker output:

{
  "artifactId": "art-77a",
  "status": "AVAILABLE",
  "contentType": "application/pdf",
  "contentLength": 240181,
  "binaryHash": "sha256:...",
  "storageObjectKey": "..."
}

Jangan bocorkan detail library render ke Quote Service.

Quote Service tidak peduli apakah PDF dibuat via Flying Saucer, wkhtmltopdf, LibreOffice, vendor API, atau engine internal. Quote Service hanya peduli artifact tersedia dan valid.


23. Watermark and Preview

Preview document harus jelas berbeda dari final artifact.

Preview policy:

  • watermark DRAFT;
  • not acceptable by customer;
  • can be regenerated;
  • shorter retention;
  • may omit legal signature;
  • generated from non-final quote state;
  • not referenced by acceptance.

Final artifact policy:

  • no draft watermark;
  • immutable;
  • linked to frozen revision;
  • linked to price/approval evidence;
  • stored with hash;
  • audit retained;
  • used for delivery/acceptance.

Jangan biarkan customer menerima preview.

Jika preview dikirim keluar tanpa kontrol, sistem kehilangan defensibility.


24. Redaction and Field-Level Security

Dokumen customer-facing tidak boleh mengandung internal-only data seperti:

  • approval threshold;
  • internal margin;
  • cost basis;
  • risk score;
  • approver comment internal;
  • supplier pricing;
  • fraud indicator;
  • tenant internal routing;
  • workflow incident note;
  • debug correlation details.

Redaction harus terjadi sebelum render input terbentuk, bukan hanya di template.

Bad:

{{#if showInternalMargin}}{{margin}}{{/if}}

Better:

CustomerRenderInputBuilder never includes margin.

Template tidak boleh menjadi primary security boundary.


25. Terms and Conditions Versioning

Terms and conditions juga artifact dependency.

Contoh:

{
  "termsFamily": "standard-b2b-connectivity",
  "termsVersion": "standard-b2b-connectivity:2026.06.1",
  "market": "US",
  "language": "en-US",
  "effectiveDate": "2026-06-01"
}

Jika quote berlaku 30 hari, terms yang diterima customer adalah terms yang ada di document artifact, bukan terms terbaru saat order dibuat.

Maka artifact metadata harus menyimpan terms version.

Jangan hanya menyisipkan text terbaru saat download.

Download harus mengembalikan binary yang sama atau versi evidence yang jelas.


26. Localization and Formatting

Localization bukan hanya translate label.

Ia mencakup:

  • language;
  • date format;
  • decimal separator;
  • currency format;
  • tax wording;
  • legal entity wording;
  • product naming;
  • address format;
  • measurement unit;
  • paper size;
  • right-to-left layout if relevant;
  • market-specific terms.

Render input sebaiknya menyimpan raw value dan display value jika display harus reproducible.

Example:

{
  "total": {
    "currency": "EUR",
    "minor": 12990000,
    "display": "129.900,00 €"
  }
}

Jika display value dihitung ulang saat render ulang dengan library locale terbaru, hasil bisa berubah.

Untuk final artifact, yang penting adalah binary evidence. Untuk reproducibility, simpan render input dan formatter version jika perlu.


27. Hashing and Integrity

Artifact harus menyimpan hash.

  • render input hash;
  • binary hash;
  • template package hash if possible;
  • optional combined evidence hash.

Example:

renderInputHash = sha256(canonicalJson(renderInput))
binaryHash = sha256(pdfBytes)
evidenceHash = sha256(artifactId + renderInputHash + binaryHash + templateVersion)

Hash berguna untuk:

  • detect binary corruption;
  • prove artifact not changed;
  • audit comparison;
  • support dispute investigation;
  • migration verification.

Hash bukan pengganti access control. Hash adalah integrity evidence.


Jika customer menerima quote berdasarkan dokumen, acceptance record harus mengacu ke artifact.

CREATE TABLE quote_acceptance (
  acceptance_id           uuid PRIMARY KEY,
  tenant_id               text NOT NULL,
  quote_id                uuid NOT NULL,
  quote_revision_id       uuid NOT NULL,
  accepted_artifact_id    uuid NOT NULL,
  accepted_by             text NOT NULL,
  accepted_at             timestamptz NOT NULL,
  acceptance_channel      text NOT NULL,
  acceptance_ip_hash      text,
  acceptance_evidence     jsonb NOT NULL
);

Acceptance tanpa artifact reference rawan dispute:

  • customer menerima dokumen yang mana?
  • apakah dokumen sama dengan yang kita simpan?
  • apakah quote revision berubah setelah dokumen dikirim?
  • apakah terms version benar?

Acceptance should attach to evidence, not just status.


29. Regeneration and Correction Policy

Ada beberapa scenario:

29.1 Same artifact download

User download artifact lama. Return same binary.

29.2 Regenerate because failed

Artifact belum available. Retry same job or create replacement depending policy.

29.3 Generate new document because quote changed

Create new artifact for new quote revision.

29.4 Correct legal/template error

Do not mutate old artifact. Create corrected artifact and mark old one superseded/withdrawn.

29.5 Binary storage corrupted

If hash mismatch, mark artifact integrity failure, investigate, restore from backup if possible. Jangan diam-diam regenerate dari current data dan overwrite.

Policy harus eksplisit.

Old artifact stays as historical evidence.
New artifact expresses new commercial/legal statement.

30. Document Generation Failure Taxonomy

FailureRetry?OwnerExample response
Template not foundno until config fixedDocument OpsTEMPLATE_NOT_FOUND
Render input invalidno until bug/data fixedService teamRENDER_INPUT_INVALID
Renderer timeoutyesPlatform/DocumentRENDER_TIMEOUT
Object storage unavailableyesPlatformSTORAGE_UNAVAILABLE
Quote revision stalenoUser/domainQUOTE_REVISION_STALE
Approval missingnoBusiness workflowAPPROVAL_REQUIRED
Unsupported localeno until template addedTemplate ownerLOCALE_UNSUPPORTED
Binary hash mismatchno automaticSecurity/OpsARTIFACT_INTEGRITY_FAILURE

Retry everything is dangerous.

A missing template will not fix itself by retrying 100 times.


31. Security Model

Document artifact security must be object-level.

User can access artifact if:

  • same tenant;
  • has permission for subject quote/order;
  • artifact type allowed for role;
  • artifact is not restricted/internal;
  • customer portal token maps to customer/quote;
  • artifact is not withdrawn for external access;
  • field-level redaction policy satisfied before generation.

Roles:

RoleAccess
Sales ownerown quote proposal, preview, customer artifacts
Sales managerteam quotes, approval packs if permitted
Approverapproval pack, quote proposal summary
Support agentcustomer-visible artifacts and timeline
Legal adminterms/template artifacts
Customer portal userdelivered customer-facing artifact only
System workerstorage write/read through service credentials only

Never expose raw storage bucket broadly.


32. Audit Trail

Audit must record:

  • artifact requested;
  • dependency validation result;
  • template resolved;
  • render started;
  • render failed/succeeded;
  • binary stored;
  • artifact downloaded;
  • artifact delivered;
  • artifact superseded;
  • artifact linked to acceptance.

Audit event example:

{
  "auditType": "DOCUMENT_ARTIFACT_AVAILABLE",
  "tenantId": "tnt-123",
  "actor": "document-worker:render-02",
  "subject": {
    "type": "DOCUMENT_ARTIFACT",
    "id": "art-77a"
  },
  "related": {
    "quoteId": "q-10029",
    "quoteRevisionId": "qr-004"
  },
  "data": {
    "templateVersion": "quote-proposal:2026.07.0",
    "binaryHash": "sha256:..."
  },
  "occurredAt": "2026-07-02T10:15:30Z"
}

Audit bukan log teks. Audit adalah queryable evidence.


33. Operational Metrics

Metrics yang penting:

  • generation request count;
  • generation success/failure count;
  • render duration p50/p95/p99;
  • storage write duration;
  • template failure count;
  • retry count;
  • job queue depth;
  • artifact availability latency;
  • download count;
  • delivery-to-available lag;
  • superseded artifact count;
  • hash mismatch count;
  • unsupported locale count.

Dashboard harus bisa menjawab:

  • apakah document generation sedang melambat?
  • template mana yang sering gagal?
  • quote mana yang blocked karena artifact gagal?
  • apakah storage down?
  • apakah ada spike failed render setelah template release?
  • apakah ada artifact downloaded oleh actor tidak biasa?

34. Testing Strategy

Test document generation tidak boleh hanya snapshot PDF pixel.

Layer test:

TestPurpose
Render input builder unit testfield correctness
Schema validation testtemplate contract
Template resolution testcorrect version by market/locale
Golden data testexpected commercial values
PDF smoke testbinary generated and readable
Hash testcanonical render input stable
Authorization testartifact access boundary
Idempotency testduplicate request returns same result
Failure injection teststorage/render/template failure
Workflow integration testCamunda waits/continues correctly

Golden data test example:

Given approved quote revision 4
And template quote-proposal:2026.07.0
When proposal is generated
Then the render input contains total USD 129,900.00
And discount reason is visible only if customer-visible policy allows
And approval internal comment is absent
And artifact metadata references priceResultId pr-1209

For PDF visual regression, sample cases should include:

  • one line quote;
  • bundle with nested options;
  • long customer name;
  • multi-currency not allowed case;
  • long terms section;
  • discount table;
  • unsupported locale;
  • page break across line item table;
  • zero discount;
  • amendment comparison.

35. Implementation Skeleton

Application service shape:

public final class RequestQuoteProposalGenerationHandler {
  private final QuoteReadClient quoteReadClient;
  private final ArtifactRepository artifactRepository;
  private final TemplateResolver templateResolver;
  private final RenderInputBuilder renderInputBuilder;
  private final GenerationJobRepository jobRepository;
  private final OutboxWriter outboxWriter;
  private final AuthorizationService authorizationService;

  public ArtifactResponse handle(RequestQuoteProposalCommand command) {
    authorizationService.assertCanGenerateQuoteArtifact(
        command.actor(), command.tenantId(), command.quoteId());

    QuoteRevisionView revision = quoteReadClient.getRevision(
        command.tenantId(), command.quoteId(), command.revisionNo());

    revision.assertCanGenerateFinalProposal();

    TemplateResolution template = templateResolver.resolve(
        ArtifactType.QUOTE_PROPOSAL,
        revision.market(),
        command.locale(),
        revision.effectiveAt());

    RenderInput renderInput = renderInputBuilder.build(revision, template);
    renderInput.validateAgainst(template.renderInputSchema());

    String renderInputHash = CanonicalJson.sha256(renderInput);

    Artifact artifact = artifactRepository.findByIdempotencyKey(command.idempotencyKey())
        .orElseGet(() -> Artifact.requested(
            command.tenantId(),
            revision.quoteId(),
            revision.revisionId(),
            ArtifactType.QUOTE_PROPOSAL,
            template,
            renderInputHash,
            command.actor()));

    artifactRepository.save(artifact);
    jobRepository.enqueueIfAbsent(artifact.artifactId(), command.idempotencyKey(), renderInput);

    outboxWriter.append(DocumentArtifactRequested.from(artifact));

    return ArtifactResponse.from(artifact);
  }
}

Worker shape:

public final class RenderArtifactWorker {
  private final GenerationJobRepository jobRepository;
  private final ArtifactRepository artifactRepository;
  private final TemplateStore templateStore;
  private final Renderer renderer;
  private final ObjectStorage storage;
  private final OutboxWriter outboxWriter;

  public void pollAndRender() {
    jobRepository.claimNextDueJob().ifPresent(job -> {
      Artifact artifact = artifactRepository.getForUpdate(job.artifactId());
      try {
        artifact.markRendering();

        TemplatePackage template = templateStore.load(artifact.templateVersion());
        byte[] pdf = renderer.render(template, job.renderInput());
        String binaryHash = Hashing.sha256(pdf);

        StoredObject stored = storage.putImmutable(
            artifact.storageKey(),
            "application/pdf",
            pdf,
            binaryHash);

        artifact.markAvailable(stored, binaryHash);
        job.markSucceeded();
        outboxWriter.append(DocumentArtifactAvailable.from(artifact));
      } catch (RetryableRenderException e) {
        job.scheduleRetry(e.code(), e.getMessage());
        artifact.markFailedRetryable(e.code(), e.getMessage());
      } catch (NonRetryableRenderException e) {
        job.markFailed(e.code(), e.getMessage());
        artifact.markFailed(e.code(), e.getMessage());
        outboxWriter.append(DocumentArtifactGenerationFailed.from(artifact, e));
      }
    });
  }
}

Important: implementation detail dapat berubah, tetapi command boundary, metadata, idempotency, dan event semantics harus stabil.


36. Anti-Patterns

36.1 Store only PDF URL in quote table

Ini menghilangkan lifecycle, template version, hash, and evidence.

36.2 Generate PDF directly in request thread for final proposal

Bisa timeout, retry ambiguity, dan user experience buruk.

36.3 Regenerate old proposal from current data

Ini menghancurkan evidence.

36.4 Template is edited in place

Dokumen lama tidak bisa dijelaskan.

36.5 Put sensitive internal data into render input

Template bukan security boundary.

36.6 Email service generates document

Notification service seharusnya mengirim artifact, bukan membuat commercial document.

36.7 Camunda stores PDF base64 in process variable

Membengkakkan engine DB dan mencampur binary dengan workflow state.

36.8 Delete superseded artifacts

Superseded artifact tetap evidence.


37. Production Readiness Checklist

Sebelum document generation dianggap siap production:

  • artifact metadata model exists;
  • artifact binary immutable;
  • template versioning exists;
  • render input schema exists;
  • quote revision freshness checked;
  • price/approval references stored;
  • idempotency key handled;
  • async lifecycle supported;
  • retry policy separated by failure type;
  • object-level authorization tested;
  • customer-facing redaction tested;
  • audit event emitted;
  • hash stored and verified;
  • artifact download access logged;
  • template release has tests;
  • preview clearly watermarked;
  • final artifact cannot be regenerated in place;
  • Camunda does not store binary;
  • notification waits for artifact available;
  • operational dashboard shows failed jobs;
  • runbook exists for template failure and storage failure.

38. Mental Model Recap

Document generation dalam CPQ/OMS enterprise bukan masalah “cara membuat PDF”.

Ini masalah:

  • freezing commercial evidence;
  • preserving legal/commercial context;
  • separating preview from final artifact;
  • versioning template and terms;
  • protecting sensitive data;
  • making generation retryable;
  • making delivery auditable;
  • making acceptance defensible.

Rule paling penting:

Never let today's mutable data rewrite yesterday's customer-facing evidence.

Jika prinsip ini dipegang, document service akan menjadi bagian kuat dari quote-to-order lifecycle, bukan utility rapuh di pinggir sistem.

Part berikutnya akan membahas Notification and Communication Service: bagaimana artifact, quote, order, workflow, dan SLA berubah menjadi komunikasi yang reliable, auditable, idempotent, dan customer-safe.

Lesson Recap

You just completed lesson 33 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.