using Microsoft.EntityFrameworkCore; using Tiku.Application.Commerce; using Tiku.Application.Points; using Tiku.Domain.Commerce; using Tiku.Infrastructure.Commerce; namespace Tiku.Infrastructure.Points; internal sealed class PointAdministrationService(CommerceAdministrationDependencies dependencies) : CommerceAdministrationServiceBase(dependencies), IPointAdministrationService { public async Task GetPointTasksAsync( CommerceAdminActor actor, TenantPointQuery query, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); var tasks = pointsPersistence.PointActivityTasks.AsNoTracking() .Where(item => item.TenantId == actor.TenantId); if (!string.IsNullOrWhiteSpace(query.Status)) tasks = tasks.Where(item => item.Status == ParsePointTaskStatus(query.Status)); var items = await tasks .OrderBy(item => item.SortOrder) .ThenByDescending(item => item.CreatedAt) .Take(Math.Clamp(query.Limit ?? 50, 1, 200)) .ToArrayAsync(cancellationToken); return new TenantPointTaskList(items); } public async Task UpsertPointTaskAsync( CommerceAdminActor actor, UpsertPointTaskCommand command, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); if (command.Points <= 0 || command.MaxClaimsPerUser <= 0) throw new CommerceException("Point task points and claim limit must be positive.", "invalid_point_task"); var task = command.Id.HasValue ? await pointsPersistence.PointActivityTasks.SingleOrDefaultAsync( item => item.TenantId == actor.TenantId && item.Id == command.Id.Value, cancellationToken) : await pointsPersistence.PointActivityTasks.SingleOrDefaultAsync( item => item.TenantId == actor.TenantId && item.TaskKey == command.TaskKey.Trim(), cancellationToken); if (task is null) { task = new PointActivityTask { TenantId = actor.TenantId }; pointsPersistence.PointActivityTasks.Add(task); } task.TaskKey = command.TaskKey.Trim(); task.Title = command.Title.Trim(); task.Description = command.Description?.Trim(); task.TaskType = command.TaskType; task.Status = command.Status; task.Points = command.Points; task.MaxClaimsPerUser = command.MaxClaimsPerUser; task.StartsAt = command.StartsAt; task.EndsAt = command.EndsAt; task.SortOrder = command.SortOrder; task.Rules = JsonObjectOrDefault(command.Rules); task.Metadata = JsonObjectOrDefault(command.Metadata); await unitOfWork.SaveChangesAsync(cancellationToken); return task; } public async Task GetPointClaimsAsync( CommerceAdminActor actor, TenantPointQuery query, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); var claims = pointsPersistence.PointActivityClaims.AsNoTracking() .Where(item => item.TenantId == actor.TenantId); if (query.UserId.HasValue) claims = claims.Where(item => item.UserId == query.UserId.Value); var items = await claims .OrderByDescending(item => item.CreatedAt) .Take(Math.Clamp(query.Limit ?? 50, 1, 200)) .ToArrayAsync(cancellationToken); return new TenantPointClaimList(items); } public async Task GetPointExchangeItemsAsync( CommerceAdminActor actor, TenantPointQuery query, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); var items = pointsPersistence.PointExchangeItems.AsNoTracking() .Where(item => item.TenantId == actor.TenantId); if (!string.IsNullOrWhiteSpace(query.Status)) items = items.Where(item => item.Status == ParsePointExchangeItemStatus(query.Status)); if (query.RegionId.HasValue) items = items.Where(item => item.RegionId == null || item.RegionId == query.RegionId.Value); var result = await items .OrderBy(item => item.SortOrder) .ThenByDescending(item => item.CreatedAt) .Take(Math.Clamp(query.Limit ?? 50, 1, 200)) .ToArrayAsync(cancellationToken); return new TenantPointExchangeItemList(result); } public async Task UpsertPointExchangeItemAsync( CommerceAdminActor actor, UpsertPointExchangeItemCommand command, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); if (command.PointsCost <= 0) throw new CommerceException("Point exchange item cost must be positive.", "invalid_point_exchange_item"); var item = command.Id.HasValue ? await pointsPersistence.PointExchangeItems.SingleOrDefaultAsync( entry => entry.TenantId == actor.TenantId && entry.Id == command.Id.Value, cancellationToken) : await pointsPersistence.PointExchangeItems.SingleOrDefaultAsync( entry => entry.TenantId == actor.TenantId && entry.ItemKey == command.ItemKey.Trim(), cancellationToken); if (item is null) { item = new PointExchangeItem { TenantId = actor.TenantId }; pointsPersistence.PointExchangeItems.Add(item); } item.RegionId = command.RegionId; item.ItemKey = command.ItemKey.Trim(); item.Name = command.Name.Trim(); item.Description = command.Description?.Trim(); item.ItemType = command.ItemType; item.Status = command.Status; item.PointsCost = command.PointsCost; item.Stock = command.Stock; item.Days = command.Days; item.SortOrder = command.SortOrder; item.StartsAt = command.StartsAt; item.EndsAt = command.EndsAt; item.FulfillmentPayload = JsonObjectOrDefault(command.FulfillmentPayload); item.Metadata = JsonObjectOrDefault(command.Metadata); await unitOfWork.SaveChangesAsync(cancellationToken); return item; } public async Task GetPointExchangeOrdersAsync( CommerceAdminActor actor, TenantPointQuery query, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); var orders = pointsPersistence.PointExchangeOrders.AsNoTracking() .Where(item => item.TenantId == actor.TenantId); if (query.UserId.HasValue) orders = orders.Where(item => item.UserId == query.UserId.Value); if (!string.IsNullOrWhiteSpace(query.Status)) orders = orders.Where(item => item.Status == ParsePointExchangeOrderStatus(query.Status)); var result = await orders .OrderByDescending(item => item.CreatedAt) .Take(Math.Clamp(query.Limit ?? 50, 1, 200)) .ToArrayAsync(cancellationToken); return new TenantPointExchangeOrderList(result); } public async Task UpdatePointExchangeOrderStatusAsync( CommerceAdminActor actor, UpdatePointExchangeOrderStatusCommand command, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); var order = await pointsPersistence.PointExchangeOrders .SingleOrDefaultAsync(item => item.TenantId == actor.TenantId && item.Id == command.OrderId, cancellationToken) ?? throw new CommerceException("Point exchange order was not found.", "point_exchange_order_not_found"); order.Status = command.Status; if (command.Status == PointExchangeOrderStatus.Completed) { order.CompletedAt ??= DateTimeOffset.UtcNow; order.CancelledAt = null; } else if (command.Status == PointExchangeOrderStatus.Cancelled) { order.CancelledAt ??= DateTimeOffset.UtcNow; } await unitOfWork.SaveChangesAsync(cancellationToken); return order; } }