Files
galaxy-game/mail/internal/domain/idempotency/model.go
T
2026-04-17 18:39:16 +02:00

75 lines
2.1 KiB
Go

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