Files
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

172 lines
6.6 KiB
C#

using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Commerce;
using Tiku.Domain.Commerce;
namespace Tiku.Infrastructure.Commerce;
internal sealed class CommerceOrderService(CommerceServiceDependencies dependencies)
: CommerceServiceBase(dependencies), ICommerceOrderService
{
public async Task<CommerceOrderItem> CreateOrderAsync(
CommerceActor actor,
CreateCommerceOrderCommand command,
CancellationToken cancellationToken = default)
{
if (command.Quantity is < 1 or > 99)
throw new CommerceException("Quantity must be between 1 and 99.", "invalid_quantity");
await AssertActiveMemberAsync(actor, cancellationToken);
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");
if (plan.CouponOnly &&
string.IsNullOrWhiteSpace(command.CouponCode) &&
!command.CouponRedemptionId.HasValue)
throw new CommerceException("This SVIP plan requires a coupon.", "coupon_required");
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");
}
await using var transaction = commercePersistence.Database.IsRelational()
? await commercePersistence.Database.BeginTransactionAsync(cancellationToken)
: null;
var originalAmountCents = checked(plan.PriceCents * command.Quantity);
var coupon = await ApplyCouponForOrderAsync(
actor,
command,
plan,
originalAmountCents,
cancellationToken);
if (plan.CouponOnly && coupon is null)
throw new CommerceException("This SVIP plan requires a coupon.", "coupon_required");
var amountCents = Math.Max(0, originalAmountCents - (coupon?.DiscountCents ?? 0));
var order = new Order
{
TenantId = actor.TenantId,
UserId = actor.UserId,
PlanId = plan.Id,
RegionId = command.RegionId ?? plan.RegionId,
OrderNo = GenerateOrderNo(),
Status = OrderStatus.Pending,
ProductType = "svip",
ProductName = plan.Name,
AmountCents = amountCents,
PayMethod = NormalizeMethod(command.PayMethod),
PayProvider = NormalizeProvider(command.PayProvider),
Days = checked(plan.Days * command.Quantity),
RawPayload = JsonSerializer.SerializeToElement(new
{
source = "student_checkout",
command.Quantity,
requestedCouponCode = command.CouponCode,
requestedCouponRedemptionId = command.CouponRedemptionId,
plan.PriceCents,
plan.OriginalPriceCents,
originalAmountCents,
discountCents = coupon?.DiscountCents ?? 0,
couponId = coupon?.Coupon.Id,
couponCode = coupon?.Coupon.Code,
couponRedemptionId = coupon?.Redemption.Id
})
};
commercePersistence.Orders.Add(order);
commercePersistence.OrderItems.Add(new OrderItem
{
TenantId = actor.TenantId,
OrderId = order.Id,
ItemType = "svip_plan",
ItemId = plan.Id,
Name = plan.Name,
Quantity = command.Quantity,
UnitAmountCents = plan.PriceCents,
TotalAmountCents = originalAmountCents,
Metadata = JsonSerializer.SerializeToElement(new
{
plan.Days,
plan.RegionId,
plan.VpProductId
})
});
if (coupon is not null)
{
coupon.Redemption.Status = CouponRedemptionStatus.Used;
coupon.Redemption.OrderId = order.Id;
coupon.Redemption.DiscountAppliedCents = coupon.DiscountCents;
coupon.Redemption.UsedAt = DateTimeOffset.UtcNow;
}
if (amountCents == 0)
{
var payment = new Payment
{
TenantId = actor.TenantId,
OrderId = order.Id,
Provider = "manual",
Method = "zero_amount",
Status = PaymentStatus.Pending,
AmountCents = 0
};
commercePersistence.Payments.Add(payment);
await MarkPaidAsync(
actor,
order,
payment,
$"zero-{order.OrderNo}",
order.RawPayload,
"zero_amount_paid",
$"zero-{order.OrderNo}",
true,
DateTimeOffset.UtcNow,
cancellationToken);
}
await commercePersistence.SaveChangesAsync(cancellationToken);
if (transaction is not null) await transaction.CommitAsync(cancellationToken);
return ToOrderItem(order);
}
public async Task<CommerceOrderList> GetOrdersAsync(
CommerceActor actor,
CommerceOrderQuery query,
CancellationToken cancellationToken = default)
{
await AssertActiveMemberAsync(actor, cancellationToken);
var orders = commercePersistence.Orders.AsNoTracking()
.Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId);
if (!string.IsNullOrWhiteSpace(query.Status))
orders = orders.Where(item => item.Status == ParseOrderStatus(query.Status));
var items = await orders
.OrderByDescending(item => item.CreatedAt)
.Take(Math.Clamp(query.Limit ?? 20, 1, 100))
.ToArrayAsync(cancellationToken);
return new CommerceOrderList(items.Select(ToOrderItem).ToArray());
}
public async Task<CommerceOrderItem> GetOrderAsync(
CommerceActor actor,
string orderNo,
CancellationToken cancellationToken = default)
{
await AssertActiveMemberAsync(actor, cancellationToken);
var order = await FindActorOrderAsync(actor, orderNo, cancellationToken);
return ToOrderItem(order);
}
}