130 lines
5.4 KiB
C#
130 lines
5.4 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Tiku.Application.Commerce;
|
|
using Tiku.Domain.Commerce;
|
|
|
|
namespace Tiku.Infrastructure.Commerce;
|
|
|
|
internal sealed class CommerceCouponService(CommerceServiceDependencies dependencies)
|
|
: CommerceServiceBase(dependencies), ICommerceCouponService
|
|
{
|
|
public async Task<CommerceCouponItem> ClaimCouponAsync(
|
|
CommerceActor actor,
|
|
ClaimCommerceCouponCommand command,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await AssertActiveMemberAsync(actor, cancellationToken);
|
|
var coupon = await FindCouponByCodeAsync(actor.TenantId, command.CouponCode, cancellationToken);
|
|
ValidateCouponClaimable(coupon, null);
|
|
|
|
var existing = await commercePersistence.CouponRedemptions
|
|
.AsNoTracking()
|
|
.Where(item =>
|
|
item.TenantId == actor.TenantId &&
|
|
item.UserId == actor.UserId &&
|
|
item.CouponId == coupon.Id)
|
|
.OrderByDescending(item => item.CreatedAt)
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
if (existing is not null) return ToCouponItem(coupon, existing, null);
|
|
|
|
if (coupon.MaxUses.HasValue && coupon.UsedCount >= coupon.MaxUses.Value)
|
|
throw new CommerceException("Coupon usage limit has been reached.", "coupon_usage_limit_reached");
|
|
|
|
var redemption = new CouponRedemption
|
|
{
|
|
TenantId = actor.TenantId,
|
|
CouponId = coupon.Id,
|
|
UserId = actor.UserId,
|
|
PlanId = coupon.PlanId,
|
|
CouponCode = coupon.Code,
|
|
Status = CouponRedemptionStatus.Claimed,
|
|
Source = "student_claim",
|
|
ClaimedAt = DateTimeOffset.UtcNow
|
|
};
|
|
coupon.UsedCount += 1;
|
|
commercePersistence.CouponRedemptions.Add(redemption);
|
|
await commercePersistence.SaveChangesAsync(cancellationToken);
|
|
return ToCouponItem(coupon, redemption, null);
|
|
}
|
|
|
|
public async Task<CommerceCouponList> GetCouponsAsync(
|
|
CommerceActor actor,
|
|
CommerceCouponQuery query,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await AssertActiveMemberAsync(actor, cancellationToken);
|
|
var redemptions = commercePersistence.CouponRedemptions
|
|
.AsNoTracking()
|
|
.Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId);
|
|
if (!string.IsNullOrWhiteSpace(query.Status))
|
|
redemptions = redemptions.Where(item => item.Status == ParseCouponRedemptionStatus(query.Status));
|
|
|
|
var items = await redemptions
|
|
.OrderByDescending(item => item.CreatedAt)
|
|
.Take(Math.Clamp(query.Limit ?? 50, 1, 100))
|
|
.ToArrayAsync(cancellationToken);
|
|
var couponIds = items
|
|
.Where(item => item.CouponId.HasValue)
|
|
.Select(item => item.CouponId!.Value)
|
|
.Distinct()
|
|
.ToArray();
|
|
var coupons = await commercePersistence.Coupons
|
|
.AsNoTracking()
|
|
.Where(item => item.TenantId == actor.TenantId && couponIds.Contains(item.Id))
|
|
.ToDictionaryAsync(item => item.Id, cancellationToken);
|
|
|
|
return new CommerceCouponList(
|
|
items.Select(item =>
|
|
{
|
|
coupons.TryGetValue(item.CouponId ?? Guid.Empty, out var coupon);
|
|
return ToCouponItem(coupon, item, item.DiscountAppliedCents);
|
|
}).ToArray());
|
|
}
|
|
|
|
public async Task<CommerceCouponCheckResult> CheckCouponAsync(
|
|
CommerceActor actor,
|
|
CheckCommerceCouponCommand command,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await AssertActiveMemberAsync(actor, cancellationToken);
|
|
if (command.Quantity is < 1 or > 99)
|
|
throw new CommerceException("Quantity must be between 1 and 99.", "invalid_quantity");
|
|
|
|
var plan = await commercePersistence.SvipPlans
|
|
.AsNoTracking()
|
|
.SingleOrDefaultAsync(item =>
|
|
item.TenantId == actor.TenantId &&
|
|
item.Id == command.PlanId &&
|
|
item.IsActive,
|
|
cancellationToken)
|
|
?? throw new CommerceException("SVIP plan was not found.", "svip_plan_not_found");
|
|
var originalAmountCents = checked(plan.PriceCents * command.Quantity);
|
|
try
|
|
{
|
|
var coupon = await ResolveCouponForCheckAsync(actor, command, plan, originalAmountCents, cancellationToken);
|
|
return new CommerceCouponCheckResult(
|
|
true,
|
|
null,
|
|
coupon.Coupon.Id,
|
|
coupon.Redemption.Id,
|
|
coupon.Coupon.Code,
|
|
originalAmountCents,
|
|
coupon.DiscountCents,
|
|
Math.Max(0, originalAmountCents - coupon.DiscountCents),
|
|
FormatCny(Math.Max(0, originalAmountCents - coupon.DiscountCents)));
|
|
}
|
|
catch (CommerceException exception) when (exception.Code.StartsWith("coupon_", StringComparison.Ordinal))
|
|
{
|
|
return new CommerceCouponCheckResult(
|
|
false,
|
|
exception.Code,
|
|
null,
|
|
command.CouponRedemptionId,
|
|
command.CouponCode,
|
|
originalAmountCents,
|
|
0,
|
|
originalAmountCents,
|
|
FormatCny(originalAmountCents));
|
|
}
|
|
}
|
|
}
|