using System.Text.Json; using Microsoft.EntityFrameworkCore; using Tiku.Application.Commerce; using Tiku.Domain.Commerce; using Tiku.Domain.Tenancy; namespace Tiku.Infrastructure.Commerce; internal sealed class ActivationCodeAdministrationService(CommerceAdministrationDependencies dependencies) : CommerceAdministrationServiceBase(dependencies), IActivationCodeAdministrationService { public async Task CreateCodeBatchAsync( CommerceAdminActor actor, CreateCodeBatchCommand command, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); if (command.TotalCount is < 1 or > 1000) throw new CommerceException("Code batch total count must be between 1 and 1000.", "invalid_code_batch_count"); if (command.Days <= 0) throw new CommerceException("Activation code days must be positive.", "invalid_activation_days"); if (command.RegionId.HasValue) { var regionExists = await catalogPersistence.Regions .AnyAsync(item => item.TenantId == actor.TenantId && item.Id == command.RegionId.Value, cancellationToken); if (!regionExists) throw new CommerceException("Region was not found.", "region_not_found"); } var batch = new CodeBatch { TenantId = actor.TenantId, RegionId = command.RegionId, CreatedBy = actor.UserId, Name = command.Name.Trim(), SaleType = command.SaleType?.Trim(), Channel = command.Channel?.Trim(), DefaultUnitPriceCents = command.DefaultUnitPriceCents ?? 0, CostPriceCents = command.CostPriceCents ?? 0, TotalCount = command.TotalCount, Days = command.Days, IssuedAt = DateTimeOffset.UtcNow, Remark = command.Remark }; commercePersistence.CodeBatches.Add(batch); for (var index = 0; index < command.TotalCount; index++) commercePersistence.ActivationCodes.Add(new ActivationCode { TenantId = actor.TenantId, BatchId = batch.Id, Code = GenerateActivationCode(), Days = command.Days, SaleType = batch.SaleType, UnitPriceCents = batch.DefaultUnitPriceCents, Remark = batch.Remark }); await unitOfWork.SaveChangesAsync(cancellationToken); return ToCodeBatchItem(batch); } public async Task GetActivationCodesAsync( CommerceAdminActor actor, CommerceAdminQuery query, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); var codes = commercePersistence.ActivationCodes.AsNoTracking() .Where(item => item.TenantId == actor.TenantId); if (!string.IsNullOrWhiteSpace(query.Status)) { var used = string.Equals(query.Status, "used", StringComparison.OrdinalIgnoreCase); codes = codes.Where(item => item.IsUsed == used); } var items = await codes .OrderByDescending(item => item.CreatedAt) .Take(Math.Clamp(query.Limit ?? 50, 1, 200)) .ToArrayAsync(cancellationToken); return new ActivationCodeList(items.Select(ToActivationCodeItem).ToArray()); } public async Task RedeemActivationCodeAsync( CommerceAdminActor actor, RedeemActivationCodeCommand command, CancellationToken cancellationToken = default) { await AssertAdminAsync(actor, cancellationToken); var code = await commercePersistence.ActivationCodes .SingleOrDefaultAsync(item => item.TenantId == actor.TenantId && item.Code == command.Code.Trim(), cancellationToken) ?? throw new CommerceException("Activation code was not found.", "activation_code_not_found"); if (code.IsUsed) throw new CommerceException("Activation code has already been used.", "activation_code_used"); var userIsMember = await identityPersistence.TenantMemberships.AnyAsync(item => item.TenantId == actor.TenantId && item.UserId == command.UserId && item.Status == MembershipStatus.Active, cancellationToken); if (!userIsMember) throw new CommerceException("Target user is not a tenant member.", "tenant_member_not_found"); code.IsUsed = true; code.UsedBy = command.UserId; code.UsedRegionId = command.RegionId; code.UsedAt = DateTimeOffset.UtcNow; commercePersistence.Entitlements.Add(new Entitlement { TenantId = actor.TenantId, UserId = command.UserId, EntitlementType = "svip", SourceType = "activation_code", SourceId = code.Id, StartsAt = DateTimeOffset.UtcNow, ExpiresAt = DateTimeOffset.UtcNow.AddDays(code.Days), Status = EntitlementStatus.Active, Metadata = JsonSerializer.SerializeToElement(new { code.Code, code.BatchId }) }); await unitOfWork.SaveChangesAsync(cancellationToken); return ToActivationCodeItem(code); } }