77 lines
1.9 KiB
Go
77 lines
1.9 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ActiveLimit is the read-side projection of a row in `limit_active`
|
|
// joined with the audit columns from the underlying `limit_records`
|
|
// row. It mirrors the OpenAPI `ActiveLimit` schema.
|
|
type ActiveLimit struct {
|
|
LimitCode string
|
|
Value int32
|
|
ReasonCode string
|
|
Actor ActorRef
|
|
AppliedAt time.Time
|
|
ExpiresAt *time.Time
|
|
}
|
|
|
|
// ApplyLimitInput carries the admin-supplied parameters of
|
|
// `POST /api/v1/admin/users/{user_id}/limits`.
|
|
type ApplyLimitInput struct {
|
|
UserID uuid.UUID
|
|
LimitCode string
|
|
Value int32
|
|
ReasonCode string
|
|
Actor ActorRef
|
|
ExpiresAt *time.Time
|
|
}
|
|
|
|
// ApplyLimit persists a fresh `limit_records` row and upserts
|
|
// `limit_active` in one transaction. The implementation keeps `limit_code` as an
|
|
// open string; The implementation may add a CHECK constraint once the closed
|
|
// set is locked in.
|
|
func (s *Service) ApplyLimit(ctx context.Context, input ApplyLimitInput) (Account, error) {
|
|
if input.UserID == uuid.Nil {
|
|
return Account{}, ErrAccountNotFound
|
|
}
|
|
if strings.TrimSpace(input.LimitCode) == "" {
|
|
return Account{}, fmt.Errorf("%w: limit_code must be non-empty", ErrInvalidInput)
|
|
}
|
|
if err := input.Actor.Validate(); err != nil {
|
|
return Account{}, err
|
|
}
|
|
if strings.TrimSpace(input.ReasonCode) == "" {
|
|
return Account{}, fmt.Errorf("%w: reason_code must be non-empty", ErrInvalidInput)
|
|
}
|
|
|
|
now := s.deps.Now().UTC()
|
|
expiresAt := input.ExpiresAt
|
|
if expiresAt != nil {
|
|
t := expiresAt.UTC()
|
|
expiresAt = &t
|
|
}
|
|
|
|
if err := s.deps.Store.ApplyLimitTx(ctx, limitInsert{
|
|
UserID: input.UserID,
|
|
LimitCode: input.LimitCode,
|
|
Value: input.Value,
|
|
ReasonCode: input.ReasonCode,
|
|
Actor: input.Actor,
|
|
AppliedAt: now,
|
|
ExpiresAt: expiresAt,
|
|
}); err != nil {
|
|
if errors.Is(err, ErrAccountNotFound) {
|
|
return Account{}, err
|
|
}
|
|
return Account{}, fmt.Errorf("user apply limit: %w", err)
|
|
}
|
|
return s.GetAccount(ctx, input.UserID)
|
|
}
|