← All @molecule/* packages · App templates

@molecule/api-resource-order

API resource · orders · API (Node) · v1.0.1 · Apache-2.0

Order management resource with status tracking, lifecycle transitions, cancellation, refunds, and event history.

npm install @molecule/api-resource-order

npm · Source on GitHub

How it works

@molecule/api-resource-order is an API resource: the routes, validation and storage for orders, built on the database and auth cores so it runs on whichever providers your app has bonded.

import { routes, requestHandlerMap } from '@molecule/api-resource-order'

Works with: @molecule/api-i18n, @molecule/api-resource

Reference

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Order resource for molecule.dev.

Provides order management with status tracking, lifecycle transitions, cancellation, refunds, and event history.

Quick Start

import { routes, requestHandlerMap } from '@molecule/api-resource-order'

Type

resource

Installation

npm install @molecule/api-resource-order @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource

API

Interfaces

Address

A physical or billing address.

interface Address {
  /** Street address line 1. */
  line1: string
  /** Street address line 2. */
  line2?: string
  /** City name. */
  city: string
  /** State or province. */
  state?: string
  /** Postal/zip code. */
  postalCode: string
  /** ISO 3166-1 alpha-2 country code. */
  country: string
}

CancelOrderInput

Input for cancelling an order.

interface CancelOrderInput {
  /** Reason for cancellation. */
  reason?: string
}

CreateOrderInput

Input for creating a new order.

interface CreateOrderInput {
  /** Items to include in the order. */
  items: CreateOrderItemInput[]
  /** Shipping address. */
  shippingAddress?: Address
  /** Billing address. */
  billingAddress?: Address
  /** Associated payment identifier. */
  paymentId?: string
  /** Customer notes. */
  notes?: string
  /** Pre-computed discount amount. */
  discount?: number
  /** Pre-computed shipping cost. */
  shipping?: number
  /** Pre-computed tax amount. */
  tax?: number
}

CreateOrderItemInput

Input for a single order item during creation.

interface CreateOrderItemInput {
  /** The product to order. */
  productId: string
  /** Optional variant identifier. */
  variantId?: string
  /** Display name. */
  name: string
  /** Unit price. */
  price: number
  /** Quantity to order. */
  quantity: number
  /** Optional product image URL. */
  image?: string
}

Order

A full order record.

interface Order {
  /** Unique order identifier. */
  id: string
  /** The user who placed this order. */
  userId: string
  /** Current status of the order. */
  status: OrderStatus
  /** Items in this order. */
  items: OrderItem[]
  /** Sum of item prices × quantities before discounts. */
  subtotal: number
  /** Computed tax amount. */
  tax: number
  /** Shipping cost. */
  shipping: number
  /** Discount amount. */
  discount: number
  /** Final total: subtotal − discount + tax + shipping. */
  total: number
  /** Shipping address, if applicable. */
  shippingAddress?: Address
  /** Billing address, if applicable. */
  billingAddress?: Address
  /** Associated payment identifier. */
  paymentId?: string
  /** Order notes from the customer. */
  notes?: string
  /** When the order was created. */
  createdAt: string
  /** When the order was last updated. */
  updatedAt: string
}

OrderEvent

A historical event in an order's lifecycle.

interface OrderEvent {
  /** Unique event identifier. */
  id: string
  /** The order this event belongs to. */
  orderId: string
  /** The status at the time of this event. */
  status: OrderStatus
  /** Optional metadata about the event. */
  metadata?: Record<string, unknown>
  /** When this event occurred. */
  createdAt: string
}

OrderEventRow

Internal database row for an order event.

interface OrderEventRow {
  /** Unique event identifier. */
  id: string
  /** The order this event belongs to. */
  orderId: string
  /** The status at the time of this event. */
  status: OrderStatus
  /** JSON-serialized metadata. */
  metadata: string | null
  /** When this event occurred. */
  createdAt: string
}

OrderItem

A single item within an order.

interface OrderItem {
  /** Unique identifier for this order line. */
  id: string
  /** The product being purchased. */
  productId: string
  /** Optional variant (size, color, etc.). */
  variantId?: string
  /** Display name of the product. */
  name: string
  /** Unit price at time of purchase. */
  price: number
  /** Quantity ordered. */
  quantity: number
  /** Optional product image URL. */
  image?: string
}

OrderItemRow

Internal database row for an order item.

interface OrderItemRow {
  /** Unique order item identifier. */
  id: string
  /** Parent order identifier. */
  orderId: string
  /** The product being purchased. */
  productId: string
  /** Optional variant identifier. */
  variantId: string | null
  /** Display name. */
  name: string
  /** Unit price at time of purchase. */
  price: number
  /** Quantity ordered. */
  quantity: number
  /** Optional product image URL. */
  image: string | null
}

OrderQuery

Query options for listing orders.

interface OrderQuery {
  /** Filter by status. */
  status?: OrderStatus
  /** Page number (1-based). */
  page?: number
  /** Number of items per page. */
  limit?: number
}

OrderRow

Internal database row for an order.

interface OrderRow {
  /** Unique order identifier. */
  id: string
  /** The user who placed this order. */
  userId: string
  /** Current status. */
  status: OrderStatus
  /** Sum of item prices × quantities. */
  subtotal: number
  /** Computed tax amount. */
  tax: number
  /** Shipping cost. */
  shipping: number
  /** Discount amount. */
  discount: number
  /** Final total. */
  total: number
  /** JSON-serialized shipping address. */
  shippingAddress: string | null
  /** JSON-serialized billing address. */
  billingAddress: string | null
  /** Associated payment identifier. */
  paymentId: string | null
  /** Customer notes. */
  notes: string | null
  /** Creation timestamp. */
  createdAt: string
  /** Last modification timestamp. */
  updatedAt: string
}

PaginatedResult

A paginated result set.

interface PaginatedResult<T> {
  /** The items in this page. */
  data: T[]
  /** Total number of items matching the query. */
  total: number
  /** Current page number (1-based). */
  page: number
  /** Number of items per page. */
  limit: number
}

RefundOrderInput

Input for refunding an order.

interface RefundOrderInput {
  /** Partial refund amount. If omitted, full refund is issued. */
  amount?: number
  /** Reason for the refund. */
  reason?: string
}

RefundResult

Result of a refund operation.

interface RefundResult {
  /** The order that was refunded. */
  orderId: string
  /** Amount refunded. */
  amount: number
  /** New order status after refund. */
  status: OrderStatus
  /** When the refund was processed. */
  refundedAt: string
}

UpdateOrderStatusInput

Input for updating an order's status.

interface UpdateOrderStatusInput {
  /** The new status. */
  status: OrderStatus
  /** Optional metadata about the status change. */
  metadata?: Record<string, unknown>
}

Types

OrderMerchantAuthorizer

Merchant-ownership predicate: returns true when userId is a merchant / seller / admin entitled to drive the given order's lifecycle (confirm, process, ship, deliver, refund, or cancel an already-progressed order). It is deliberately distinct from the buyer ownership check (orderRow.userId === userId): the order row records only the BUYER, so it has no inherent knowledge of who the seller is — only the consuming app does.

type OrderMerchantAuthorizer = (
  orderRow: OrderRow,
  userId: string,
  req?: MoleculeRequest,
) => Promise<boolean> | boolean

OrderStatus

Possible states of an order throughout its lifecycle.

type OrderStatus =
  'pending' | 'confirmed' | 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'refunded'

Functions

assembleOrder(orderRow, itemRows)

Assembles a full {@link Order} object from a database order row and item rows.

function assembleOrder(orderRow: OrderRow, itemRows: OrderItemRow[]): Order
  • orderRow — The order database row.
  • itemRows — The order item database rows.

Returns: The assembled order.

cancel(req, res)

Cancels an order. Only the order owner can cancel, and only from valid states.

function cancel(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with params.id and optional {@link CancelOrderInput} body.
  • res — The response object.

canDriveOrderLifecycle(orderRow, userId, req)

Default-DENY merchant gate. Returns true only when an authorizer has been registered via {@link setOrderMerchantAuthorizer} AND that authorizer allows userId to drive this order's lifecycle. When no authorizer is registered, this returns false — the merchant-only handlers respond 403.

function canDriveOrderLifecycle(
  orderRow: OrderRow,
  userId: string,
  req?: MoleculeRequest,
): Promise<boolean>
  • orderRow — The order being acted on.
  • userId — The authenticated user ID.
  • req — The originating request (optional).

Returns: true if the merchant op is allowed, otherwise false.

computeSubtotal(items)

Computes the subtotal from a list of order items (price × quantity).

function computeSubtotal(items: { price: number; quantity: number }[]): number
  • items — The order items.

Returns: The subtotal amount.

create(req, res)

Creates a new order from the request body.

⚠️ CLIENT-PRICE TRUST BOUNDARY — READ BEFORE WIRING TO PAYMENTS ⚠️

This handler TRUSTS the client-supplied money fields verbatim: items[].price, items[].quantity, discount, tax, and shipping all come straight from the request body, and the order total is computed from them (subtotal − discount + tax + shipping). This resource is GENERIC — it owns no product/catalog table — so it CANNOT and DOES NOT verify a client price against a real unit price. The validation below only rejects malformed money (negative amounts, non-integer/zero quantities); it does NOT establish that the prices are correct.

Therefore create() MUST NOT be wired directly to a payment-charging path. A caller that charges off this order's total lets a malicious client set their own prices (e.g. price: 0 or a negative discount that zeroes the total). Charging code MUST resolve each unit price SERVER-SIDE from the product/menu table (keyed by productId/variantId), ignore the client's price, and recompute subtotal/tax/shipping/total from those trusted values — exactly as every flagship checkout flow does. Use this handler only for non-charging flows (drafts, internal/admin order entry, an already-server-priced order), or replace it with an app-specific create that does the server-side price lookup.

function create(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with {@link CreateOrderInput} body.
  • res — The response object.

getHistory(req, res)

Returns the event history for an order. Only the order owner can view history.

function getHistory(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with params.id.
  • res — The response object.

getOrderMerchantAuthorizer()

Returns the currently-registered merchant authorizer, or null when none has been registered.

function getOrderMerchantAuthorizer(): OrderMerchantAuthorizer | null

Returns: The registered authorizer, or null.

list(req, res)

Lists orders for the authenticated user with pagination and optional status filter.

function list(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with optional query params: status, page, limit.
  • res — The response object.

read(req, res)

Retrieves a single order by ID. Only the order owner can read it.

function read(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with params.id.
  • res — The response object.

refund(req, res)

Issues a full or partial refund for an order.

function refund(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with params.id and optional {@link RefundOrderInput} body.
  • res — The response object.

setOrderMerchantAuthorizer(authorizer)

Registers the merchant authorizer consulted by the order lifecycle handlers (refund, a merchant-state updateStatus, and cancellation of an already-progressed order) before any mutation. Until an app registers one, every merchant op is DENIED (secure by default) — the order row records only the buyer, so the consuming app MUST supply who is entitled to act as the seller/merchant on an order.

Pass null to clear a previously-registered authorizer (restores default deny).

function setOrderMerchantAuthorizer(authorizer: OrderMerchantAuthorizer | null): void
  • authorizer — The merchant predicate, or null to clear.

toOrderEvent(row)

Converts a database order-event row into a typed {@link OrderEvent}.

function toOrderEvent(row: OrderEventRow): OrderEvent
  • row — The raw database row.

Returns: The deserialized order event.

toOrderItem(row)

Converts a database order-item row into a typed {@link OrderItem}.

function toOrderItem(row: OrderItemRow): OrderItem
  • row — The raw database row.

Returns: The deserialized order item.

updateStatus(req, res)

Updates an order's status with transition validation.

function updateStatus(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with params.id and {@link UpdateOrderStatusInput} body.
  • res — The response object.

Constants

BUYER_ALLOWED_TRANSITIONS

Buyer-reachable status transitions: maps current status to the set of next statuses an ORDER OWNER (buyer) may drive themselves. A buyer may only cancel an order while it is still pending; every other lifecycle transition is merchant-only. Any transition NOT listed here requires the merchant authorizer (see setOrderMerchantAuthorizer).

const BUYER_ALLOWED_TRANSITIONS: Record<OrderStatus, readonly OrderStatus[]>

i18nRegistered

Whether i18n registration has been attempted. Always true; this module is a placeholder for symmetry with locale-bonded resources.

const i18nRegistered: true

MERCHANT_STATES

Statuses that represent a MERCHANT-driven lifecycle stage. Transitioning an order INTO any of these is a merchant-only operation: it requires the merchant authorizer (see setOrderMerchantAuthorizer) and is never something the buyer (order owner) may self-service.

const MERCHANT_STATES: readonly OrderStatus[]

ORDER_STATUSES

Valid order statuses.

const ORDER_STATUSES: readonly OrderStatus[]

requestHandlerMap

Handler map for the order resource routes.

const requestHandlerMap: {
  readonly create: typeof create
  readonly list: typeof list
  readonly read: typeof read
  readonly updateStatus: typeof updateStatus
  readonly cancel: typeof cancel
  readonly refund: typeof refund
  readonly getHistory: typeof getHistory
}

routes

Order resource routes. All routes require authentication. The lifecycle mutations (updateStatus/refund/cancel) additionally gate merchant-only operations behind setOrderMerchantAuthorizer (see the module SECURITY note).

const routes: readonly [
  {
    readonly method: 'post'
    readonly path: '/orders'
    readonly handler: 'create'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'get'
    readonly path: '/orders'
    readonly handler: 'list'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'get'
    readonly path: '/orders/:id'
    readonly handler: 'read'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'put'
    readonly path: '/orders/:id/status'
    readonly handler: 'updateStatus'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'post'
    readonly path: '/orders/:id/cancel'
    readonly handler: 'cancel'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'post'
    readonly path: '/orders/:id/refund'
    readonly handler: 'refund'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'get'
    readonly path: '/orders/:id/history'
    readonly handler: 'getHistory'
    readonly middlewares: readonly ['authenticate']
  },
]

STATUS_TRANSITIONS

Status transitions: maps current status to the set of valid next statuses.

const STATUS_TRANSITIONS: Record<OrderStatus, readonly OrderStatus[]>

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-database ^1.0.1
  • @molecule/api-i18n ^1.0.1
  • @molecule/api-logger ^1.0.1
  • @molecule/api-resource ^1.0.1

Runtime Dependencies

  • @molecule/api-database
  • @molecule/api-i18n
  • @molecule/api-logger
  • @molecule/api-resource

SECURITY — create() TRUSTS client-supplied prices; do NOT wire it to a payment-charging path. This resource is GENERIC: it owns no product/catalog table, so it CANNOT verify a price. create() builds the order — and its total (subtotal − discount + tax + shipping) — from the request body's items[].price, quantity, discount, tax, and shipping. Input validation rejects malformed money (negative price/discount/tax/ shipping, non-integer or < 1 quantity) but does NOT establish that the prices are CORRECT. A client can therefore submit price: 0 (or otherwise understate the total). Any code that CHARGES off an order MUST resolve each unit price SERVER-SIDE from the product/menu table (keyed by productId/ variantId), ignore the client's price, and recompute the totals from those trusted values — as every flagship checkout flow does. Use the stock create() only for non-charging flows (drafts, internal/admin order entry, an order that was already server-priced upstream).

Lifecycle ops (confirm/process/ship/deliver/refund, and cancelling an already-progressed order) are MERCHANT-ONLY and DENY by default until an app registers a merchant authorizer via setOrderMerchantAuthorizer — the order row records only the BUYER (userId), so it cannot know who the seller is.

Tables: src/__setup__/orders.sql creates orders, order_items, and order_events. An mlcl-scaffolded API replays __setup__/*.sql automatically on migrate; anywhere else run it once — nothing at runtime creates them.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual order/checkout screens, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip. Ground every check in the REAL statuses (pending/confirmed/processing/shipped/delivered/cancelled/refunded) and the defined transitions — never a status the interface lacks:

  • Placing an order creates it pending with the exact line items submitted (productId, name, price, quantity) and a correctly-computed total: subtotal = the sum of price x quantity across items, and total = subtotal - discount + tax + shipping. The amount the UI shows matches that formula to the cent.
  • The fulfillment lifecycle advances ONLY through the defined transitions — pending -> confirmed -> processing -> shipped -> delivered. An illegal jump (e.g. pending -> shipped, or shipped -> pending) is rejected 409 and the order's stored status is left unchanged.
  • Cancel is honored only from a cancellable state (pending, confirmed, or processing); cancelling a shipped, delivered, cancelled, or already refunded order is rejected 409 — you cannot cancel or ship a cancelled order.
  • Refund is honored ONLY from delivered (the sole state whose transitions include refunded); refunding an unpaid/pending or a merely shipped order is rejected 409, and a refund amount <= 0 or greater than the order total is rejected 400.
  • Line items and money stay consistent across the flow: a status change never alters the stored items, subtotal, or total, and a refund records its amount (<= total) without corrupting the order total.
  • AUTHORIZATION — a user sees and mutates only their OWN orders. The list returns just the caller's orders; reading or acting on another user's order id returns 403 (or 404 when it does not exist) — guessing an id never leaks or mutates someone else's order.
  • AUTHORIZATION — no endpoint lets a normal user push an order into a privileged merchant state. Marking an order confirmed/processing/shipped/ delivered, or issuing a refund, is DENIED 403 unless a merchant authorizer (setOrderMerchantAuthorizer) approves the caller — deny by default. The only buyer-driven lifecycle action is cancelling a still-pending order.