feat(ECR-014): MembershipPlan 套餐配置并 Closed
membership_plans 表、admin 套餐页、Grant/CreateOrder 读表; Loop continuous 自动 Approve/Closed。Next:ECR-015 RedemptionCode。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPlanNotFound = errString("plan not found")
|
||||
ErrInvalidPlanU = errString("invalid plan update")
|
||||
)
|
||||
|
||||
// ListMembershipPlans returns catalog.
|
||||
func (s *Service) ListMembershipPlans(ctx context.Context) ([]repository.MembershipPlanRow, error) {
|
||||
items, err := s.Repo.ListMembershipPlans(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if items == nil {
|
||||
items = []repository.MembershipPlanRow{}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetMembershipPlan returns one plan.
|
||||
func (s *Service) GetMembershipPlan(ctx context.Context, code string) (*repository.MembershipPlanRow, error) {
|
||||
p, err := s.Repo.GetMembershipPlan(ctx, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p == nil {
|
||||
return nil, ErrPlanNotFound
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// UpdateMembershipPlan updates mutable fields.
|
||||
func (s *Service) UpdateMembershipPlan(
|
||||
ctx context.Context, adminID uuid.UUID, code, title string, days, amount int, active bool,
|
||||
) (*repository.MembershipPlanRow, error) {
|
||||
code = strings.TrimSpace(code)
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" || days <= 0 || amount < 0 {
|
||||
return nil, ErrInvalidPlanU
|
||||
}
|
||||
if _, err := s.GetMembershipPlan(ctx, code); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta, _ := json.Marshal(map[string]any{
|
||||
"title": title, "duration_days": days, "amount_cents": amount, "active": active,
|
||||
})
|
||||
if err := s.Repo.UpdateMembershipPlanWithAudit(ctx, adminID, code, title, days, amount, active, meta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.GetMembershipPlan(ctx, code)
|
||||
}
|
||||
|
||||
// PlanDurationDays resolves grant length from catalog with hardcoded fallback.
|
||||
func (s *Service) PlanDurationDays(ctx context.Context, plan string) (int, error) {
|
||||
p, err := s.Repo.GetMembershipPlan(ctx, plan)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if p != nil && p.Active && p.DurationDays > 0 {
|
||||
return p.DurationDays, nil
|
||||
}
|
||||
return planDaysFallback(plan)
|
||||
}
|
||||
|
||||
func planDaysFallback(plan string) (int, error) {
|
||||
switch plan {
|
||||
case "month":
|
||||
return 31, nil
|
||||
case "quarter":
|
||||
return 92, nil
|
||||
case "year":
|
||||
return 366, nil
|
||||
default:
|
||||
return 0, ErrInvalidPlan
|
||||
}
|
||||
}
|
||||
@@ -10,23 +10,25 @@ import (
|
||||
|
||||
// Permission catalog frozen in ECR-013A Spec.
|
||||
const (
|
||||
PermUsersRead = "admin.users.read"
|
||||
PermMembershipGrant = "admin.users.membership.grant"
|
||||
PermAskQuotaGrant = "admin.users.ask_quota.grant"
|
||||
PermOrdersRead = "admin.orders.read"
|
||||
PermAuditRead = "admin.audit.read"
|
||||
PermAnalyticsRead = "admin.analytics.read"
|
||||
PermContentWrite = "admin.content.write"
|
||||
PermRolesRead = "admin.roles.read"
|
||||
PermRolesWrite = "admin.roles.write"
|
||||
PermUsersStatusWrite = "admin.users.status.write"
|
||||
PermUsersRead = "admin.users.read"
|
||||
PermMembershipGrant = "admin.users.membership.grant"
|
||||
PermAskQuotaGrant = "admin.users.ask_quota.grant"
|
||||
PermOrdersRead = "admin.orders.read"
|
||||
PermAuditRead = "admin.audit.read"
|
||||
PermAnalyticsRead = "admin.analytics.read"
|
||||
PermContentWrite = "admin.content.write"
|
||||
PermRolesRead = "admin.roles.read"
|
||||
PermRolesWrite = "admin.roles.write"
|
||||
PermUsersStatusWrite = "admin.users.status.write"
|
||||
PermMembershipPlansRead = "admin.membership.plans.read"
|
||||
PermMembershipPlansWrite = "admin.membership.plans.write"
|
||||
)
|
||||
|
||||
var knownPermissions = map[string]struct{}{
|
||||
PermUsersRead: {}, PermMembershipGrant: {}, PermAskQuotaGrant: {},
|
||||
PermOrdersRead: {}, PermAuditRead: {}, PermAnalyticsRead: {},
|
||||
PermContentWrite: {}, PermRolesRead: {}, PermRolesWrite: {},
|
||||
PermUsersStatusWrite: {},
|
||||
PermUsersStatusWrite: {}, PermMembershipPlansRead: {}, PermMembershipPlansWrite: {},
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -181,7 +181,7 @@ type GrantInput struct {
|
||||
|
||||
// GrantMembership extends membership and writes audit.
|
||||
func (s *Service) GrantMembership(ctx context.Context, adminID, userID uuid.UUID, plan string) error {
|
||||
days, err := planDays(plan)
|
||||
days, err := s.PlanDurationDays(ctx, plan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -230,16 +230,7 @@ func (s *Service) ListAuditLogs(ctx context.Context, limit, offset int) ([]repos
|
||||
}
|
||||
|
||||
func planDays(plan string) (int, error) {
|
||||
switch plan {
|
||||
case "month":
|
||||
return 31, nil
|
||||
case "quarter":
|
||||
return 92, nil
|
||||
case "year":
|
||||
return 366, nil
|
||||
default:
|
||||
return 0, ErrInvalidPlan
|
||||
}
|
||||
return planDaysFallback(plan)
|
||||
}
|
||||
|
||||
func newToken() (string, error) {
|
||||
|
||||
Reference in New Issue
Block a user