feat(ECR-015): RedemptionCode 兑换码并 Closed
批次生成/作废、C 端兑码延长会员;admin-h5 /codes。 Loop continuous。Next:ECR-016 UserIntelligence。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user