| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 |
- package repository
- import (
- "context"
- "strings"
- "time"
- dbent "github.com/Wei-Shaw/sub2api/ent"
- "github.com/Wei-Shaw/sub2api/ent/redeemcode"
- "github.com/Wei-Shaw/sub2api/ent/user"
- "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
- "github.com/Wei-Shaw/sub2api/internal/service"
- entsql "entgo.io/ent/dialect/sql"
- )
- type redeemCodeRepository struct {
- client *dbent.Client
- }
- func NewRedeemCodeRepository(client *dbent.Client) service.RedeemCodeRepository {
- return &redeemCodeRepository{client: client}
- }
- func (r *redeemCodeRepository) Create(ctx context.Context, code *service.RedeemCode) error {
- created, err := r.client.RedeemCode.Create().
- SetCode(code.Code).
- SetType(code.Type).
- SetValue(code.Value).
- SetStatus(code.Status).
- SetNotes(code.Notes).
- SetValidityDays(code.ValidityDays).
- SetNillableExpiresAt(code.ExpiresAt).
- SetNillableUsedBy(code.UsedBy).
- SetNillableUsedAt(code.UsedAt).
- SetNillableGroupID(code.GroupID).
- Save(ctx)
- if err == nil {
- code.ID = created.ID
- code.CreatedAt = created.CreatedAt
- }
- return err
- }
- func (r *redeemCodeRepository) CreateBatch(ctx context.Context, codes []service.RedeemCode) error {
- if len(codes) == 0 {
- return nil
- }
- builders := make([]*dbent.RedeemCodeCreate, 0, len(codes))
- for i := range codes {
- c := &codes[i]
- b := r.client.RedeemCode.Create().
- SetCode(c.Code).
- SetType(c.Type).
- SetValue(c.Value).
- SetStatus(c.Status).
- SetNotes(c.Notes).
- SetValidityDays(c.ValidityDays).
- SetNillableExpiresAt(c.ExpiresAt).
- SetNillableUsedBy(c.UsedBy).
- SetNillableUsedAt(c.UsedAt).
- SetNillableGroupID(c.GroupID)
- builders = append(builders, b)
- }
- return r.client.RedeemCode.CreateBulk(builders...).Exec(ctx)
- }
- func (r *redeemCodeRepository) GetByID(ctx context.Context, id int64) (*service.RedeemCode, error) {
- m, err := r.client.RedeemCode.Query().
- Where(redeemcode.IDEQ(id)).
- Only(ctx)
- if err != nil {
- if dbent.IsNotFound(err) {
- return nil, service.ErrRedeemCodeNotFound
- }
- return nil, err
- }
- return redeemCodeEntityToService(m), nil
- }
- func (r *redeemCodeRepository) GetByCode(ctx context.Context, code string) (*service.RedeemCode, error) {
- m, err := r.client.RedeemCode.Query().
- Where(redeemcode.CodeEQ(code)).
- Only(ctx)
- if err != nil {
- if dbent.IsNotFound(err) {
- return nil, service.ErrRedeemCodeNotFound
- }
- return nil, err
- }
- return redeemCodeEntityToService(m), nil
- }
- func (r *redeemCodeRepository) Delete(ctx context.Context, id int64) error {
- _, err := r.client.RedeemCode.Delete().Where(redeemcode.IDEQ(id)).Exec(ctx)
- return err
- }
- func (r *redeemCodeRepository) List(ctx context.Context, params pagination.PaginationParams) ([]service.RedeemCode, *pagination.PaginationResult, error) {
- return r.ListWithFilters(ctx, params, "", "", "")
- }
- func (r *redeemCodeRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, codeType, status, search string) ([]service.RedeemCode, *pagination.PaginationResult, error) {
- q := r.client.RedeemCode.Query()
- if codeType != "" {
- q = q.Where(redeemcode.TypeEQ(codeType))
- }
- if status != "" {
- now := time.Now()
- switch status {
- case service.StatusExpired:
- q = q.Where(redeemcode.Or(
- redeemcode.StatusEQ(service.StatusExpired),
- redeemcode.And(
- redeemcode.StatusEQ(service.StatusUnused),
- redeemcode.ExpiresAtNotNil(),
- redeemcode.ExpiresAtLTE(now),
- ),
- ))
- case service.StatusUnused:
- q = q.Where(
- redeemcode.StatusEQ(service.StatusUnused),
- redeemcode.Or(
- redeemcode.ExpiresAtIsNil(),
- redeemcode.ExpiresAtGT(now),
- ),
- )
- default:
- q = q.Where(redeemcode.StatusEQ(status))
- }
- }
- if search != "" {
- q = q.Where(
- redeemcode.Or(
- redeemcode.CodeContainsFold(search),
- redeemcode.HasUserWith(user.EmailContainsFold(search)),
- ),
- )
- }
- total, err := q.Count(ctx)
- if err != nil {
- return nil, nil, err
- }
- codesQuery := q.
- WithUser().
- WithGroup().
- Offset(params.Offset()).
- Limit(params.Limit())
- for _, order := range redeemCodeListOrder(params) {
- codesQuery = codesQuery.Order(order)
- }
- codes, err := codesQuery.All(ctx)
- if err != nil {
- return nil, nil, err
- }
- outCodes := redeemCodeEntitiesToService(codes)
- return outCodes, paginationResultFromTotal(int64(total), params), nil
- }
- func redeemCodeListOrder(params pagination.PaginationParams) []func(*entsql.Selector) {
- sortBy := strings.ToLower(strings.TrimSpace(params.SortBy))
- sortOrder := params.NormalizedSortOrder(pagination.SortOrderDesc)
- var field string
- switch sortBy {
- case "type":
- field = redeemcode.FieldType
- case "value":
- field = redeemcode.FieldValue
- case "status":
- field = redeemcode.FieldStatus
- case "used_at":
- field = redeemcode.FieldUsedAt
- case "created_at":
- field = redeemcode.FieldCreatedAt
- case "expires_at":
- field = redeemcode.FieldExpiresAt
- case "code":
- field = redeemcode.FieldCode
- default:
- field = redeemcode.FieldID
- }
- if sortOrder == pagination.SortOrderAsc {
- return []func(*entsql.Selector){dbent.Asc(field), dbent.Asc(redeemcode.FieldID)}
- }
- return []func(*entsql.Selector){dbent.Desc(field), dbent.Desc(redeemcode.FieldID)}
- }
- func (r *redeemCodeRepository) Update(ctx context.Context, code *service.RedeemCode) error {
- up := r.client.RedeemCode.UpdateOneID(code.ID).
- SetCode(code.Code).
- SetType(code.Type).
- SetValue(code.Value).
- SetStatus(code.Status).
- SetNotes(code.Notes).
- SetValidityDays(code.ValidityDays)
- if code.UsedBy != nil {
- up.SetUsedBy(*code.UsedBy)
- } else {
- up.ClearUsedBy()
- }
- if code.UsedAt != nil {
- up.SetUsedAt(*code.UsedAt)
- } else {
- up.ClearUsedAt()
- }
- if code.GroupID != nil {
- up.SetGroupID(*code.GroupID)
- } else {
- up.ClearGroupID()
- }
- if code.ExpiresAt != nil {
- up.SetExpiresAt(*code.ExpiresAt)
- } else {
- up.ClearExpiresAt()
- }
- updated, err := up.Save(ctx)
- if err != nil {
- if dbent.IsNotFound(err) {
- return service.ErrRedeemCodeNotFound
- }
- return err
- }
- code.CreatedAt = updated.CreatedAt
- return nil
- }
- func (r *redeemCodeRepository) BatchUpdate(ctx context.Context, ids []int64, fields service.RedeemCodeBatchUpdateFields) (int64, error) {
- uniqueIDs := make([]int64, 0, len(ids))
- seen := make(map[int64]struct{}, len(ids))
- for _, id := range ids {
- if _, ok := seen[id]; ok {
- continue
- }
- seen[id] = struct{}{}
- uniqueIDs = append(uniqueIDs, id)
- }
- if len(uniqueIDs) == 0 {
- return 0, nil
- }
- if tx := dbent.TxFromContext(ctx); tx != nil {
- return r.batchUpdate(ctx, tx.Client(), uniqueIDs, fields)
- }
- tx, err := r.client.Tx(ctx)
- if err != nil {
- return 0, err
- }
- txCtx := dbent.NewTxContext(ctx, tx)
- defer func() { _ = tx.Rollback() }()
- updated, err := r.batchUpdate(txCtx, tx.Client(), uniqueIDs, fields)
- if err != nil {
- return 0, err
- }
- if err := tx.Commit(); err != nil {
- return 0, err
- }
- return updated, nil
- }
- func (r *redeemCodeRepository) batchUpdate(ctx context.Context, client *dbent.Client, ids []int64, fields service.RedeemCodeBatchUpdateFields) (int64, error) {
- existing, err := client.RedeemCode.Query().
- Where(redeemcode.IDIn(ids...)).
- All(ctx)
- if err != nil {
- return 0, err
- }
- if len(existing) != len(ids) {
- return 0, service.ErrRedeemCodeNotFound
- }
- if fields.TouchesUsedSensitiveFields() {
- for _, code := range existing {
- if code.Status == service.StatusUsed {
- return 0, service.ErrRedeemCodeUsed
- }
- }
- }
- up := client.RedeemCode.Update().Where(redeemcode.IDIn(ids...))
- if fields.Status != nil {
- up.SetStatus(*fields.Status)
- }
- if fields.Notes != nil {
- up.SetNotes(*fields.Notes)
- }
- if fields.ExpiresAt.Set {
- if fields.ExpiresAt.Value != nil {
- up.SetExpiresAt(*fields.ExpiresAt.Value)
- } else {
- up.ClearExpiresAt()
- }
- }
- if fields.GroupID.Set {
- if fields.GroupID.Value != nil {
- up.SetGroupID(*fields.GroupID.Value)
- } else {
- up.ClearGroupID()
- }
- }
- affected, err := up.Save(ctx)
- if err != nil {
- return 0, err
- }
- if affected != len(ids) {
- return 0, service.ErrRedeemCodeNotFound
- }
- return int64(affected), nil
- }
- func (r *redeemCodeRepository) Use(ctx context.Context, id, userID int64) error {
- now := time.Now()
- client := clientFromContext(ctx, r.client)
- affected, err := client.RedeemCode.Update().
- Where(redeemcode.IDEQ(id), redeemcode.StatusEQ(service.StatusUnused)).
- SetStatus(service.StatusUsed).
- SetUsedBy(userID).
- SetUsedAt(now).
- Save(ctx)
- if err != nil {
- return err
- }
- if affected == 0 {
- return service.ErrRedeemCodeUsed
- }
- return nil
- }
- func (r *redeemCodeRepository) ListByUser(ctx context.Context, userID int64, limit int) ([]service.RedeemCode, error) {
- if limit <= 0 {
- limit = 10
- }
- codes, err := r.client.RedeemCode.Query().
- Where(redeemcode.UsedByEQ(userID)).
- WithGroup().
- Order(dbent.Desc(redeemcode.FieldUsedAt)).
- Limit(limit).
- All(ctx)
- if err != nil {
- return nil, err
- }
- return redeemCodeEntitiesToService(codes), nil
- }
- // ListByUserPaginated returns paginated balance/concurrency history for a user.
- // Supports optional type filter (e.g. "balance", "admin_balance", "concurrency", "admin_concurrency", "subscription").
- func (r *redeemCodeRepository) ListByUserPaginated(ctx context.Context, userID int64, params pagination.PaginationParams, codeType string) ([]service.RedeemCode, *pagination.PaginationResult, error) {
- q := r.client.RedeemCode.Query().
- Where(redeemcode.UsedByEQ(userID))
- // Optional type filter
- if codeType != "" {
- q = q.Where(redeemcode.TypeEQ(codeType))
- }
- total, err := q.Count(ctx)
- if err != nil {
- return nil, nil, err
- }
- codes, err := q.
- WithGroup().
- Offset(params.Offset()).
- Limit(params.Limit()).
- Order(dbent.Desc(redeemcode.FieldUsedAt)).
- All(ctx)
- if err != nil {
- return nil, nil, err
- }
- return redeemCodeEntitiesToService(codes), paginationResultFromTotal(int64(total), params), nil
- }
- // SumPositiveBalanceByUser returns total recharged amount (sum of value > 0 where type is balance/admin_balance).
- func (r *redeemCodeRepository) SumPositiveBalanceByUser(ctx context.Context, userID int64) (float64, error) {
- var result []struct {
- Sum float64 `json:"sum"`
- }
- err := r.client.RedeemCode.Query().
- Where(
- redeemcode.UsedByEQ(userID),
- redeemcode.ValueGT(0),
- redeemcode.TypeIn("balance", "admin_balance"),
- ).
- Aggregate(dbent.As(dbent.Sum(redeemcode.FieldValue), "sum")).
- Scan(ctx, &result)
- if err != nil {
- return 0, err
- }
- if len(result) == 0 {
- return 0, nil
- }
- return result[0].Sum, nil
- }
- func redeemCodeEntityToService(m *dbent.RedeemCode) *service.RedeemCode {
- if m == nil {
- return nil
- }
- out := &service.RedeemCode{
- ID: m.ID,
- Code: m.Code,
- Type: m.Type,
- Value: m.Value,
- Status: m.Status,
- UsedBy: m.UsedBy,
- UsedAt: m.UsedAt,
- Notes: derefString(m.Notes),
- CreatedAt: m.CreatedAt,
- ExpiresAt: m.ExpiresAt,
- GroupID: m.GroupID,
- ValidityDays: m.ValidityDays,
- }
- if m.Edges.User != nil {
- out.User = userEntityToService(m.Edges.User)
- }
- if m.Edges.Group != nil {
- out.Group = groupEntityToService(m.Edges.Group)
- }
- return out
- }
- func redeemCodeEntitiesToService(models []*dbent.RedeemCode) []service.RedeemCode {
- out := make([]service.RedeemCode, 0, len(models))
- for i := range models {
- if s := redeemCodeEntityToService(models[i]); s != nil {
- out = append(out, *s)
- }
- }
- return out
- }
|