Files
tiku-backend.net/Tiku.Infrastructure/Commerce/ActivationCodes/ActivationCodeAdministrationService.cs
xiong 33375a38d7
Some checks failed
ci / release-gate (push) Has been cancelled
refactor(architecture): harden module boundaries
2026-08-04 12:10:36 +08:00

129 lines
5.4 KiB
C#

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<CodeBatchItem> 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<ActivationCodeList> 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<ActivationCodeItem> 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);
}
}