Skip to content

Support for Weighted and Variable-Quantity Items #259

@poojithg14

Description

@poojithg14

Summary

UCP treats quantity as an integer everywhere. That works for discrete items like shirts and phones, but grocery, deli, bulk foods, and fabric all deal in fractional amounts. You can't represent "1.5 kg of apples" or "0.75 lb of deli turkey" today. This proposal adds a dev.ucp.shopping.weighted_items extension to handle measured quantities without touching existing core schemas.

Motivation

I was looking at how UCP would work for grocery and hit a wall. The schema rejects "quantity": 1.5 because it's "type": "integer". That constraint runs through line_item.json, order_line_item.json, fulfillment_event.json, and adjustment.json. The whole lifecycle assumes you're buying discrete, countable things.

There's a unit_price field on variant.json with measure/reference objects, but that's just for regulatory shelf labels (EU "price per 100ml" stuff). It's not wired into checkout or cart. No spec doc even mentions it.

Things you can't represent right now:

  • 2.5 lbs chicken at $8.99/lb
  • 0.75 kg cheddar at $12.00/kg
  • 3.2 meters fabric at $15.00/m
  • 1.5 liters olive oil at $10.00/L

Goals

  • Allow line items to carry fractional, unit-based quantities (weight, volume, length) through cart, checkout, and order
  • Support estimated-to-actual quantity adjustment at fulfillment time (customer orders 2 kg, store picks 2.2 kg, total updates)
  • Maintain full backward compatibility so platforms that don't understand the extension still see valid line items

Non-Goals

  • Changing the core quantity field from integer to number. That cascades through 6+ schemas and breaks existing implementations
  • Unit conversion logic (kg to lb, etc.). The business handles conversion based on buyer context
  • Catch weight, random weight, or price-embedded barcode workflows. Those are follow-ups
  • Inventory or stock management for weighted items

Detailed Design

Extension Schema

The extension adds three fields to line items: measured_quantity, price_measurement, and quantity_status.

A customer adding 2 kg of tomatoes at $3.00/kg to their cart:

{
  "id": "li_1",
  "item": { "id": "tomato-roma", "title": "Roma Tomatoes", "price": 600 },
  "quantity": 1,
  "measured_quantity": { "value": 2.0, "unit": "kg", "type": "weight" },
  "price_measurement": { "unit": "kg", "reference_value": 1 },
  "quantity_status": "estimated",
  "totals": [{ "type": "subtotal", "amount": 600 }]
}

Base quantity stays at 1 so platforms without the extension still see a valid line item with the pre-computed total. The real quantity lives in measured_quantity.

measured_quantity is what you're actually buying. Has a value (number), unit (string like "kg", "lb", "m"), and type (the dimension: weight, volume, length, area).

price_measurement tells you how the item is priced. Per kg, per 100ml, per meter. Has a unit and a reference_value (1 for "per kg", 100 for "per 100ml").

quantity_status is either "estimated" or "actual". Drives the weight adjustment flow described below.

Estimated to Actual Flow

This is the core grocery scenario. Customer orders 2 kg of tomatoes, store picks 2.2 kg, total needs to update.

At checkout, the line item has quantity_status: "estimated" with totals based on the requested weight (2 kg * $3.00/kg = $6.00).

At fulfillment, the store weighs the actual pick. The order line item updates:

{
  "id": "li_1",
  "item": { "id": "tomato-roma", "title": "Roma Tomatoes", "price": 660 },
  "quantity": 1,
  "measured_quantity": { "value": 2.2, "unit": "kg", "type": "weight" },
  "quantity_status": "actual",
  "totals": [{ "type": "subtotal", "amount": 660 }]
}

Total goes from $6.00 to $6.60. The $0.60 difference gets recorded through the existing adjustment.json:

{
  "id": "adj_1",
  "type": "price_adjustment",
  "status": "completed",
  "occurred_at": "2026-03-09T14:30:00Z",
  "line_items": [{ "id": "li_1", "quantity": 1 }],
  "amount": 60,
  "description": "Weight adjustment: 2.0 kg estimated → 2.2 kg actual"
}

Works the other way too. Customer orders 2 kg, store picks 1.8 kg, total drops from $6.00 to $5.40, adjustment is a $0.60 credit back.

Units

Lowercase short codes following Google Merchant Center: kg, g, lb, oz, l, ml, m, cm, ft, yd. Businesses can use other values and platforms just display the string if they don't recognize it.

New Files

  • source/schemas/shopping/weighted_items.json (extension schema)
  • source/schemas/shopping/types/measured_quantity.json
  • source/schemas/shopping/types/price_measurement.json
  • docs/specification/weighted-items.md
  • Updates to mkdocs.yml and .cspell/custom-words.txt

Risks and Mitigations

Backward Compatibility: The extension approach avoids this entirely. Base quantity stays integer, platforms without the extension see valid line items. No migration needed. This is the same pattern used by discounts and fees.

Floating Point and Money: Fractional quantities multiplied by prices can produce rounding issues. Mitigation: cap measured_quantity.value at 3 decimal places and document that businesses should round totals to whole minor units before returning them.

Unit Conversion: A product priced in kg but a buyer expecting lb creates confusion. Mitigation: the business handles conversion based on buyer context (locale, preferences). The protocol carries the unit as-is, no conversion logic in the spec.

Complexity: Three new type schemas and one extension schema. Comparable to the fee extension in scope. Businesses that don't sell weighted items never advertise the capability, so there's no burden on them.

Test Plan

Schema Validation:

  • Existing integer-quantity line items validate unchanged
  • measured_quantity.value accepts decimals, rejects strings/negatives
  • quantity_status only accepts "estimated" or "actual"
  • price_measurement.reference_value rejects zero (division by zero)

Integration:

  • Measured quantity flows through cart, checkout, order without data loss
  • Mixed carts with both discrete and measured items work correctly
  • Platform without the extension sees quantity: 1 and pre-computed totals

End-to-End:

  • Full estimated-to-actual flow: checkout with estimated weight, fulfillment with actual weight, adjustment created with correct difference
  • Totals recompute correctly for weight-based line items
  • Negative adjustment (actual < estimated) credits the buyer

Graduation Criteria

Working Draft → Candidate:

  • Extension schema merged with Working Draft disclaimer
  • Type schemas (measured_quantity.json, price_measurement.json) merged
  • Schema linting passes
  • Spec docs with examples covering weight, volume, and length
  • TC majority vote to advance

Candidate → Stable:

  • At least one grocery or marketplace implementation running the weighted-item flow end to end
  • Estimated-to-actual adjustment flow validated in production
  • Backward compatibility confirmed with platforms that don't advertise the capability
  • Adoption feedback collected and addressed
  • TC majority vote to advance

Implementation History

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