// Package idempotency defines the deduplication record used by Mail Service // acceptance flows. package idempotency import ( "fmt" "strings" "time" "galaxy/mail/internal/domain/common" "galaxy/mail/internal/domain/delivery" ) // Record stores the first accepted fingerprint bound to one `(source, // idempotency_key)` scope. type Record struct { // Source stores the frozen delivery source vocabulary value. Source delivery.Source // IdempotencyKey stores the caller-owned deduplication key. IdempotencyKey common.IdempotencyKey // DeliveryID stores the accepted delivery linked to the scope. DeliveryID common.DeliveryID // RequestFingerprint stores the stable fingerprint of the first accepted // request. RequestFingerprint string // CreatedAt stores when the deduplication record was created. CreatedAt time.Time // ExpiresAt stores when the deduplication record becomes invalid. ExpiresAt time.Time } // Validate reports whether Record satisfies the frozen Stage 2 structural // invariants. func (record Record) Validate() error { if !record.Source.IsKnown() { return fmt.Errorf("idempotency source %q is unsupported", record.Source) } if err := record.IdempotencyKey.Validate(); err != nil { return fmt.Errorf("idempotency key: %w", err) } if err := record.DeliveryID.Validate(); err != nil { return fmt.Errorf("idempotency delivery id: %w", err) } if err := validateToken("idempotency request fingerprint", record.RequestFingerprint); err != nil { return err } if err := common.ValidateTimestamp("idempotency created at", record.CreatedAt); err != nil { return err } if err := common.ValidateTimestamp("idempotency expires at", record.ExpiresAt); err != nil { return err } if !record.ExpiresAt.After(record.CreatedAt) { return fmt.Errorf("idempotency expires at must be after created at") } return nil } func validateToken(name string, value string) error { switch { case strings.TrimSpace(value) == "": return fmt.Errorf("%s must not be empty", name) case strings.TrimSpace(value) != value: return fmt.Errorf("%s must not contain surrounding whitespace", name) default: return nil } }