Skip to content

EP: unified ledger via polymorphic totals #258

@gsmith85

Description

@gsmith85

Summary

Currently, UCP models price modifications across disparate extensions: Discounts for negative adjustments and the proposed Fee Extension (#245) for positive ones.

This creates a pointer problem where the amount (in the totals array) is detached from its metadata (in an extension object). This forces platforms and agents to perform a join between two disparate objects to understand a single financial event, which which creates a referential integrity gap. Without a schema-enforced link between the ledger and its metadata, the system is prone to 'zombie totals' that persist in the calculation but have lost their provenance and intent.

Proposal: Polymorphic Composition

Instead of introducing new top-level extensions, we should use JSON Schema’s allOf composition to decorate
totals directly. This allows the ledger to be self-describing, while injecting structured metadata only when a specific type is present.

Schema Pattern

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://ucp.dev/schemas/shopping/types/fee_total.json",
  "title": "FeeTotal",
  "allOf": [
    { "$ref": "total.json" },
    {
      "type": "object",
      "properties": {
        "type": { "const": "fee" },
        "fee_type": {
          "type": "string",
          "description": "Well-known values: service, handling, recycling, convenience."
        },
        "description": {
          "type": "string",
          "description":"Optional plain-text explanation of why the fee is charged"
        },
        "taxable": { "type": "boolean", "default": false },
        "waivable": { "type": "boolean", "default": false }
      }
    }
  ]
}

This example provides equivalent functionality to the proposed Fees Extension (fees.json) and an Option C for (#219). In contrast, the example avoids introducing an additional extension for fees.

Case Study: Correlating Discount Codes

The current model for the discounts extension (discounts.md) illustrates why the unified ledger is beneficial.

In the example below, a client has to match or assume the relationship between the entries in discounts and totals:

{
  "discounts": {
    "applied": [
      { "code": "SAVE10", "title": "$10 Off", "amount": 1000 },
    ]
  },
  "totals": [
    {"type": "subtotal", "amount": 5500},
    {"type": "discount", "display_text": "Order Discount", "amount": 1000},
    {"type": "total", "amount": 4000}
  ]
}

By attaching metadata directly to the line item, the ledger entry explains its own existence. The price and the logic that created it (e.g., the specific discount code) become a single unit.

{
  "discounts": { /* continues to hold non-total related properties for extension */ }
  "totals": [
    {"type": "subtotal", "amount": 5000},
    {
      "type": "discount",
      "display_text": "$10 Off Your Order", 
      "amount": 1000,
      "applied_code": "SAVE10",
      "adjustment_type": "PROMOTION"
    },
    {"type": "total", "amount": 4000}
  ]
}

Tradeoffs

Benefits

  1. Direct Correlation: Eliminates the pointer problem. Audit trails are self-contained; the "why" (e.g., SAVE10) stays physically attached to the "How Much" ($10.00).
  2. State Machine Stability: Moving from Cart to Checkout becomes a non-lossy transition. Deterministic costs (like service fees) can be disclosed early and persist through the lifecycle without hunting for metadata.
  3. Simplified Protocol Surface: Provides a path to add properties without the overhead new extensions.
  4. Ledger Mental Model: Aligns with POS/ERP modeling where every adjustment is accompanied by its own context.

Drawbacks & Mitigations

  1. Polymorphic Checking: Platforms will need to use type-checking (e.g., instanceof or checking the type discriminator) to determine if additional context is available on a totals entry. Modern language features (Pattern Matching, Type Guards) make this trivial.

When are Extensions necessary?

We should distinguish between informational metadata (which belongs in totals) and behavioral capabilities (which require Extensions). Extensions related to totals should be reserved for scenarios that:

  1. Require Native UX Components: The consumer must render specific input fields or interactive elements (e.g., a Promo Code entry box or a Gift Card balance check).
  2. Compel Platform Behavior: The data dictates a non-standard operational flow or necessitates a specific handshake/validation before the checkout can proceed (e.g., handling a "Discount Limit Reached" error, collecting a Tax-Exempt Certificate).

In this model:

  • Discounts are Behavioral: They necessitate a discounts extension because they require the platform to provide the native UX for collection, real-time validation, and error handling.
  • Fees are Informational: The proposed fees.json are strictly disclosures ("Hints"). Metadata like taxable, fee_type, or waivable does not prohibit rendering a standard native checkout; it simply enriches the existing ledger entries. Therefore, these belong in totals as a decoration on a FeeTotal type.

Importantly, regardless of whether additional properties are introduced into the base Protocol or via extension, the resulting financial delta MUST be recorded in the totals[] array as a self-describing entry.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions