Skip to content

MatchProposal — Lifecycle State Machine

Generated from specification/state-machines/match-proposal.md. Edit the canonical source file, not this page.

RFC 2119 normative language applies throughout.

State Meaning
open Proposal created and active; awaiting decisions from required parties.
accepted All required MatchDecision records agree to accept; the match is committed.
rejected At least one required party has rejected the proposal; the match will not proceed.
expired validUntil passed before a conclusive decision was reached.
superseded A newer proposal has been created for the same offer/request pair; this proposal is no longer actionable.
withdrawn The broker or an authorized party has retracted the proposal before a decision.

Terminal states: accepted, rejected, expired, superseded, withdrawn

From State Command To State Notes
(none) Broker creates proposal (system) open Broker records a compatible offer/request pair. Proposal is immutable once created.
open accept (triggered by MatchDecision records meeting policy) accepted All required decisions recorded; policy satisfied. Atomic.
open reject (triggered by any rejecting MatchDecision) rejected Any required party rejects; match cannot proceed.
open expire (system, clock) expired validUntil reached with no conclusive decision.
open supersede (system, on new proposal for same pair) superseded A newer proposal is created; existing open proposal is closed.
open withdraw (broker or authorized party) withdrawn Broker retracts the proposal before decision.

Any command applied to a terminal state MUST return 422 Unprocessable Entity with error type urn:freyt:protocol:error:invalid-state-transition. A MatchProposal is immutable after creation; its fields MUST NOT be updated. A newer rationale requires a new proposal, transitioning this one to superseded.

  • Authorization: Broker system only.
  • Scope required: Broker holds matches:read and capacity:read + loads:read on behalf of both sides.
  • Relationship state: PartnerRelationship between the carrier and shipper organizations MUST be active.
  • Version check: capacityOfferId + observed offer resourceVersion AND loadRequestId + observed request resourceVersion MUST be recorded on the proposal at creation time.
  • Policy check: requiredDecisionPolicy MUST be set (e.g. both-accept, carrier-only, shipper-only). validUntil MUST be set and MUST be in the future. The proposal MUST NOT disclose protected fields from either resource.
  • Authorization: Broker system, triggered when the count and roles of MatchDecision records with decision: accept satisfies requiredDecisionPolicy.
  • Version check: The observedProposalVersion on each MatchDecision MUST match the current resourceVersion of the proposal. A stale decision returns 409 Conflict with stale-resource-version; the server MUST NOT silently apply a decision made against a changed proposal.
  • Policy check: All required organizations for the given requiredDecisionPolicy MUST have an accept decision recorded.
  • Atomicity: The transition to accepted and the corresponding transitions on CapacityOffer and LoadRequest MUST commit atomically.
  • Authorization: Triggered by any MatchDecision record with decision: reject from a required party.
  • Version check: observedProposalVersion MUST match current resourceVersion.
  • Policy check: Rejection by a single required party is sufficient unless policy requires unanimous rejection.
  • Authorization: System clock process.
  • Precondition: Current time ≥ validUntil and status is still open.
  • Authorization: Broker system only.
  • Precondition: A new MatchProposal referencing the same (capacityOfferId, loadRequestId) pair is being created. All existing open proposals for that pair MUST be transitioned to superseded atomically with the creation of the new proposal.
  • Authorization: Broker system or an authorized protocol operator.
  • Scope required: matches:read + operator flag.
  • Version check: If-Match MUST match current resourceVersion.
  • Policy check: Withdrawal reason MUST be provided.
Command Postconditions
Proposal creation status = open; resourceVersion = 1; createdAt recorded; capacityOfferId + version + loadRequestId + version recorded; event pcx.match-proposal.created.v1 emitted.
accept status = accepted; acceptedAt recorded; resourceVersion incremented; CapacityOffer status updated to matched or partiallyMatched; LoadRequest status updated to matched; ExecutionReference record created; event pcx.match-proposal.accepted.v1 emitted.
reject status = rejected; rejectedAt recorded; rejecting organizationId and reasonCode noted; resourceVersion incremented; event pcx.match-proposal.rejected.v1 emitted.
expire status = expired; expiredAt recorded; resourceVersion incremented; linked LoadRequest may revert to published if no other proposals remain.
supersede status = superseded; supersededAt recorded; supersededByProposalId set; resourceVersion incremented.
withdraw status = withdrawn; withdrawnAt recorded; withdrawal reason stored; resourceVersion incremented.
  • Accept and withdraw racing (critical rule): accept and withdraw commands on the same proposal MUST be handled atomically within a single serializable transaction. Whichever acquires the exclusive lock first wins. A committed accepted state MUST NOT be reversed by a delayed withdraw command. If withdraw arrives after the proposal is accepted, it MUST return 422 Unprocessable Entity with invalid-state-transition.
  • Multiple decisions racing: Two MatchDecision records from different organizations may arrive concurrently. Each decision MUST be appended independently. The broker evaluates whether the policy threshold is met only after both are durably stored. The policy-satisfaction check MUST be serialized to prevent double-firing of the accept transition.
  • Supersede and accept racing: If a new proposal creation (which would supersede the current one) races with an accept trigger, the accept MUST take precedence if it commits first. A supersede MUST NOT overwrite accepted.
  • Expire and accept racing: If validUntil is reached at the same moment a decision is committed, the accept transition takes precedence if it commits within the same transaction boundary; otherwise expired wins.
  • All proposal state transitions MUST be applied atomically.
  1. Immutability after creation: A MatchProposal’s fields (offer/request versions, compatibility data, rationale, policy) MUST NOT be modified after status is open. A correction requires a new proposal.
  2. Version snapshot integrity: observedCapacityOfferVersion and observedLoadRequestVersion MUST be recorded on creation and MUST match the versions at creation time.
  3. No protected field disclosure: The proposal MUST NOT surface protected fields (customer identity, exact address, price, complete goods details) unless the field-disclosure policy explicitly permits it.
  4. Decision policy completeness: requiredDecisionPolicy MUST be set before a proposal is created and MUST NOT be changed.
  5. Terminal state finality: No transition out of accepted, rejected, expired, superseded, or withdrawn is permitted.
  6. Cascading consistency: When a proposal enters accepted, the linked CapacityOffer and LoadRequest MUST be updated in the same atomic operation.