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

281 lines
8.0 KiB
Go

// Package listdeliveries implements trusted operator listing of accepted mail
// deliveries.
package listdeliveries
import (
"context"
"errors"
"fmt"
"strings"
"time"
"galaxy/mail/internal/domain/common"
deliverydomain "galaxy/mail/internal/domain/delivery"
)
var (
// ErrInvalidCursor reports that the supplied opaque pagination cursor is
// malformed or no longer matches durable state.
ErrInvalidCursor = errors.New("list deliveries invalid cursor")
// ErrServiceUnavailable reports that trusted listing could not load durable
// state safely.
ErrServiceUnavailable = errors.New("list deliveries service unavailable")
)
const (
// DefaultLimit stores the frozen default page size used by the operator
// listing surface.
DefaultLimit = 50
// MaxLimit stores the frozen maximum page size accepted by the operator
// listing surface.
MaxLimit = 200
)
// Cursor stores one deterministic continuation position in the delivery sort
// order `created_at_ms DESC, delivery_id DESC`.
type Cursor struct {
// CreatedAt stores the durable creation time of the last visible delivery.
CreatedAt time.Time
// DeliveryID stores the durable identifier of the last visible delivery.
DeliveryID common.DeliveryID
}
// Validate reports whether cursor contains a complete continuation tuple.
func (cursor Cursor) Validate() error {
if err := common.ValidateTimestamp("delivery list cursor created at", cursor.CreatedAt); err != nil {
return err
}
if err := cursor.DeliveryID.Validate(); err != nil {
return fmt.Errorf("delivery list cursor delivery id: %w", err)
}
return nil
}
// Filters stores the supported operator-listing filters.
type Filters struct {
// Recipient stores the optional recipient envelope filter covering `to`,
// `cc`, and `bcc`.
Recipient common.Email
// Status stores the optional delivery lifecycle filter.
Status deliverydomain.Status
// Source stores the optional delivery source filter.
Source deliverydomain.Source
// TemplateID stores the optional template family filter.
TemplateID common.TemplateID
// IdempotencyKey stores the optional idempotency-key filter.
IdempotencyKey common.IdempotencyKey
// FromCreatedAt stores the optional inclusive lower creation-time bound.
FromCreatedAt *time.Time
// ToCreatedAt stores the optional inclusive upper creation-time bound.
ToCreatedAt *time.Time
}
// Validate reports whether filters is structurally valid.
func (filters Filters) Validate() error {
if !filters.Recipient.IsZero() {
if err := filters.Recipient.Validate(); err != nil {
return fmt.Errorf("recipient: %w", err)
}
}
if filters.Status != "" && !filters.Status.IsKnown() {
return fmt.Errorf("status %q is unsupported", filters.Status)
}
if filters.Source != "" && !filters.Source.IsKnown() {
return fmt.Errorf("source %q is unsupported", filters.Source)
}
if !filters.TemplateID.IsZero() {
if err := filters.TemplateID.Validate(); err != nil {
return fmt.Errorf("template id: %w", err)
}
}
if !filters.IdempotencyKey.IsZero() {
if err := filters.IdempotencyKey.Validate(); err != nil {
return fmt.Errorf("idempotency key: %w", err)
}
}
if filters.FromCreatedAt != nil {
if err := common.ValidateTimestamp("from created at", *filters.FromCreatedAt); err != nil {
return err
}
}
if filters.ToCreatedAt != nil {
if err := common.ValidateTimestamp("to created at", *filters.ToCreatedAt); err != nil {
return err
}
}
if filters.FromCreatedAt != nil && filters.ToCreatedAt != nil && filters.FromCreatedAt.After(*filters.ToCreatedAt) {
return errors.New("from created at must not be after to created at")
}
return nil
}
// Input stores one trusted operator-listing request.
type Input struct {
// Limit stores the maximum number of returned deliveries. The zero value
// selects the frozen default limit.
Limit int
// Cursor stores the optional continuation cursor for the next page.
Cursor *Cursor
// Filters stores the normalized listing filters.
Filters Filters
}
// Validate reports whether input contains a complete supported listing
// request.
func (input Input) Validate() error {
switch {
case input.Limit < 0:
return errors.New("limit must not be negative")
case input.Limit > MaxLimit:
return fmt.Errorf("limit must be at most %d", MaxLimit)
}
if input.Cursor != nil {
if err := input.Cursor.Validate(); err != nil {
return fmt.Errorf("cursor: %w", err)
}
}
if err := input.Filters.Validate(); err != nil {
return fmt.Errorf("filters: %w", err)
}
return nil
}
// Result stores one deterministic ordered page of delivery records.
type Result struct {
// Items stores the returned deliveries in `created_at DESC, delivery_id
// DESC` order.
Items []deliverydomain.Delivery
// NextCursor stores the optional cursor for the next page.
NextCursor *Cursor
}
// Validate reports whether result contains valid delivery records and an
// optional next cursor.
func (result Result) Validate() error {
for index, record := range result.Items {
if err := record.Validate(); err != nil {
return fmt.Errorf("items[%d]: %w", index, err)
}
}
if result.NextCursor != nil {
if err := result.NextCursor.Validate(); err != nil {
return fmt.Errorf("next cursor: %w", err)
}
}
return nil
}
// Store provides deterministic ordered listing over durable delivery state.
type Store interface {
// List returns one filtered ordered page of delivery records.
List(context.Context, Input) (Result, error)
}
// Config stores the dependencies used by Service.
type Config struct {
// Store loads one deterministic ordered page of durable deliveries.
Store Store
}
// Service executes trusted operator delivery-list reads.
type Service struct {
store Store
}
// New constructs Service from cfg.
func New(cfg Config) (*Service, error) {
if cfg.Store == nil {
return nil, errors.New("new list deliveries service: nil store")
}
return &Service{store: cfg.Store}, nil
}
// Execute validates input, applies the default limit when omitted, and loads
// one deterministic page of deliveries.
func (service *Service) Execute(ctx context.Context, input Input) (Result, error) {
if ctx == nil {
return Result{}, errors.New("execute list deliveries: nil context")
}
if service == nil {
return Result{}, errors.New("execute list deliveries: nil service")
}
if input.Limit == 0 {
input.Limit = DefaultLimit
}
if err := input.Validate(); err != nil {
return Result{}, fmt.Errorf("execute list deliveries: %w", err)
}
result, err := service.store.List(ctx, input)
switch {
case errors.Is(err, ErrInvalidCursor):
return Result{}, err
case err != nil:
return Result{}, fmt.Errorf("%w: %v", ErrServiceUnavailable, err)
}
if err := result.Validate(); err != nil {
return Result{}, fmt.Errorf("%w: invalid result: %v", ErrServiceUnavailable, err)
}
if len(result.Items) > input.Limit {
return Result{}, fmt.Errorf("%w: invalid result: returned %d items for limit %d", ErrServiceUnavailable, len(result.Items), input.Limit)
}
return result, nil
}
// Matches reports whether record satisfies filters.
func (filters Filters) Matches(record deliverydomain.Delivery) bool {
if filters.Recipient != "" && !containsRecipient(record.Envelope, filters.Recipient) {
return false
}
if filters.Status != "" && record.Status != filters.Status {
return false
}
if filters.Source != "" && record.Source != filters.Source {
return false
}
if filters.TemplateID != "" && record.TemplateID != filters.TemplateID {
return false
}
if filters.IdempotencyKey != "" && record.IdempotencyKey != filters.IdempotencyKey {
return false
}
if filters.FromCreatedAt != nil && record.CreatedAt.Before(filters.FromCreatedAt.UTC()) {
return false
}
if filters.ToCreatedAt != nil && record.CreatedAt.After(filters.ToCreatedAt.UTC()) {
return false
}
return true
}
func containsRecipient(envelope deliverydomain.Envelope, email common.Email) bool {
for _, group := range [][]common.Email{envelope.To, envelope.Cc, envelope.Bcc} {
for _, candidate := range group {
if strings.EqualFold(candidate.String(), email.String()) {
return true
}
}
}
return false
}