批次生成/作废、C 端兑码延长会员;admin-h5 /codes。 Loop continuous。Next:ECR-016 UserIntelligence。 Co-authored-by: Cursor <cursoragent@cursor.com>
126 lines
3.0 KiB
Go
126 lines
3.0 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// MembershipPlanDurationDays returns catalog days or fallback.
|
|
func (r *ReportRepo) MembershipPlanDurationDays(ctx context.Context, plan string) (int, error) {
|
|
var days int
|
|
var active bool
|
|
err := r.Pool.QueryRow(ctx, `
|
|
SELECT duration_days, active FROM membership_plans WHERE code=$1`, plan,
|
|
).Scan(&days, &active)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return membershipDaysFallback(plan), nil
|
|
}
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if !active || days <= 0 {
|
|
return membershipDaysFallback(plan), nil
|
|
}
|
|
return days, nil
|
|
}
|
|
|
|
func membershipDaysFallback(plan string) int {
|
|
switch plan {
|
|
case "month":
|
|
return 31
|
|
case "quarter":
|
|
return 92
|
|
case "year":
|
|
return 366
|
|
default:
|
|
return 31
|
|
}
|
|
}
|
|
|
|
// RedeemCode applies an unused redemption code to user membership.
|
|
func (r *ReportRepo) RedeemCode(ctx context.Context, userID uuid.UUID, rawCode string) (plan string, err error) {
|
|
tx, err := r.Pool.Begin(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
var codeID uuid.UUID
|
|
var status string
|
|
err = tx.QueryRow(ctx, `
|
|
SELECT id, plan_code, status FROM redemption_codes
|
|
WHERE code=$1 FOR UPDATE`, rawCode,
|
|
).Scan(&codeID, &plan, &status)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", errString("invalid code")
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if status != "unused" {
|
|
return "", errString("code not redeemable")
|
|
}
|
|
days, err := r.MembershipPlanDurationDays(ctx, plan)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := tx.Exec(ctx, `
|
|
UPDATE redemption_codes
|
|
SET status='redeemed', redeemed_by=$2, redeemed_at=now()
|
|
WHERE id=$1 AND status='unused'`, codeID, userID); err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := tx.Exec(ctx, `
|
|
INSERT INTO memberships(user_id, plan, status, expires_at, ask_quota_left)
|
|
VALUES ($1,$2,'active', now() + ($3 * interval '1 day'), 100)
|
|
ON CONFLICT (user_id) DO UPDATE SET
|
|
plan=EXCLUDED.plan, status='active',
|
|
expires_at=(CASE
|
|
WHEN memberships.expires_at IS NOT NULL AND memberships.expires_at > now()
|
|
THEN memberships.expires_at ELSE now()
|
|
END) + ($3 * interval '1 day'),
|
|
ask_quota_left=100, updated_at=now()`,
|
|
userID, plan, days); err != nil {
|
|
return "", err
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return "", err
|
|
}
|
|
return plan, nil
|
|
}
|