Skip to content

feat!: update Order capability#254

Open
richmolj wants to merge 5 commits intomainfrom
lr/order-spec-updates
Open

feat!: update Order capability#254
richmolj wants to merge 5 commits intomainfrom
lr/order-spec-updates

Conversation

@richmolj
Copy link
Contributor

@richmolj richmolj commented Mar 11, 2026

Description

Collection of updates to the Order capability to make the spec more flexible, robust, and clear.

  • Soften "append-only" on adjustments and "immutable" on line items
  • Remove "derived from events" quantity language
  • Model Order:Checkout as 1:N via checkouts array (order edits create new sessions)
  • Add signed quantities/amounts on adjustments and quantity.original on line items
  • Rename adjustment amount to totals for consistency with Order and OrderLineItem

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing
    functionality to not work as expected, including removal of schema files
    or fields
    )
  • Documentation update

Is this a Breaking Change or Removal?

  • I have added ! to my PR title (e.g., feat!: remove field).
  • I have added justification below.

Breaking Changes / Removal Justification

  • checkout_id (string) replaced with checkouts (array of { id, created_at } objects) — orders can reference multiple checkout sessions via edits and exchanges
  • amount on Adjustment replaced with totals (array of Total objects) — aligns with the pattern used by Order and OrderLineItem
  • minimum: 1 removed from adjustment line item quantities — signed values needed for returns and exchanges
  • minimum: 0 removed from amount.json — amounts can be negative for refunds, credits, and adjustment totals across all capabilities

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings

@richmolj richmolj requested review from a team as code owners March 11, 2026 14:20
@igrigorik igrigorik added the TC review Ready for TC review label Mar 11, 2026
"properties": {
"id": {
"type": "string",
"description": "Checkout session identifier."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"description": "Checkout session identifier."
"description": ""Unique identifier of the checkout session."

To be consistent with checkout docs / schema desc

"type": "string",
"format": "date-time",
"description": "RFC 3339 timestamp when this checkout session was created."
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the purpose of created_at to identify the sequential order of multiple checkouts? If so, do we need to include updated_at as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or is the purpose here just to distinguish the original checkout and edit/exchange checkouts ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created_at is there so the sequence is clear. Checkouts are frozen after creation - they are never updated - so updated_at is not needed here.

"totals": {
"type": "array",
"items": {
"$ref": "total.json"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the current total and amount schema allows negative - should update them as part of this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Removed minimum: 0 from amount.json so negative values are allowed for signed adjustment totals.

**Line Items** — what was purchased at checkout:

* Includes current quantity counts (total, fulfilled)
* Can change post-order (e.g. order edits, exchanges)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With order edits now in the picture, can we add a guideline around what line_items SHOULD include post these changes, is it:

  1. A comprehensive list of all the items that once existed in the order, even if they were altered/removed via an edit?
    OR
  2. Only contain remaining items that are present after the latest edit?

My hunch is that 1) is more comprehensive from a data/audit perspective given there may have been adjustments that reference back to these altered products?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option 1 - comprehensive list. Line items should include all items that ever existed on the order, even if altered/removed via an edit. This is why quantity.original exists on line items - it preserves what was originally ordered at checkout, even when quantity.total changes to 0 after an edit. Adjustments can then always reference back to these items.


* Item details (product, price, quantity ordered)
* Quantity counts and status are derived
* Quantity counts and fulfillment status
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify what this change mean here given I think status of the line_item is still derived based on it's relationship with quantity.total and quantity.fulfilled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change here is removing the claim that quantities are "derived from events" - merchants may not send events, so we cannot say that is where the values come from, even though that may be the case the majority of the time. Status is still derived from the quantity fields, and that logic is still documented in the Status Derivation section below.

* Include amount when relevant
* Quantities and amounts are signed—negative for reductions (returns, refunds),
positive for additions (exchanges)
* Include totals breakdown when relevant
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to visualize how totals can be used here:

  • For a full refund, I can see this would be very similar to the totals construct in checkout.
  • However, for things like partial refund or any credit/price adjustment (i.e. resulting in an arbitrary money movement), I fail to fully understand what should be returned in the array here - if we have type: TOTAL for these scenarios here, then it "violates"/"invalidates" our general rule of how total should be calculated that currently is present in totals.json.

Alternatively, we can consider adding another categorization into totals.type for adjustment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totals on an adjustment is scoped to that adjustment, not the order. A partial refund of $5 would just be [{ "type": "total", "amount": -500 }]. If the business wants to break it down further (e.g. the subtotal and tax portions of the refund separately), they can, but a single total entry works fine for simple cases. The calculation rules in totals.json apply to order-level and checkout-level totals, not adjustment-level.

"type": "object",
"required": ["total", "fulfilled"],
"properties": {
"original": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not super clear about the use case we have in-mind for this field (especially given other quantity fields are used for deriving status).

Tied with a comment below, I think maybe one use case is to use it along with quantity.total to under if status should be in cancelled state?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Primary use case is what we discussed above - line items are a comprehensive list that includes items removed via edits. quantity.original lets platforms see what was originally ordered without fetching the Checkout record. For example, an item edited down to 0 would have original: 2, total: 0, fulfilled: 0 - the platform can see it was a real item that got removed, not a phantom entry.

```json
{
"total": 3, // Current total quantity
"original": 3, // Quantity at checkout
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Maybe we should clarify a bit further that this is the checkout with the oldest created_at timestamp (and not just any checkout)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. This refers to the original checkout (the one with the earliest created_at). Updated the description to clarify.

"partial",
"fulfilled"
],
"description": "Derived status: fulfilled if quantity.fulfilled == quantity.total, partial if quantity.fulfilled > 0, otherwise processing."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we will need some more status or at least add some more conditions to this described logic to cover the following 2 scenarios:

  1. If order is cancelled prior to fulfillment, then by this logic today, item-level status will be stuck in processing. We should probably add some terminal state to catch cancelled.
  2. If an item is fulfilled, then edited from an exchange, then I'd imagine the old item that got exchanged would have totals = 0 but fulfilled > 0, in that case, what status should it be in (maybe we should have fulfilled if quantity.fulfilled >= quantity.total instead)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good observations. The status derivation documented here covers the common case - it is not meant to be exhaustive. The spec treats status as an open string, so businesses can use additional values like cancelled or exchanged for these scenarios. We will keep the derivation simple for now and can expand the documented examples in a follow-up if needed. Does that sound good?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants