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,94 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// MembershipPlanRow is a configurable growth membership SKU.
|
||||
type MembershipPlanRow struct {
|
||||
Code string `json:"code"`
|
||||
Title string `json:"title"`
|
||||
DurationDays int `json:"duration_days"`
|
||||
AmountCents int `json:"amount_cents"`
|
||||
Active bool `json:"active"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ListMembershipPlans returns all plans ordered by code.
|
||||
func (r *AdminRepo) ListMembershipPlans(ctx context.Context) ([]MembershipPlanRow, error) {
|
||||
rows, err := r.Pool.Query(ctx, `
|
||||
SELECT code, title, duration_days, amount_cents, active, updated_at
|
||||
FROM membership_plans ORDER BY code`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []MembershipPlanRow
|
||||
for rows.Next() {
|
||||
var p MembershipPlanRow
|
||||
if err := rows.Scan(&p.Code, &p.Title, &p.DurationDays, &p.AmountCents, &p.Active, &p.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// GetMembershipPlan loads one plan by code.
|
||||
func (r *AdminRepo) GetMembershipPlan(ctx context.Context, code string) (*MembershipPlanRow, error) {
|
||||
var p MembershipPlanRow
|
||||
err := r.Pool.QueryRow(ctx, `
|
||||
SELECT code, title, duration_days, amount_cents, active, updated_at
|
||||
FROM membership_plans WHERE code=$1`, code,
|
||||
).Scan(&p.Code, &p.Title, &p.DurationDays, &p.AmountCents, &p.Active, &p.UpdatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// UpdateMembershipPlanWithAudit updates mutable fields and audits.
|
||||
func (r *AdminRepo) UpdateMembershipPlanWithAudit(
|
||||
ctx context.Context,
|
||||
adminID uuid.UUID,
|
||||
code, title string,
|
||||
days, amountCents int,
|
||||
active bool,
|
||||
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 membership_plans
|
||||
SET title=$2, duration_days=$3, amount_cents=$4, active=$5, updated_at=now()
|
||||
WHERE code=$1`, code, title, days, amountCents, active)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return errString("plan not found")
|
||||
}
|
||||
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,'membership.plans.update','membership_plan',$2,$3)`,
|
||||
adminID, code, meta,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
@@ -316,6 +316,38 @@ func AskPackAmountCents(plan string) int {
|
||||
}
|
||||
}
|
||||
|
||||
// MembershipPlanAmountCents returns catalog price or fallback for membership plans.
|
||||
func (r *ReportRepo) MembershipPlanAmountCents(ctx context.Context, plan string) (int, error) {
|
||||
var amount int
|
||||
var active bool
|
||||
err := r.Pool.QueryRow(ctx, `
|
||||
SELECT amount_cents, active FROM membership_plans WHERE code=$1`, plan,
|
||||
).Scan(&amount, &active)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return membershipAmountFallback(plan), nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !active {
|
||||
return 0, errString("plan inactive")
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
func membershipAmountFallback(plan string) int {
|
||||
switch plan {
|
||||
case "month":
|
||||
return 2500
|
||||
case "quarter":
|
||||
return 6800
|
||||
case "year":
|
||||
return 19800
|
||||
default:
|
||||
return 2500
|
||||
}
|
||||
}
|
||||
|
||||
var errMissingReport = errString("report_id required for deep_access")
|
||||
|
||||
type errString string
|
||||
|
||||
Reference in New Issue
Block a user