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 }