marketplace v1

The marketplace keeps a balance for each shop. A sale splits what the buyer paid between the shop's balance and the marketplace's fee, in amounts the caller works out (a rule decides the fee). A payout is held until the bank confirms it, and a refund takes both parts back

examples/marketplace/marketplace.book, as chobo doc writes it. Each account keeps a balance: what came in, less what went out. Every transfer keeps the bounds of the accounts: one that would break a bound is refused with the name the book gives that bound, and nothing of it moves. A transfer either moves at once (do), or first holds what it moves (hold); a hold is then posted (post, all of it or part), voided (void), or expires.

Accounts

AccountOne for eachUnitBoundsWhat it is
shop_balanceshopUSDat least 0.00; a transfer that would go below is refused with insufficient_balancewhat the marketplace owes the shop
buyersone accountUSDoutside the book: no bounds, and it may go below 0
feesone accountUSDoutside the book: no bounds, and it may go below 0
bankone accountUSDoutside the book: no bounds, and it may go below 0

How things move

How things move between the accounts of marketplace sale (to_shop) sale (fee) refund (fee) refund (to_shop) payout shop_balance(shop)USDat least 0.00 (insufficient_balance) buyersUSD · outside feesUSD · outside bankUSD · outside

A box is an account, and an arrow a move of a transfer. A rounded, dashed box is an account outside the book, which has no bounds. A dashed arrow belongs to a transfer that holds first, and moves when the hold is posted.

Transfers

sale

to_shop and fee add up to what the buyer paid; the caller works out the fee

  • 2 moves, made in this order, all or none:
    1. to_shop from buyers to shop_balance(shop)
    2. fee from buyers to fees
  • Key: once per order. The same call again does nothing, and answers done_before; one that differs only in shop, to_shop or fee is refused with key_conflict.
  • Moves at once (do).
OperationMay be refused withWhen
sale.dokey_conflicta call with the same order and other arguments came before
How each refusal comes about

sale.do: key_conflict

 1  sale.do(order: order-1, shop: shop-2, to_shop: 0.01, fee: 0.02)  done
 2  sale.do(order: order-1, shop: shop-2, to_shop: 0.03, fee: 0.02)  refused: key_conflict

refund

gives back the fee and the shop's part, both or neither

  • 2 moves, made in this order, all or none:
    1. fee from fees to buyers
    2. to_shop from shop_balance(shop) to buyers
  • Key: once per refund_id. The same call again does nothing, and answers done_before; one that differs only in shop, to_shop or fee is refused with key_conflict.
  • Moves at once (do).
OperationMay be refused withWhen
refund.doinsufficient_balancemove 2 would take shop_balance(shop) below 0.00
refund.dokey_conflicta call with the same refund_id and other arguments came before
refund.doalready_refuseda call with the same refund_id was refused by a bound before; a key a bound refused stays refused, even once there is enough
How each refusal comes about

refund.do: insufficient_balance

 1  refund.do(refund_id: refund_id-1, shop: shop-2, to_shop: 0.01, fee: 0.02)  refused: insufficient_balance (move 2 takes 0.01 from shop_balance(shop-2): posted 0.00, held out 0.00)

refund.do: key_conflict

 1  sale.do(order: order-1, shop: shop-2, to_shop: 0.01, fee: 0.03)            done
 2  refund.do(refund_id: refund_id-3, shop: shop-2, to_shop: 0.01, fee: 0.02)  done
 3  refund.do(refund_id: refund_id-3, shop: shop-2, to_shop: 0.04, fee: 0.02)  refused: key_conflict

refund.do: already_refused

 1  refund.do(refund_id: refund_id-1, shop: shop-2, to_shop: 0.01, fee: 0.02)  refused: insufficient_balance (move 2 takes 0.01 from shop_balance(shop-2): posted 0.00, held out 0.00)
 2  refund.do(refund_id: refund_id-1, shop: shop-2, to_shop: 0.01, fee: 0.02)  refused: already_refused

payout

held when the bank transfer starts: posted when the bank confirms it, voided when it fails

  • Moves amount from shop_balance(shop) to bank.
  • Key: once per payout_id. The same call again does nothing, and answers done_before; one that differs only in shop or amount is refused with key_conflict; holding again with the same key after the hold has ended answers done_before, and holds nothing.
  • Holds first: the caller posts or voids the hold. It never expires, and stays held until one of them comes.
payout.hold held post posted void is refused: already_posted post for other amounts: key_conflict void voided post is refused: already_voided
The holdpostvoid
heldposts it: the amounts given, or all of it, and the rest goes back; more than it holds is refused with over_holdvoids it: what it holds goes back
posteddone_before with the same amounts; refused with key_conflict with othersrefused with already_posted
voidedrefused with already_voideddone_before
no hold with the keyrefused with no_such_holdrefused with no_such_hold
OperationMay be refused withWhen
payout.holdinsufficient_balanceshop_balance(shop) would go below 0.00
payout.holdkey_conflicta call with the same payout_id and other arguments came before
payout.holdalready_refuseda call with the same payout_id was refused by a bound before; a key a bound refused stays refused, even once there is enough
payout.postkey_conflictthe hold was posted before, for other amounts
payout.postalready_voidedthe hold is voided already
payout.postover_holdmore than the hold holds
payout.postno_such_holdthere is no hold with that payout_id
payout.voidalready_postedthe hold is posted already
payout.voidno_such_holdthere is no hold with that payout_id
How each refusal comes about

payout.hold: insufficient_balance

 1  payout.hold(payout_id: payout_id-1, shop: shop-2, amount: 0.01)  refused: insufficient_balance (move 1 takes 0.01 from shop_balance(shop-2): posted 0.00, held out 0.00)

payout.hold: key_conflict

 1  sale.do(order: order-1, shop: shop-2, to_shop: 0.01, fee: 0.02)  done
 2  payout.hold(payout_id: payout_id-3, shop: shop-2, amount: 0.01)  done
 3  payout.hold(payout_id: payout_id-3, shop: shop-2, amount: 0.03)  refused: key_conflict

payout.hold: already_refused

 1  payout.hold(payout_id: payout_id-1, shop: shop-2, amount: 0.01)  refused: insufficient_balance (move 1 takes 0.01 from shop_balance(shop-2): posted 0.00, held out 0.00)
 2  payout.hold(payout_id: payout_id-1, shop: shop-2, amount: 0.01)  refused: already_refused

payout.post: key_conflict

 1  sale.do(order: order-1, shop: shop-2, to_shop: 0.02, fee: 0.03)  done
 2  payout.hold(payout_id: payout_id-3, shop: shop-2, amount: 0.02)  done
 3  payout.post(payout_id: payout_id-3)                              done
 4  payout.post(payout_id: payout_id-3, amount: 0.01)                refused: key_conflict

payout.post: already_voided

 1  sale.do(order: order-1, shop: shop-2, to_shop: 0.02, fee: 0.03)  done
 2  payout.hold(payout_id: payout_id-3, shop: shop-2, amount: 0.02)  done
 3  payout.void(payout_id: payout_id-3)                              done
 4  payout.post(payout_id: payout_id-3)                              refused: already_voided

payout.post: over_hold

 1  sale.do(order: order-1, shop: shop-2, to_shop: 0.02, fee: 0.03)  done
 2  payout.hold(payout_id: payout_id-3, shop: shop-2, amount: 0.02)  done
 3  payout.post(payout_id: payout_id-3, amount: 0.03)                refused: over_hold

payout.post: no_such_hold

 1  payout.post(payout_id: payout_id-1)  refused: no_such_hold

payout.void: already_posted

 1  sale.do(order: order-1, shop: shop-2, to_shop: 0.02, fee: 0.03)  done
 2  payout.hold(payout_id: payout_id-3, shop: shop-2, amount: 0.02)  done
 3  payout.post(payout_id: payout_id-3)                              done
 4  payout.void(payout_id: payout_id-3)                              refused: already_posted

payout.void: no_such_hold

 1  payout.void(payout_id: payout_id-1)  refused: no_such_hold

Scenarios

25 scenarios, which chobo scenarios makes from the book. Pick one and step through it: after each step come the balances and the holds it left, and on the chart the arrows of the transfers it called light up (red when refused).

    AccountPostedHeld outHeld in
    HoldState
    How things move between the accounts of marketplace sale (to_shop) sale (fee) refund (fee) refund (to_shop) payout shop_balance(shop)USDat least 0.00 (insufficient_balance) buyersUSD · outside feesUSD · outside bankUSD · outside