diff --git a/internal/api/handler/invite.go b/internal/api/handler/invite.go index 20d570b..5261a97 100644 --- a/internal/api/handler/invite.go +++ b/internal/api/handler/invite.go @@ -175,3 +175,34 @@ func (h *InviteHandler) GetInvites(w http.ResponseWriter, r *http.Request) { Data: invites, }) } + +// GetInvitesStatus handles fetching the counts of a users invitations +// +// @Summary Get invitations status +// @Description Gets active/expired/used counts for all the users invites +// @Tags Invite +// @Accept json +// @Produce json +// @Success 200 {object} domain.InviteCounts +// @Error 500 {object} response.ErrorResponse "failed to count invites" +// @Router /user/invites [get] + +func (h *InviteHandler) GetInvitesStatus(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + authContext, err := domain.GetAuthContext(ctx) + if err != nil { + slog.ErrorContext(ctx, "failed to get auth context", slog.Any("error", err)) + response.Error(ctx, w, http.StatusInternalServerError, "failed to count invites") + return + } + + counts, err := h.InviteService.CountInvitesStructured(ctx, authContext.UserID) + if err != nil { + slog.ErrorContext(ctx, "failed to count invites", slog.Any("error", err)) + response.Error(ctx, w, http.StatusInternalServerError, "failed to count invites") + return + } + + response.JSON(ctx, w, http.StatusOK, counts) +} diff --git a/internal/api/router/router.go b/internal/api/router/router.go index 16f8ac8..f1f74bc 100644 --- a/internal/api/router/router.go +++ b/internal/api/router/router.go @@ -50,6 +50,7 @@ func NewMux(cfg Config) http.Handler { apiMux.Handle("POST /user/invites", protected(inviteHandler.CreateInvite)) apiMux.Handle("DELETE /user/invites/{id}", protected(inviteHandler.DeleteInvite)) apiMux.Handle("GET /user/invites", protected(inviteHandler.GetInvites)) + apiMux.Handle("GET /user/invites/status", protected(inviteHandler.GetInvitesStatus)) // Lists apiMux.Handle("POST /lists", protected(listHandler.CreateList)) diff --git a/internal/domain/invite.go b/internal/domain/invite.go index 1ffb8ed..f9e65ef 100644 --- a/internal/domain/invite.go +++ b/internal/domain/invite.go @@ -21,6 +21,12 @@ type Invite struct { ConsumedAt *time.Time `json:"consumed_at"` } +type InviteCounts struct { + Active int `json:"active"` + Expired int `json:"expired"` + Used int `json:"used"` +} + type InviteRepository interface { CreateInvite(ctx context.Context, inviterUserID string, code string, expiresAt time.Time) (*Invite, error) DeleteInvite(ctx context.Context, userID string, inviteID string) error @@ -28,6 +34,7 @@ type InviteRepository interface { GetInvite(ctx context.Context, code string) (*Invite, error) GetInvites(ctx context.Context, userID string, offset int, count int) ([]Invite, error) CountInvites(ctx context.Context, userID string) (int, error) + CountInvitesStructured(ctx context.Context, userID string) (*InviteCounts, error) } type InviteService struct { @@ -107,3 +114,11 @@ func (s *InviteService) CountInvites(ctx context.Context, userID string) (int, e return s.repo.CountInvites(ctx, userID) } + +func (s *InviteService) CountInvitesStructured(ctx context.Context, userID string) (*InviteCounts, error) { + if userID == "" { + return nil, ErrUserIDMissing + } + + return s.repo.CountInvitesStructured(ctx, userID) +} diff --git a/internal/repository/invite.go b/internal/repository/invite.go index 746651a..28436c9 100644 --- a/internal/repository/invite.go +++ b/internal/repository/invite.go @@ -128,3 +128,33 @@ func (r *InviteRepo) CountInvites(ctx context.Context, userID string) (int, erro return count, nil } + +func (r *InviteRepo) CountInvitesStructured(ctx context.Context, userID string) (*domain.InviteCounts, error) { + var counts domain.InviteCounts + conn := r.conn(ctx) + err := conn.QueryRowContext(ctx, + "SELECT COUNT(*) FROM invites WHERE inviter_user_id=$1 AND expires_at > NOW() AND consumed_at IS NULL", + userID, + ).Scan(&counts.Active) + if err != nil { + return nil, fmt.Errorf("failed to count active invites: %w", err) + } + + err = conn.QueryRowContext(ctx, + "SELECT COUNT(*) FROM invites WHERE inviter_user_id=$1 AND expires_at < NOW() AND consumed_at IS NULL", + userID, + ).Scan(&counts.Expired) + if err != nil { + return nil, fmt.Errorf("failed to count expired invites: %w", err) + } + + err = conn.QueryRowContext(ctx, + "SELECT COUNT(consumed_at) FROM invites WHERE inviter_user_id=$1", + userID, + ).Scan(&counts.Used) + if err != nil { + return nil, fmt.Errorf("failed to count consumed invites: %w", err) + } + + return &counts, nil +}