96 lines
3.5 KiB
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))
|
|
}
|