129 lines
5.4 KiB
C#
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);
|
|
}
|
|
}
|