Approval Workflow and Decision Evidence Model
Model approval workflow dan decision evidence untuk enterprise CPQ/Quote/Order/Billing systems, termasuk approval request, approval step, approval decision, threshold evaluation, maker-checker, delegation, escalation, approval snapshot, workflow integration, auditability, and production correctness.
Approval Workflow and Decision Evidence Model
1. Core Idea
Approval workflow bukan sekadar approval_status = APPROVED.
Dalam enterprise CPQ / Quote / Order / Billing, approval adalah decision process yang harus menjawab:
- siapa meminta approval,
- apa yang diminta untuk disetujui,
- kenapa approval dibutuhkan,
- rule/threshold apa yang memicu approval,
- siapa yang berwenang menyetujui,
- decision apa yang dibuat,
- evidence data apa yang dilihat saat decision,
- apakah ada delegation,
- apakah maker-checker berlaku,
- apakah approval masih valid setelah data berubah,
- bagaimana approval direkonsiliasi dengan quote/order state.
Mental model:
Approval is not only workflow state. Approval is auditable decision evidence over a specific business snapshot.
2. Why Approval Modelling Matters
Approval melindungi commercial correctness.
Tanpa model approval yang kuat, sistem rawan:
- quote approved tanpa bukti threshold,
- discount diubah setelah approval,
- approver tidak punya authority,
- approval lama dipakai untuk quote revision baru,
- maker-checker dilanggar,
- delegated approver tidak tercatat,
- approval workflow selesai tetapi quote status tidak sinkron,
- quote converted to order tanpa valid approval,
- manual override tidak punya reason,
- approval dispute tidak bisa dibuktikan,
- reporting approval SLA tidak akurat.
Approval model harus menjaga hubungan antara:
business object + approval reason + decision authority + decision snapshot + lifecycle effect
3. Approval Object Types
Approval bisa berlaku untuk banyak object.
Examples:
| Approval target | Example |
|---|---|
| Quote | High discount quote needs approval. |
| Quote item | Special product/item exception. |
| Price override | Manual price override beyond threshold. |
| Discount adjustment | Discount exceeds sales authority. |
| Margin exception | Margin below guardrail. |
| Contract exception | Non-standard term. |
| Order cancellation | Cancel in-progress order. |
| Billing adjustment | Credit/debit over threshold. |
| Data correction | Production data repair. |
| Catalog publication | Publish product/price/rule change. |
| Serviceability override | Sell product where serviceability uncertain. |
A reusable approval model should support multiple target types, but with controlled semantics.
4. Approval Request
Approval request represents a request for decision.
Core fields:
| Field | Purpose |
|---|---|
id | Internal approval request ID. |
request_number | Human-facing approval reference. |
target_type | Quote, order, billing adjustment, etc. |
target_id | Target object ID. |
target_version | Version/snapshot version at request time. |
approval_type | Discount, margin, contract exception, cancellation, etc. |
status | Pending, approved, rejected, withdrawn, expired, invalidated. |
requested_by | Actor who requested approval. |
requested_at | Timestamp. |
reason_code | Why approval is needed/requested. |
reason_text | Human explanation. |
priority | Optional priority/escalation. |
correlation_id | Trace across logs/events/workflow. |
Important:
target_versionis critical. Approval must be tied to a specific version/snapshot, not a mutable object in general.
5. Approval Step
Approval request may have one or many steps.
Examples:
- manager approval,
- deal desk approval,
- finance approval,
- legal approval,
- product approval,
- billing approval,
- security approval,
- executive approval.
Fields:
approval_step
- id
- approval_request_id
- step_number
- step_type
- required_authority_type
- required_role
- required_scope_type
- required_scope_id
- status
- assigned_to_user_id
- assigned_to_group_id
- due_at
- started_at
- completed_at
Steps can be:
- sequential,
- parallel,
- conditional,
- optional,
- escalation-based,
- auto-approved by rule,
- manual-only.
6. Approval Decision
Approval decision is the actual decision.
Fields:
approval_decision
- id
- approval_request_id
- approval_step_id
- decision
- decided_by
- decided_by_actor_type
- decided_at
- reason_code
- comment
- authority_rule_id
- delegation_id
- maker_checker_result
- decision_snapshot_id
- correlation_id
Decision values:
- approved,
- rejected,
- returned for changes,
- escalated,
- delegated,
- withdrawn,
- expired,
- invalidated,
- auto-approved.
Do not store only current approval status. Store each decision.
7. Decision Evidence
Decision evidence is the snapshot of facts used when approving.
Examples for quote discount approval:
- quote version,
- quote total,
- discount percentage,
- discount amount,
- margin,
- currency,
- product family,
- customer/account,
- agreement reference,
- previous approvals,
- rule thresholds,
- requested reason,
- pricing snapshot,
- validity date,
- approval route.
Evidence model:
approval_decision_evidence
- id
- approval_request_id
- target_type
- target_id
- target_version
- evidence_type
- evidence_payload
- evidence_hash
- created_at
Payload can be JSONB or external document/reference. If sensitive, store hash/reference and secure the payload.
8. Approval Snapshot
Approval should be based on immutable snapshot.
Example:
Quote Q-100 v4
total = 500,000
discount = 18%
margin = 22%
requested term = 36 months
If quote changes to v5:
Quote Q-100 v5
total = 650,000
discount = 25%
Approval for v4 should not automatically apply to v5.
Possible statuses:
approval_request.status = INVALIDATED
invalidated_reason = TARGET_CHANGED
new_approval_required = true
Critical rule:
Material change after approval must either invalidate approval or create explicit delta approval.
9. Material Change Detection
Material changes are changes that can invalidate approval.
Examples:
- total price changes,
- discount changes,
- margin changes,
- product item added/removed,
- contract term changes,
- customer/account changes,
- billing account changes,
- price list changes,
- validity date extended,
- tax/currency changes,
- manual override changes,
- serviceability override changes.
Model:
approval_invalidation_rule
- approval_type
- changed_field
- invalidates
- requires_reapproval
Or evaluate in code with domain policy.
Store invalidation event:
approval_invalidation
- approval_request_id
- target_type
- target_id
- old_target_version
- new_target_version
- reason_code
- invalidated_at
10. Approval Routing
Approval routing determines required steps.
Inputs:
- target type,
- approval type,
- account/customer hierarchy,
- sales region,
- product family,
- discount/margin/deal value,
- quote status,
- channel,
- contract term,
- billing risk,
- credit status,
- authority rules,
- escalation policy.
Routing output:
- steps,
- assigned group/users,
- required authority,
- due date,
- escalation path,
- parallel/sequential order.
Routing should be traceable:
approval_route
- id
- approval_request_id
- route_rule_id
- route_rule_version
- route_snapshot
- created_at
If routing changes later, old request should still explain why it went to certain approver.
11. Sequential vs Parallel Approval
Sequential
Sales Manager -> Deal Desk -> Finance
Next step starts after previous approval.
Parallel
Finance and Legal review at same time.
Request approved only when all mandatory parallel steps approve.
Model options:
approval_step.group_id
approval_step.step_order
approval_step.parallel_group
approval_step.mandatory
Aggregation rule:
Approval request is APPROVED when all mandatory steps are approved and no blocking step is rejected.
12. Conditional Approval
Conditional approval is triggered only under certain facts.
Examples:
- legal approval only if custom terms exist,
- finance approval only if payment term > 45 days,
- executive approval only if deal value > threshold,
- product approval only for non-standard product,
- billing approval only for credit override.
Conditional steps need evidence of why they were included or skipped.
Store:
step_condition_result
- approval_step_id
- condition_code
- evaluated_result
- evaluated_value
- evaluated_at
13. Auto Approval
Some approvals can be auto-approved.
Examples:
- discount within requester's authority,
- no threshold breached,
- trusted channel,
- renewal under standard terms,
- correction below threshold.
Auto approval still needs evidence:
- evaluated rule,
- authority,
- threshold,
- evaluated values,
- timestamp.
Do not confuse "no approval required" with "approved". They are different semantic states.
Possible statuses:
NOT_REQUIRED
AUTO_APPROVED
MANUAL_APPROVAL_REQUIRED
14. Maker-Checker
Maker-checker prevents conflict of interest.
Examples:
- requester cannot approve own discount,
- creator cannot approve own billing adjustment,
- data corrector cannot verify own correction,
- same user cannot both override and approve.
Decision guard:
if approval_request.requested_by == decision.decided_by:
reject unless policy allows exception
But real policy may also consider:
- same team,
- same manager chain,
- delegated user,
- emergency override,
- system-generated request.
Store result:
maker_checker_result
- passed
- rule_id
- reason
15. Delegated Approval
Delegated approval must be explicit.
Fields in decision:
decided_by = delegate_user_id
delegation_id = ...
delegator_user_id = ...
delegation_effective_from/to
Decision evidence should show:
- delegate had active delegation,
- delegation covered approval type,
- delegation covered scope,
- decision occurred within delegation period,
- delegation was approved/valid.
Failure mode:
Approval says User B approved, but audit cannot show User B was delegated by User A.
16. Escalation
Approval can be escalated if:
- SLA breached,
- approver unavailable,
- amount exceeds threshold,
- rejection disputed,
- urgent order,
- customer escalation,
- repeated fallout.
Escalation model:
approval_escalation
- id
- approval_request_id
- from_step_id
- from_user_or_group
- to_user_or_group
- escalation_reason
- escalated_at
- escalated_by
- status
Escalation does not remove need for authority. New approver must still be authorized.
17. Expiry and Withdrawal
Approval request can expire or be withdrawn.
Expiry reasons:
- target quote expired,
- due date passed,
- target version superseded,
- policy timeout,
- request no longer relevant.
Withdrawal reasons:
- requester cancelled request,
- quote revised,
- deal lost,
- order cancelled,
- duplicate request.
Store:
closed_reason_code
closed_at
closed_by
This matters for reporting and audit.
18. Approval and Quote Lifecycle
Quote lifecycle depends on approval.
Possible model:
quote.status = SUBMITTED
approval_request.status = PENDING
quote.status = APPROVED
approval_request.status = APPROVED
But avoid circular unclear states.
Important invariants:
Quote cannot transition to APPROVED unless required approval requests are approved.
Quote cannot transition to ACCEPTED if approval was invalidated.
Quote material change after approval invalidates approval or creates revision.
Quote approval status can be derived from approval requests, stored as projection, or hybrid.
19. Approval and Order/Billing Operations
Approval may be required for:
- order cancellation after fulfillment start,
- amendment of in-flight order,
- manual billing adjustment,
- credit/debit over threshold,
- data correction,
- serviceability override,
- fulfillment bypass,
- product inventory correction.
Use same approval infrastructure, but with target-specific policies.
Do not reuse quote discount approval semantics for billing correction blindly.
20. Workflow Engine Integration
If using workflow engine such as Camunda:
- approval request is domain data,
- workflow task is process execution,
- task completion calls approval decision command,
- domain service validates authority,
- workflow state does not replace approval evidence,
- process instance ID should be linked to approval request.
Conceptual linkage:
approval_request.workflow_instance_id
approval_step.workflow_task_id
Bad pattern:
Camunda task completed = approval decision, with no domain approval record.
Better:
Camunda task completed -> ApprovalDecisionService.decide()
21. Event Model
Events:
ApprovalRequestedApprovalRouteCreatedApprovalStepAssignedApprovalStepApprovedApprovalStepRejectedApprovalRequestApprovedApprovalRequestRejectedApprovalRequestWithdrawnApprovalRequestInvalidatedApprovalEscalatedApprovalDelegationUsed
Payload example:
{
"eventId": "uuid",
"eventType": "ApprovalRequestApproved",
"eventVersion": 1,
"occurredAt": "2026-07-12T10:00:00Z",
"approvalRequestId": "approval-request-id",
"targetType": "QUOTE",
"targetId": "quote-id",
"targetVersion": 4,
"approvalType": "DISCOUNT_APPROVAL",
"decidedBy": "user-id",
"correlationId": "corr-123"
}
Avoid sending sensitive margin/cost in broad events. Use secure references if needed.
22. PostgreSQL Physical Design
Approval request:
create table approval_request (
id uuid primary key,
request_number text not null unique,
target_type text not null,
target_id uuid not null,
target_version integer,
approval_type text not null,
status text not null,
requested_by text not null,
requested_actor_type text not null,
reason_code text,
reason_text text,
priority text,
workflow_instance_id text,
correlation_id text,
requested_at timestamptz not null,
closed_at timestamptz,
closed_reason_code text,
created_at timestamptz not null,
updated_at timestamptz not null
);
Approval step:
create table approval_step (
id uuid primary key,
approval_request_id uuid not null references approval_request(id),
step_number integer not null,
parallel_group text,
step_type text not null,
required_authority_type text,
required_role text,
required_scope_type text,
required_scope_id uuid,
assigned_to_user_id uuid,
assigned_to_group_id uuid,
mandatory boolean not null default true,
status text not null,
due_at timestamptz,
started_at timestamptz,
completed_at timestamptz
);
Approval decision:
create table approval_decision (
id uuid primary key,
approval_request_id uuid not null references approval_request(id),
approval_step_id uuid references approval_step(id),
decision text not null,
decided_by text not null,
decided_actor_type text not null,
decided_at timestamptz not null,
reason_code text,
comment text,
authority_rule_id uuid,
delegation_id uuid,
maker_checker_passed boolean,
decision_snapshot_id uuid,
correlation_id text
);
Decision evidence:
create table approval_decision_evidence (
id uuid primary key,
approval_request_id uuid not null references approval_request(id),
target_type text not null,
target_id uuid not null,
target_version integer,
evidence_type text not null,
evidence_payload jsonb,
evidence_hash text,
created_at timestamptz not null
);
Useful indexes:
create index idx_approval_request_target
on approval_request (target_type, target_id, target_version);
create index idx_approval_request_status
on approval_request (status, requested_at);
create index idx_approval_step_assigned_user
on approval_step (assigned_to_user_id, status, due_at);
create index idx_approval_step_assigned_group
on approval_step (assigned_to_group_id, status, due_at);
create index idx_approval_decision_request_time
on approval_decision (approval_request_id, decided_at desc);
23. Java/JAX-RS Backend Implications
Approval APIs:
POST /approval-requests
GET /approval-requests/{id}
POST /approval-requests/{id}/decisions
POST /approval-requests/{id}/withdraw
POST /approval-requests/{id}/escalate
GET /approval-tasks?assignedTo=me
Service structure:
ApprovalResource
-> ApprovalRequestService
-> ApprovalRoutingService
-> CommercialAuthorityService
-> MakerCheckerPolicy
-> ApprovalSnapshotService
-> ApprovalRepository
-> OutboxRepository
-> AuditRepository
Decision command must:
- load approval request,
- validate request status,
- validate target version/snapshot,
- validate actor permission,
- validate commercial authority,
- validate maker-checker,
- validate delegation if used,
- persist decision,
- update step/request status,
- publish event,
- update target lifecycle if needed.
24. MyBatis/JPA/JDBC Implications
MyBatis
Useful for:
- approval task queues,
- SLA/aging queries,
- assigned group lookup,
- target approval lookup,
- reporting approval route/decision.
JPA
Be careful with:
- step/decision collection lazy loading,
- updating request status without decision row,
- cascade deleting evidence,
- optimistic locking on concurrent decisions.
JDBC
Useful for deterministic approval decision transaction and queue updates.
General rule:
Approval decision must be an append-only-ish event with evidence, not only a mutable status update.
25. Reporting Impact
Approval model supports:
- approval volume,
- approval SLA,
- pending approvals by group/user,
- rejection rate,
- discount approval rate,
- margin exception rate,
- approval aging,
- escalation rate,
- delegation usage,
- auto-approval rate,
- approval invalidation rate,
- deal cycle time,
- approval by customer/account/product.
Definitions:
- request created time,
- first assigned time,
- decision time,
- total approval time,
- business hours vs calendar time,
- rejected vs withdrawn vs invalidated,
- approval per quote revision or quote overall.
26. Observability
Key monitors:
- approval pending too long,
- step overdue,
- approval request without steps,
- target quote submitted but no approval request,
- approved request invalidated by later change,
- decision by actor without authority,
- duplicate active approval request for same target/version/type,
- workflow task completed but no approval decision,
- approval request approved but target status not updated.
Example queries:
-- Pending approval older than SLA threshold
select id, request_number, approval_type, requested_at, status
from approval_request
where status = 'PENDING'
and requested_at < now() - interval '24 hours';
-- Approval request without steps
select ar.id, ar.request_number
from approval_request ar
left join approval_step s on s.approval_request_id = ar.id
where ar.status = 'PENDING'
and s.id is null;
-- Duplicate active approval request for same target/version/type
select target_type, target_id, target_version, approval_type, count(*)
from approval_request
where status in ('PENDING', 'IN_PROGRESS')
group by target_type, target_id, target_version, approval_type
having count(*) > 1;
27. Security and Privacy
Approval evidence can contain sensitive information:
- discount,
- margin,
- cost,
- customer financial data,
- contract terms,
- payment terms,
- billing correction amount,
- internal thresholds,
- comments.
Controls:
- field-level access to evidence,
- mask margin/cost for unauthorized users,
- avoid sensitive payload in broad events,
- encrypt evidence payload if needed,
- audit evidence access,
- define retention,
- protect approver comments.
28. Failure Modes
| Failure mode | Symptom | Likely cause | Prevention |
|---|---|---|---|
| Approved wrong quote version | Quote changed after approval | No target version/snapshot | Approval tied to version |
| Unauthorized approval | Audit/compliance issue | Permission not authority-aware | Commercial authority check |
| Approval status without decision | Cannot explain who approved | Mutable status only | Decision table |
| Discount changed after approval | Revenue leakage | No material change invalidation | Approval invalidation rules |
| Workflow/task mismatch | Camunda complete but domain pending | Workflow as source of truth | Domain approval record |
| Maker-checker violated | Same requester approved | No conflict rule | Maker-checker guard |
| Delegation not auditable | Cannot justify substitute approver | Delegation not linked | Delegation reference |
| Approval SLA invisible | Aging not tracked | No step timestamps | Step lifecycle timestamps |
| Evidence too sensitive | Data leak | Over-broad evidence/event payload | Secure evidence design |
| Duplicate approval requests | Confusing decisions | No uniqueness/idempotency | Active request constraint/policy |
29. PR Review Checklist
When reviewing approval changes, ask:
- What target object is being approved?
- What approval type is this?
- Is target version/snapshot captured?
- What rule/threshold triggered approval?
- Is approval routing traceable?
- Are steps sequential, parallel, or conditional?
- Does approver authority get checked?
- Is maker-checker required?
- Can delegation be used?
- What evidence is stored?
- What changes invalidate approval?
- How does approval update quote/order/billing lifecycle?
- Is workflow engine only orchestration, not source of truth?
- Are decisions append-only/auditable?
- Are events outbox-backed?
- Are sensitive fields protected?
- Are SLA and pending approval dashboards supported?
30. Internal Verification Checklist
Verify these in the internal CSG/team context:
- Actual approval objects and workflow engine usage.
- Whether approval is first-class domain data or only workflow task data.
- Whether approval request stores target version/snapshot.
- Whether quote material change invalidates approval.
- Whether discount/margin/override approvals store evidence.
- Whether commercial authority is checked at decision time.
- Whether maker-checker is required.
- Whether delegation is supported and stored.
- Whether approval route/rule version is stored.
- Whether approval steps are sequential/parallel/conditional.
- Whether auto-approval exists.
- Whether approval events are published.
- Whether approval status syncs correctly with quote/order state.
- Whether approval SLA dashboards exist.
- Whether approval comments/evidence contain sensitive data and are protected.
- Whether incidents mention stale approval, unauthorized approval, missing evidence, workflow mismatch, or quote changed after approval.
31. Summary
Approval workflow is decision evidence over a business snapshot.
A strong approval model must define:
- approval request,
- target type/version,
- approval type,
- route and steps,
- decision,
- authority evidence,
- maker-checker,
- delegation,
- material change invalidation,
- decision snapshot,
- workflow linkage,
- SLA/aging,
- events,
- audit/security.
The key principle:
Approval is not the status. Approval is who decided what, under which authority, based on which immutable facts, at what time, and for which exact version of the business object.
You just completed lesson 43 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.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.