<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P146-order-states/ | updated: 2026-10-18 -->

# P146 Order states

Orders kept as three fields - payment, fulfilment, called off - and as one status column of five values for eighteen real states: what the column reads back as, and the answers it gets wrong.

EML-P project `projects/order-states` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/order-states` runs it and `eml project verify projects/order-states` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: status-field-conflation (https://efficientnewlanguage.org/cases/290-status-field-conflation/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
2

2
x
2
1234
2
9
2
1

2
1
fly
2
2
pay
2
1
return
2
6
ship
2
5
return
2
3
cancel
2
6
cancel
2
1
refund
2
1
cancel
2
6
refund
1
3
6
```

Screen:

```text
== Order states ==
Payment, fulfilment and calling off, kept as three fields and as one status column.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 0
Pick a number from 1 to 6.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> x
Pick a number from 1 to 6.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 
Cancelled.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> x
Type an order number.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 1234
Type an order number.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 9
There is no order #9.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 1
event (pay, ship, refund, return, cancel)> 
Cancelled.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 1
event (pay, ship, refund, return, cancel)> fly
Type pay, ship, refund, return or cancel.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 2
event (pay, ship, refund, return, cancel)> pay
Order #2 cannot pay: it is already paid.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 1
event (pay, ship, refund, return, cancel)> return
Order #1 cannot return: it was never shipped.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 6
event (pay, ship, refund, return, cancel)> ship
Order #6 cannot ship: it was cancelled.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 5
event (pay, ship, refund, return, cancel)> return
Order #5 cannot return: it is already returned.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 3
event (pay, ship, refund, return, cancel)> cancel
Order #3 cannot cancel: it is already shipped.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 6
event (pay, ship, refund, return, cancel)> cancel
Order #6 cannot cancel: it is already cancelled.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 1
event (pay, ship, refund, return, cancel)> refund
Order #1 cannot refund: it is unpaid, not paid.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 1
event (pay, ship, refund, return, cancel)> cancel
Order #1, cancel: three fields unpaid, none, cancelled; the column pending -> cancelled.
The column names this state exactly: cancelled.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 6
event (pay, ship, refund, return, cancel)> refund
Order #6, refund: three fields refunded, none, cancelled; the column cancelled -> refunded.
No value names refunded, none, cancelled; the column reads back as refunded, none, open, wrong on called off.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 1
  order  three fields                  column     reads back wrong
  #1     unpaid, none, cancelled       cancelled  -
  #2     paid, none, open              paid       -
  #3     paid, shipped, open           shipped    -
  #4     refunded, shipped, open       refunded   fulfilment
  #5     paid, returned, open          shipped    fulfilment
  #6     refunded, none, cancelled     refunded   called off

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 3
  question                    three fields          the column
  money we hold               3: #2 #3 #5           3: #2 #3 #5
  parcels with the customer   2: #3 #4              2: #3 #5
  refunds we owe              1: #5                 0: none
The column answers 2 of the 3 questions wrong, and raises no error doing it.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 6
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
3
4
5
2
7
ship
2
7
pay
2
7
refund
2
3
return
1
3
6
```

Screen:

```text
== Order states ==
Payment, fulfilment and calling off, kept as three fields and as one status column.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 1
  order  three fields                  column     reads back wrong
  #1     unpaid, none, open            pending    -
  #2     paid, none, open              paid       -
  #3     paid, shipped, open           shipped    -
  #4     refunded, shipped, open       refunded   fulfilment
  #5     paid, returned, open          shipped    fulfilment
  #6     paid, none, cancelled         cancelled  payment

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 3
  question                    three fields          the column
  money we hold               4: #2 #3 #5 #6        3: #2 #3 #5
  parcels with the customer   2: #3 #4              2: #3 #5
  refunds we owe              2: #5 #6              0: none
The column answers 3 of the 3 questions wrong, and raises no error doing it.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 4
Every real state, and the value that names it:
  unpaid, none, open            pending
  unpaid, shipped, open         -
  unpaid, returned, open        -
  paid, none, open              paid
  paid, shipped, open           shipped
  paid, returned, open          -
  refunded, none, open          refunded
  refunded, shipped, open       -
  refunded, returned, open      -
  unpaid, none, cancelled       cancelled
  unpaid, shipped, cancelled    -
  unpaid, returned, cancelled   -
  paid, none, cancelled         -
  paid, shipped, cancelled      -
  paid, returned, cancelled     -
  refunded, none, cancelled     -
  refunded, shipped, cancelled  -
  refunded, returned, cancelled -
5 values name 5 of the 18 real states; 13 have no value, and the code that meets one writes a neighbour.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 5
Order #7 placed: unpaid, none, open; the column pending.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 7
event (pay, ship, refund, return, cancel)> ship
Order #7, ship: three fields unpaid, shipped, open; the column pending -> shipped.
No value names unpaid, shipped, open; the column reads back as paid, shipped, open, wrong on payment.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 7
event (pay, ship, refund, return, cancel)> pay
Order #7, pay: three fields paid, shipped, open; the column shipped -> paid.
The column reads back as paid, none, open, wrong on fulfilment, though shipped would name it.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 7
event (pay, ship, refund, return, cancel)> refund
Order #7, refund: three fields refunded, shipped, open; the column paid -> refunded.
No value names refunded, shipped, open; the column reads back as refunded, none, open, wrong on fulfilment.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 2
order number> 3
event (pay, ship, refund, return, cancel)> return
Order #3, return: three fields paid, returned, open; the column shipped -> shipped.
No value names paid, returned, open; the column reads back as paid, shipped, open, wrong on fulfilment.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 1
  order  three fields                  column     reads back wrong
  #1     unpaid, none, open            pending    -
  #2     paid, none, open              paid       -
  #3     paid, returned, open          shipped    fulfilment
  #4     refunded, shipped, open       refunded   fulfilment
  #5     paid, returned, open          shipped    fulfilment
  #6     paid, none, cancelled         cancelled  payment
  #7     refunded, shipped, open       refunded   fulfilment

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 3
  question                    three fields          the column
  money we hold               4: #2 #3 #5 #6        3: #2 #3 #5
  parcels with the customer   2: #4 #7              2: #3 #5
  refunds we owe              3: #3 #5 #6           0: none
The column answers 3 of the 3 questions wrong, and raises no error doing it.

1) orders  2) an event  3) reports  4) every state  5) new order  6) quit
choice> 6
Bye.
```

## Modules

### main.eml

```eml
# P146 order states: orders kept as three fields and as one status column,
# events applied to both, what the column reads back as, the reports each
# model gives, and the real states the column can name.
import orders

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def pad(s, n):
    s => out
    while len(out) < n:
        out + " " => out
    return out

def lpad(s, n):
    s => out
    while len(out) < n:
        " " + out => out
    return out

def triple(st):
    return st[0] + ", " + st[1] + ", " + st[2]

def wrong(st, col):
    # the fields the column reads back wrong, or ""
    orders.read(col) => back
    ["payment", "fulfilment", "called off"] => names
    "" => out
    0 => i
    while i < 3:
        if back[i] != st[i]:
            if out == "":
                names[i] => out
            else:
                out + " and " + names[i] => out
        i + 1 => i
    return out

def make(number, evs):
    orders.start() => st
    "pending" => col
    for e in evs:
        orders.apply(st, e) => st
        orders.write(col, e) => col
    return [number, st, col]

def show(os):
    ("  " + pad("order", 7) + pad("three fields", 30) + pad("column", 11) + "reads back wrong") ^0
    for o in os:
        wrong(o[1], o[2]) => w
        if w == "":
            "-" => w
        ("  " + pad("#" + str(o[0]), 7) + pad(triple(o[1]), 30) + pad(o[2], 11) + w) ^0

def find(os, n):
    0 => i
    for o in os:
        if o[0] == n:
            return i
        i + 1 => i
    return 0 - 1

def event(os):
    plain(input("order number> ")) => t
    if t == "":
        "Cancelled." ^0
        return os
    if len(t) > 3:
        "Type an order number." ^0
        return os
    for c in t:
        if c < "0" or c > "9":
            "Type an order number." ^0
            return os
    find(os, int(t)) => k
    if k < 0:
        ("There is no order #" + t + ".") ^0
        return os
    plain(input("event (pay, ship, refund, return, cancel)> ")) => e
    if e == "":
        "Cancelled." ^0
        return os
    False => known
    for x in orders.events():
        if x == e:
            True => known
    if not known:
        "Type pay, ship, refund, return or cancel." ^0
        return os
    os[k] => o
    orders.problem(o[1], e) => why
    if why != "":
        ("Order #" + str(o[0]) + " cannot " + e + ": " + why + ".") ^0
        return os
    orders.apply(o[1], e) => st
    orders.write(o[2], e) => col
    ("Order #" + str(o[0]) + ", " + e + ": three fields " + triple(st) + "; the column " + o[2] + " -> " + col + ".") ^0
    orders.name_of(st) => exact
    wrong(st, col) => w
    if w == "":
        ("The column names this state exactly: " + col + ".") ^0
    elif exact == "":
        ("No value names " + triple(st) + "; the column reads back as " + triple(orders.read(col)) + ", wrong on " + w + ".") ^0
    else:
        ("The column reads back as " + triple(orders.read(col)) + ", wrong on " + w + ", though " + exact + " would name it.") ^0
    [] => out
    0 => i
    for x in os:
        if i == k:
            out + [[o[0], st, col]] => out
        else:
            out + [x] => out
        i + 1 => i
    return out

def numbers(os, which, from_column):
    "" => out
    0 => n
    for o in os:
        if from_column:
            orders.read(o[2]) => st
        else:
            o[1] => st
        if which == 0:
            orders.money_held(st) => yes
        elif which == 1:
            orders.parcel_out(st) => yes
        else:
            orders.refund_owed(st) => yes
        if yes:
            n + 1 => n
            if out == "":
                "#" + str(o[0]) => out
            else:
                out + " #" + str(o[0]) => out
    if out == "":
        "none" => out
    return [n, out]

def reports(os):
    ["money we hold", "parcels with the customer", "refunds we owe"] => qs
    ("  " + pad("question", 28) + pad("three fields", 22) + "the column") ^0
    0 => differ
    0 => i
    while i < 3:
        numbers(os, i, False) => a
        numbers(os, i, True) => b
        ("  " + pad(qs[i], 28) + pad(str(a[0]) + ": " + a[1], 22) + str(b[0]) + ": " + b[1]) ^0
        if a[1] != b[1]:
            differ + 1 => differ
        i + 1 => i
    if differ == 0:
        "Both models answer every question the same, for these orders." ^0
    else:
        ("The column answers " + str(differ) + " of the 3 questions wrong, and raises no error doing it.") ^0

def space():
    "Every real state, and the value that names it:" ^0
    0 => named
    0 => total
    for c in ["open", "cancelled"]:
        for p in orders.payments():
            for f in orders.fulfilments():
                [p, f, c] => st
                orders.name_of(st) => v
                total + 1 => total
                if v == "":
                    "-" => v
                else:
                    named + 1 => named
                ("  " + pad(triple(st), 30) + v) ^0
    (str(len(orders.columns())) + " values name " + str(named) + " of the " + str(total) + " real states; " + str(total - named) + " have no value, and the code that meets one writes a neighbour.") ^0

[] => os
for o in orders.opening():
    os + [make(o[0], o[1])] => os

"== Order states ==" ^0
"Payment, fulfilment and calling off, kept as three fields and as one status column." ^0
True => running
while running:
    "" ^0
    "1) orders  2) an event  3) reports  4) every state  5) new order  6) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show(os)
    elif choice == "2":
        event(os) => os
    elif choice == "3":
        reports(os)
    elif choice == "4":
        space()
    elif choice == "5":
        os[len(os) - 1][0] + 1 => n
        os + [make(n, [])] => os
        ("Order #" + str(n) + " placed: unpaid, none, open; the column pending.") ^0
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import orders

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def pad(s, n):
    out = s
    while len(out) < n:
        out = out + " "
    return out

def lpad(s, n):
    out = s
    while len(out) < n:
        out = " " + out
    return out

def triple(st):
    return st[0] + ", " + st[1] + ", " + st[2]

def wrong(st, col):
    back = orders.read(col)
    names = ["payment", "fulfilment", "called off"]
    out = ""
    i = 0
    while i < 3:
        if back[i] != st[i]:
            if out == "":
                out = names[i]
            else:
                out = out + " and " + names[i]
        i = i + 1
    return out

def make(number, evs):
    st = orders.start()
    col = "pending"
    for e in evs:
        st = orders.apply(st, e)
        col = orders.write(col, e)
    return [number, st, col]

def show(os):
    print("  " + pad("order", 7) + pad("three fields", 30) + pad("column", 11) + "reads back wrong")
    for o in os:
        w = wrong(o[1], o[2])
        if w == "":
            w = "-"
        print("  " + pad("#" + str(o[0]), 7) + pad(triple(o[1]), 30) + pad(o[2], 11) + w)

def find(os, n):
    i = 0
    for o in os:
        if o[0] == n:
            return i
        i = i + 1
    return 0 - 1

def event(os):
    t = plain(input("order number> "))
    if t == "":
        print("Cancelled.")
        return os
    if len(t) > 3:
        print("Type an order number.")
        return os
    for c in t:
        if c < "0" or c > "9":
            print("Type an order number.")
            return os
    k = find(os, int(t))
    if k < 0:
        print("There is no order #" + t + ".")
        return os
    e = plain(input("event (pay, ship, refund, return, cancel)> "))
    if e == "":
        print("Cancelled.")
        return os
    known = False
    for x in orders.events():
        if x == e:
            known = True
    if not known:
        print("Type pay, ship, refund, return or cancel.")
        return os
    o = os[k]
    why = orders.problem(o[1], e)
    if why != "":
        print("Order #" + str(o[0]) + " cannot " + e + ": " + why + ".")
        return os
    st = orders.apply(o[1], e)
    col = orders.write(o[2], e)
    print("Order #" + str(o[0]) + ", " + e + ": three fields " + triple(st) + "; the column " + o[2] + " -> " + col + ".")
    exact = orders.name_of(st)
    w = wrong(st, col)
    if w == "":
        print("The column names this state exactly: " + col + ".")
    elif exact == "":
        print("No value names " + triple(st) + "; the column reads back as " + triple(orders.read(col)) + ", wrong on " + w + ".")
    else:
        print("The column reads back as " + triple(orders.read(col)) + ", wrong on " + w + ", though " + exact + " would name it.")
    out = []
    i = 0
    for x in os:
        if i == k:
            out = out + [[o[0], st, col]]
        else:
            out = out + [x]
        i = i + 1
    return out

def numbers(os, which, from_column):
    out = ""
    n = 0
    for o in os:
        if from_column:
            st = orders.read(o[2])
        else:
            st = o[1]
        if which == 0:
            yes = orders.money_held(st)
        elif which == 1:
            yes = orders.parcel_out(st)
        else:
            yes = orders.refund_owed(st)
        if yes:
            n = n + 1
            if out == "":
                out = "#" + str(o[0])
            else:
                out = out + " #" + str(o[0])
    if out == "":
        out = "none"
    return [n, out]

def reports(os):
    qs = ["money we hold", "parcels with the customer", "refunds we owe"]
    print("  " + pad("question", 28) + pad("three fields", 22) + "the column")
    differ = 0
    i = 0
    while i < 3:
        a = numbers(os, i, False)
        b = numbers(os, i, True)
        print("  " + pad(qs[i], 28) + pad(str(a[0]) + ": " + a[1], 22) + str(b[0]) + ": " + b[1])
        if a[1] != b[1]:
            differ = differ + 1
        i = i + 1
    if differ == 0:
        print("Both models answer every question the same, for these orders.")
    else:
        print("The column answers " + str(differ) + " of the 3 questions wrong, and raises no error doing it.")

def space():
    print("Every real state, and the value that names it:")
    named = 0
    total = 0
    for c in ["open", "cancelled"]:
        for p in orders.payments():
            for f in orders.fulfilments():
                st = [p, f, c]
                v = orders.name_of(st)
                total = total + 1
                if v == "":
                    v = "-"
                else:
                    named = named + 1
                print("  " + pad(triple(st), 30) + v)
    print(str(len(orders.columns())) + " values name " + str(named) + " of the " + str(total) + " real states; " + str(total - named) + " have no value, and the code that meets one writes a neighbour.")

os = []
for o in orders.opening():
    os = os + [make(o[0], o[1])]
print("== Order states ==")
print("Payment, fulfilment and calling off, kept as three fields and as one status column.")
running = True
while running:
    print("")
    print("1) orders  2) an event  3) reports  4) every state  5) new order  6) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show(os)
    elif choice == "2":
        os = event(os)
    elif choice == "3":
        reports(os)
    elif choice == "4":
        space()
    elif choice == "5":
        n = os[len(os) - 1][0] + 1
        os = os + [make(n, [])]
        print("Order #" + str(n) + " placed: unpaid, none, open; the column pending.")
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")
```

### orders.eml

```eml
# P146 order states - an order's payment, fulfilment and whether it was
# called off, kept as three fields and as one status column. As in the
# corpus case status-field-conflation, the column is asked to hold a pair
# (here a triple) in one slot: five values for eighteen real states. The
# event handlers write the value nearest to what happened, and reading the
# column back gives a state that did not happen. A state is [payment,
# fulfilment, called off]: payment unpaid, paid or refunded; fulfilment
# none, shipped or returned; called off "open" or "cancelled".

def payments():
    return ["unpaid", "paid", "refunded"]

def fulfilments():
    return ["none", "shipped", "returned"]

def columns():
    return ["pending", "paid", "shipped", "cancelled", "refunded"]

def events():
    return ["pay", "ship", "refund", "return", "cancel"]

def start():
    return ["unpaid", "none", "open"]

def problem(st, e):
    # why event e cannot happen to state st, or "" when it can
    if e == "pay":
        if st[0] != "unpaid":
            return "it is already " + st[0]
        if st[2] == "cancelled":
            return "it was cancelled"
    if e == "ship":
        if st[1] != "none":
            return "it is already " + st[1]
        if st[2] == "cancelled":
            return "it was cancelled"
    if e == "refund" and st[0] != "paid":
        return "it is " + st[0] + ", not paid"
    if e == "return" and st[1] != "shipped":
        if st[1] == "none":
            return "it was never shipped"
        return "it is already returned"
    if e == "cancel":
        if st[2] == "cancelled":
            return "it is already cancelled"
        if st[1] != "none":
            return "it is already " + st[1]
    return ""

def apply(st, e):
    # the three fields after event e
    if e == "pay":
        return ["paid", st[1], st[2]]
    if e == "ship":
        return [st[0], "shipped", st[2]]
    if e == "refund":
        return ["refunded", st[1], st[2]]
    if e == "return":
        return [st[0], "returned", st[2]]
    return [st[0], st[1], "cancelled"]

def write(col, e):
    # the column after event e: each handler writes the value nearest to
    # what happened; a return has no value, so it leaves the column
    if e == "pay":
        return "paid"
    if e == "ship":
        return "shipped"
    if e == "refund":
        return "refunded"
    if e == "cancel":
        return "cancelled"
    return col

def name_of(st):
    # the value that names state st exactly, or ""
    if st[2] == "open":
        if st[0] == "unpaid" and st[1] == "none":
            return "pending"
        if st[0] == "paid" and st[1] == "none":
            return "paid"
        if st[0] == "paid" and st[1] == "shipped":
            return "shipped"
        if st[0] == "refunded" and st[1] == "none":
            return "refunded"
    elif st[0] == "unpaid" and st[1] == "none":
        return "cancelled"
    return ""

def read(col):
    # the state the column claims
    if col == "paid":
        return ["paid", "none", "open"]
    if col == "shipped":
        return ["paid", "shipped", "open"]
    if col == "refunded":
        return ["refunded", "none", "open"]
    if col == "cancelled":
        return ["unpaid", "none", "cancelled"]
    return ["unpaid", "none", "open"]

def opening():
    # [order number, events so far]
    return [[1, []], [2, ["pay"]], [3, ["pay", "ship"]], [4, ["pay", "ship", "refund"]], [5, ["pay", "ship", "return"]], [6, ["pay", "cancel"]]]

def money_held(st):
    return st[0] == "paid"

def parcel_out(st):
    return st[1] == "shipped"

def refund_owed(st):
    return st[0] == "paid" and (st[2] == "cancelled" or st[1] == "returned")
```

Python projection of orders.eml:

```python
def payments():
    return ["unpaid", "paid", "refunded"]

def fulfilments():
    return ["none", "shipped", "returned"]

def columns():
    return ["pending", "paid", "shipped", "cancelled", "refunded"]

def events():
    return ["pay", "ship", "refund", "return", "cancel"]

def start():
    return ["unpaid", "none", "open"]

def problem(st, e):
    if e == "pay":
        if st[0] != "unpaid":
            return "it is already " + st[0]
        if st[2] == "cancelled":
            return "it was cancelled"
    if e == "ship":
        if st[1] != "none":
            return "it is already " + st[1]
        if st[2] == "cancelled":
            return "it was cancelled"
    if e == "refund" and st[0] != "paid":
        return "it is " + st[0] + ", not paid"
    if e == "return" and st[1] != "shipped":
        if st[1] == "none":
            return "it was never shipped"
        return "it is already returned"
    if e == "cancel":
        if st[2] == "cancelled":
            return "it is already cancelled"
        if st[1] != "none":
            return "it is already " + st[1]
    return ""

def apply(st, e):
    if e == "pay":
        return ["paid", st[1], st[2]]
    if e == "ship":
        return [st[0], "shipped", st[2]]
    if e == "refund":
        return ["refunded", st[1], st[2]]
    if e == "return":
        return [st[0], "returned", st[2]]
    return [st[0], st[1], "cancelled"]

def write(col, e):
    if e == "pay":
        return "paid"
    if e == "ship":
        return "shipped"
    if e == "refund":
        return "refunded"
    if e == "cancel":
        return "cancelled"
    return col

def name_of(st):
    if st[2] == "open":
        if st[0] == "unpaid" and st[1] == "none":
            return "pending"
        if st[0] == "paid" and st[1] == "none":
            return "paid"
        if st[0] == "paid" and st[1] == "shipped":
            return "shipped"
        if st[0] == "refunded" and st[1] == "none":
            return "refunded"
    elif st[0] == "unpaid" and st[1] == "none":
        return "cancelled"
    return ""

def read(col):
    if col == "paid":
        return ["paid", "none", "open"]
    if col == "shipped":
        return ["paid", "shipped", "open"]
    if col == "refunded":
        return ["refunded", "none", "open"]
    if col == "cancelled":
        return ["unpaid", "none", "cancelled"]
    return ["unpaid", "none", "open"]

def opening():
    return [[1, []], [2, ["pay"]], [3, ["pay", "ship"]], [4, ["pay", "ship", "refund"]], [5, ["pay", "ship", "return"]], [6, ["pay", "cancel"]]]

def money_held(st):
    return st[0] == "paid"

def parcel_out(st):
    return st[1] == "shipped"

def refund_owed(st):
    return st[0] == "paid" and (st[2] == "cancelled" or st[1] == "returned")
```

## README

# P146 - Order states

Orders kept two ways - as three fields, and as one status column - with
every event applied to both, what the column reads back as, and the
answers each model gives.

- `main.eml` - the menu, the orders in both models, an event, the reports,
  every real state, and a new order
- `orders.eml` - the fields and the column's values, when an event can
  happen, the event applied to the fields and to the column, the value that
  names a state, the state a value claims, and the three questions

How each part works:

- An order's state is three facts: payment (unpaid, paid, refunded),
  fulfilment (none, shipped, returned) and whether it was called off. As in
  the corpus case `status-field-conflation`, the column has five values -
  pending, paid, shipped, cancelled, refunded - for eighteen real states,
  and only five of those have a value; a new order can reach twelve of
  them.
- Each event handler writes the value nearest to what happened, and a
  return has no value at all, so it leaves the column as it was. Read back,
  the column gives a state that did not happen: refunded after shipping
  reads as never shipped, cancelled after paying reads as unpaid.
- The column can be wrong even where a value exists: an order shipped
  before it is paid ends as paid and shipped, which `shipped` names, but the
  payment handler wrote `paid` over `shipped`.
- The questions: the money we hold, the parcels with the customer, and the
  refunds we owe. The column never shows a refund owed; for the opening
  orders it answers all three wrong, and nothing raises an error.

What is checked: menu choices 1 to 6; an order number that exists and one
of the five events, and only when the event can happen to that order. An
empty answer cancels.

Sessions: `sessions/basic.in` gives:
- the orders, the reports and every state;
- a new order shipped, paid and refunded; order 3 returned; the orders and
  the reports again.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- order numbers: a cancel, x, 1234 and 9; events: a cancel and fly;
- events that cannot happen: paying a paid order, returning one never
  shipped, shipping a cancelled one, returning one returned, cancelling a
  shipped one and a cancelled one, refunding an unpaid one;
- order 1 cancelled and order 6 refunded; the orders and the reports.

Built on the verified corpus case `status-field-conflation` (payment and
fulfilment as a pair in one column, the states with no value, and an order
history the column cannot hold).
