package repository import ( "context" "encoding/json" "errors" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) // AccountTransition is an append-only UserStatus change. type AccountTransition struct { ID uuid.UUID `json:"id"` UserID uuid.UUID `json:"user_id"` FromStatus string `json:"from_status"` ToStatus string `json:"to_status"` AdminID uuid.UUID `json:"admin_id"` Reason string `json:"reason"` CreatedAt time.Time `json:"created_at"` } // GetUserStatus returns users.status or empty if missing. func (r *AdminRepo) GetUserStatus(ctx context.Context, userID uuid.UUID) (string, error) { var status string err := r.Pool.QueryRow(ctx, ` SELECT status FROM users WHERE id=$1 AND deleted_at IS NULL`, userID, ).Scan(&status) if errors.Is(err, pgx.ErrNoRows) { return "", nil } return status, err } // TransitionUserStatusWithAudit updates status, inserts transition + audit in one tx. func (r *AdminRepo) TransitionUserStatusWithAudit( ctx context.Context, adminID, userID uuid.UUID, fromStatus, toStatus, reason string, meta json.RawMessage, ) error { tx, err := r.Pool.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) tag, err := tx.Exec(ctx, ` UPDATE users SET status=$2, updated_at=now() WHERE id=$1 AND deleted_at IS NULL AND status=$3`, userID, toStatus, fromStatus, ) if err != nil { return err } if tag.RowsAffected() == 0 { return errString("status conflict") } if _, err := tx.Exec(ctx, ` INSERT INTO account_state_transitions(user_id, from_status, to_status, admin_id, reason) VALUES ($1,$2,$3,$4,$5)`, userID, fromStatus, toStatus, adminID, reason, ); err != nil { return err } if meta == nil { meta = json.RawMessage(`{}`) } if _, err := tx.Exec(ctx, ` INSERT INTO admin_audit_logs(admin_id, action, target_type, target_id, meta) VALUES ($1,'users.status.transition','user',$2,$3)`, adminID, userID.String(), meta, ); err != nil { return err } return tx.Commit(ctx) } // ListStatusTransitions returns newest first. func (r *AdminRepo) ListStatusTransitions(ctx context.Context, userID uuid.UUID, limit int) ([]AccountTransition, error) { if limit <= 0 || limit > 100 { limit = 50 } rows, err := r.Pool.Query(ctx, ` SELECT id, user_id, from_status, to_status, admin_id, reason, created_at FROM account_state_transitions WHERE user_id=$1 ORDER BY created_at DESC LIMIT $2`, userID, limit) if err != nil { return nil, err } defer rows.Close() var out []AccountTransition for rows.Next() { var t AccountTransition if err := rows.Scan(&t.ID, &t.UserID, &t.FromStatus, &t.ToStatus, &t.AdminID, &t.Reason, &t.CreatedAt); err != nil { return nil, err } out = append(out, t) } return out, rows.Err() }