Files
galaxy-game/notification/internal/adapters/postgres/notificationstore/acceptance.go
T
2026-04-26 20:34:39 +02:00

119 lines
4.2 KiB
Go

package notificationstore
import (
"context"
"database/sql"
"errors"
"fmt"
"galaxy/notification/internal/api/intentstream"
"galaxy/notification/internal/service/acceptintent"
)
// Compile-time confirmation that *Store satisfies acceptintent.Store. The
// runtime wiring depends on this so the accept-intent service can consume
// the PostgreSQL adapter directly.
var _ acceptintent.Store = (*Store)(nil)
// CreateAcceptance writes one notification record together with its derived
// route slots inside one BEGIN … COMMIT transaction. Idempotency races
// surface as `acceptintent.ErrConflict`.
func (store *Store) CreateAcceptance(ctx context.Context, input acceptintent.CreateAcceptanceInput) error {
if store == nil {
return errors.New("create notification acceptance: nil store")
}
if ctx == nil {
return errors.New("create notification acceptance: nil context")
}
if err := input.Validate(); err != nil {
return fmt.Errorf("create notification acceptance: %w", err)
}
return store.withTx(ctx, "create notification acceptance", func(ctx context.Context, tx *sql.Tx) error {
if err := insertRecord(ctx, tx, input.Notification, input.Idempotency.ExpiresAt); err != nil {
if isUniqueViolation(err) {
return acceptintent.ErrConflict
}
return fmt.Errorf("create notification acceptance: insert record: %w", err)
}
for index, route := range input.Routes {
if err := insertRoute(ctx, tx, route); err != nil {
return fmt.Errorf("create notification acceptance: insert route[%d]: %w", index, err)
}
}
return nil
})
}
// GetIdempotency loads one accepted idempotency reservation. Because the
// records row IS the idempotency reservation, the lookup keys on
// `(producer, idempotency_key)` and projects the relevant subset of the row
// into an IdempotencyRecord.
func (store *Store) GetIdempotency(ctx context.Context, producer intentstream.Producer, idempotencyKey string) (acceptintent.IdempotencyRecord, bool, error) {
if store == nil {
return acceptintent.IdempotencyRecord{}, false, errors.New("get notification idempotency: nil store")
}
if ctx == nil {
return acceptintent.IdempotencyRecord{}, false, errors.New("get notification idempotency: nil context")
}
operationCtx, cancel, err := store.operationContext(ctx, "get notification idempotency")
if err != nil {
return acceptintent.IdempotencyRecord{}, false, err
}
defer cancel()
scanned, found, err := loadIdempotencyByKey(operationCtx, store.db, string(producer), idempotencyKey)
if err != nil {
return acceptintent.IdempotencyRecord{}, false, err
}
if !found {
return acceptintent.IdempotencyRecord{}, false, nil
}
return idempotencyRecordFromScanned(scanned), true, nil
}
// GetNotification loads one accepted notification by NotificationID.
func (store *Store) GetNotification(ctx context.Context, notificationID string) (acceptintent.NotificationRecord, bool, error) {
if store == nil {
return acceptintent.NotificationRecord{}, false, errors.New("get notification record: nil store")
}
if ctx == nil {
return acceptintent.NotificationRecord{}, false, errors.New("get notification record: nil context")
}
operationCtx, cancel, err := store.operationContext(ctx, "get notification record")
if err != nil {
return acceptintent.NotificationRecord{}, false, err
}
defer cancel()
scanned, found, err := loadRecord(operationCtx, store.db, notificationID)
if err != nil {
return acceptintent.NotificationRecord{}, false, err
}
if !found {
return acceptintent.NotificationRecord{}, false, nil
}
return scanned.Record, true, nil
}
// GetRoute loads one accepted notification route by `(notificationID,
// routeID)`. Required by the publisher worker contracts.
func (store *Store) GetRoute(ctx context.Context, notificationID string, routeID string) (acceptintent.NotificationRoute, bool, error) {
if store == nil {
return acceptintent.NotificationRoute{}, false, errors.New("get notification route: nil store")
}
if ctx == nil {
return acceptintent.NotificationRoute{}, false, errors.New("get notification route: nil context")
}
operationCtx, cancel, err := store.operationContext(ctx, "get notification route")
if err != nil {
return acceptintent.NotificationRoute{}, false, err
}
defer cancel()
return loadRoute(operationCtx, store.db, notificationID, routeID)
}