From 0bd075e20da4ae94a577a95215cf84f1a8abed1d Mon Sep 17 00:00:00 2001 From: xiong Date: Tue, 4 Aug 2026 09:29:20 +0800 Subject: [PATCH] refactor(commerce): split administration services --- .../ApiPresentationExtensions.cs | 1 + .../ActivationCodeAdministrationController.cs | 64 ++ .../CommerceAdjustmentController.cs | 115 ++++ .../Controllers/CommerceAdminActorResolver.cs | 28 + Tiku.Api/Controllers/CommerceController.cs | 8 +- .../CommerceOrderAdministrationController.cs | 48 ++ .../CouponAdministrationController.cs | 78 +++ .../PaymentConfigurationController.cs | 64 ++ .../PointAdministrationController.cs | 120 ++++ .../ReconciliationAdministrationController.cs | 162 +++++ .../RefundAdministrationController.cs | 74 +++ .../Controllers/TenantCommerceController.cs | 589 ------------------ .../Commerce/CommerceAdminModels.cs | 103 ++- .../Points/IPointAdministrationService.cs | 42 ++ ...=> ActivationCodeAdministrationService.cs} | 5 +- ...tments.cs => CommerceAdjustmentService.cs} | 229 +------ .../Commerce/CommerceAdminService.cs | 17 - ...pons.cs => CouponAdministrationService.cs} | 5 +- ...mmerceAdministrationFoundation.Helpers.cs} | 80 +-- .../CommerceAdministrationFoundation.cs | 25 + ... => CommerceOrderAdministrationService.cs} | 5 +- ...tion.cs => PaymentConfigurationService.cs} | 5 +- ...liationAdministrationService.Operations.cs | 170 +++++ ...=> ReconciliationAdministrationService.cs} | 5 +- ...fundAdministrationService.Notifications.cs | 72 +++ ...unds.cs => RefundAdministrationService.cs} | 5 +- Tiku.Infrastructure/Modules/CommerceModule.cs | 10 +- .../PointAdministrationService.cs} | 9 +- .../ArchitectureBoundaryTests.cs | 1 - 29 files changed, 1187 insertions(+), 952 deletions(-) create mode 100644 Tiku.Api/Controllers/ActivationCodeAdministrationController.cs create mode 100644 Tiku.Api/Controllers/CommerceAdjustmentController.cs create mode 100644 Tiku.Api/Controllers/CommerceAdminActorResolver.cs create mode 100644 Tiku.Api/Controllers/CommerceOrderAdministrationController.cs create mode 100644 Tiku.Api/Controllers/CouponAdministrationController.cs create mode 100644 Tiku.Api/Controllers/PaymentConfigurationController.cs create mode 100644 Tiku.Api/Controllers/PointAdministrationController.cs create mode 100644 Tiku.Api/Controllers/ReconciliationAdministrationController.cs create mode 100644 Tiku.Api/Controllers/RefundAdministrationController.cs delete mode 100644 Tiku.Api/Controllers/TenantCommerceController.cs create mode 100644 Tiku.Application/Points/IPointAdministrationService.cs rename Tiku.Infrastructure/Commerce/ActivationCodes/{CommerceAdminService.ActivationCodes.cs => ActivationCodeAdministrationService.cs} (96%) rename Tiku.Infrastructure/Commerce/Adjustments/{CommerceAdminService.Adjustments.cs => CommerceAdjustmentService.cs} (52%) delete mode 100644 Tiku.Infrastructure/Commerce/CommerceAdminService.cs rename Tiku.Infrastructure/Commerce/Coupons/{CommerceAdminService.Coupons.cs => CouponAdministrationService.cs} (95%) rename Tiku.Infrastructure/Commerce/Foundation/{CommerceAdminService.Foundation.cs => CommerceAdministrationFoundation.Helpers.cs} (85%) create mode 100644 Tiku.Infrastructure/Commerce/Foundation/CommerceAdministrationFoundation.cs rename Tiku.Infrastructure/Commerce/Orders/{CommerceAdminService.Orders.cs => CommerceOrderAdministrationService.cs} (93%) rename Tiku.Infrastructure/Commerce/PaymentConfiguration/{CommerceAdminService.PaymentConfiguration.cs => PaymentConfigurationService.cs} (94%) create mode 100644 Tiku.Infrastructure/Commerce/Reconciliation/ReconciliationAdministrationService.Operations.cs rename Tiku.Infrastructure/Commerce/Reconciliation/{CommerceAdminService.Reconciliation.cs => ReconciliationAdministrationService.cs} (96%) create mode 100644 Tiku.Infrastructure/Commerce/Refunds/RefundAdministrationService.Notifications.cs rename Tiku.Infrastructure/Commerce/Refunds/{CommerceAdminService.Refunds.cs => RefundAdministrationService.cs} (97%) rename Tiku.Infrastructure/{Commerce/Points/CommerceAdminService.Points.cs => Points/PointAdministrationService.cs} (96%) diff --git a/Tiku.Api/Configuration/ApiPresentationExtensions.cs b/Tiku.Api/Configuration/ApiPresentationExtensions.cs index ae734b6..2507a9b 100644 --- a/Tiku.Api/Configuration/ApiPresentationExtensions.cs +++ b/Tiku.Api/Configuration/ApiPresentationExtensions.cs @@ -24,6 +24,7 @@ internal static class ApiPresentationExtensions services.AddProblemDetails(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/Tiku.Api/Controllers/ActivationCodeAdministrationController.cs b/Tiku.Api/Controllers/ActivationCodeAdministrationController.cs new file mode 100644 index 0000000..280c6ef --- /dev/null +++ b/Tiku.Api/Controllers/ActivationCodeAdministrationController.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tiku.Api.Contracts; +using Tiku.Api.Security; +using Tiku.Application.Commerce; +using Tiku.Application.Jobs; +using Tiku.Application.Points; +using Tiku.Application.Security; +using Tiku.Domain.Commerce; + +namespace Tiku.Api.Controllers; + +[ApiController] +[Tags("租户端-交易运营")] +[Authorize(Policy = BackendPermissions.TenantCommerceOperate)] +[RequireSaasFeature(SaasFeatureCatalog.StudentStore)] +[Produces("application/json")] +[Route("api/tenant/commerce")] +public sealed class ActivationCodeAdministrationController( + IActivationCodeAdministrationService service, + CommerceAdminActorResolver actorResolver) : ControllerBase +{ + [HttpPost("code-batches")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("创建兑换码批次")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CreateCodeBatch( + CreateCodeBatchDto request, + CancellationToken cancellationToken) + { + return Ok(await service.CreateCodeBatchAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } + + [HttpGet("activation-codes")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询兑换码")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> ActivationCodes( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetActivationCodesAsync( + actorResolver.Resolve(), + actorResolver.ToQuery(query), + cancellationToken)); + } + + [HttpPost("activation-codes/redeem")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("后台核销兑换码")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> RedeemActivationCode( + RedeemActivationCodeDto request, + CancellationToken cancellationToken) + { + return Ok(await service.RedeemActivationCodeAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } +} diff --git a/Tiku.Api/Controllers/CommerceAdjustmentController.cs b/Tiku.Api/Controllers/CommerceAdjustmentController.cs new file mode 100644 index 0000000..7b53538 --- /dev/null +++ b/Tiku.Api/Controllers/CommerceAdjustmentController.cs @@ -0,0 +1,115 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tiku.Api.Contracts; +using Tiku.Api.Security; +using Tiku.Application.Commerce; +using Tiku.Application.Jobs; +using Tiku.Application.Points; +using Tiku.Application.Security; +using Tiku.Domain.Commerce; + +namespace Tiku.Api.Controllers; + +[ApiController] +[Tags("租户端-交易运营")] +[Authorize(Policy = BackendPermissions.TenantCommerceOperate)] +[RequireSaasFeature(SaasFeatureCatalog.StudentStore)] +[Produces("application/json")] +[Route("api/tenant/commerce")] +public sealed class CommerceAdjustmentController( + ICommerceAdjustmentService service, + CommerceAdminActorResolver actorResolver) : ControllerBase +{ + [HttpGet("adjustment-vouchers")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询调账凭证")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> AdjustmentVouchers( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetAdjustmentVouchersAsync( + actorResolver.Resolve(), + actorResolver.ToQuery(query), + cancellationToken)); + } + + [HttpGet("adjustment-vouchers/detail")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询调账凭证详情")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> AdjustmentVoucherDetail( + [FromQuery] Guid voucherId, + CancellationToken cancellationToken) + { + return Ok(await service.GetAdjustmentVoucherAsync( + actorResolver.Resolve(), + voucherId, + cancellationToken)); + } + + [HttpPost("adjustment-vouchers")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("创建调账凭证")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CreateAdjustmentVoucher( + CreateAdjustmentVoucherDto request, + CancellationToken cancellationToken) + { + return Ok(await service.CreateAdjustmentVoucherAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } + + [HttpPost("adjustment-vouchers/status")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("审核或关闭调账凭证")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> UpdateAdjustmentVoucherStatus( + UpdateAdjustmentVoucherStatusDto request, + CancellationToken cancellationToken) + { + return Ok(await service.UpdateAdjustmentVoucherStatusAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } + + [HttpGet("adjustment-vouchers/events")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询调账凭证事件")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> AdjustmentVoucherEvents( + [FromQuery] Guid voucherId, + CancellationToken cancellationToken) + { + return Ok(await service.GetAdjustmentVoucherEventsAsync( + actorResolver.Resolve(), + voucherId, + cancellationToken)); + } + + [HttpGet("adjustment-vouchers/report")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询调账统计报告")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> AdjustmentVoucherReport(CancellationToken cancellationToken) + { + return Ok(await service.GetAdjustmentReportAsync( + actorResolver.Resolve(), + cancellationToken)); + } + + [HttpGet("reconciliation/anomalies")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询对账异常汇总")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> ReconciliationAnomalies( + CancellationToken cancellationToken) + { + return Ok(await service.GetAnomalySummaryAsync( + actorResolver.Resolve(), + cancellationToken)); + } +} diff --git a/Tiku.Api/Controllers/CommerceAdminActorResolver.cs b/Tiku.Api/Controllers/CommerceAdminActorResolver.cs new file mode 100644 index 0000000..582c46e --- /dev/null +++ b/Tiku.Api/Controllers/CommerceAdminActorResolver.cs @@ -0,0 +1,28 @@ +using Tiku.Api.Contracts; +using Tiku.Application.Commerce; +using Tiku.Application.Security; + +namespace Tiku.Api.Controllers; + +public sealed class CommerceAdminActorResolver( + ICurrentUser currentUser, + ITenantContext currentTenant) +{ + internal CommerceAdminActor Resolve() + { + if (currentTenant.TenantId is null || currentUser.UserId is null) + throw new CommerceException("Tenant commerce actor was not resolved.", "tenant_admin_access_denied"); + + return new CommerceAdminActor(currentTenant.TenantId.Value, currentUser.UserId.Value); + } + + internal CommerceAdminQuery ToQuery(TenantCommerceQueryDto query) + { + return new CommerceAdminQuery(query.Provider, query.Status, query.Limit); + } + + internal TenantPointQuery ToPointQuery(TenantCommerceQueryDto query) + { + return new TenantPointQuery(query.Status, query.Limit, query.UserId, query.RegionId); + } +} diff --git a/Tiku.Api/Controllers/CommerceController.cs b/Tiku.Api/Controllers/CommerceController.cs index adda838..50788d8 100644 --- a/Tiku.Api/Controllers/CommerceController.cs +++ b/Tiku.Api/Controllers/CommerceController.cs @@ -20,7 +20,7 @@ namespace Tiku.Api.Controllers; [Route("api/student/commerce")] public sealed class CommerceController( ICommerceService commerceService, - ICommerceAdminService commerceAdminService, + IRefundAdministrationService refundAdministrationService, ICurrentUser currentUser, ITenantContext currentTenant, ITenantContextInitializer tenantInitializer, @@ -165,7 +165,7 @@ public sealed class CommerceController( RefundNotificationDto request, CancellationToken cancellationToken) { - return Ok(await commerceAdminService.ProcessRefundNotificationAsync( + return Ok(await refundAdministrationService.ProcessRefundNotificationAsync( await ResolveNotificationTenantAsync(tenantCode, cancellationToken), request.ToCommand(PaymentProviders.WechatPay), cancellationToken)); @@ -180,7 +180,7 @@ public sealed class CommerceController( RefundNotificationDto request, CancellationToken cancellationToken) { - return Ok(await commerceAdminService.ProcessRefundNotificationAsync( + return Ok(await refundAdministrationService.ProcessRefundNotificationAsync( await ResolveNotificationTenantAsync(tenantCode, cancellationToken), request.ToCommand(PaymentProviders.Alipay), cancellationToken)); @@ -263,4 +263,4 @@ public sealed class CommerceController( return JsonDocument.Parse(rawBody); } -} \ No newline at end of file +} diff --git a/Tiku.Api/Controllers/CommerceOrderAdministrationController.cs b/Tiku.Api/Controllers/CommerceOrderAdministrationController.cs new file mode 100644 index 0000000..247743d --- /dev/null +++ b/Tiku.Api/Controllers/CommerceOrderAdministrationController.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tiku.Api.Contracts; +using Tiku.Api.Security; +using Tiku.Application.Commerce; +using Tiku.Application.Jobs; +using Tiku.Application.Points; +using Tiku.Application.Security; +using Tiku.Domain.Commerce; + +namespace Tiku.Api.Controllers; + +[ApiController] +[Tags("租户端-交易运营")] +[Authorize(Policy = BackendPermissions.TenantCommerceOperate)] +[RequireSaasFeature(SaasFeatureCatalog.StudentStore)] +[Produces("application/json")] +[Route("api/tenant/commerce")] +public sealed class CommerceOrderAdministrationController( + ICommerceOrderAdministrationService service, + CommerceAdminActorResolver actorResolver) : ControllerBase +{ + [HttpGet("orders")] + [EndpointSummary("查询租户订单")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> Orders( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetOrdersAsync( + actorResolver.Resolve(), + actorResolver.ToQuery(query), + cancellationToken)); + } + + [HttpGet("payments")] + [EndpointSummary("查询租户支付记录")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> Payments( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetPaymentsAsync( + actorResolver.Resolve(), + actorResolver.ToQuery(query), + cancellationToken)); + } +} diff --git a/Tiku.Api/Controllers/CouponAdministrationController.cs b/Tiku.Api/Controllers/CouponAdministrationController.cs new file mode 100644 index 0000000..fea72a4 --- /dev/null +++ b/Tiku.Api/Controllers/CouponAdministrationController.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tiku.Api.Contracts; +using Tiku.Api.Security; +using Tiku.Application.Commerce; +using Tiku.Application.Jobs; +using Tiku.Application.Points; +using Tiku.Application.Security; +using Tiku.Domain.Commerce; + +namespace Tiku.Api.Controllers; + +[ApiController] +[Tags("租户端-交易运营")] +[Authorize(Policy = BackendPermissions.TenantCommerceOperate)] +[RequireSaasFeature(SaasFeatureCatalog.StudentStore)] +[Produces("application/json")] +[Route("api/tenant/commerce")] +public sealed class CouponAdministrationController( + ICouponAdministrationService service, + CommerceAdminActorResolver actorResolver) : ControllerBase +{ + [HttpGet("coupons")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询租户优惠券")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> Coupons( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetCouponsAsync( + actorResolver.Resolve(), + actorResolver.ToQuery(query), + cancellationToken)); + } + + [HttpPut("coupons")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("新增或更新租户优惠券")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> UpsertCoupon( + UpsertTenantCouponDto request, + CancellationToken cancellationToken) + { + return Ok(await service.UpsertCouponAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } + + [HttpGet("coupons/redemptions")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询优惠券领取和核销记录")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CouponRedemptions( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetCouponRedemptionsAsync( + actorResolver.Resolve(), + actorResolver.ToQuery(query), + cancellationToken)); + } + + [HttpGet("coupons/report")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询优惠券基础报表")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CouponReport( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetCouponReportAsync( + actorResolver.Resolve(), + actorResolver.ToQuery(query), + cancellationToken)); + } +} diff --git a/Tiku.Api/Controllers/PaymentConfigurationController.cs b/Tiku.Api/Controllers/PaymentConfigurationController.cs new file mode 100644 index 0000000..68b4796 --- /dev/null +++ b/Tiku.Api/Controllers/PaymentConfigurationController.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tiku.Api.Contracts; +using Tiku.Api.Security; +using Tiku.Application.Commerce; +using Tiku.Application.Jobs; +using Tiku.Application.Points; +using Tiku.Application.Security; +using Tiku.Domain.Commerce; + +namespace Tiku.Api.Controllers; + +[ApiController] +[Tags("租户端-交易运营")] +[Authorize(Policy = BackendPermissions.TenantCommerceOperate)] +[RequireSaasFeature(SaasFeatureCatalog.StudentStore)] +[Produces("application/json")] +[Route("api/tenant/commerce")] +public sealed class PaymentConfigurationController( + IPaymentConfigurationService service, + CommerceAdminActorResolver actorResolver) : ControllerBase +{ + [HttpGet("payment-accounts")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询租户支付账号")] + [ProducesResponseType>(StatusCodes.Status200OK)] + public async Task>> PaymentAccounts( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetPaymentAccountsAsync( + actorResolver.Resolve(), + actorResolver.ToQuery(query), + cancellationToken)); + } + + [HttpPut("payment-accounts")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("新增或更新租户支付账号")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> UpsertPaymentAccount( + UpsertPaymentAccountDto request, + CancellationToken cancellationToken) + { + return Ok(await service.UpsertPaymentAccountAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } + + [HttpPut("secrets")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("写入或轮换租户密钥")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> UpsertSecret( + UpsertTenantSecretDto request, + CancellationToken cancellationToken) + { + return Ok(await service.UpsertTenantSecretAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } +} diff --git a/Tiku.Api/Controllers/PointAdministrationController.cs b/Tiku.Api/Controllers/PointAdministrationController.cs new file mode 100644 index 0000000..93ccfb3 --- /dev/null +++ b/Tiku.Api/Controllers/PointAdministrationController.cs @@ -0,0 +1,120 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tiku.Api.Contracts; +using Tiku.Api.Security; +using Tiku.Application.Commerce; +using Tiku.Application.Jobs; +using Tiku.Application.Points; +using Tiku.Application.Security; +using Tiku.Domain.Commerce; + +namespace Tiku.Api.Controllers; + +[ApiController] +[Tags("租户端-交易运营")] +[Authorize(Policy = BackendPermissions.TenantCommerceOperate)] +[RequireSaasFeature(SaasFeatureCatalog.StudentStore)] +[Produces("application/json")] +[Route("api/tenant/commerce")] +public sealed class PointAdministrationController( + IPointAdministrationService service, + CommerceAdminActorResolver actorResolver) : ControllerBase +{ + [HttpGet("point-activity-tasks")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询积分活动任务")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> PointTasks( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetPointTasksAsync( + actorResolver.Resolve(), + actorResolver.ToPointQuery(query), + cancellationToken)); + } + + [HttpPut("point-activity-tasks")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("新增或更新积分活动任务")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> UpsertPointTask( + UpsertPointTaskDto request, + CancellationToken cancellationToken) + { + return Ok(await service.UpsertPointTaskAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } + + [HttpGet("point-activity-claims")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询积分任务领取记录")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> PointClaims( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetPointClaimsAsync( + actorResolver.Resolve(), + actorResolver.ToPointQuery(query), + cancellationToken)); + } + + [HttpGet("point-exchange-items")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询积分兑换项")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> PointExchangeItems( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetPointExchangeItemsAsync( + actorResolver.Resolve(), + actorResolver.ToPointQuery(query), + cancellationToken)); + } + + [HttpPut("point-exchange-items")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("新增或更新积分兑换项")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> UpsertPointExchangeItem( + UpsertPointExchangeItemDto request, + CancellationToken cancellationToken) + { + return Ok(await service.UpsertPointExchangeItemAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } + + [HttpGet("point-exchange-orders")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询积分兑换订单")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> PointExchangeOrders( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetPointExchangeOrdersAsync( + actorResolver.Resolve(), + actorResolver.ToPointQuery(query), + cancellationToken)); + } + + [HttpPost("point-exchange-orders/status")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("更新积分兑换订单状态")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> UpdatePointExchangeOrderStatus( + UpdatePointExchangeOrderStatusDto request, + CancellationToken cancellationToken) + { + return Ok(await service.UpdatePointExchangeOrderStatusAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } +} diff --git a/Tiku.Api/Controllers/ReconciliationAdministrationController.cs b/Tiku.Api/Controllers/ReconciliationAdministrationController.cs new file mode 100644 index 0000000..dcb4f72 --- /dev/null +++ b/Tiku.Api/Controllers/ReconciliationAdministrationController.cs @@ -0,0 +1,162 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tiku.Api.Contracts; +using Tiku.Api.Security; +using Tiku.Application.Commerce; +using Tiku.Application.Jobs; +using Tiku.Application.Points; +using Tiku.Application.Security; +using Tiku.Domain.Commerce; + +namespace Tiku.Api.Controllers; + +[ApiController] +[Tags("租户端-交易运营")] +[Authorize(Policy = BackendPermissions.TenantCommerceOperate)] +[RequireSaasFeature(SaasFeatureCatalog.StudentStore)] +[Produces("application/json")] +[Route("api/tenant/commerce")] +public sealed class ReconciliationAdministrationController( + IReconciliationAdministrationService service, + CommerceAdminActorResolver actorResolver) : ControllerBase +{ + [HttpGet("reconciliation/batches")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询对账批次")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> ReconciliationBatches( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetReconciliationBatchesAsync( + actorResolver.Resolve(), + actorResolver.ToQuery(query), + cancellationToken)); + } + + [HttpPost("reconciliation/batches")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("创建对账批次")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CreateReconciliationBatch( + CreateReconciliationBatchDto request, + CancellationToken cancellationToken) + { + return Ok(await service.CreateReconciliationBatchAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } + + [HttpGet("reconciliation/issues")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询对账异常")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> ReconciliationIssues( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetReconciliationIssuesAsync( + actorResolver.Resolve(), + actorResolver.ToQuery(query), + cancellationToken)); + } + + [HttpGet("reconciliation/items")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询对账明细")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> ReconciliationItems( + [FromQuery] Guid batchId, + CancellationToken cancellationToken) + { + return Ok(await service.GetReconciliationItemsAsync( + actorResolver.Resolve(), + batchId, + cancellationToken)); + } + + [HttpGet("reconciliation/issues/events")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询对账工单事件")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> ReconciliationIssueEvents( + [FromQuery] Guid issueId, + CancellationToken cancellationToken) + { + return Ok(await service.GetReconciliationIssueEventsAsync( + actorResolver.Resolve(), + issueId, + cancellationToken)); + } + + [HttpPost("reconciliation/issues/status")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("更新对账异常状态")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> UpdateReconciliationIssue( + UpdateReconciliationIssueDto request, + CancellationToken cancellationToken) + { + return Ok(await service.UpdateReconciliationIssueAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } + + [HttpPost("reconciliation/preview")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("预览支付渠道账单对账")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> PreviewReconciliation( + PreviewReconciliationImportDto request, + CancellationToken cancellationToken) + { + return Ok(await service.PreviewReconciliationImportAsync( + actorResolver.Resolve(), + request.ToPreviewCommand(), + cancellationToken)); + } + + [HttpPost("reconciliation/import")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("导入支付渠道账单并创建对账批次")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> ImportReconciliation( + PreviewReconciliationImportDto request, + CancellationToken cancellationToken) + { + return Ok(await service.ImportReconciliationAsync( + actorResolver.Resolve(), + request.ToImportCommand(), + cancellationToken)); + } + + [HttpGet("reconciliation/provider-bills/jobs")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("查询渠道账单下载任务")] + [ProducesResponseType>(StatusCodes.Status200OK)] + public async Task>> ProviderBillJobs( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetProviderBillJobsAsync( + actorResolver.Resolve(), + actorResolver.ToQuery(query), + cancellationToken)); + } + + [HttpPost("reconciliation/provider-bills/request")] + [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] + [EndpointSummary("创建渠道官方账单下载任务")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> RequestProviderBill( + RequestProviderBillJobDto request, + CancellationToken cancellationToken) + { + return Ok(await service.RequestProviderBillJobAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } +} diff --git a/Tiku.Api/Controllers/RefundAdministrationController.cs b/Tiku.Api/Controllers/RefundAdministrationController.cs new file mode 100644 index 0000000..63a511d --- /dev/null +++ b/Tiku.Api/Controllers/RefundAdministrationController.cs @@ -0,0 +1,74 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Tiku.Api.Contracts; +using Tiku.Api.Security; +using Tiku.Application.Commerce; +using Tiku.Application.Jobs; +using Tiku.Application.Points; +using Tiku.Application.Security; +using Tiku.Domain.Commerce; + +namespace Tiku.Api.Controllers; + +[ApiController] +[Tags("租户端-交易运营")] +[Authorize(Policy = BackendPermissions.TenantCommerceOperate)] +[RequireSaasFeature(SaasFeatureCatalog.StudentStore)] +[Produces("application/json")] +[Route("api/tenant/commerce")] +public sealed class RefundAdministrationController( + IRefundAdministrationService service, + CommerceAdminActorResolver actorResolver) : ControllerBase +{ + [HttpGet("refunds")] + [EndpointSummary("查询退款申请")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> Refunds( + [FromQuery] TenantCommerceQueryDto query, + CancellationToken cancellationToken) + { + return Ok(await service.GetRefundsAsync( + actorResolver.Resolve(), + actorResolver.ToQuery(query), + cancellationToken)); + } + + [HttpPost("refunds")] + [EndpointSummary("创建退款申请")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CreateRefund( + CreateRefundRequestDto request, + CancellationToken cancellationToken) + { + return Ok(await service.CreateRefundRequestAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } + + [HttpPost("refunds/status")] + [EndpointSummary("更新退款状态")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> UpdateRefundStatus( + UpdateRefundStatusDto request, + CancellationToken cancellationToken) + { + return Ok(await service.UpdateRefundStatusAsync( + actorResolver.Resolve(), + request.ToCommand(), + cancellationToken)); + } + + [HttpGet("refunds/{refundRequestId:guid}/events")] + [EndpointSummary("查询退款事件")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> RefundEvents( + Guid refundRequestId, + CancellationToken cancellationToken) + { + return Ok(await service.GetRefundEventsAsync( + actorResolver.Resolve(), + refundRequestId, + cancellationToken)); + } +} diff --git a/Tiku.Api/Controllers/TenantCommerceController.cs b/Tiku.Api/Controllers/TenantCommerceController.cs deleted file mode 100644 index 14a88ec..0000000 --- a/Tiku.Api/Controllers/TenantCommerceController.cs +++ /dev/null @@ -1,589 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Tiku.Api.Contracts; -using Tiku.Api.Security; -using Tiku.Application.Commerce; -using Tiku.Application.Jobs; -using Tiku.Application.Security; -using Tiku.Domain.Commerce; - -namespace Tiku.Api.Controllers; - -[ApiController] -[Tags("租户端-交易运营")] -[Authorize(Policy = BackendPermissions.TenantCommerceOperate)] -[RequireSaasFeature(SaasFeatureCatalog.StudentStore)] -[Produces("application/json")] -[Route("api/tenant/commerce")] -public sealed class TenantCommerceController( - ICommerceAdminService commerceAdminService, - ICurrentUser currentUser, - ITenantContext currentTenant) : ControllerBase -{ - [HttpGet("payment-accounts")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询租户支付账号")] - [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task>> PaymentAccounts( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetPaymentAccountsAsync( - ResolveActor(), - ToQuery(query), - cancellationToken)); - } - - [HttpPut("payment-accounts")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("新增或更新租户支付账号")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> UpsertPaymentAccount( - UpsertPaymentAccountDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.UpsertPaymentAccountAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpPut("secrets")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("写入或轮换租户密钥")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> UpsertSecret( - UpsertTenantSecretDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.UpsertTenantSecretAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("orders")] - [EndpointSummary("查询租户订单")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> Orders( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetOrdersAsync( - ResolveActor(), - ToQuery(query), - cancellationToken)); - } - - [HttpGet("payments")] - [EndpointSummary("查询租户支付记录")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> Payments( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetPaymentsAsync( - ResolveActor(), - ToQuery(query), - cancellationToken)); - } - - [HttpPost("code-batches")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("创建兑换码批次")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> CreateCodeBatch( - CreateCodeBatchDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.CreateCodeBatchAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("activation-codes")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询兑换码")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> ActivationCodes( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetActivationCodesAsync( - ResolveActor(), - ToQuery(query), - cancellationToken)); - } - - [HttpPost("activation-codes/redeem")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("后台核销兑换码")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> RedeemActivationCode( - RedeemActivationCodeDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.RedeemActivationCodeAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("point-activity-tasks")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询积分活动任务")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> PointTasks( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetPointTasksAsync( - ResolveActor(), - ToPointQuery(query), - cancellationToken)); - } - - [HttpPut("point-activity-tasks")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("新增或更新积分活动任务")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> UpsertPointTask( - UpsertPointTaskDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.UpsertPointTaskAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("point-activity-claims")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询积分任务领取记录")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> PointClaims( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetPointClaimsAsync( - ResolveActor(), - ToPointQuery(query), - cancellationToken)); - } - - [HttpGet("point-exchange-items")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询积分兑换项")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> PointExchangeItems( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetPointExchangeItemsAsync( - ResolveActor(), - ToPointQuery(query), - cancellationToken)); - } - - [HttpPut("point-exchange-items")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("新增或更新积分兑换项")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> UpsertPointExchangeItem( - UpsertPointExchangeItemDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.UpsertPointExchangeItemAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("point-exchange-orders")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询积分兑换订单")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> PointExchangeOrders( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetPointExchangeOrdersAsync( - ResolveActor(), - ToPointQuery(query), - cancellationToken)); - } - - [HttpPost("point-exchange-orders/status")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("更新积分兑换订单状态")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> UpdatePointExchangeOrderStatus( - UpdatePointExchangeOrderStatusDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.UpdatePointExchangeOrderStatusAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("coupons")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询租户优惠券")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> Coupons( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetCouponsAsync( - ResolveActor(), - ToQuery(query), - cancellationToken)); - } - - [HttpPut("coupons")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("新增或更新租户优惠券")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> UpsertCoupon( - UpsertTenantCouponDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.UpsertCouponAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("coupons/redemptions")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询优惠券领取和核销记录")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> CouponRedemptions( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetCouponRedemptionsAsync( - ResolveActor(), - ToQuery(query), - cancellationToken)); - } - - [HttpGet("coupons/report")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询优惠券基础报表")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> CouponReport( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetCouponReportAsync( - ResolveActor(), - ToQuery(query), - cancellationToken)); - } - - [HttpGet("refunds")] - [EndpointSummary("查询退款申请")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> Refunds( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetRefundsAsync( - ResolveActor(), - ToQuery(query), - cancellationToken)); - } - - [HttpPost("refunds")] - [EndpointSummary("创建退款申请")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> CreateRefund( - CreateRefundRequestDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.CreateRefundRequestAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpPost("refunds/status")] - [EndpointSummary("更新退款状态")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> UpdateRefundStatus( - UpdateRefundStatusDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.UpdateRefundStatusAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("refunds/{refundRequestId:guid}/events")] - [EndpointSummary("查询退款事件")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> RefundEvents( - Guid refundRequestId, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetRefundEventsAsync( - ResolveActor(), - refundRequestId, - cancellationToken)); - } - - [HttpGet("reconciliation/batches")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询对账批次")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> ReconciliationBatches( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetReconciliationBatchesAsync( - ResolveActor(), - ToQuery(query), - cancellationToken)); - } - - [HttpPost("reconciliation/batches")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("创建对账批次")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> CreateReconciliationBatch( - CreateReconciliationBatchDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.CreateReconciliationBatchAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("reconciliation/issues")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询对账异常")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> ReconciliationIssues( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetReconciliationIssuesAsync( - ResolveActor(), - ToQuery(query), - cancellationToken)); - } - - [HttpGet("reconciliation/items")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询对账明细")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> ReconciliationItems( - [FromQuery] Guid batchId, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetReconciliationItemsAsync( - ResolveActor(), - batchId, - cancellationToken)); - } - - [HttpGet("reconciliation/issues/events")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询对账工单事件")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> ReconciliationIssueEvents( - [FromQuery] Guid issueId, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetReconciliationIssueEventsAsync( - ResolveActor(), - issueId, - cancellationToken)); - } - - [HttpPost("reconciliation/issues/status")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("更新对账异常状态")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> UpdateReconciliationIssue( - UpdateReconciliationIssueDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.UpdateReconciliationIssueAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("adjustment-vouchers")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询调账凭证")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> AdjustmentVouchers( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetAdjustmentVouchersAsync( - ResolveActor(), - ToQuery(query), - cancellationToken)); - } - - [HttpGet("adjustment-vouchers/detail")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询调账凭证详情")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> AdjustmentVoucherDetail( - [FromQuery] Guid voucherId, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetAdjustmentVoucherAsync( - ResolveActor(), - voucherId, - cancellationToken)); - } - - [HttpPost("adjustment-vouchers")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("创建调账凭证")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> CreateAdjustmentVoucher( - CreateAdjustmentVoucherDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.CreateAdjustmentVoucherAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpPost("adjustment-vouchers/status")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("审核或关闭调账凭证")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> UpdateAdjustmentVoucherStatus( - UpdateAdjustmentVoucherStatusDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.UpdateAdjustmentVoucherStatusAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - [HttpGet("adjustment-vouchers/events")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询调账凭证事件")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> AdjustmentVoucherEvents( - [FromQuery] Guid voucherId, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetAdjustmentVoucherEventsAsync( - ResolveActor(), - voucherId, - cancellationToken)); - } - - [HttpGet("adjustment-vouchers/report")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询调账统计报告")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> AdjustmentVoucherReport(CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetAdjustmentReportAsync( - ResolveActor(), - cancellationToken)); - } - - [HttpGet("reconciliation/anomalies")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询对账异常汇总")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> ReconciliationAnomalies( - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetAnomalySummaryAsync( - ResolveActor(), - cancellationToken)); - } - - [HttpPost("reconciliation/preview")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("预览支付渠道账单对账")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> PreviewReconciliation( - PreviewReconciliationImportDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.PreviewReconciliationImportAsync( - ResolveActor(), - request.ToPreviewCommand(), - cancellationToken)); - } - - [HttpPost("reconciliation/import")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("导入支付渠道账单并创建对账批次")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> ImportReconciliation( - PreviewReconciliationImportDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.ImportReconciliationAsync( - ResolveActor(), - request.ToImportCommand(), - cancellationToken)); - } - - [HttpGet("reconciliation/provider-bills/jobs")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("查询渠道账单下载任务")] - [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task>> ProviderBillJobs( - [FromQuery] TenantCommerceQueryDto query, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.GetProviderBillJobsAsync( - ResolveActor(), - ToQuery(query), - cancellationToken)); - } - - [HttpPost("reconciliation/provider-bills/request")] - [Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)] - [EndpointSummary("创建渠道官方账单下载任务")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> RequestProviderBill( - RequestProviderBillJobDto request, - CancellationToken cancellationToken) - { - return Ok(await commerceAdminService.RequestProviderBillJobAsync( - ResolveActor(), - request.ToCommand(), - cancellationToken)); - } - - private CommerceAdminActor ResolveActor() - { - if (currentTenant.TenantId is null || currentUser.UserId is null) - throw new CommerceException("Tenant commerce actor was not resolved.", "tenant_admin_access_denied"); - - return new CommerceAdminActor(currentTenant.TenantId.Value, currentUser.UserId.Value); - } - - private static CommerceAdminQuery ToQuery(TenantCommerceQueryDto query) - { - return new CommerceAdminQuery(query.Provider, query.Status, query.Limit); - } - - private static TenantPointQuery ToPointQuery(TenantCommerceQueryDto query) - { - return new TenantPointQuery(query.Status, query.Limit, query.UserId, query.RegionId); - } -} \ No newline at end of file diff --git a/Tiku.Application/Commerce/CommerceAdminModels.cs b/Tiku.Application/Commerce/CommerceAdminModels.cs index cf6668c..be3d6a4 100644 --- a/Tiku.Application/Commerce/CommerceAdminModels.cs +++ b/Tiku.Application/Commerce/CommerceAdminModels.cs @@ -280,7 +280,7 @@ public sealed record UpdateReconciliationIssueCommand( string? Note, Guid? AssignedTo = null); -public interface ICommerceAdminService +public interface IPaymentConfigurationService { Task> GetPaymentAccountsAsync( CommerceAdminActor actor, @@ -296,7 +296,10 @@ public interface ICommerceAdminService CommerceAdminActor actor, UpsertTenantSecretCommand command, CancellationToken cancellationToken = default); +} +public interface ICommerceOrderAdministrationService +{ Task GetOrdersAsync( CommerceAdminActor actor, CommerceAdminQuery query, @@ -306,7 +309,10 @@ public interface ICommerceAdminService CommerceAdminActor actor, CommerceAdminQuery query, CancellationToken cancellationToken = default); +} +public interface IActivationCodeAdministrationService +{ Task CreateCodeBatchAsync( CommerceAdminActor actor, CreateCodeBatchCommand command, @@ -321,42 +327,10 @@ public interface ICommerceAdminService CommerceAdminActor actor, RedeemActivationCodeCommand command, CancellationToken cancellationToken = default); +} - Task GetPointTasksAsync( - CommerceAdminActor actor, - TenantPointQuery query, - CancellationToken cancellationToken = default); - - Task UpsertPointTaskAsync( - CommerceAdminActor actor, - UpsertPointTaskCommand command, - CancellationToken cancellationToken = default); - - Task GetPointClaimsAsync( - CommerceAdminActor actor, - TenantPointQuery query, - CancellationToken cancellationToken = default); - - Task GetPointExchangeItemsAsync( - CommerceAdminActor actor, - TenantPointQuery query, - CancellationToken cancellationToken = default); - - Task UpsertPointExchangeItemAsync( - CommerceAdminActor actor, - UpsertPointExchangeItemCommand command, - CancellationToken cancellationToken = default); - - Task GetPointExchangeOrdersAsync( - CommerceAdminActor actor, - TenantPointQuery query, - CancellationToken cancellationToken = default); - - Task UpdatePointExchangeOrderStatusAsync( - CommerceAdminActor actor, - UpdatePointExchangeOrderStatusCommand command, - CancellationToken cancellationToken = default); - +public interface ICouponAdministrationService +{ Task GetCouponsAsync( CommerceAdminActor actor, CommerceAdminQuery query, @@ -376,7 +350,10 @@ public interface ICommerceAdminService CommerceAdminActor actor, CommerceAdminQuery query, CancellationToken cancellationToken = default); +} +public interface IRefundAdministrationService +{ Task GetRefundsAsync( CommerceAdminActor actor, CommerceAdminQuery query, @@ -397,6 +374,14 @@ public interface ICommerceAdminService Guid refundRequestId, CancellationToken cancellationToken = default); + Task ProcessRefundNotificationAsync( + Guid tenantId, + RefundNotificationCommand command, + CancellationToken cancellationToken = default); +} + +public interface IReconciliationAdministrationService +{ Task GetReconciliationBatchesAsync( CommerceAdminActor actor, CommerceAdminQuery query, @@ -427,10 +412,29 @@ public interface ICommerceAdminService Guid issueId, CancellationToken cancellationToken = default); - Task GetAnomalySummaryAsync( + Task PreviewReconciliationImportAsync( CommerceAdminActor actor, + PreviewReconciliationImportCommand command, CancellationToken cancellationToken = default); + Task ImportReconciliationAsync( + CommerceAdminActor actor, + ImportReconciliationCommand command, + CancellationToken cancellationToken = default); + + Task RequestProviderBillJobAsync( + CommerceAdminActor actor, + RequestProviderBillJobCommand command, + CancellationToken cancellationToken = default); + + Task> GetProviderBillJobsAsync( + CommerceAdminActor actor, + CommerceAdminQuery query, + CancellationToken cancellationToken = default); +} + +public interface ICommerceAdjustmentService +{ Task GetAdjustmentVouchersAsync( CommerceAdminActor actor, CommerceAdminQuery query, @@ -460,28 +464,7 @@ public interface ICommerceAdminService CommerceAdminActor actor, CancellationToken cancellationToken = default); - Task PreviewReconciliationImportAsync( + Task GetAnomalySummaryAsync( CommerceAdminActor actor, - PreviewReconciliationImportCommand command, CancellationToken cancellationToken = default); - - Task ImportReconciliationAsync( - CommerceAdminActor actor, - ImportReconciliationCommand command, - CancellationToken cancellationToken = default); - - Task RequestProviderBillJobAsync( - CommerceAdminActor actor, - RequestProviderBillJobCommand command, - CancellationToken cancellationToken = default); - - Task> GetProviderBillJobsAsync( - CommerceAdminActor actor, - CommerceAdminQuery query, - CancellationToken cancellationToken = default); - - Task ProcessRefundNotificationAsync( - Guid tenantId, - RefundNotificationCommand command, - CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/Tiku.Application/Points/IPointAdministrationService.cs b/Tiku.Application/Points/IPointAdministrationService.cs new file mode 100644 index 0000000..520f487 --- /dev/null +++ b/Tiku.Application/Points/IPointAdministrationService.cs @@ -0,0 +1,42 @@ +using Tiku.Application.Commerce; +using Tiku.Domain.Commerce; + +namespace Tiku.Application.Points; + +public interface IPointAdministrationService +{ + Task GetPointTasksAsync( + CommerceAdminActor actor, + TenantPointQuery query, + CancellationToken cancellationToken = default); + + Task UpsertPointTaskAsync( + CommerceAdminActor actor, + UpsertPointTaskCommand command, + CancellationToken cancellationToken = default); + + Task GetPointClaimsAsync( + CommerceAdminActor actor, + TenantPointQuery query, + CancellationToken cancellationToken = default); + + Task GetPointExchangeItemsAsync( + CommerceAdminActor actor, + TenantPointQuery query, + CancellationToken cancellationToken = default); + + Task UpsertPointExchangeItemAsync( + CommerceAdminActor actor, + UpsertPointExchangeItemCommand command, + CancellationToken cancellationToken = default); + + Task GetPointExchangeOrdersAsync( + CommerceAdminActor actor, + TenantPointQuery query, + CancellationToken cancellationToken = default); + + Task UpdatePointExchangeOrderStatusAsync( + CommerceAdminActor actor, + UpdatePointExchangeOrderStatusCommand command, + CancellationToken cancellationToken = default); +} diff --git a/Tiku.Infrastructure/Commerce/ActivationCodes/CommerceAdminService.ActivationCodes.cs b/Tiku.Infrastructure/Commerce/ActivationCodes/ActivationCodeAdministrationService.cs similarity index 96% rename from Tiku.Infrastructure/Commerce/ActivationCodes/CommerceAdminService.ActivationCodes.cs rename to Tiku.Infrastructure/Commerce/ActivationCodes/ActivationCodeAdministrationService.cs index 2ec27fa..94bd892 100644 --- a/Tiku.Infrastructure/Commerce/ActivationCodes/CommerceAdminService.ActivationCodes.cs +++ b/Tiku.Infrastructure/Commerce/ActivationCodes/ActivationCodeAdministrationService.cs @@ -6,7 +6,8 @@ using Tiku.Domain.Tenancy; namespace Tiku.Infrastructure.Commerce; -internal sealed partial class CommerceAdminService +internal sealed class ActivationCodeAdministrationService(CommerceAdministrationDependencies dependencies) + : CommerceAdministrationServiceBase(dependencies), IActivationCodeAdministrationService { public async Task CreateCodeBatchAsync( CommerceAdminActor actor, @@ -124,4 +125,4 @@ internal sealed partial class CommerceAdminService await dbContext.SaveChangesAsync(cancellationToken); return ToActivationCodeItem(code); } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/Commerce/Adjustments/CommerceAdminService.Adjustments.cs b/Tiku.Infrastructure/Commerce/Adjustments/CommerceAdjustmentService.cs similarity index 52% rename from Tiku.Infrastructure/Commerce/Adjustments/CommerceAdminService.Adjustments.cs rename to Tiku.Infrastructure/Commerce/Adjustments/CommerceAdjustmentService.cs index eb488a4..56dd401 100644 --- a/Tiku.Infrastructure/Commerce/Adjustments/CommerceAdminService.Adjustments.cs +++ b/Tiku.Infrastructure/Commerce/Adjustments/CommerceAdjustmentService.cs @@ -6,7 +6,8 @@ using Tiku.Domain.Commerce; namespace Tiku.Infrastructure.Commerce; -internal sealed partial class CommerceAdminService +internal sealed class CommerceAdjustmentService(CommerceAdministrationDependencies dependencies) + : CommerceAdministrationServiceBase(dependencies), ICommerceAdjustmentService { public async Task GetAdjustmentVouchersAsync( CommerceAdminActor actor, @@ -182,46 +183,7 @@ internal sealed partial class CommerceAdminService .SumAsync(item => item.AmountCents, cancellationToken)); } - public async Task GetReconciliationItemsAsync( - CommerceAdminActor actor, - Guid batchId, - CancellationToken cancellationToken = default) - { - await AssertAdminAsync(actor, cancellationToken); - var batchExists = await dbContext.CommerceReconciliationBatches.AnyAsync( - item => item.TenantId == actor.TenantId && item.Id == batchId, - cancellationToken); - if (!batchExists) - throw new CommerceException("Reconciliation batch was not found.", "reconciliation_batch_not_found"); - - var items = await dbContext.CommerceReconciliationItems.AsNoTracking() - .Where(item => item.TenantId == actor.TenantId && item.BatchId == batchId) - .OrderBy(item => item.RowNo) - .Take(500) - .ToArrayAsync(cancellationToken); - return new TenantReconciliationItemList(items); - } - - public async Task GetReconciliationIssueEventsAsync( - CommerceAdminActor actor, - Guid issueId, - CancellationToken cancellationToken = default) - { - await AssertAdminAsync(actor, cancellationToken); - var issueExists = await dbContext.CommerceReconciliationIssues.AnyAsync( - item => item.TenantId == actor.TenantId && item.Id == issueId, - cancellationToken); - if (!issueExists) - throw new CommerceException("Reconciliation issue was not found.", "reconciliation_issue_not_found"); - - var events = await dbContext.CommerceReconciliationIssueEvents.AsNoTracking() - .Where(item => item.TenantId == actor.TenantId && item.IssueId == issueId) - .OrderBy(item => item.CreatedAt) - .ToArrayAsync(cancellationToken); - return new TenantReconciliationIssueEventList(events); - } - - public async Task GetAnomalySummaryAsync( +public async Task GetAnomalySummaryAsync( CommerceAdminActor actor, CancellationToken cancellationToken = default) { @@ -256,187 +218,4 @@ internal sealed partial class CommerceAdminService pendingPayments, mismatchCount); } - - public async Task PreviewReconciliationImportAsync( - CommerceAdminActor actor, - PreviewReconciliationImportCommand command, - CancellationToken cancellationToken = default) - { - await AssertAdminAsync(actor, cancellationToken); - return BuildImportPreview(command.Provider, command.Rows); - } - - public async Task ImportReconciliationAsync( - CommerceAdminActor actor, - ImportReconciliationCommand command, - CancellationToken cancellationToken = default) - { - await AssertAdminAsync(actor, cancellationToken); - var preview = BuildImportPreview(command.Provider, command.Rows); - var batch = new CommerceReconciliationBatch - { - TenantId = actor.TenantId, - CreatedBy = actor.UserId, - Provider = NormalizeProvider(command.Provider), - BillDate = command.BillDate, - BillType = command.BillType, - Source = ReconciliationSource.ManualUpload, - SourceName = command.SourceName.Trim(), - SourceHash = preview.SourceHash, - Status = preview.InvalidCount == 0 - ? ReconciliationBatchStatus.Completed - : ReconciliationBatchStatus.CompletedWithIssues, - TotalCount = preview.TotalCount, - MatchedCount = preview.TotalCount - preview.InvalidCount, - MismatchCount = preview.InvalidCount, - AmountCents = preview.AmountCents, - RefundAmountCents = preview.RefundAmountCents, - CompletedAt = DateTimeOffset.UtcNow, - Metadata = JsonSerializer.SerializeToElement(new - { - preview.PaymentCount, - preview.RefundCount, - preview.InvalidCount - }) - }; - dbContext.CommerceReconciliationBatches.Add(batch); - var rowNo = 0; - foreach (var row in EnumerateImportRows(command.Rows)) - { - rowNo++; - var item = CreateReconciliationItem(actor.TenantId, batch.Id, rowNo, NormalizeProvider(command.Provider), - row); - dbContext.CommerceReconciliationItems.Add(item); - if (item.MatchStatus != ReconciliationMatchStatus.Matched) - dbContext.CommerceReconciliationIssues.Add(new CommerceReconciliationIssue - { - TenantId = actor.TenantId, - BatchId = batch.Id, - Provider = item.Provider, - TransactionType = item.TransactionType, - IssueNo = $"RC{DateTimeOffset.UtcNow:yyyyMMddHHmmss}{rowNo:0000}", - MatchStatus = item.MatchStatus switch - { - ReconciliationMatchStatus.MissingLocal => ReconciliationIssueMatchStatus.MissingLocal, - ReconciliationMatchStatus.MissingProvider => ReconciliationIssueMatchStatus.MissingProvider, - ReconciliationMatchStatus.Duplicate => ReconciliationIssueMatchStatus.Duplicate, - ReconciliationMatchStatus.StatusMismatch => ReconciliationIssueMatchStatus.StatusMismatch, - _ => ReconciliationIssueMatchStatus.AmountMismatch - }, - Severity = item.Severity, - Status = ReconciliationIssueStatus.Open, - OrderNo = item.OrderNo, - RefundNo = item.RefundNo, - ProviderTradeNo = item.ProviderTradeNo, - ProviderRefundNo = item.ProviderRefundNo, - AmountCents = item.AmountCents, - RefundAmountCents = item.RefundAmountCents, - Summary = item.IssueCode, - CreatedBy = actor.UserId, - Metadata = item.Details - }); - } - - await AddAuditAsync(actor, "commerce.reconciliation.imported", "commerce_reconciliation_batches", batch.Id, - new { batch.Provider, batch.BillDate, batch.TotalCount }, cancellationToken); - await dbContext.SaveChangesAsync(cancellationToken); - return batch; - } - - public async Task RequestProviderBillJobAsync( - CommerceAdminActor actor, - RequestProviderBillJobCommand command, - CancellationToken cancellationToken = default) - { - await AssertAdminAsync(actor, cancellationToken); - var job = await backgroundJobQueue.EnqueueAsync( - new CreateBackgroundJobCommand( - actor.TenantId, - "commerce_reconciliation", - JsonSerializer.SerializeToElement(new - { - provider = NormalizeProvider(command.Provider), - command.BillDate, - billType = command.BillType.ToString() - }), - command.RunAfter, - 5), - cancellationToken); - await AddAuditAsync(actor, "commerce.reconciliation.provider_bill_requested", "background_jobs", job.Id, - new { command.Provider, command.BillDate, command.BillType }, cancellationToken); - await dbContext.SaveChangesAsync(cancellationToken); - return job; - } - - public async Task> GetProviderBillJobsAsync( - CommerceAdminActor actor, - CommerceAdminQuery query, - CancellationToken cancellationToken = default) - { - await AssertAdminAsync(actor, cancellationToken); - return await backgroundJobOperations.ListAsync(actor.TenantId, "commerce_reconciliation", - Math.Clamp(query.Limit ?? 50, 1, 200), cancellationToken); - } - - public async Task ProcessRefundNotificationAsync( - Guid tenantId, - RefundNotificationCommand command, - CancellationToken cancellationToken = default) - { - var provider = NormalizeProvider(command.Provider); - var refund = await dbContext.CommerceRefundRequests.SingleOrDefaultAsync( - item => item.TenantId == tenantId && item.RefundNo == command.RefundNo, - cancellationToken) ?? throw new CommerceException("Refund request was not found.", "refund_not_found"); - var eventId = string.IsNullOrWhiteSpace(command.EventId) - ? $"{provider}:{command.RefundNo}:{command.Status}" - : command.EventId.Trim(); - var duplicate = await dbContext.PaymentEvents.AnyAsync( - item => item.TenantId == tenantId && - item.Provider == provider && - item.EventType == "refund" && - item.EventId == eventId, - cancellationToken); - if (duplicate) return refund; - - dbContext.PaymentEvents.Add(new PaymentEvent - { - TenantId = tenantId, - Provider = provider, - EventType = "refund", - EventId = eventId, - SignatureValid = true, - Payload = JsonObjectOrDefault(command.Payload), - ProcessedAt = DateTimeOffset.UtcNow - }); - var fromStatus = refund.Status; - if (fromStatus != command.Status && IsAllowedRefundTransition(fromStatus, command.Status)) - { - refund.Status = command.Status; - refund.ProviderRefundNo = string.IsNullOrWhiteSpace(command.ProviderRefundNo) - ? refund.ProviderRefundNo - : command.ProviderRefundNo.Trim(); - if (command.Status == CommerceRefundStatus.Succeeded) - { - refund.SucceededAt = DateTimeOffset.UtcNow; - await ApplyRefundToOrderAsync(refund, cancellationToken); - } - else if (command.Status == CommerceRefundStatus.Failed) - { - refund.FailedAt = DateTimeOffset.UtcNow; - } - - dbContext.CommerceRefundEvents.Add(new CommerceRefundEvent - { - TenantId = tenantId, - RefundRequestId = refund.Id, - FromStatus = fromStatus, - ToStatus = command.Status, - EventType = "provider_notify", - Details = JsonObjectOrDefault(command.Payload) - }); - } - - await dbContext.SaveChangesAsync(cancellationToken); - return refund; - } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/Commerce/CommerceAdminService.cs b/Tiku.Infrastructure/Commerce/CommerceAdminService.cs deleted file mode 100644 index 38642dc..0000000 --- a/Tiku.Infrastructure/Commerce/CommerceAdminService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Tiku.Application.Commerce; -using Tiku.Application.Jobs; -using Tiku.Application.Security; -using Tiku.Application.Tenancy; -using Tiku.Infrastructure.Persistence; - -namespace Tiku.Infrastructure.Commerce; - -internal sealed partial class CommerceAdminService( - TikuDbContext dbContext, - ITenantSecretProtector tenantSecretProtector, - ITenantExternalProviderConfigService providerConfigService, - ICurrentAccessContext currentAccessContext, - IBackgroundJobQueue backgroundJobQueue, - IBackgroundJobOperations backgroundJobOperations) : ICommerceAdminService -{ -} \ No newline at end of file diff --git a/Tiku.Infrastructure/Commerce/Coupons/CommerceAdminService.Coupons.cs b/Tiku.Infrastructure/Commerce/Coupons/CouponAdministrationService.cs similarity index 95% rename from Tiku.Infrastructure/Commerce/Coupons/CommerceAdminService.Coupons.cs rename to Tiku.Infrastructure/Commerce/Coupons/CouponAdministrationService.cs index 56cdb2f..cedb410 100644 --- a/Tiku.Infrastructure/Commerce/Coupons/CommerceAdminService.Coupons.cs +++ b/Tiku.Infrastructure/Commerce/Coupons/CouponAdministrationService.cs @@ -4,7 +4,8 @@ using Tiku.Domain.Commerce; namespace Tiku.Infrastructure.Commerce; -internal sealed partial class CommerceAdminService +internal sealed class CouponAdministrationService(CommerceAdministrationDependencies dependencies) + : CommerceAdministrationServiceBase(dependencies), ICouponAdministrationService { public async Task GetCouponsAsync( CommerceAdminActor actor, @@ -90,4 +91,4 @@ internal sealed partial class CommerceAdminService .SumAsync(item => item.DiscountAppliedCents, cancellationToken) ?? 0; return new TenantCouponReport(couponCount, claimedCount, usedCount, discountApplied); } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/Commerce/Foundation/CommerceAdminService.Foundation.cs b/Tiku.Infrastructure/Commerce/Foundation/CommerceAdministrationFoundation.Helpers.cs similarity index 85% rename from Tiku.Infrastructure/Commerce/Foundation/CommerceAdminService.Foundation.cs rename to Tiku.Infrastructure/Commerce/Foundation/CommerceAdministrationFoundation.Helpers.cs index c5d0c84..8516263 100644 --- a/Tiku.Infrastructure/Commerce/Foundation/CommerceAdminService.Foundation.cs +++ b/Tiku.Infrastructure/Commerce/Foundation/CommerceAdministrationFoundation.Helpers.cs @@ -13,9 +13,9 @@ using NotificationSeverity = Tiku.Domain.Commerce.NotificationSeverity; namespace Tiku.Infrastructure.Commerce; -internal sealed partial class CommerceAdminService +internal abstract partial class CommerceAdministrationServiceBase { - private async Task AssertAdminAsync(CommerceAdminActor actor, CancellationToken cancellationToken) + protected async Task AssertAdminAsync(CommerceAdminActor actor, CancellationToken cancellationToken) { var access = await currentAccessContext.GetAsync(cancellationToken); if (!access.IsCurrentTenantMember || @@ -25,7 +25,7 @@ internal sealed partial class CommerceAdminService throw new CommerceException("Tenant admin access is required.", "tenant_admin_access_denied"); } - private async Task RequireDataScopeAsync( + protected async Task RequireDataScopeAsync( CommerceAdminActor actor, CancellationToken cancellationToken) { @@ -33,7 +33,7 @@ internal sealed partial class CommerceAdminService return (await currentAccessContext.GetAsync(cancellationToken)).DataScope; } - private static TenantPaymentProviderItem ToPaymentAccountItem(TenantExternalProviderItem item) + protected static TenantPaymentProviderItem ToPaymentAccountItem(TenantExternalProviderItem item) { return new TenantPaymentProviderItem( item.Id, @@ -48,28 +48,28 @@ internal sealed partial class CommerceAdminService item.UpdatedAt); } - private static TenantSecretItem ToSecretItem(TenantSecret item) + protected static TenantSecretItem ToSecretItem(TenantSecret item) { return new TenantSecretItem(item.Id, item.Purpose, item.Provider, item.SecretKey, item.SecretRef, item.Status.ToString(), item.RotatedAt, item.ExpiresAt, item.UpdatedAt); } - private static CodeBatchItem ToCodeBatchItem(CodeBatch item) + protected static CodeBatchItem ToCodeBatchItem(CodeBatch item) { return new CodeBatchItem(item.Id, item.Name, item.TotalCount, item.Days ?? 0, item.RegionId, item.SaleType, item.Channel, item.DefaultUnitPriceCents, item.CostPriceCents, item.IssuedAt, item.Remark, item.CreatedAt); } - private static ActivationCodeItem ToActivationCodeItem(ActivationCode item) + protected static ActivationCodeItem ToActivationCodeItem(ActivationCode item) { return new ActivationCodeItem(item.Id, item.BatchId, item.Code, item.Days, item.IsUsed, item.UsedBy, item.UsedAt, item.SaleType, item.SoldTo, item.Remark, item.CreatedAt); } - private static CommerceOrderItem ToOrderItem(Order order) + protected static CommerceOrderItem ToOrderItem(Order order) { return new CommerceOrderItem(order.Id, order.OrderNo, order.Status.ToString(), order.PlanId, order.RegionId, order.ProductType, @@ -77,7 +77,7 @@ internal sealed partial class CommerceAdminService order.TradeNo, order.Days, order.PaidAt, order.CreatedAt, order.RawPayload); } - private static CommercePaymentItem ToPaymentItem(Payment payment, string orderNo) + protected static CommercePaymentItem ToPaymentItem(Payment payment, string orderNo) { return new CommercePaymentItem(payment.Id, payment.OrderId, orderNo, payment.Provider, payment.Method, payment.Status.ToString(), @@ -85,28 +85,28 @@ internal sealed partial class CommerceAdminService JsonSerializer.SerializeToElement(new { }), payment.RawPayload); } - private static OrderStatus ParseOrderStatus(string? status) + protected static OrderStatus ParseOrderStatus(string? status) { return Enum.TryParse(NormalizeEnum(status), true, out var parsed) ? parsed : throw new CommerceException("Order status is invalid.", "invalid_order_status"); } - private static PaymentStatus ParsePaymentStatus(string? status) + protected static PaymentStatus ParsePaymentStatus(string? status) { return Enum.TryParse(NormalizeEnum(status), true, out var parsed) ? parsed : throw new CommerceException("Payment status is invalid.", "invalid_payment_status"); } - private static PointActivityTaskStatus ParsePointTaskStatus(string? status) + protected static PointActivityTaskStatus ParsePointTaskStatus(string? status) { return Enum.TryParse(NormalizeEnum(status), true, out var parsed) ? parsed : throw new CommerceException("Point task status is invalid.", "invalid_point_task_status"); } - private static PointExchangeItemStatus ParsePointExchangeItemStatus(string? status) + protected static PointExchangeItemStatus ParsePointExchangeItemStatus(string? status) { return Enum.TryParse(NormalizeEnum(status), true, out var parsed) ? parsed @@ -114,7 +114,7 @@ internal sealed partial class CommerceAdminService "invalid_point_exchange_item_status"); } - private static PointExchangeOrderStatus ParsePointExchangeOrderStatus(string? status) + protected static PointExchangeOrderStatus ParsePointExchangeOrderStatus(string? status) { return Enum.TryParse(NormalizeEnum(status), true, out var parsed) ? parsed @@ -122,21 +122,21 @@ internal sealed partial class CommerceAdminService "invalid_point_exchange_order_status"); } - private static CouponRedemptionStatus ParseCouponRedemptionStatus(string? status) + protected static CouponRedemptionStatus ParseCouponRedemptionStatus(string? status) { return Enum.TryParse(NormalizeEnum(status), true, out var parsed) ? parsed : throw new CommerceException("Coupon redemption status is invalid.", "invalid_coupon_redemption_status"); } - private static CommerceRefundStatus ParseRefundStatus(string? status) + protected static CommerceRefundStatus ParseRefundStatus(string? status) { return Enum.TryParse(NormalizeEnum(status), true, out var parsed) ? parsed : throw new CommerceException("Refund status is invalid.", "invalid_refund_status"); } - private static ReconciliationBatchStatus ParseReconciliationBatchStatus(string? status) + protected static ReconciliationBatchStatus ParseReconciliationBatchStatus(string? status) { return Enum.TryParse(NormalizeEnum(status), true, out var parsed) ? parsed @@ -144,7 +144,7 @@ internal sealed partial class CommerceAdminService "invalid_reconciliation_batch_status"); } - private static ReconciliationIssueStatus ParseReconciliationIssueStatus(string? status) + protected static ReconciliationIssueStatus ParseReconciliationIssueStatus(string? status) { return Enum.TryParse(NormalizeEnum(status), true, out var parsed) ? parsed @@ -152,14 +152,14 @@ internal sealed partial class CommerceAdminService "invalid_reconciliation_issue_status"); } - private static CommerceAdjustmentVoucherStatus ParseAdjustmentVoucherStatus(string? status) + protected static CommerceAdjustmentVoucherStatus ParseAdjustmentVoucherStatus(string? status) { return Enum.TryParse(NormalizeEnum(status), true, out var parsed) ? parsed : throw new CommerceException("Adjustment voucher status is invalid.", "invalid_adjustment_voucher_status"); } - private async Task AssertOptionalReferenceAsync( + protected async Task AssertOptionalReferenceAsync( DbSet set, Guid tenantId, Guid? id, @@ -175,7 +175,7 @@ internal sealed partial class CommerceAdminService if (!exists) throw new CommerceException("Referenced commerce entity was not found.", code); } - private async Task ApplyRefundToOrderAsync(CommerceRefundRequest refund, CancellationToken cancellationToken) + protected async Task ApplyRefundToOrderAsync(CommerceRefundRequest refund, CancellationToken cancellationToken) { var order = await dbContext.Orders.SingleAsync( item => item.TenantId == refund.TenantId && item.Id == refund.OrderId, @@ -204,7 +204,7 @@ internal sealed partial class CommerceAdminService } } - private static bool IsAllowedRefundTransition(CommerceRefundStatus from, CommerceRefundStatus to) + protected static bool IsAllowedRefundTransition(CommerceRefundStatus from, CommerceRefundStatus to) { return from switch { @@ -217,7 +217,7 @@ internal sealed partial class CommerceAdminService }; } - private static bool IsAllowedAdjustmentTransition(CommerceAdjustmentVoucherStatus from, + protected static bool IsAllowedAdjustmentTransition(CommerceAdjustmentVoucherStatus from, CommerceAdjustmentVoucherStatus to) { return from switch @@ -231,7 +231,7 @@ internal sealed partial class CommerceAdminService }; } - private void AddRefundEvent( + protected void AddRefundEvent( CommerceRefundRequest refund, CommerceRefundStatus? fromStatus, CommerceRefundStatus toStatus, @@ -251,7 +251,7 @@ internal sealed partial class CommerceAdminService }); } - private Task AddAuditAsync( + protected Task AddAuditAsync( CommerceAdminActor actor, string action, string targetType, @@ -272,12 +272,12 @@ internal sealed partial class CommerceAdminService return Task.CompletedTask; } - private static string NormalizeEnum(string? value) + protected static string NormalizeEnum(string? value) { return string.Concat((value ?? string.Empty).Split(['_', '-', ' '], StringSplitOptions.RemoveEmptyEntries)); } - private static string NormalizeProvider(string? provider) + protected static string NormalizeProvider(string? provider) { var normalized = (provider ?? string.Empty).Trim().ToLowerInvariant() .Replace("-", "_", StringComparison.Ordinal); @@ -290,7 +290,7 @@ internal sealed partial class CommerceAdminService }; } - private static JsonElement WithPaymentMode(JsonElement element, string? mode) + protected static JsonElement WithPaymentMode(JsonElement element, string? mode) { var values = new Dictionary(StringComparer.Ordinal); if (element.ValueKind == JsonValueKind.Object) @@ -302,7 +302,7 @@ internal sealed partial class CommerceAdminService return JsonSerializer.SerializeToElement(values); } - private static string? GetJsonString(JsonElement element, params string[] keys) + protected static string? GetJsonString(JsonElement element, params string[] keys) { if (element.ValueKind != JsonValueKind.Object) return null; @@ -313,14 +313,14 @@ internal sealed partial class CommerceAdminService return null; } - private static JsonElement JsonObjectOrDefault(JsonElement element) + protected static JsonElement JsonObjectOrDefault(JsonElement element) { return element.ValueKind == JsonValueKind.Object ? element.Clone() : JsonSerializer.SerializeToElement(new { }); } - private static void AssertNoSecrets(JsonElement element, string path) + protected static void AssertNoSecrets(JsonElement element, string path) { if (element.ValueKind != JsonValueKind.Object) return; @@ -338,7 +338,7 @@ internal sealed partial class CommerceAdminService } } - private static ReconciliationImportPreview BuildImportPreview(string provider, JsonElement rows) + protected static ReconciliationImportPreview BuildImportPreview(string provider, JsonElement rows) { var normalizedProvider = NormalizeProvider(provider); var parsedRows = EnumerateImportRows(rows).ToArray(); @@ -360,7 +360,7 @@ internal sealed partial class CommerceAdminService sourceHash); } - private static IEnumerable EnumerateImportRows(JsonElement rows) + protected static IEnumerable EnumerateImportRows(JsonElement rows) { if (rows.ValueKind != JsonValueKind.Array) throw new CommerceException("Reconciliation rows must be an array.", "invalid_reconciliation_rows"); @@ -417,7 +417,7 @@ internal sealed partial class CommerceAdminService } } - private static ReconciliationImportRow InvalidImportRow(string issueCode, JsonElement row) + protected static ReconciliationImportRow InvalidImportRow(string issueCode, JsonElement row) { return new ReconciliationImportRow( ReconciliationTransactionType.Payment, @@ -434,7 +434,7 @@ internal sealed partial class CommerceAdminService row.Clone()); } - private static CommerceReconciliationItem CreateReconciliationItem( + protected static CommerceReconciliationItem CreateReconciliationItem( Guid tenantId, Guid batchId, int rowNo, @@ -465,7 +465,7 @@ internal sealed partial class CommerceAdminService }; } - private static int GetJsonInt(JsonElement element, params string[] keys) + protected static int GetJsonInt(JsonElement element, params string[] keys) { foreach (var key in keys) { @@ -480,19 +480,19 @@ internal sealed partial class CommerceAdminService return 0; } - private static string GenerateActivationCode() + protected static string GenerateActivationCode() { Span bytes = stackalloc byte[8]; RandomNumberGenerator.Fill(bytes); return $"TKU{Convert.ToHexString(bytes)}"; } - private static string FormatCny(int cents) + protected static string FormatCny(int cents) { return (cents / 100m).ToString("0.00", CultureInfo.InvariantCulture); } - private sealed record ReconciliationImportRow( + protected sealed record ReconciliationImportRow( ReconciliationTransactionType TransactionType, string? ProviderTradeNo, string? ProviderRefundNo, @@ -505,4 +505,4 @@ internal sealed partial class CommerceAdminService ReconciliationMatchStatus MatchStatus, string? IssueCode, JsonElement Details); -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/Commerce/Foundation/CommerceAdministrationFoundation.cs b/Tiku.Infrastructure/Commerce/Foundation/CommerceAdministrationFoundation.cs new file mode 100644 index 0000000..2dbbb13 --- /dev/null +++ b/Tiku.Infrastructure/Commerce/Foundation/CommerceAdministrationFoundation.cs @@ -0,0 +1,25 @@ +using Tiku.Application.Commerce; +using Tiku.Application.Jobs; +using Tiku.Application.Security; +using Tiku.Application.Tenancy; +using Tiku.Infrastructure.Persistence; + +namespace Tiku.Infrastructure.Commerce; + +internal sealed record CommerceAdministrationDependencies( + TikuDbContext DbContext, + ITenantSecretProtector TenantSecretProtector, + ITenantExternalProviderConfigService ProviderConfigService, + ICurrentAccessContext CurrentAccessContext, + IBackgroundJobQueue BackgroundJobQueue, + IBackgroundJobOperations BackgroundJobOperations); + +internal abstract partial class CommerceAdministrationServiceBase(CommerceAdministrationDependencies dependencies) +{ + protected TikuDbContext dbContext { get; } = dependencies.DbContext; + protected ITenantSecretProtector tenantSecretProtector { get; } = dependencies.TenantSecretProtector; + protected ITenantExternalProviderConfigService providerConfigService { get; } = dependencies.ProviderConfigService; + protected ICurrentAccessContext currentAccessContext { get; } = dependencies.CurrentAccessContext; + protected IBackgroundJobQueue backgroundJobQueue { get; } = dependencies.BackgroundJobQueue; + protected IBackgroundJobOperations backgroundJobOperations { get; } = dependencies.BackgroundJobOperations; +} diff --git a/Tiku.Infrastructure/Commerce/Orders/CommerceAdminService.Orders.cs b/Tiku.Infrastructure/Commerce/Orders/CommerceOrderAdministrationService.cs similarity index 93% rename from Tiku.Infrastructure/Commerce/Orders/CommerceAdminService.Orders.cs rename to Tiku.Infrastructure/Commerce/Orders/CommerceOrderAdministrationService.cs index 35b975a..97f44cb 100644 --- a/Tiku.Infrastructure/Commerce/Orders/CommerceAdminService.Orders.cs +++ b/Tiku.Infrastructure/Commerce/Orders/CommerceOrderAdministrationService.cs @@ -4,7 +4,8 @@ using Tiku.Infrastructure.Security; namespace Tiku.Infrastructure.Commerce; -internal sealed partial class CommerceAdminService +internal sealed class CommerceOrderAdministrationService(CommerceAdministrationDependencies dependencies) + : CommerceAdministrationServiceBase(dependencies), ICommerceOrderAdministrationService { public async Task GetOrdersAsync( CommerceAdminActor actor, @@ -64,4 +65,4 @@ internal sealed partial class CommerceAdminService .ToArrayAsync(cancellationToken); return new AdminPaymentList(rows.Select(item => ToPaymentItem(item.payment, item.OrderNo)).ToArray()); } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/Commerce/PaymentConfiguration/CommerceAdminService.PaymentConfiguration.cs b/Tiku.Infrastructure/Commerce/PaymentConfiguration/PaymentConfigurationService.cs similarity index 94% rename from Tiku.Infrastructure/Commerce/PaymentConfiguration/CommerceAdminService.PaymentConfiguration.cs rename to Tiku.Infrastructure/Commerce/PaymentConfiguration/PaymentConfigurationService.cs index 46cc697..ea836d5 100644 --- a/Tiku.Infrastructure/Commerce/PaymentConfiguration/CommerceAdminService.PaymentConfiguration.cs +++ b/Tiku.Infrastructure/Commerce/PaymentConfiguration/PaymentConfigurationService.cs @@ -5,7 +5,8 @@ using Tiku.Domain.Tenancy; namespace Tiku.Infrastructure.Commerce; -internal sealed partial class CommerceAdminService +internal sealed class PaymentConfigurationService(CommerceAdministrationDependencies dependencies) + : CommerceAdministrationServiceBase(dependencies), IPaymentConfigurationService { public async Task> GetPaymentAccountsAsync( CommerceAdminActor actor, @@ -90,4 +91,4 @@ internal sealed partial class CommerceAdminService await dbContext.SaveChangesAsync(cancellationToken); return ToSecretItem(secret); } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/Commerce/Reconciliation/ReconciliationAdministrationService.Operations.cs b/Tiku.Infrastructure/Commerce/Reconciliation/ReconciliationAdministrationService.Operations.cs new file mode 100644 index 0000000..005b46c --- /dev/null +++ b/Tiku.Infrastructure/Commerce/Reconciliation/ReconciliationAdministrationService.Operations.cs @@ -0,0 +1,170 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Tiku.Application.Commerce; +using Tiku.Application.Jobs; +using Tiku.Domain.Commerce; + +namespace Tiku.Infrastructure.Commerce; + +internal sealed partial class ReconciliationAdministrationService +{ + public async Task GetReconciliationItemsAsync( + CommerceAdminActor actor, + Guid batchId, + CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + var batchExists = await dbContext.CommerceReconciliationBatches.AnyAsync( + item => item.TenantId == actor.TenantId && item.Id == batchId, + cancellationToken); + if (!batchExists) + throw new CommerceException("Reconciliation batch was not found.", "reconciliation_batch_not_found"); + + var items = await dbContext.CommerceReconciliationItems.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && item.BatchId == batchId) + .OrderBy(item => item.RowNo) + .Take(500) + .ToArrayAsync(cancellationToken); + return new TenantReconciliationItemList(items); + } + + public async Task GetReconciliationIssueEventsAsync( + CommerceAdminActor actor, + Guid issueId, + CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + var issueExists = await dbContext.CommerceReconciliationIssues.AnyAsync( + item => item.TenantId == actor.TenantId && item.Id == issueId, + cancellationToken); + if (!issueExists) + throw new CommerceException("Reconciliation issue was not found.", "reconciliation_issue_not_found"); + + var events = await dbContext.CommerceReconciliationIssueEvents.AsNoTracking() + .Where(item => item.TenantId == actor.TenantId && item.IssueId == issueId) + .OrderBy(item => item.CreatedAt) + .ToArrayAsync(cancellationToken); + return new TenantReconciliationIssueEventList(events); + } + +public async Task PreviewReconciliationImportAsync( + CommerceAdminActor actor, + PreviewReconciliationImportCommand command, + CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + return BuildImportPreview(command.Provider, command.Rows); + } + + public async Task ImportReconciliationAsync( + CommerceAdminActor actor, + ImportReconciliationCommand command, + CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + var preview = BuildImportPreview(command.Provider, command.Rows); + var batch = new CommerceReconciliationBatch + { + TenantId = actor.TenantId, + CreatedBy = actor.UserId, + Provider = NormalizeProvider(command.Provider), + BillDate = command.BillDate, + BillType = command.BillType, + Source = ReconciliationSource.ManualUpload, + SourceName = command.SourceName.Trim(), + SourceHash = preview.SourceHash, + Status = preview.InvalidCount == 0 + ? ReconciliationBatchStatus.Completed + : ReconciliationBatchStatus.CompletedWithIssues, + TotalCount = preview.TotalCount, + MatchedCount = preview.TotalCount - preview.InvalidCount, + MismatchCount = preview.InvalidCount, + AmountCents = preview.AmountCents, + RefundAmountCents = preview.RefundAmountCents, + CompletedAt = DateTimeOffset.UtcNow, + Metadata = JsonSerializer.SerializeToElement(new + { + preview.PaymentCount, + preview.RefundCount, + preview.InvalidCount + }) + }; + dbContext.CommerceReconciliationBatches.Add(batch); + var rowNo = 0; + foreach (var row in EnumerateImportRows(command.Rows)) + { + rowNo++; + var item = CreateReconciliationItem(actor.TenantId, batch.Id, rowNo, NormalizeProvider(command.Provider), + row); + dbContext.CommerceReconciliationItems.Add(item); + if (item.MatchStatus != ReconciliationMatchStatus.Matched) + dbContext.CommerceReconciliationIssues.Add(new CommerceReconciliationIssue + { + TenantId = actor.TenantId, + BatchId = batch.Id, + Provider = item.Provider, + TransactionType = item.TransactionType, + IssueNo = $"RC{DateTimeOffset.UtcNow:yyyyMMddHHmmss}{rowNo:0000}", + MatchStatus = item.MatchStatus switch + { + ReconciliationMatchStatus.MissingLocal => ReconciliationIssueMatchStatus.MissingLocal, + ReconciliationMatchStatus.MissingProvider => ReconciliationIssueMatchStatus.MissingProvider, + ReconciliationMatchStatus.Duplicate => ReconciliationIssueMatchStatus.Duplicate, + ReconciliationMatchStatus.StatusMismatch => ReconciliationIssueMatchStatus.StatusMismatch, + _ => ReconciliationIssueMatchStatus.AmountMismatch + }, + Severity = item.Severity, + Status = ReconciliationIssueStatus.Open, + OrderNo = item.OrderNo, + RefundNo = item.RefundNo, + ProviderTradeNo = item.ProviderTradeNo, + ProviderRefundNo = item.ProviderRefundNo, + AmountCents = item.AmountCents, + RefundAmountCents = item.RefundAmountCents, + Summary = item.IssueCode, + CreatedBy = actor.UserId, + Metadata = item.Details + }); + } + + await AddAuditAsync(actor, "commerce.reconciliation.imported", "commerce_reconciliation_batches", batch.Id, + new { batch.Provider, batch.BillDate, batch.TotalCount }, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + return batch; + } + + public async Task RequestProviderBillJobAsync( + CommerceAdminActor actor, + RequestProviderBillJobCommand command, + CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + var job = await backgroundJobQueue.EnqueueAsync( + new CreateBackgroundJobCommand( + actor.TenantId, + "commerce_reconciliation", + JsonSerializer.SerializeToElement(new + { + provider = NormalizeProvider(command.Provider), + command.BillDate, + billType = command.BillType.ToString() + }), + command.RunAfter, + 5), + cancellationToken); + await AddAuditAsync(actor, "commerce.reconciliation.provider_bill_requested", "background_jobs", job.Id, + new { command.Provider, command.BillDate, command.BillType }, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + return job; + } + + public async Task> GetProviderBillJobsAsync( + CommerceAdminActor actor, + CommerceAdminQuery query, + CancellationToken cancellationToken = default) + { + await AssertAdminAsync(actor, cancellationToken); + return await backgroundJobOperations.ListAsync(actor.TenantId, "commerce_reconciliation", + Math.Clamp(query.Limit ?? 50, 1, 200), cancellationToken); + } +} diff --git a/Tiku.Infrastructure/Commerce/Reconciliation/CommerceAdminService.Reconciliation.cs b/Tiku.Infrastructure/Commerce/Reconciliation/ReconciliationAdministrationService.cs similarity index 96% rename from Tiku.Infrastructure/Commerce/Reconciliation/CommerceAdminService.Reconciliation.cs rename to Tiku.Infrastructure/Commerce/Reconciliation/ReconciliationAdministrationService.cs index 5ada10c..709bed5 100644 --- a/Tiku.Infrastructure/Commerce/Reconciliation/CommerceAdminService.Reconciliation.cs +++ b/Tiku.Infrastructure/Commerce/Reconciliation/ReconciliationAdministrationService.cs @@ -5,7 +5,8 @@ using Tiku.Domain.Commerce; namespace Tiku.Infrastructure.Commerce; -internal sealed partial class CommerceAdminService +internal sealed partial class ReconciliationAdministrationService(CommerceAdministrationDependencies dependencies) + : CommerceAdministrationServiceBase(dependencies), IReconciliationAdministrationService { public async Task GetReconciliationBatchesAsync( CommerceAdminActor actor, @@ -117,4 +118,4 @@ internal sealed partial class CommerceAdminService await dbContext.SaveChangesAsync(cancellationToken); return issue; } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/Commerce/Refunds/RefundAdministrationService.Notifications.cs b/Tiku.Infrastructure/Commerce/Refunds/RefundAdministrationService.Notifications.cs new file mode 100644 index 0000000..6a31ca0 --- /dev/null +++ b/Tiku.Infrastructure/Commerce/Refunds/RefundAdministrationService.Notifications.cs @@ -0,0 +1,72 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Tiku.Application.Commerce; +using Tiku.Application.Jobs; +using Tiku.Domain.Commerce; + +namespace Tiku.Infrastructure.Commerce; + +internal sealed partial class RefundAdministrationService +{ + public async Task ProcessRefundNotificationAsync( + Guid tenantId, + RefundNotificationCommand command, + CancellationToken cancellationToken = default) + { + var provider = NormalizeProvider(command.Provider); + var refund = await dbContext.CommerceRefundRequests.SingleOrDefaultAsync( + item => item.TenantId == tenantId && item.RefundNo == command.RefundNo, + cancellationToken) ?? throw new CommerceException("Refund request was not found.", "refund_not_found"); + var eventId = string.IsNullOrWhiteSpace(command.EventId) + ? $"{provider}:{command.RefundNo}:{command.Status}" + : command.EventId.Trim(); + var duplicate = await dbContext.PaymentEvents.AnyAsync( + item => item.TenantId == tenantId && + item.Provider == provider && + item.EventType == "refund" && + item.EventId == eventId, + cancellationToken); + if (duplicate) return refund; + + dbContext.PaymentEvents.Add(new PaymentEvent + { + TenantId = tenantId, + Provider = provider, + EventType = "refund", + EventId = eventId, + SignatureValid = true, + Payload = JsonObjectOrDefault(command.Payload), + ProcessedAt = DateTimeOffset.UtcNow + }); + var fromStatus = refund.Status; + if (fromStatus != command.Status && IsAllowedRefundTransition(fromStatus, command.Status)) + { + refund.Status = command.Status; + refund.ProviderRefundNo = string.IsNullOrWhiteSpace(command.ProviderRefundNo) + ? refund.ProviderRefundNo + : command.ProviderRefundNo.Trim(); + if (command.Status == CommerceRefundStatus.Succeeded) + { + refund.SucceededAt = DateTimeOffset.UtcNow; + await ApplyRefundToOrderAsync(refund, cancellationToken); + } + else if (command.Status == CommerceRefundStatus.Failed) + { + refund.FailedAt = DateTimeOffset.UtcNow; + } + + dbContext.CommerceRefundEvents.Add(new CommerceRefundEvent + { + TenantId = tenantId, + RefundRequestId = refund.Id, + FromStatus = fromStatus, + ToStatus = command.Status, + EventType = "provider_notify", + Details = JsonObjectOrDefault(command.Payload) + }); + } + + await dbContext.SaveChangesAsync(cancellationToken); + return refund; + } +} diff --git a/Tiku.Infrastructure/Commerce/Refunds/CommerceAdminService.Refunds.cs b/Tiku.Infrastructure/Commerce/Refunds/RefundAdministrationService.cs similarity index 97% rename from Tiku.Infrastructure/Commerce/Refunds/CommerceAdminService.Refunds.cs rename to Tiku.Infrastructure/Commerce/Refunds/RefundAdministrationService.cs index af01fa0..e4d10e7 100644 --- a/Tiku.Infrastructure/Commerce/Refunds/CommerceAdminService.Refunds.cs +++ b/Tiku.Infrastructure/Commerce/Refunds/RefundAdministrationService.cs @@ -6,7 +6,8 @@ using Tiku.Infrastructure.Security; namespace Tiku.Infrastructure.Commerce; -internal sealed partial class CommerceAdminService +internal sealed partial class RefundAdministrationService(CommerceAdministrationDependencies dependencies) + : CommerceAdministrationServiceBase(dependencies), IRefundAdministrationService { public async Task GetRefundsAsync( CommerceAdminActor actor, @@ -180,4 +181,4 @@ internal sealed partial class CommerceAdminService .ToArrayAsync(cancellationToken); return new TenantRefundEventList(items); } -} \ No newline at end of file +} diff --git a/Tiku.Infrastructure/Modules/CommerceModule.cs b/Tiku.Infrastructure/Modules/CommerceModule.cs index f3ca031..5562d2e 100644 --- a/Tiku.Infrastructure/Modules/CommerceModule.cs +++ b/Tiku.Infrastructure/Modules/CommerceModule.cs @@ -17,7 +17,15 @@ internal static class CommerceModule internal static IServiceCollection AddCommerceModule(this IServiceCollection services) { services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Tiku.Infrastructure/Commerce/Points/CommerceAdminService.Points.cs b/Tiku.Infrastructure/Points/PointAdministrationService.cs similarity index 96% rename from Tiku.Infrastructure/Commerce/Points/CommerceAdminService.Points.cs rename to Tiku.Infrastructure/Points/PointAdministrationService.cs index cbbd6ed..5a7e55a 100644 --- a/Tiku.Infrastructure/Commerce/Points/CommerceAdminService.Points.cs +++ b/Tiku.Infrastructure/Points/PointAdministrationService.cs @@ -1,10 +1,13 @@ using Microsoft.EntityFrameworkCore; using Tiku.Application.Commerce; +using Tiku.Application.Points; using Tiku.Domain.Commerce; +using Tiku.Infrastructure.Commerce; -namespace Tiku.Infrastructure.Commerce; +namespace Tiku.Infrastructure.Points; -internal sealed partial class CommerceAdminService +internal sealed class PointAdministrationService(CommerceAdministrationDependencies dependencies) + : CommerceAdministrationServiceBase(dependencies), IPointAdministrationService { public async Task GetPointTasksAsync( CommerceAdminActor actor, @@ -189,4 +192,4 @@ internal sealed partial class CommerceAdminService await dbContext.SaveChangesAsync(cancellationToken); return order; } -} \ No newline at end of file +} diff --git a/Tiku.IntegrationTests/ArchitectureBoundaryTests.cs b/Tiku.IntegrationTests/ArchitectureBoundaryTests.cs index f416713..0f6a5f9 100644 --- a/Tiku.IntegrationTests/ArchitectureBoundaryTests.cs +++ b/Tiku.IntegrationTests/ArchitectureBoundaryTests.cs @@ -35,7 +35,6 @@ public sealed class ArchitectureBoundaryTests var root = FindRepositoryRoot(); var legacyLineBudgets = new Dictionary(StringComparer.Ordinal) { - ["CommerceAdminService"] = 1842, ["LearningActivityService"] = 1781, ["PlatformAdminService"] = 1719, ["ContentManagementService"] = 1096,