points v1

Loyalty points per member. What a purchase earns is held until its return period is over, what a checkout spends is held for 15 minutes while the payment goes through, and what is left unused at the end of a period lapses

examples/points/points.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
pointsmemberptat least 0; a transfer that would go below is refused with not_enough_points
at most 1000000; a transfer that would go above is refused with points_cap
issuedone accountptoutside the book: no bounds, and it may go below 0
redeemedone accountptoutside the book: no bounds, and it may go below 0
lapsedone accountptoutside the book: no bounds, and it may go below 0

How things move

How things move between the accounts of points earn spend lapse points(member)ptat least 0 (not_enough_points)at most 1000000 (points_cap) issuedpt · outside redeemedpt · outside lapsedpt · 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

earn

posted when the return period is over, voided when the purchase is returned

  • Moves pts from issued to points(member).
  • Key: once per purchase. The same call again does nothing, and answers done_before; one that differs only in member or pts 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.
earn.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
earn.holdpoints_cappoints(member) would go above 1000000
earn.holdkey_conflicta call with the same purchase and other arguments came before
earn.holdalready_refuseda call with the same purchase was refused by a bound before; a key a bound refused stays refused, even once there is enough
earn.postkey_conflictthe hold was posted before, for other amounts
earn.postalready_voidedthe hold is voided already
earn.postover_holdmore than the hold holds
earn.postno_such_holdthere is no hold with that purchase
earn.voidalready_postedthe hold is posted already
earn.voidno_such_holdthere is no hold with that purchase
How each refusal comes about

earn.hold: points_cap

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 1000000)  done
 2  earn.post(purchase: purchase-1)                                  done
 3  earn.hold(purchase: purchase-3, member: member-2, pts: 1)        refused: points_cap (move 1 puts 1 into points(member-2): posted 1000000, held in 0)

earn.hold: key_conflict

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 1)  done
 2  earn.hold(purchase: purchase-1, member: member-2, pts: 2)  refused: key_conflict

earn.hold: already_refused

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 1000000)  done
 2  earn.post(purchase: purchase-1)                                  done
 3  earn.hold(purchase: purchase-3, member: member-2, pts: 1)        refused: points_cap (move 1 puts 1 into points(member-2): posted 1000000, held in 0)
 4  earn.hold(purchase: purchase-3, member: member-2, pts: 1)        refused: already_refused

earn.post: key_conflict

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 2)  done
 2  earn.post(purchase: purchase-1)                            done
 3  earn.post(purchase: purchase-1, pts: 1)                    refused: key_conflict

earn.post: already_voided

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 2)  done
 2  earn.void(purchase: purchase-1)                            done
 3  earn.post(purchase: purchase-1)                            refused: already_voided

earn.post: over_hold

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 2)  done
 2  earn.post(purchase: purchase-1, pts: 3)                    refused: over_hold

earn.post: no_such_hold

 1  earn.post(purchase: purchase-1)  refused: no_such_hold

earn.void: already_posted

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 2)  done
 2  earn.post(purchase: purchase-1)                            done
 3  earn.void(purchase: purchase-1)                            refused: already_posted

earn.void: no_such_hold

 1  earn.void(purchase: purchase-1)  refused: no_such_hold

spend

posted when the payment goes through, voided when it fails

  • Moves pts from points(member) to redeemed.
  • Key: once per checkout. The same call again does nothing, and answers done_before; one that differs only in member or pts 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 expires 15 minutes after it was made, and what it holds goes back.
spend.hold held post posted void is refused: already_posted post for other amounts: key_conflict void voided post is refused: already_voided after 15 minutes expired post and void are refused: expired
The holdpostvoid
heldposts it: the amounts given, or all of it, and the rest goes back; more than it holds is refused with over_hold; once 15 minutes have passed since it was held, refused with expiredvoids it: what it holds goes back; once 15 minutes have passed since it was held, refused with expired
posteddone_before with the same amounts; refused with key_conflict with othersrefused with already_posted
voidedrefused with already_voideddone_before
expiredrefused with expiredrefused with expired
no hold with the keyrefused with no_such_holdrefused with no_such_hold
OperationMay be refused withWhen
spend.holdnot_enough_pointspoints(member) would go below 0
spend.holdkey_conflicta call with the same checkout and other arguments came before
spend.holdalready_refuseda call with the same checkout was refused by a bound before; a key a bound refused stays refused, even once there is enough
spend.postkey_conflictthe hold was posted before, for other amounts
spend.postalready_voidedthe hold is voided already
spend.postexpiredthe hold has expired
spend.postover_holdmore than the hold holds
spend.postno_such_holdthere is no hold with that checkout
spend.voidalready_postedthe hold is posted already
spend.voidexpiredthe hold has expired
spend.voidno_such_holdthere is no hold with that checkout
How each refusal comes about

spend.hold: not_enough_points

 1  spend.hold(checkout: checkout-1, member: member-2, pts: 1)  refused: not_enough_points (move 1 takes 1 from points(member-2): posted 0, held out 0)

spend.hold: key_conflict

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 1)   done
 2  earn.post(purchase: purchase-1)                             done
 3  spend.hold(checkout: checkout-3, member: member-2, pts: 1)  done
 4  spend.hold(checkout: checkout-3, member: member-2, pts: 2)  refused: key_conflict

spend.hold: already_refused

 1  spend.hold(checkout: checkout-1, member: member-2, pts: 1)  refused: not_enough_points (move 1 takes 1 from points(member-2): posted 0, held out 0)
 2  spend.hold(checkout: checkout-1, member: member-2, pts: 1)  refused: already_refused

spend.post: key_conflict

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 2)   done
 2  earn.post(purchase: purchase-1)                             done
 3  spend.hold(checkout: checkout-3, member: member-2, pts: 2)  done
 4  spend.post(checkout: checkout-3)                            done
 5  spend.post(checkout: checkout-3, pts: 1)                    refused: key_conflict

spend.post: already_voided

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 2)   done
 2  earn.post(purchase: purchase-1)                             done
 3  spend.hold(checkout: checkout-3, member: member-2, pts: 2)  done
 4  spend.void(checkout: checkout-3)                            done
 5  spend.post(checkout: checkout-3)                            refused: already_voided

spend.post: expired

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 2)   done
 2  earn.post(purchase: purchase-1)                             done
 3  spend.hold(checkout: checkout-3, member: member-2, pts: 2)  done
 4  pass 15 minutes                                             spend(checkout-3) expired
 5  spend.post(checkout: checkout-3)                            refused: expired

spend.post: over_hold

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 2)   done
 2  earn.post(purchase: purchase-1)                             done
 3  spend.hold(checkout: checkout-3, member: member-2, pts: 2)  done
 4  spend.post(checkout: checkout-3, pts: 3)                    refused: over_hold

spend.post: no_such_hold

 1  spend.post(checkout: checkout-1)  refused: no_such_hold

spend.void: already_posted

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 2)   done
 2  earn.post(purchase: purchase-1)                             done
 3  spend.hold(checkout: checkout-3, member: member-2, pts: 2)  done
 4  spend.post(checkout: checkout-3)                            done
 5  spend.void(checkout: checkout-3)                            refused: already_posted

spend.void: expired

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 2)   done
 2  earn.post(purchase: purchase-1)                             done
 3  spend.hold(checkout: checkout-3, member: member-2, pts: 2)  done
 4  pass 15 minutes                                             spend(checkout-3) expired
 5  spend.void(checkout: checkout-3)                            refused: expired

spend.void: no_such_hold

 1  spend.void(checkout: checkout-1)  refused: no_such_hold

lapse

what the member did not use in the period

  • Moves pts from points(member) to lapsed.
  • Key: once per member and period. The same call again does nothing, and answers done_before; one that differs only in pts is refused with key_conflict.
  • Moves at once (do).
OperationMay be refused withWhen
lapse.donot_enough_pointspoints(member) would go below 0
lapse.dokey_conflicta call with the same member and period and other arguments came before
lapse.doalready_refuseda call with the same member and period was refused by a bound before; a key a bound refused stays refused, even once there is enough
How each refusal comes about

lapse.do: not_enough_points

 1  lapse.do(member: member-1, period: period-2, pts: 1)  refused: not_enough_points (move 1 takes 1 from points(member-1): posted 0, held out 0)

lapse.do: key_conflict

 1  earn.hold(purchase: purchase-1, member: member-2, pts: 1)  done
 2  earn.post(purchase: purchase-1)                            done
 3  lapse.do(member: member-2, period: period-3, pts: 1)       done
 4  lapse.do(member: member-2, period: period-3, pts: 2)       refused: key_conflict

lapse.do: already_refused

 1  lapse.do(member: member-1, period: period-2, pts: 1)  refused: not_enough_points (move 1 takes 1 from points(member-1): posted 0, held out 0)
 2  lapse.do(member: member-1, period: period-2, pts: 1)  refused: already_refused

Scenarios

39 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 points earn spend lapse points(member)ptat least 0 (not_enough_points)at most 1000000 (points_cap) issuedpt · outside redeemedpt · outside lapsedpt · outside