2025-12-18 13:50:39 +08:00
|
|
|
|
package repository
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
dbent "github.com/Wei-Shaw/sub2api/ent"
|
|
|
|
|
|
"github.com/Wei-Shaw/sub2api/ent/usersubscription"
|
2025-12-24 21:07:21 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
2025-12-25 20:52:47 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-25 20:52:47 +08:00
|
|
|
|
type userSubscriptionRepository struct {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
client *dbent.Client
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
func NewUserSubscriptionRepository(client *dbent.Client) service.UserSubscriptionRepository {
|
|
|
|
|
|
return &userSubscriptionRepository{client: client}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) Create(ctx context.Context, sub *service.UserSubscription) error {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
if sub == nil {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
return service.ErrSubscriptionNilInput
|
2025-12-29 10:03:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
builder := client.UserSubscription.Create().
|
2025-12-29 10:03:27 +08:00
|
|
|
|
SetUserID(sub.UserID).
|
|
|
|
|
|
SetGroupID(sub.GroupID).
|
|
|
|
|
|
SetExpiresAt(sub.ExpiresAt).
|
|
|
|
|
|
SetNillableDailyWindowStart(sub.DailyWindowStart).
|
|
|
|
|
|
SetNillableWeeklyWindowStart(sub.WeeklyWindowStart).
|
|
|
|
|
|
SetNillableMonthlyWindowStart(sub.MonthlyWindowStart).
|
|
|
|
|
|
SetDailyUsageUsd(sub.DailyUsageUSD).
|
|
|
|
|
|
SetWeeklyUsageUsd(sub.WeeklyUsageUSD).
|
|
|
|
|
|
SetMonthlyUsageUsd(sub.MonthlyUsageUSD).
|
|
|
|
|
|
SetNillableAssignedBy(sub.AssignedBy)
|
|
|
|
|
|
|
|
|
|
|
|
if sub.StartsAt.IsZero() {
|
|
|
|
|
|
builder.SetStartsAt(time.Now())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
builder.SetStartsAt(sub.StartsAt)
|
|
|
|
|
|
}
|
|
|
|
|
|
if sub.Status != "" {
|
|
|
|
|
|
builder.SetStatus(sub.Status)
|
|
|
|
|
|
}
|
|
|
|
|
|
if !sub.AssignedAt.IsZero() {
|
|
|
|
|
|
builder.SetAssignedAt(sub.AssignedAt)
|
|
|
|
|
|
}
|
|
|
|
|
|
// Keep compatibility with historical behavior: always store notes as a string value.
|
|
|
|
|
|
builder.SetNotes(sub.Notes)
|
|
|
|
|
|
|
|
|
|
|
|
created, err := builder.Save(ctx)
|
2025-12-26 15:40:24 +08:00
|
|
|
|
if err == nil {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
applyUserSubscriptionEntityToService(sub, created)
|
2025-12-26 15:40:24 +08:00
|
|
|
|
}
|
2025-12-25 20:52:47 +08:00
|
|
|
|
return translatePersistenceError(err, nil, service.ErrSubscriptionAlreadyExists)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) GetByID(ctx context.Context, id int64) (*service.UserSubscription, error) {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
m, err := client.UserSubscription.Query().
|
2025-12-29 10:03:27 +08:00
|
|
|
|
Where(usersubscription.IDEQ(id)).
|
|
|
|
|
|
WithUser().
|
|
|
|
|
|
WithGroup().
|
|
|
|
|
|
WithAssignedByUser().
|
|
|
|
|
|
Only(ctx)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil {
|
2025-12-25 20:52:47 +08:00
|
|
|
|
return nil, translatePersistenceError(err, service.ErrSubscriptionNotFound, nil)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return userSubscriptionEntityToService(m), nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) GetByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
m, err := client.UserSubscription.Query().
|
2025-12-29 10:03:27 +08:00
|
|
|
|
Where(usersubscription.UserIDEQ(userID), usersubscription.GroupIDEQ(groupID)).
|
|
|
|
|
|
WithGroup().
|
|
|
|
|
|
Only(ctx)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil {
|
2025-12-25 20:52:47 +08:00
|
|
|
|
return nil, translatePersistenceError(err, service.ErrSubscriptionNotFound, nil)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return userSubscriptionEntityToService(m), nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) GetActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
m, err := client.UserSubscription.Query().
|
2025-12-29 10:03:27 +08:00
|
|
|
|
Where(
|
|
|
|
|
|
usersubscription.UserIDEQ(userID),
|
|
|
|
|
|
usersubscription.GroupIDEQ(groupID),
|
|
|
|
|
|
usersubscription.StatusEQ(service.SubscriptionStatusActive),
|
|
|
|
|
|
usersubscription.ExpiresAtGT(time.Now()),
|
|
|
|
|
|
).
|
|
|
|
|
|
WithGroup().
|
|
|
|
|
|
Only(ctx)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil {
|
2025-12-25 20:52:47 +08:00
|
|
|
|
return nil, translatePersistenceError(err, service.ErrSubscriptionNotFound, nil)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return userSubscriptionEntityToService(m), nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) Update(ctx context.Context, sub *service.UserSubscription) error {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
if sub == nil {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
return service.ErrSubscriptionNilInput
|
2025-12-29 10:03:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
builder := client.UserSubscription.UpdateOneID(sub.ID).
|
2025-12-29 10:03:27 +08:00
|
|
|
|
SetUserID(sub.UserID).
|
|
|
|
|
|
SetGroupID(sub.GroupID).
|
|
|
|
|
|
SetStartsAt(sub.StartsAt).
|
|
|
|
|
|
SetExpiresAt(sub.ExpiresAt).
|
|
|
|
|
|
SetStatus(sub.Status).
|
|
|
|
|
|
SetNillableDailyWindowStart(sub.DailyWindowStart).
|
|
|
|
|
|
SetNillableWeeklyWindowStart(sub.WeeklyWindowStart).
|
|
|
|
|
|
SetNillableMonthlyWindowStart(sub.MonthlyWindowStart).
|
|
|
|
|
|
SetDailyUsageUsd(sub.DailyUsageUSD).
|
|
|
|
|
|
SetWeeklyUsageUsd(sub.WeeklyUsageUSD).
|
|
|
|
|
|
SetMonthlyUsageUsd(sub.MonthlyUsageUSD).
|
|
|
|
|
|
SetNillableAssignedBy(sub.AssignedBy).
|
|
|
|
|
|
SetAssignedAt(sub.AssignedAt).
|
|
|
|
|
|
SetNotes(sub.Notes)
|
|
|
|
|
|
|
|
|
|
|
|
updated, err := builder.Save(ctx)
|
2025-12-26 15:40:24 +08:00
|
|
|
|
if err == nil {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
applyUserSubscriptionEntityToService(sub, updated)
|
|
|
|
|
|
return nil
|
2025-12-26 15:40:24 +08:00
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return translatePersistenceError(err, service.ErrSubscriptionNotFound, service.ErrSubscriptionAlreadyExists)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 20:52:47 +08:00
|
|
|
|
func (r *userSubscriptionRepository) Delete(ctx context.Context, id int64) error {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
// Match GORM semantics: deleting a missing row is not an error.
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
_, err := client.UserSubscription.Delete().Where(usersubscription.IDEQ(id)).Exec(ctx)
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return err
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) ListByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
subs, err := client.UserSubscription.Query().
|
2025-12-29 10:03:27 +08:00
|
|
|
|
Where(usersubscription.UserIDEQ(userID)).
|
|
|
|
|
|
WithGroup().
|
|
|
|
|
|
Order(dbent.Desc(usersubscription.FieldCreatedAt)).
|
|
|
|
|
|
All(ctx)
|
2025-12-26 15:40:24 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return userSubscriptionEntitiesToService(subs), nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) ListActiveByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
subs, err := client.UserSubscription.Query().
|
2025-12-29 10:03:27 +08:00
|
|
|
|
Where(
|
|
|
|
|
|
usersubscription.UserIDEQ(userID),
|
|
|
|
|
|
usersubscription.StatusEQ(service.SubscriptionStatusActive),
|
|
|
|
|
|
usersubscription.ExpiresAtGT(time.Now()),
|
|
|
|
|
|
).
|
|
|
|
|
|
WithGroup().
|
|
|
|
|
|
Order(dbent.Desc(usersubscription.FieldCreatedAt)).
|
|
|
|
|
|
All(ctx)
|
2025-12-26 15:40:24 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return userSubscriptionEntitiesToService(subs), nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]service.UserSubscription, *pagination.PaginationResult, error) {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
q := client.UserSubscription.Query().Where(usersubscription.GroupIDEQ(groupID))
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
total, err := q.Clone().Count(ctx)
|
|
|
|
|
|
if err != nil {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return nil, nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
subs, err := q.
|
|
|
|
|
|
WithUser().
|
|
|
|
|
|
WithGroup().
|
|
|
|
|
|
Order(dbent.Desc(usersubscription.FieldCreatedAt)).
|
2025-12-18 13:50:39 +08:00
|
|
|
|
Offset(params.Offset()).
|
|
|
|
|
|
Limit(params.Limit()).
|
2025-12-29 10:03:27 +08:00
|
|
|
|
All(ctx)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return userSubscriptionEntitiesToService(subs), paginationResultFromTotal(int64(total), params), nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-24 20:20:48 +08:00
|
|
|
|
func (r *userSubscriptionRepository) List(ctx context.Context, params pagination.PaginationParams, userID, groupID *int64, status, sortBy, sortOrder string) ([]service.UserSubscription, *pagination.PaginationResult, error) {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
q := client.UserSubscription.Query()
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if userID != nil {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
q = q.Where(usersubscription.UserIDEQ(*userID))
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
if groupID != nil {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
q = q.Where(usersubscription.GroupIDEQ(*groupID))
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
2026-01-24 20:20:48 +08:00
|
|
|
|
|
|
|
|
|
|
// Status filtering with real-time expiration check
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
switch status {
|
|
|
|
|
|
case service.SubscriptionStatusActive:
|
|
|
|
|
|
// Active: status is active AND not yet expired
|
|
|
|
|
|
q = q.Where(
|
|
|
|
|
|
usersubscription.StatusEQ(service.SubscriptionStatusActive),
|
|
|
|
|
|
usersubscription.ExpiresAtGT(now),
|
|
|
|
|
|
)
|
|
|
|
|
|
case service.SubscriptionStatusExpired:
|
|
|
|
|
|
// Expired: status is expired OR (status is active but already expired)
|
|
|
|
|
|
q = q.Where(
|
|
|
|
|
|
usersubscription.Or(
|
|
|
|
|
|
usersubscription.StatusEQ(service.SubscriptionStatusExpired),
|
|
|
|
|
|
usersubscription.And(
|
|
|
|
|
|
usersubscription.StatusEQ(service.SubscriptionStatusActive),
|
|
|
|
|
|
usersubscription.ExpiresAtLTE(now),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
case "":
|
|
|
|
|
|
// No filter
|
|
|
|
|
|
default:
|
|
|
|
|
|
// Other status (e.g., revoked)
|
2025-12-29 10:03:27 +08:00
|
|
|
|
q = q.Where(usersubscription.StatusEQ(status))
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
total, err := q.Clone().Count(ctx)
|
|
|
|
|
|
if err != nil {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return nil, nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-24 20:20:48 +08:00
|
|
|
|
// Apply sorting
|
|
|
|
|
|
q = q.WithUser().WithGroup().WithAssignedByUser()
|
|
|
|
|
|
|
|
|
|
|
|
// Determine sort field
|
|
|
|
|
|
var field string
|
|
|
|
|
|
switch sortBy {
|
|
|
|
|
|
case "expires_at":
|
|
|
|
|
|
field = usersubscription.FieldExpiresAt
|
|
|
|
|
|
case "status":
|
|
|
|
|
|
field = usersubscription.FieldStatus
|
|
|
|
|
|
default:
|
|
|
|
|
|
field = usersubscription.FieldCreatedAt
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Determine sort order (default: desc)
|
|
|
|
|
|
if sortOrder == "asc" && sortBy != "" {
|
|
|
|
|
|
q = q.Order(dbent.Asc(field))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
q = q.Order(dbent.Desc(field))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
subs, err := q.
|
2025-12-18 13:50:39 +08:00
|
|
|
|
Offset(params.Offset()).
|
|
|
|
|
|
Limit(params.Limit()).
|
2025-12-29 10:03:27 +08:00
|
|
|
|
All(ctx)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return userSubscriptionEntitiesToService(subs), paginationResultFromTotal(int64(total), params), nil
|
2025-12-26 15:40:24 +08:00
|
|
|
|
}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) ExistsByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
return client.UserSubscription.Query().
|
2025-12-29 10:03:27 +08:00
|
|
|
|
Where(usersubscription.UserIDEQ(userID), usersubscription.GroupIDEQ(groupID)).
|
|
|
|
|
|
Exist(ctx)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) ExtendExpiry(ctx context.Context, subscriptionID int64, newExpiresAt time.Time) error {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
_, err := client.UserSubscription.UpdateOneID(subscriptionID).
|
2025-12-29 10:03:27 +08:00
|
|
|
|
SetExpiresAt(newExpiresAt).
|
|
|
|
|
|
Save(ctx)
|
|
|
|
|
|
return translatePersistenceError(err, service.ErrSubscriptionNotFound, nil)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) UpdateStatus(ctx context.Context, subscriptionID int64, status string) error {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
_, err := client.UserSubscription.UpdateOneID(subscriptionID).
|
2025-12-29 10:03:27 +08:00
|
|
|
|
SetStatus(status).
|
|
|
|
|
|
Save(ctx)
|
|
|
|
|
|
return translatePersistenceError(err, service.ErrSubscriptionNotFound, nil)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) UpdateNotes(ctx context.Context, subscriptionID int64, notes string) error {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
_, err := client.UserSubscription.UpdateOneID(subscriptionID).
|
2025-12-29 10:03:27 +08:00
|
|
|
|
SetNotes(notes).
|
|
|
|
|
|
Save(ctx)
|
|
|
|
|
|
return translatePersistenceError(err, service.ErrSubscriptionNotFound, nil)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) ActivateWindows(ctx context.Context, id int64, start time.Time) error {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
_, err := client.UserSubscription.UpdateOneID(id).
|
2025-12-29 10:03:27 +08:00
|
|
|
|
SetDailyWindowStart(start).
|
|
|
|
|
|
SetWeeklyWindowStart(start).
|
|
|
|
|
|
SetMonthlyWindowStart(start).
|
|
|
|
|
|
Save(ctx)
|
|
|
|
|
|
return translatePersistenceError(err, service.ErrSubscriptionNotFound, nil)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) ResetDailyUsage(ctx context.Context, id int64, newWindowStart time.Time) error {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
_, err := client.UserSubscription.UpdateOneID(id).
|
2025-12-29 10:03:27 +08:00
|
|
|
|
SetDailyUsageUsd(0).
|
|
|
|
|
|
SetDailyWindowStart(newWindowStart).
|
|
|
|
|
|
Save(ctx)
|
|
|
|
|
|
return translatePersistenceError(err, service.ErrSubscriptionNotFound, nil)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) ResetWeeklyUsage(ctx context.Context, id int64, newWindowStart time.Time) error {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
_, err := client.UserSubscription.UpdateOneID(id).
|
2025-12-29 10:03:27 +08:00
|
|
|
|
SetWeeklyUsageUsd(0).
|
|
|
|
|
|
SetWeeklyWindowStart(newWindowStart).
|
|
|
|
|
|
Save(ctx)
|
|
|
|
|
|
return translatePersistenceError(err, service.ErrSubscriptionNotFound, nil)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) ResetMonthlyUsage(ctx context.Context, id int64, newWindowStart time.Time) error {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
_, err := client.UserSubscription.UpdateOneID(id).
|
2025-12-29 10:03:27 +08:00
|
|
|
|
SetMonthlyUsageUsd(0).
|
|
|
|
|
|
SetMonthlyWindowStart(newWindowStart).
|
|
|
|
|
|
Save(ctx)
|
|
|
|
|
|
return translatePersistenceError(err, service.ErrSubscriptionNotFound, nil)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-31 22:48:35 +08:00
|
|
|
|
// IncrementUsage 原子性地累加订阅用量。
|
|
|
|
|
|
// 限额检查已在请求前由 BillingCacheService.CheckBillingEligibility 完成,
|
|
|
|
|
|
// 此处仅负责记录实际消费,确保消费数据的完整性。
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (r *userSubscriptionRepository) IncrementUsage(ctx context.Context, id int64, costUSD float64) error {
|
2025-12-31 22:48:35 +08:00
|
|
|
|
const updateSQL = `
|
2025-12-31 14:11:57 +08:00
|
|
|
|
UPDATE user_subscriptions us
|
|
|
|
|
|
SET
|
|
|
|
|
|
daily_usage_usd = us.daily_usage_usd + $1,
|
|
|
|
|
|
weekly_usage_usd = us.weekly_usage_usd + $1,
|
|
|
|
|
|
monthly_usage_usd = us.monthly_usage_usd + $1,
|
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
|
FROM groups g
|
|
|
|
|
|
WHERE us.id = $2
|
|
|
|
|
|
AND us.deleted_at IS NULL
|
|
|
|
|
|
AND us.group_id = g.id
|
|
|
|
|
|
AND g.deleted_at IS NULL
|
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
|
|
client := clientFromContext(ctx, r.client)
|
2025-12-31 22:48:35 +08:00
|
|
|
|
result, err := client.ExecContext(ctx, updateSQL, costUSD, id)
|
2025-12-31 14:11:57 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
affected, err := result.RowsAffected()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if affected > 0 {
|
2025-12-31 22:48:35 +08:00
|
|
|
|
return nil
|
2025-12-31 14:11:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-31 22:48:35 +08:00
|
|
|
|
// affected == 0:订阅不存在或已删除
|
|
|
|
|
|
return service.ErrSubscriptionNotFound
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 20:52:47 +08:00
|
|
|
|
func (r *userSubscriptionRepository) BatchUpdateExpiredStatus(ctx context.Context) (int64, error) {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
n, err := client.UserSubscription.Update().
|
2025-12-29 10:03:27 +08:00
|
|
|
|
Where(
|
|
|
|
|
|
usersubscription.StatusEQ(service.SubscriptionStatusActive),
|
|
|
|
|
|
usersubscription.ExpiresAtLTE(time.Now()),
|
|
|
|
|
|
).
|
|
|
|
|
|
SetStatus(service.SubscriptionStatusExpired).
|
|
|
|
|
|
Save(ctx)
|
|
|
|
|
|
return int64(n), err
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
// Extra repository helpers (currently used only by integration tests).
|
|
|
|
|
|
|
|
|
|
|
|
func (r *userSubscriptionRepository) ListExpired(ctx context.Context) ([]service.UserSubscription, error) {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
subs, err := client.UserSubscription.Query().
|
2025-12-29 10:03:27 +08:00
|
|
|
|
Where(
|
|
|
|
|
|
usersubscription.StatusEQ(service.SubscriptionStatusActive),
|
|
|
|
|
|
usersubscription.ExpiresAtLTE(time.Now()),
|
|
|
|
|
|
).
|
|
|
|
|
|
All(ctx)
|
2025-12-26 15:40:24 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return userSubscriptionEntitiesToService(subs), nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 20:52:47 +08:00
|
|
|
|
func (r *userSubscriptionRepository) CountByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
count, err := client.UserSubscription.Query().Where(usersubscription.GroupIDEQ(groupID)).Count(ctx)
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return int64(count), err
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 20:52:47 +08:00
|
|
|
|
func (r *userSubscriptionRepository) CountActiveByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
count, err := client.UserSubscription.Query().
|
2025-12-29 10:03:27 +08:00
|
|
|
|
Where(
|
|
|
|
|
|
usersubscription.GroupIDEQ(groupID),
|
|
|
|
|
|
usersubscription.StatusEQ(service.SubscriptionStatusActive),
|
|
|
|
|
|
usersubscription.ExpiresAtGT(time.Now()),
|
|
|
|
|
|
).
|
|
|
|
|
|
Count(ctx)
|
|
|
|
|
|
return int64(count), err
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 20:52:47 +08:00
|
|
|
|
func (r *userSubscriptionRepository) DeleteByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
2025-12-31 14:11:57 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
|
|
|
|
|
n, err := client.UserSubscription.Delete().Where(usersubscription.GroupIDEQ(groupID)).Exec(ctx)
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return int64(n), err
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
2025-12-26 15:40:24 +08:00
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
func userSubscriptionEntityToService(m *dbent.UserSubscription) *service.UserSubscription {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
if m == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
out := &service.UserSubscription{
|
2025-12-26 15:40:24 +08:00
|
|
|
|
ID: m.ID,
|
|
|
|
|
|
UserID: m.UserID,
|
|
|
|
|
|
GroupID: m.GroupID,
|
|
|
|
|
|
StartsAt: m.StartsAt,
|
|
|
|
|
|
ExpiresAt: m.ExpiresAt,
|
|
|
|
|
|
Status: m.Status,
|
|
|
|
|
|
DailyWindowStart: m.DailyWindowStart,
|
|
|
|
|
|
WeeklyWindowStart: m.WeeklyWindowStart,
|
|
|
|
|
|
MonthlyWindowStart: m.MonthlyWindowStart,
|
2025-12-29 10:03:27 +08:00
|
|
|
|
DailyUsageUSD: m.DailyUsageUsd,
|
|
|
|
|
|
WeeklyUsageUSD: m.WeeklyUsageUsd,
|
|
|
|
|
|
MonthlyUsageUSD: m.MonthlyUsageUsd,
|
2025-12-26 15:40:24 +08:00
|
|
|
|
AssignedBy: m.AssignedBy,
|
|
|
|
|
|
AssignedAt: m.AssignedAt,
|
2025-12-29 10:03:27 +08:00
|
|
|
|
Notes: derefString(m.Notes),
|
2025-12-26 15:40:24 +08:00
|
|
|
|
CreatedAt: m.CreatedAt,
|
|
|
|
|
|
UpdatedAt: m.UpdatedAt,
|
|
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
if m.Edges.User != nil {
|
|
|
|
|
|
out.User = userEntityToService(m.Edges.User)
|
|
|
|
|
|
}
|
|
|
|
|
|
if m.Edges.Group != nil {
|
|
|
|
|
|
out.Group = groupEntityToService(m.Edges.Group)
|
|
|
|
|
|
}
|
|
|
|
|
|
if m.Edges.AssignedByUser != nil {
|
|
|
|
|
|
out.AssignedByUser = userEntityToService(m.Edges.AssignedByUser)
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
2025-12-26 15:40:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
func userSubscriptionEntitiesToService(models []*dbent.UserSubscription) []service.UserSubscription {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
out := make([]service.UserSubscription, 0, len(models))
|
|
|
|
|
|
for i := range models {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
if s := userSubscriptionEntityToService(models[i]); s != nil {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
out = append(out, *s)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
func applyUserSubscriptionEntityToService(dst *service.UserSubscription, src *dbent.UserSubscription) {
|
|
|
|
|
|
if dst == nil || src == nil {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
dst.ID = src.ID
|
|
|
|
|
|
dst.CreatedAt = src.CreatedAt
|
|
|
|
|
|
dst.UpdatedAt = src.UpdatedAt
|
2025-12-26 15:40:24 +08:00
|
|
|
|
}
|