package controllers import ( "fmt" "math/rand" "time" "doormile/db" "doormile/utils" ) // Human-facing identifiers. // // A pickup booking is DM-######; an order is DMX########. Both columns are // UNIQUE, and both used to be minted from four random bytes plus a truncated // unix second. Random short ids collide long before the id space runs out, and // a collision here is not a retry — it is a failed booking at the moment the // customer taps Confirm. So both come off a Postgres sequence, which is the // only generator in this system that can promise uniqueness. // // The sequences have no MAXVALUE and no CYCLE (migrations/migrate.go): past // 999999 the reference simply grows a digit rather than wrapping onto an id // that already exists. Rows written before this change keep their old // DM-BK-/DM-TRK- strings; nothing anywhere parses either format, so the two // coexist and no backfill is needed. // nextSequenceValue draws the next value from a Postgres sequence. func nextSequenceValue(sequence string) (int64, bool) { if db.DB == nil { return 0, false } var n int64 if err := db.DB.Raw(fmt.Sprintf("SELECT nextval('%s')", sequence)).Scan(&n).Error; err != nil { utils.Warn("identifier sequence unavailable, falling back", "sequence", sequence, "error", err) return 0, false } return n, true } // fallbackNumber is used only when the sequence cannot be read — a database // that is unreachable, or a deployment where the migration has not run yet. It // keeps the shape of the identifier (so the client and the console never see a // second format) and takes its entropy from the clock plus a random tail, // which makes a collision vanishingly unlikely for the short window this path // is ever live. It is a degradation, not a design: the sequence is the // guarantee. func fallbackNumber(digits int) int64 { span := int64(1) for i := 0; i < digits; i++ { span *= 10 } base := time.Now().UnixNano() % span jitter := rand.Int63n(1000) n := (base + jitter) % span // Never return a value that would render with fewer digits than the format // promises — DM-000042 reads as a broken reference, not a short one. if n < span/10 { n += span / 10 } return n } // generateBookingNo mints a pickup booking reference: DM-482913. // // The sequence guarantees uniqueness; cxScrambledBooking scatters it so // consecutive bookings do not get adjacent references. See // cxIdentifierScramble.go for why the scattering is a keyed permutation rather // than arithmetic. // // Past 900,000 bookings the fixed-width range is exhausted and the reference // grows a digit instead of wrapping onto one already issued. Uniqueness is // never traded for appearance. func generateBookingNo() string { if n, ok := nextSequenceValue("cx_booking_reference_seq"); ok { if scrambled, inRange := cxScrambledBooking(n); inRange { return fmt.Sprintf("DM-%06d", scrambled) } return fmt.Sprintf("DM-%06d", n) } return fmt.Sprintf("DM-%06d", fallbackNumber(6)) } // generateTrackingNo mints an order's tracking number: DMX10482913. One per // destination, minted when the miler completes the pickup — never at booking // time, because until the parcels are actually collected there is no order to // track. func generateTrackingNo() string { if n, ok := nextSequenceValue("cx_tracking_seq"); ok { if scrambled, inRange := cxScrambledTracking(n); inRange { return fmt.Sprintf("DMX%08d", scrambled) } return fmt.Sprintf("DMX%08d", n) } return fmt.Sprintf("DMX%08d", fallbackNumber(8)) }