backend requirements onthe xustomer app
This commit is contained in:
@@ -168,3 +168,74 @@ const (
|
||||
ExceptionResolved = "Resolved"
|
||||
ExceptionClosed = "Closed"
|
||||
)
|
||||
|
||||
// Customer-app stages. Nine operational stages, spelt exactly as the customer
|
||||
// client parses them: lowercase snake_case, on the wire verbatim. The client
|
||||
// rolls these up into seven milestones itself and falls back to "booked" on an
|
||||
// unknown key, silently — so adding a value here without an app release makes a
|
||||
// parcel look un-started. Never rename one; add and coordinate.
|
||||
//
|
||||
// Stages 0-5 belong to the booking. Stages 6-8 belong to each order and may
|
||||
// differ between destinations of the same booking.
|
||||
const (
|
||||
CxStageBooked = "booked" // 0 — pickup requested
|
||||
CxStageAssigned = "assigned" // 1 — a miler accepted it
|
||||
CxStageOnTheWay = "on_the_way" // 2 — rider en route, distance/ETA live
|
||||
CxStageArrived = "arrived" // 3 — rider at the door; LAST cancellable stage
|
||||
CxStagePickedUp = "picked_up" // 4 — weighed, photographed, price settled
|
||||
CxStageOrderCreated = "order_created" // 5 — one tracking number minted per destination
|
||||
CxStageInTransit = "in_transit" // 6 — per order from here on
|
||||
CxStageOutForDelivery = "out_for_delivery" // 7 — delivery agent carrying it
|
||||
CxStageDelivered = "delivered" // 8 — handed over
|
||||
)
|
||||
|
||||
// Customer-facing booking status. Derived from the stage but sent explicitly,
|
||||
// because a client that has to infer it will eventually infer it differently.
|
||||
const (
|
||||
CxStatusActive = "active"
|
||||
CxStatusCompleted = "completed"
|
||||
CxStatusCancelled = "cancelled"
|
||||
)
|
||||
|
||||
// Who caused a stage transition. Recorded on every bookingstageevents row: the
|
||||
// customer timeline is derived from that table, so it has to be real, and a
|
||||
// cancellation the customer did not make is unexplainable without this.
|
||||
const (
|
||||
CxActorMiler = "miler"
|
||||
CxActorOps = "ops"
|
||||
CxActorCustomer = "customer"
|
||||
CxActorSystem = "system"
|
||||
)
|
||||
|
||||
// CxStageOrder is the rank of each stage, used to decide whether a transition
|
||||
// moves forward and whether cancellation is still open. Cancellation closes
|
||||
// after arrived, so anything at or past picked_up is refused.
|
||||
var CxStageOrder = map[string]int{
|
||||
CxStageBooked: 0,
|
||||
CxStageAssigned: 1,
|
||||
CxStageOnTheWay: 2,
|
||||
CxStageArrived: 3,
|
||||
CxStagePickedUp: 4,
|
||||
CxStageOrderCreated: 5,
|
||||
CxStageInTransit: 6,
|
||||
CxStageOutForDelivery: 7,
|
||||
CxStageDelivered: 8,
|
||||
}
|
||||
|
||||
// CxStageRank returns the rank of a stage, or -1 when the stage is unknown or
|
||||
// empty. A booking written before this surface existed has no stage at all,
|
||||
// and -1 keeps it strictly behind every real stage rather than tying with
|
||||
// "booked".
|
||||
func CxStageRank(stage string) int {
|
||||
if r, ok := CxStageOrder[stage]; ok {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// CxCancellable reports whether a booking at this stage may still be cancelled.
|
||||
// The UI mirrors this to hide the button, but the server re-checks on the
|
||||
// cancel call — the button state is a hint, never the authority.
|
||||
func CxCancellable(stage string) bool {
|
||||
return CxStageRank(stage) <= CxStageOrder[CxStageArrived]
|
||||
}
|
||||
|
||||
116
constants/constants_test.go
Normal file
116
constants/constants_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package constants
|
||||
|
||||
import "testing"
|
||||
|
||||
// The nine stage keys are a wire contract. The customer client parses them
|
||||
// verbatim and silently falls back to `booked` on anything it does not
|
||||
// recognise — so a renamed or misspelt key here does not fail loudly, it makes
|
||||
// a moving parcel look un-started on someone's tracking screen.
|
||||
|
||||
func TestStageKeysAreSpeltExactlyAsTheClientParsesThem(t *testing.T) {
|
||||
want := map[string]string{
|
||||
CxStageBooked: "booked",
|
||||
CxStageAssigned: "assigned",
|
||||
CxStageOnTheWay: "on_the_way",
|
||||
CxStageArrived: "arrived",
|
||||
CxStagePickedUp: "picked_up",
|
||||
CxStageOrderCreated: "order_created",
|
||||
CxStageInTransit: "in_transit",
|
||||
CxStageOutForDelivery: "out_for_delivery",
|
||||
CxStageDelivered: "delivered",
|
||||
}
|
||||
for got, expected := range want {
|
||||
if got != expected {
|
||||
t.Errorf("stage key = %q, want %q", got, expected)
|
||||
}
|
||||
}
|
||||
if len(CxStageOrder) != 9 {
|
||||
t.Errorf("CxStageOrder has %d entries, want 9 — every stage the client "+
|
||||
"knows must be rankable", len(CxStageOrder))
|
||||
}
|
||||
}
|
||||
|
||||
// Ranks must be strictly increasing in journey order. advanceBooking refuses to
|
||||
// move a booking backwards by comparing these, so a tie or an inversion would
|
||||
// let a stage silently fail to apply.
|
||||
func TestStageRanksIncreaseInJourneyOrder(t *testing.T) {
|
||||
ordered := []string{
|
||||
CxStageBooked, CxStageAssigned, CxStageOnTheWay, CxStageArrived,
|
||||
CxStagePickedUp, CxStageOrderCreated, CxStageInTransit,
|
||||
CxStageOutForDelivery, CxStageDelivered,
|
||||
}
|
||||
for i := 1; i < len(ordered); i++ {
|
||||
if CxStageRank(ordered[i]) <= CxStageRank(ordered[i-1]) {
|
||||
t.Errorf("%q ranks %d, not after %q at %d",
|
||||
ordered[i], CxStageRank(ordered[i]), ordered[i-1], CxStageRank(ordered[i-1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An unknown or empty stage must rank BEHIND booked, not tie with it. A booking
|
||||
// written before this surface existed has no stage at all, and a tie would stop
|
||||
// it ever advancing off nothing.
|
||||
func TestUnknownStageRanksBehindEverything(t *testing.T) {
|
||||
if CxStageRank("") != -1 {
|
||||
t.Errorf("empty stage ranks %d, want -1", CxStageRank(""))
|
||||
}
|
||||
if CxStageRank("teleported") != -1 {
|
||||
t.Errorf("unknown stage ranks %d, want -1", CxStageRank("teleported"))
|
||||
}
|
||||
if CxStageRank("") >= CxStageRank(CxStageBooked) {
|
||||
t.Error("an unknown stage does not rank behind booked")
|
||||
}
|
||||
}
|
||||
|
||||
// Cancellation closes after `arrived` — the contract's rule, and money depends
|
||||
// on it: cancelling after pickup would mean a parcel already collected and paid
|
||||
// for is marked cancelled.
|
||||
func TestCancellationWindowClosesAfterArrived(t *testing.T) {
|
||||
open := []string{"", CxStageBooked, CxStageAssigned, CxStageOnTheWay, CxStageArrived}
|
||||
for _, stage := range open {
|
||||
if !CxCancellable(stage) {
|
||||
t.Errorf("stage %q should still be cancellable", stage)
|
||||
}
|
||||
}
|
||||
|
||||
closed := []string{
|
||||
CxStagePickedUp, CxStageOrderCreated, CxStageInTransit,
|
||||
CxStageOutForDelivery, CxStageDelivered,
|
||||
}
|
||||
for _, stage := range closed {
|
||||
if CxCancellable(stage) {
|
||||
t.Errorf("stage %q must not be cancellable — the parcel is collected", stage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The customer statuses and actor types are also on the wire.
|
||||
func TestCustomerStatusAndActorValues(t *testing.T) {
|
||||
pairs := map[string]string{
|
||||
CxStatusActive: "active",
|
||||
CxStatusCompleted: "completed",
|
||||
CxStatusCancelled: "cancelled",
|
||||
CxActorMiler: "miler",
|
||||
CxActorOps: "ops",
|
||||
CxActorCustomer: "customer",
|
||||
CxActorSystem: "system",
|
||||
}
|
||||
for got, want := range pairs {
|
||||
if got != want {
|
||||
t.Errorf("constant = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Booking source decides whether a booking gets a customer projection at all —
|
||||
// cxstage returns early for anything that is not Customer_App. The stored value
|
||||
// predates this work and must not be "fixed".
|
||||
func TestBookingSourceValuesAreTheStoredOnes(t *testing.T) {
|
||||
if BookingSourceCustomerApp != "Customer_App" {
|
||||
t.Errorf("BookingSourceCustomerApp = %q, want %q", BookingSourceCustomerApp, "Customer_App")
|
||||
}
|
||||
if BookingSourceExpress != "CRM_Console" {
|
||||
t.Errorf("BookingSourceExpress = %q — this is an existing database value, "+
|
||||
"not a label to rename", BookingSourceExpress)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user