Files
doormile_backend/controllers/cxIdentifiers.go

96 lines
3.5 KiB
Go

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))
}