feat: backend service
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// SoftDelete marks the account as soft-deleted with an audit trail of
|
||||
// who initiated the operation, then drives the documented in-process
|
||||
// cascade across `auth`, `lobby`, `notification`, and `geo`.
|
||||
//
|
||||
// The `accounts` row is the canonical state; cascade calls run after
|
||||
// the database commit and are best-effort. Cascade failures are joined
|
||||
// into the returned error and logged but never roll back the
|
||||
// soft-delete: the producer signal is "this user is gone", and
|
||||
// downstream cleanup is idempotent so a future retry can finish the
|
||||
// job.
|
||||
//
|
||||
// Repeated calls on an already-soft-deleted account are no-ops: the
|
||||
// store reports `false` for "row changed" and the cascade is skipped.
|
||||
func (s *Service) SoftDelete(ctx context.Context, userID uuid.UUID, actor ActorRef) error {
|
||||
if userID == uuid.Nil {
|
||||
return ErrAccountNotFound
|
||||
}
|
||||
if err := actor.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := s.deps.Now().UTC()
|
||||
changed, err := s.deps.Store.SoftDeleteAccount(ctx, userID, actor, now)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user soft delete: %w", err)
|
||||
}
|
||||
if !changed {
|
||||
s.deps.Logger.Info("user soft delete skipped (already deleted)",
|
||||
zap.String("user_id", userID.String()),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
s.deps.Logger.Info("user soft deleted",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.String("actor_type", actor.Type),
|
||||
)
|
||||
return s.runSoftDeleteCascade(ctx, userID)
|
||||
}
|
||||
|
||||
// runSoftDeleteCascade fans the soft-delete signal out to dependent
|
||||
// modules in the documented order: auth → lobby → notification → geo.
|
||||
// Each call's error is joined; the loop continues even after a
|
||||
// failure so the remaining modules still get notified.
|
||||
func (s *Service) runSoftDeleteCascade(ctx context.Context, userID uuid.UUID) error {
|
||||
var joined error
|
||||
if s.deps.SessionRevoker != nil {
|
||||
if err := s.deps.SessionRevoker.RevokeAllForUser(ctx, userID); err != nil {
|
||||
joined = errors.Join(joined, fmt.Errorf("session revoke: %w", err))
|
||||
}
|
||||
}
|
||||
if s.deps.Lobby != nil {
|
||||
if err := s.deps.Lobby.OnUserDeleted(ctx, userID); err != nil {
|
||||
joined = errors.Join(joined, fmt.Errorf("lobby on-user-deleted: %w", err))
|
||||
}
|
||||
}
|
||||
if s.deps.Notification != nil {
|
||||
if err := s.deps.Notification.OnUserDeleted(ctx, userID); err != nil {
|
||||
joined = errors.Join(joined, fmt.Errorf("notification on-user-deleted: %w", err))
|
||||
}
|
||||
}
|
||||
if s.deps.Geo != nil {
|
||||
if err := s.deps.Geo.OnUserDeleted(ctx, userID); err != nil {
|
||||
joined = errors.Join(joined, fmt.Errorf("geo on-user-deleted: %w", err))
|
||||
}
|
||||
}
|
||||
if joined != nil {
|
||||
s.deps.Logger.Warn("soft-delete cascade returned errors",
|
||||
zap.String("user_id", userID.String()),
|
||||
zap.Error(joined),
|
||||
)
|
||||
}
|
||||
return joined
|
||||
}
|
||||
Reference in New Issue
Block a user