package controllers import ( "doormile/constants" "doormile/internal/cxstage" "doormile/utils" "gorm.io/gorm" ) // Per-order stages — the second half of §8.3. // // Stages 6-8 belong to each order rather than to the booking, and may differ // between destinations of the same pickup: one parcel out for delivery in // Chennai while another is still at a hub in Kerala. Each of the operational // writes that moves a consignment records its customer stage against the // destination that consignment belongs to, and the booking's own stage falls // back to the least-advanced of them. // cxStageForConsignmentStatus maps an operational consignment status onto the // customer stage it means. Not every status has one: a parcel sitting on a // tripsheet, or one flagged missing, is still "in transit" as far as the // customer's seven milestones go, and inventing a stage for it would put a key // on the wire the client silently falls back to `booked` for. func cxStageForConsignmentStatus(status string) (string, bool) { switch status { case constants.ConsignmentInwardedAtHub, constants.ConsignmentTripsheetLoaded, constants.ConsignmentInTransit: return constants.CxStageInTransit, true case constants.ConsignmentOutForDelivery: return constants.CxStageOutForDelivery, true case constants.ConsignmentDelivered: return constants.CxStageDelivered, true default: // Created and Collected_By_Miler both mean "the order exists and is in // the rider's hands", which is order_created — already recorded at // pickup-complete, so there is nothing new to say. return "", false } } // recordCxConsignmentStage records the customer stage for one consignment, // inside the caller's transaction, and returns the notification to fire once // that transaction commits. // // The consignment is resolved to its destination through bookingdestinations, // not through pickupbookings.consignmentid. That column names only the FIRST // order of a multi-destination pickup, so a lookup through it finds nothing for // destinations 2..N — which would mean no stage advance and no notification on // every order after the first. // // A consignment that belongs to no customer booking at all — a console-created // express shipment — is a no-op, not an error. Those have no customer app // watching them. func recordCxConsignmentStage(tx *gorm.DB, consignmentID int, status string, actorType string, actorID *int, source string) (afterCommit func(), err error) { noop := func() {} stage, ok := cxStageForConsignmentStatus(status) if !ok { return noop, nil } dest, booking, found := cxDestinationForConsignment(consignmentID) if !found || booking == nil { return noop, nil } if booking.Bookingsource != constants.BookingSourceCustomerApp { return noop, nil } destinationID := cxDestinationIDFor(dest) if err := cxstage.Record(tx, cxstage.Event{ BookingID: booking.Bookingid, DestinationID: destinationID, Stage: stage, ActorType: actorType, ActorID: actorID, Source: source, At: utils.DBNow(), }); err != nil { return noop, err } bookingID := booking.Bookingid return func() { go cxstage.Notify(bookingID, destinationID, stage) }, nil } // cancelCxBookingFromOps stands a pickup down on behalf of a rider or an ops // user, recording who did it and why. // // A customer whose pickup disappears with no explanation has no way to tell a // cancellation from a bug, and support has no way to answer them — so the // actor and the reason are part of the write, not an afterthought. func cancelCxBookingFromOps(tx *gorm.DB, bookingID int, reason, actorType string, actorID *int, source string) error { if reason == "" { reason = "Cancelled by Doormile" } return cxstage.Cancel(tx, bookingID, reason, actorType, actorID, source) }