批次生成/作废、C 端兑码延长会员;admin-h5 /codes。 Loop continuous。Next:ECR-016 UserIntelligence。 Co-authored-by: Cursor <cursoragent@cursor.com>
99 lines
2.6 KiB
Go
99 lines
2.6 KiB
Go
// Package membership handles growth membership status and mock commerce orders.
|
|
package membership
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
|
|
)
|
|
|
|
// Service is membership + order use-cases (extracted from report service).
|
|
type Service struct {
|
|
Reports *repository.ReportRepo
|
|
}
|
|
|
|
// CreateOrderInput for commerce.
|
|
type CreateOrderInput struct {
|
|
Kind string
|
|
Plan string
|
|
ReportID *uuid.UUID
|
|
}
|
|
|
|
// CreateOrder starts membership, deep_access, or ask_pack order.
|
|
func (s *Service) CreateOrder(ctx context.Context, userID uuid.UUID, in CreateOrderInput) (uuid.UUID, error) {
|
|
if in.Kind != "membership" && in.Kind != "deep_access" && in.Kind != "ask_pack" {
|
|
return uuid.Nil, errors.New("invalid kind")
|
|
}
|
|
if in.Kind == "deep_access" && in.ReportID == nil {
|
|
return uuid.Nil, errors.New("report_id required")
|
|
}
|
|
amount := 990
|
|
plan := in.Plan
|
|
if in.Kind == "membership" {
|
|
if plan == "" {
|
|
plan = "month"
|
|
}
|
|
a, err := s.Reports.MembershipPlanAmountCents(ctx, plan)
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
if a <= 0 {
|
|
return uuid.Nil, errors.New("invalid membership plan")
|
|
}
|
|
amount = a
|
|
}
|
|
if in.Kind == "ask_pack" {
|
|
if plan == "" {
|
|
plan = "pack10"
|
|
}
|
|
amount = repository.AskPackAmountCents(plan)
|
|
if amount <= 0 || repository.AskPackQuota(plan) <= 0 {
|
|
return uuid.Nil, errors.New("invalid ask_pack plan")
|
|
}
|
|
}
|
|
return s.Reports.CreateOrder(ctx, userID, in.Kind, plan, in.ReportID, amount)
|
|
}
|
|
|
|
// Redeem applies a redemption code for the current registered user.
|
|
func (s *Service) Redeem(ctx context.Context, userID uuid.UUID, code string) (string, error) {
|
|
code = strings.TrimSpace(code)
|
|
if code == "" {
|
|
return "", errors.New("code required")
|
|
}
|
|
return s.Reports.RedeemCode(ctx, userID, code)
|
|
}
|
|
|
|
// PayMock completes mock payment.
|
|
func (s *Service) PayMock(ctx context.Context, userID, orderID uuid.UUID) error {
|
|
return s.Reports.PayMock(ctx, userID, orderID)
|
|
}
|
|
|
|
// Me is the public membership snapshot.
|
|
type Me struct {
|
|
Active bool `json:"active"`
|
|
Plan string `json:"plan,omitempty"`
|
|
Status string `json:"status"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
AskQuotaLeft int `json:"ask_quota_left,omitempty"`
|
|
}
|
|
|
|
// Get returns current growth membership for the user.
|
|
func (s *Service) Get(ctx context.Context, userID uuid.UUID) (*Me, error) {
|
|
row, err := s.Reports.GetMembership(ctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Me{
|
|
Active: row.Active,
|
|
Plan: row.Plan,
|
|
Status: row.Status,
|
|
ExpiresAt: row.ExpiresAt,
|
|
AskQuotaLeft: row.AskQuotaLeft,
|
|
}, nil
|
|
}
|