Focus mode active/Press Alt+Shift+R to toggle/Esc to exit
Build CoreOrdered learning track

Discount, Margin, and Approval Model

Enterprise data modelling untuk discount, margin, commercial guardrail, approval rule, approval decision, dan auditability dalam CPQ/quote-to-cash systems.

13 min read2549 words
PrevNext
Lesson 2182 lesson track16–45 Build Core
#enterprise-data-modelling#cpq#discount#margin+5 more

Discount, Margin, and Approval Model

Fokus part ini: bagaimana memodelkan discount, margin, dan approval agar CPQ/Quote system tidak hanya bisa menghitung harga, tetapi juga bisa membuktikan kenapa sebuah commercial decision valid, siapa yang menyetujui, berdasarkan aturan apa, dan apakah keputusan itu masih defensible ketika diaudit.

Dalam enterprise CPQ, discount bukan sekadar angka negatif di price line. Discount adalah commercial decision. Margin bukan sekadar hasil kalkulasi. Margin adalah guardrail. Approval bukan sekadar tombol approve/reject. Approval adalah control mechanism yang menghubungkan pricing, authority, risk, audit, dan revenue governance.

Kalau model discount/approval lemah, sistem bisa tetap terlihat berjalan, tetapi correctness-nya rapuh:

  • quote bisa berisi discount melebihi otoritas sales;
  • approval bisa hilang setelah quote direvisi;
  • margin bisa dihitung dari cost version yang salah;
  • accepted quote bisa tidak punya evidence approval yang cukup;
  • billing bisa menerima charge yang secara commercial tidak valid;
  • audit tidak bisa menjawab kenapa deal disetujui.

Part ini membahas data model untuk mencegah kondisi tersebut.


1. Core Mental Model

Discount, margin, dan approval harus dipisahkan secara konseptual:

AreaPertanyaan utamaContoh data
DiscountBerapa pengurangan harga yang diberikan?discount type, amount, percentage, reason
MarginApakah deal masih menguntungkan?cost, net price, margin amount, margin percentage
ApprovalSiapa yang berhak menyetujui exception?approval request, step, approver, decision
GuardrailKapan approval diperlukan?threshold, policy, rule, delegation
EvidenceBagaimana keputusan dibuktikan?approval history, actor, timestamp, reason

Kesalahan umum adalah mencampur semuanya menjadi satu kolom seperti:

discount_percent
approved_by
approval_status

Model seperti itu mungkin cukup untuk demo, tetapi tidak cukup untuk enterprise CPQ karena tidak menjawab:

  • discount ini berlaku untuk item mana?
  • discount ini manual atau rule-based?
  • discount ini absolute atau percentage?
  • approval diperlukan karena threshold apa?
  • approval dilakukan sebelum atau setelah quote revision?
  • apakah approval masih valid setelah price recalculation?
  • apakah approver punya authority pada waktu approval?
  • apakah margin dihitung sebelum atau setelah tax?
  • cost source mana yang dipakai?

Enterprise model harus menyimpan decision structure, bukan hanya final value.


2. Discount as Commercial Data

Discount adalah adjustment terhadap price. Dalam CPQ, discount bisa berada di beberapa level:

  • quote-level discount;
  • quote item-level discount;
  • product offering-level discount;
  • bundle-level discount;
  • customer agreement discount;
  • promotion discount;
  • manual sales discount;
  • retention discount;
  • approval-based exception discount.

Secara data modelling, minimal perlu dibedakan:

Price Definition  -> base price dari catalog/pricing engine
Price Calculation -> hasil kalkulasi untuk quote context
Discount          -> adjustment terhadap price
Net Price         -> price setelah discount
Approval          -> control untuk discount exception
Snapshot          -> evidence final pada quote version tertentu

Discount sebaiknya tidak dimodelkan sebagai field tunggal di quote item. Ia lebih aman sebagai child entity atau price component.

Contoh logical model sederhana:

erDiagram QUOTE ||--o{ QUOTE_ITEM : contains QUOTE_ITEM ||--o{ PRICE_COMPONENT : has PRICE_COMPONENT ||--o{ DISCOUNT_COMPONENT : adjusted_by DISCOUNT_COMPONENT }o--|| DISCOUNT_REASON : classified_by DISCOUNT_COMPONENT }o--o| APPROVAL_REQUEST : may_require QUOTE { uuid id string quote_number string status integer revision string currency } QUOTE_ITEM { uuid id uuid quote_id uuid product_offering_ref string action string state } PRICE_COMPONENT { uuid id uuid quote_item_id string price_type decimal base_amount decimal net_amount string currency } DISCOUNT_COMPONENT { uuid id uuid price_component_id string discount_type decimal discount_value decimal discount_amount string discount_reason_code string source_type uuid source_ref_id } APPROVAL_REQUEST { uuid id string target_type uuid target_id string status string trigger_reason }

3. Discount Type

Discount type menjelaskan mekanisme discount, bukan alasan bisnisnya.

Contoh discount type:

TypeMeaningExample
PERCENTAGEDiskon berdasarkan persentase10% off monthly recurring charge
ABSOLUTE_AMOUNTPengurangan nominal tetap$100 off setup fee
PRICE_OVERRIDEHarga diganti dengan angka baruprice changed from 1,000to1,000 to 850
FREE_PERIODGratis beberapa periodefirst 3 months free
WAIVERCharge dihapusinstallation fee waived
PROMOTIONDiscount dari campaign/promotionlaunch discount
AGREEMENT_BASEDDiscount dari agreement/contractenterprise agreement discount
MANUAL_EXCEPTIONException manual oleh usersales discount requiring approval

Jangan mencampur discount_type dengan discount_reason.

Contoh buruk:

discount_type = "VIP customer"

Contoh lebih baik:

discount_type = "PERCENTAGE"
discount_reason_code = "STRATEGIC_CUSTOMER_RETENTION"
source_type = "MANUAL_EXCEPTION"

4. Discount Reason

Discount reason menjelaskan kenapa discount diberikan.

Reason penting untuk:

  • audit;
  • approval routing;
  • sales analytics;
  • margin leakage analysis;
  • compliance;
  • dispute handling;
  • pricing governance;
  • product profitability review.

Contoh reason:

COMPETITIVE_MATCH
STRATEGIC_CUSTOMER
RENEWAL_INCENTIVE
VOLUME_COMMITMENT
SERVICE_CREDIT
RETENTION_OFFER
PROMOTIONAL_CAMPAIGN
MANUAL_EXCEPTION
CONTRACTUAL_COMMITMENT

Reason sebaiknya reference data yang governed, bukan free text murni. Free text tetap bisa ada sebagai comment, tetapi jangan menjadi classification utama.

Contoh fields:

discount_reason_code
discount_reason_description
user_comment
policy_reference
campaign_reference
agreement_reference

5. Discount Source

Salah satu pertanyaan penting dalam CPQ adalah: discount ini berasal dari mana?

Discount source dapat berupa:

  • pricing rule;
  • promotion;
  • agreement;
  • manual sales input;
  • approval exception;
  • migration/import;
  • customer entitlement;
  • retention policy;
  • system correction.

Model source penting karena menentukan traceability.

Contoh:

source_type:
  PRICING_RULE
  PROMOTION
  AGREEMENT
  MANUAL
  APPROVAL_EXCEPTION
  MIGRATION
  CORRECTION

source_ref_id:
  references rule/promotion/agreement/request/etc

Kalau discount source tidak disimpan, audit hanya melihat final discount, bukan alasan sistem menghasilkan discount itu.


6. Discount Scope

Discount bisa berlaku pada level berbeda:

ScopeDescriptionExample
QuoteBerlaku untuk seluruh quoteenterprise deal discount
Quote itemBerlaku untuk satu line20% off one product
Price componentBerlaku untuk charge tertentuwaive installation fee
BundleBerlaku untuk bundle/packagepackage discount
TermBerlaku untuk periode tertentu3 months free
Usage tierBerlaku untuk usage blockdiscounted overage

Model discount harus menyimpan target scope dengan jelas.

Contoh:

target_type = QUOTE | QUOTE_ITEM | PRICE_COMPONENT | BUNDLE | TERM | USAGE_TIER
target_id

Atau, dalam model yang lebih normalized, discount ditempatkan sebagai child langsung dari price component/item yang relevan.

Trade-off:

ApproachKelebihanRisiko
Generic target_type/target_idfleksibelweak referential integrity
Separate tables per targetstrong constraintslebih banyak table/mapping
Price component modelcocok untuk pricing breakdownbutuh discipline mapping

Untuk enterprise CPQ, price component model sering lebih defensible karena discount melekat pada komponen harga yang jelas.


7. Margin Model

Margin adalah selisih antara revenue dan cost.

Minimal:

margin_amount = net_revenue - cost
margin_percent = margin_amount / net_revenue

Namun di enterprise system, definisinya bisa berbeda tergantung konteks:

  • gross margin;
  • net margin;
  • contribution margin;
  • product margin;
  • bundle margin;
  • quote margin;
  • recurring margin;
  • one-time margin;
  • term-adjusted margin;
  • first-year margin;
  • lifetime margin.

Karena itu, margin model harus menyimpan:

  • basis revenue;
  • basis cost;
  • calculation version;
  • currency;
  • exchange rate if applicable;
  • cost source;
  • calculation timestamp;
  • quote version/revision;
  • whether margin is estimated or final.

Contoh logical entity:

MARGIN_CALCULATION
- id
- quote_id
- quote_revision
- quote_item_id nullable
- revenue_amount
- cost_amount
- margin_amount
- margin_percent
- currency
- margin_type
- cost_source_type
- cost_source_ref
- calculation_version
- calculated_at
- calculated_by

8. Cost Model Awareness

Margin tidak bisa dipercaya jika cost source tidak jelas.

Cost dapat berasal dari:

  • product catalog cost;
  • cost table;
  • vendor cost;
  • network cost;
  • operational cost;
  • manual estimate;
  • financial system;
  • imported cost baseline;
  • negotiated cost;
  • default cost fallback.

Failure mode umum:

  • cost null dianggap zero;
  • cost version tidak sama dengan price version;
  • cost currency beda dari quote currency;
  • cost effective date tidak cocok dengan quote effective date;
  • cost disimpan hanya sebagai derived number tanpa source;
  • margin dihitung ulang setelah approval tetapi approval tidak di-reset.

Untuk correctness, margin calculation harus menjawab:

margin ini dihitung dari revenue apa,
cost apa,
versi cost mana,
versi price mana,
pada waktu kapan,
untuk quote revision mana.

9. Commercial Guardrail

Commercial guardrail adalah rule yang menentukan kapan deal boleh langsung jalan dan kapan perlu approval.

Contoh guardrail:

  • discount > 10% requires manager approval;
  • discount > 20% requires director approval;
  • margin < 30% requires finance approval;
  • waived installation fee requires commercial approval;
  • contract term below minimum requires legal approval;
  • strategic customer exception requires VP approval;
  • non-standard payment term requires finance approval;
  • product bundle discount cannot exceed threshold.

Guardrail bisa dimodelkan sebagai rule data:

APPROVAL_RULE
- id
- rule_code
- rule_type
- applies_to
- condition_expression
- threshold_value
- threshold_unit
- required_approval_level
- effective_from
- effective_to
- status
- version

Atau sebagai policy/config yang dievaluasi oleh rule engine.

Yang penting: approval request harus menyimpan hasil evaluasi rule, bukan hanya status akhir.


10. Approval Request Model

Approval request adalah representasi formal bahwa sebuah quote/item/discount/margin exception perlu keputusan.

Contoh fields:

APPROVAL_REQUEST
- id
- request_number
- target_type
- target_id
- quote_id
- quote_revision
- status
- trigger_type
- trigger_reason
- requested_by
- requested_at
- current_step
- required_level
- decision_summary
- completed_at

Target bisa berupa:

  • quote;
  • quote item;
  • discount component;
  • price override;
  • margin exception;
  • non-standard term;
  • contract exception.

Approval request harus version-aware. Approval untuk quote revision 3 tidak otomatis valid untuk revision 4 jika commercial terms berubah.


11. Approval Step Model

Approval sering multi-step:

  • sales manager;
  • regional manager;
  • finance;
  • legal;
  • product owner;
  • executive approver.

Model step:

APPROVAL_STEP
- id
- approval_request_id
- step_order
- step_type
- required_role
- required_level
- assigned_approver_id
- assigned_group_id
- status
- due_at
- escalated_at
- completed_at

Status step:

PENDING
IN_REVIEW
APPROVED
REJECTED
SKIPPED
DELEGATED
ESCALATED
CANCELLED
EXPIRED

Step perlu immutable history. Jangan overwrite approver lama ketika delegation terjadi. Buat event/history.


12. Approval Decision Model

Decision adalah evidence final dari approver.

Contoh:

APPROVAL_DECISION
- id
- approval_step_id
- decision
- decided_by
- decided_at
- reason_code
- comment
- delegated_from
- authority_snapshot
- decision_context_snapshot

Decision values:

APPROVE
REJECT
REQUEST_CHANGES
DELEGATE
ESCALATE
CANCEL

Decision context snapshot penting karena authority/role approver bisa berubah setelah approval.

Contoh authority snapshot:

{
  "approverId": "u-123",
  "role": "Regional Sales Director",
  "discountAuthorityPercent": 25,
  "marginFloorPercent": 20,
  "effectiveAtDecisionTime": "2026-07-12T10:15:00Z"
}

13. Approval Lifecycle

Approval lifecycle harus terhubung dengan quote lifecycle.

stateDiagram-v2 [*] --> NotRequired NotRequired --> Required: guardrail breached Required --> Pending: submit approval Pending --> InReview: approver opens task InReview --> Approved: approve InReview --> Rejected: reject InReview --> ChangesRequested: request changes Pending --> Expired: SLA expired Pending --> Cancelled: quote revised/cancelled Approved --> Invalidated: quote commercial terms changed Rejected --> Pending: resubmit after revision ChangesRequested --> Pending: resubmit Invalidated --> Pending: resubmit required

Key invariant:

Accepted quote with approval-required commercial exceptions must have valid approval evidence for the exact quote revision being accepted.

14. Approval Invalidation

Approval tidak selalu bertahan setelah quote berubah.

Perubahan yang biasanya harus meng-invalidasi approval:

  • discount percentage naik;
  • net price turun;
  • margin turun;
  • product mix berubah;
  • contract term berubah;
  • billing term berubah;
  • customer/account berubah;
  • currency berubah;
  • validity period berubah;
  • quote revision baru dibuat;
  • price recalculation terjadi.

Perubahan yang mungkin tidak perlu invalidation:

  • typo di description;
  • internal note;
  • non-commercial metadata;
  • attachment tambahan;
  • contact phone update.

Model perlu menyimpan invalidation reason:

APPROVAL_INVALIDATION
- id
- approval_request_id
- quote_id
- previous_revision
- new_revision
- invalidation_reason
- invalidated_by
- invalidated_at

15. Discount and Approval State Coupling

Quote state dan approval state saling terkait, tetapi jangan disamakan.

Contoh buruk:

quote.status = APPROVED

Lalu sistem menganggap semua approval selesai.

Lebih baik:

quote.status = SUBMITTED | APPROVED | ACCEPTED
quote.approval_status = NOT_REQUIRED | REQUIRED | PENDING | APPROVED | REJECTED | INVALIDATED

Alasannya:

  • quote lifecycle adalah lifecycle commercial proposal;
  • approval lifecycle adalah lifecycle control decision;
  • approval bisa invalidated walaupun quote masih draft/revised;
  • quote bisa submitted dengan approval pending;
  • quote bisa approved secara business state tetapi belum accepted customer.

16. Data Invariants

Beberapa invariant penting:

Discount invariant

Discount amount must not exceed applicable base price unless explicitly modelled as credit/waiver policy.

Price component invariant

Net amount = base amount + adjustments - discounts, according to calculation version.

Margin invariant

Margin calculation must reference the quote revision, cost source, price source, and currency used.

Approval required invariant

If discount/margin breaches active guardrail, quote cannot be accepted without valid approval.

Approval revision invariant

Approval is valid only for the quote revision and commercial context evaluated.

Authority invariant

Approver must have sufficient authority at decision time.

Audit invariant

Approval decision must preserve actor, timestamp, target, decision, reason, and evaluated rule context.

17. PostgreSQL Modelling Considerations

Possible tables:

quote_discount
quote_item_discount
price_component
margin_calculation
approval_rule
approval_request
approval_step
approval_decision
approval_history
approval_invalidation

Common constraints:

-- example only
CHECK (discount_amount >= 0)
CHECK (margin_percent >= -100)
CHECK (status IN ('PENDING', 'APPROVED', 'REJECTED', 'INVALIDATED'))
UNIQUE (quote_id, quote_revision, rule_code) WHERE status = 'APPROVED'

Useful indexes:

approval_request(status, created_at)
approval_request(quote_id, quote_revision)
approval_step(assigned_approver_id, status)
approval_step(assigned_group_id, status)
discount_component(price_component_id)
margin_calculation(quote_id, quote_revision)

Be careful with money fields:

  • use numeric, not floating point;
  • always store currency;
  • define precision/scale deliberately;
  • do not mix tax-inclusive and tax-exclusive values silently;
  • store calculation version if pricing algorithm can change.

18. Java/JAX-RS Backend Impact

In Java/JAX-RS service, discount and approval model affects:

  • request validation;
  • quote recalculation endpoint;
  • submit-for-approval endpoint;
  • approval decision endpoint;
  • quote revision endpoint;
  • quote acceptance endpoint;
  • audit event publishing;
  • concurrency control.

Example API boundary:

POST /quotes/{quoteId}/approval-requests
POST /approval-requests/{requestId}/decisions
GET  /quotes/{quoteId}/approval-status
POST /quotes/{quoteId}/recalculate-price
POST /quotes/{quoteId}/revisions

Do not expose database approval tables directly as DTOs. API model should be task-oriented:

  • submit approval;
  • approve;
  • reject;
  • request changes;
  • list pending approvals;
  • inspect approval trace.

19. MyBatis/JPA/JDBC Considerations

MyBatis

Good for explicit pricing/approval queries:

  • approval queue;
  • quote approval trace;
  • margin threshold search;
  • dashboard queries.

But be careful with manual joins and duplicated mapping logic.

JPA

Can model aggregate relationships, but approval history and price components can become heavy object graphs.

Avoid loading full quote + all items + all price components + all approval history for simple approval queue screen.

JDBC

Useful for batch jobs:

  • approval SLA escalation;
  • invalidation scan;
  • migration/backfill;
  • data quality checks.

20. Event-Driven Impact

Relevant domain/integration events:

DiscountApplied
DiscountRemoved
MarginCalculated
ApprovalRequired
ApprovalRequested
ApprovalStepAssigned
ApprovalDecisionRecorded
ApprovalRejected
ApprovalInvalidated
QuoteCommercialTermsChanged
QuoteApproved
QuoteApprovalExpired

Event metadata should include:

  • event id;
  • aggregate id;
  • quote id;
  • quote revision;
  • correlation id;
  • actor;
  • source system;
  • decision timestamp;
  • rule version if applicable.

Do not publish sensitive margin/cost data to broad consumers unless contractually allowed.


21. Reporting and Analytics Impact

Discount/margin/approval data supports:

  • average discount by product;
  • discount leakage;
  • approval SLA;
  • rejection rate;
  • margin floor breach;
  • manual exception frequency;
  • discount by sales channel;
  • discount by customer segment;
  • quote win/loss by discount level;
  • approval bottleneck by role/team;
  • commercial policy effectiveness.

Reporting model should distinguish:

requested discount
approved discount
final accepted discount
billed discount

These are not always the same.


22. Security and Privacy Concerns

Discount and margin data are commercially sensitive.

Access control should consider:

  • who can see margin;
  • who can edit discount;
  • who can approve exception;
  • who can see cost;
  • who can export quote data;
  • who can view approval comments;
  • who can see rejected commercial reasoning.

Margin and cost should often be more restricted than price.


23. Failure Modes

Failure modeSymptomLikely data cause
Discount exceeds authorityquote accepted with excessive discountapproval guard not enforced
Approval lost after revisionaccepted quote has no valid approvalapproval not revision-aware
Margin mismatchdashboard margin differs from quotecost/price version mismatch
Stale approvalold approval used for changed quoteinvalidation missing
Wrong approverapproval by unauthorized userauthority not snapshotted
Duplicate approvalmultiple active approval requestsmissing uniqueness/idempotency
Billing mismatchbilled amount differs from approved quoteprice snapshot not carried over
Audit gapcannot explain decisionmissing rule/decision trace

24. Debugging Data Issues

When debugging discount/approval issue, ask:

  1. Which quote revision is affected?
  2. Which price calculation version was used?
  3. Which discount components exist?
  4. Are discount source and reason clear?
  5. Was margin calculated after final discount?
  6. Which guardrail rule was evaluated?
  7. Was approval required?
  8. Was approval request created?
  9. Which approver decided?
  10. Did approver have sufficient authority at decision time?
  11. Was quote changed after approval?
  12. Was approval invalidated or reused?
  13. Was final accepted price copied to order/billing?
  14. Are events/API/read models consistent?

25. PR Review Checklist

Use this checklist when reviewing changes related to discount, margin, or approval:

  • Is discount represented as structured data, not just one field?
  • Is discount scope explicit?
  • Is discount source traceable?
  • Is discount reason governed?
  • Is margin calculation versioned?
  • Is cost source captured?
  • Is currency handled correctly?
  • Are approval rules versioned/effective-dated?
  • Is approval request tied to quote revision?
  • Is approval invalidated on commercial changes?
  • Is approval history immutable?
  • Is approver authority captured or reconstructable?
  • Are API DTOs separated from persistence model?
  • Are events safe for sensitive commercial data?
  • Are dashboard/read model implications considered?
  • Are PostgreSQL constraints/indexes sufficient?
  • Are security permissions enforced for margin/cost?
  • Are data quality checks defined?

26. Internal Verification Checklist

Verify with internal codebase/team:

  • Where are discounts stored: quote-level, item-level, price component-level, or mixed?
  • Is discount type separated from discount reason?
  • Are manual discounts distinguishable from rule/promotion/agreement discounts?
  • Is there a pricing trace explaining each discount?
  • How is margin calculated?
  • What is the authoritative source of cost?
  • Are cost and margin stored, recalculated, or both?
  • Does approval depend on discount threshold, margin threshold, product, customer, or contract term?
  • Are approval rules configurable or hardcoded?
  • Is approval tied to quote revision/version?
  • What invalidates an approval?
  • Can an accepted quote exist without valid approval evidence?
  • Are approval decisions immutable?
  • Is approver authority snapshotted or reconstructable?
  • What events are published for approval and pricing decisions?
  • What data is sent to billing?
  • Are margin/cost fields restricted by permission?
  • Are there historical incidents related to discount, margin, or approval mismatch?

27. Key Takeaways

Discount, margin, and approval are not peripheral CPQ features. They are part of the commercial correctness model.

A production-grade model must preserve:

  • what discount was applied;
  • why it was applied;
  • where it came from;
  • what margin resulted;
  • what rule was breached;
  • who approved it;
  • whether they had authority;
  • which quote revision was approved;
  • whether the approval remained valid;
  • whether the approved commercial terms reached order and billing unchanged.

If the model cannot answer those questions, the system may calculate prices, but it cannot defend commercial decisions.

Lesson Recap

You just completed lesson 21 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.