package repository import ( "context" "encoding/json" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) // RedemptionBatch is a generation batch of codes. type RedemptionBatch struct { ID uuid.UUID `json:"id"` Label string `json:"label"` PlanCode string `json:"plan_code"` Quantity int `json:"quantity"` CreatedBy uuid.UUID `json:"created_by"` CreatedAt time.Time `json:"created_at"` } // RedemptionCodeRow is one redeemable code. type RedemptionCodeRow struct { ID uuid.UUID `json:"id"` BatchID uuid.UUID `json:"batch_id"` Code string `json:"code"` PlanCode string `json:"plan_code"` Status string `json:"status"` RedeemedBy *uuid.UUID `json:"redeemed_by,omitempty"` RedeemedAt *time.Time `json:"redeemed_at,omitempty"` CreatedAt time.Time `json:"created_at"` } // CreateRedemptionBatchWithCodes inserts batch + codes + audit. func (r *AdminRepo) CreateRedemptionBatchWithCodes( ctx context.Context, adminID uuid.UUID, label, planCode string, codes []string, meta json.RawMessage, ) (*RedemptionBatch, []RedemptionCodeRow, error) { tx, err := r.Pool.Begin(ctx) if err != nil { return nil, nil, err } defer tx.Rollback(ctx) var b RedemptionBatch err = tx.QueryRow(ctx, ` INSERT INTO redemption_batches(label, plan_code, quantity, created_by) VALUES ($1,$2,$3,$4) RETURNING id, label, plan_code, quantity, created_by, created_at`, label, planCode, len(codes), adminID, ).Scan(&b.ID, &b.Label, &b.PlanCode, &b.Quantity, &b.CreatedBy, &b.CreatedAt) if err != nil { return nil, nil, err } out := make([]RedemptionCodeRow, 0, len(codes)) for _, code := range codes { var row RedemptionCodeRow err = tx.QueryRow(ctx, ` INSERT INTO redemption_codes(batch_id, code, plan_code, status) VALUES ($1,$2,$3,'unused') RETURNING id, batch_id, code, plan_code, status, created_at`, b.ID, code, planCode, ).Scan(&row.ID, &row.BatchID, &row.Code, &row.PlanCode, &row.Status, &row.CreatedAt) if err != nil { return nil, nil, err } out = append(out, row) } 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,'redemption.batch.create','redemption_batch',$2,$3)`, adminID, b.ID.String(), meta, ); err != nil { return nil, nil, err } if err := tx.Commit(ctx); err != nil { return nil, nil, err } return &b, out, nil } // ListRedemptionBatches newest first. func (r *AdminRepo) ListRedemptionBatches(ctx context.Context, limit int) ([]RedemptionBatch, error) { if limit <= 0 || limit > 100 { limit = 50 } rows, err := r.Pool.Query(ctx, ` SELECT id, label, plan_code, quantity, created_by, created_at FROM redemption_batches ORDER BY created_at DESC LIMIT $1`, limit) if err != nil { return nil, err } defer rows.Close() var out []RedemptionBatch for rows.Next() { var b RedemptionBatch if err := rows.Scan(&b.ID, &b.Label, &b.PlanCode, &b.Quantity, &b.CreatedBy, &b.CreatedAt); err != nil { return nil, err } out = append(out, b) } return out, rows.Err() } // ListRedemptionCodesByBatch returns codes for a batch. func (r *AdminRepo) ListRedemptionCodesByBatch(ctx context.Context, batchID uuid.UUID) ([]RedemptionCodeRow, error) { rows, err := r.Pool.Query(ctx, ` SELECT id, batch_id, code, plan_code, status, redeemed_by, redeemed_at, created_at FROM redemption_codes WHERE batch_id=$1 ORDER BY created_at`, batchID) if err != nil { return nil, err } defer rows.Close() return scanRedemptionCodes(rows) } // DisableRedemptionCode marks unused code disabled. func (r *AdminRepo) DisableRedemptionCode(ctx context.Context, adminID, codeID uuid.UUID) error { tx, err := r.Pool.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) tag, err := tx.Exec(ctx, ` UPDATE redemption_codes SET status='disabled' WHERE id=$1 AND status='unused'`, codeID) if err != nil { return err } if tag.RowsAffected() == 0 { return errString("code not unused") } meta, _ := json.Marshal(map[string]string{"code_id": codeID.String()}) if _, err := tx.Exec(ctx, ` INSERT INTO admin_audit_logs(admin_id, action, target_type, target_id, meta) VALUES ($1,'redemption.code.disable','redemption_code',$2,$3)`, adminID, codeID.String(), meta, ); err != nil { return err } return tx.Commit(ctx) } func scanRedemptionCodes(rows pgx.Rows) ([]RedemptionCodeRow, error) { var out []RedemptionCodeRow for rows.Next() { var c RedemptionCodeRow if err := rows.Scan(&c.ID, &c.BatchID, &c.Code, &c.PlanCode, &c.Status, &c.RedeemedBy, &c.RedeemedAt, &c.CreatedAt); err != nil { return nil, err } out = append(out, c) } return out, rows.Err() } // BatchExists reports whether batch id exists. func (r *AdminRepo) BatchExists(ctx context.Context, id uuid.UUID) (bool, error) { var ok bool err := r.Pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM redemption_batches WHERE id=$1)`, id).Scan(&ok) return ok, err }