using System.Net; using System.Net.Http.Json; using System.Text.Json; using Tiku.Api.Contracts; using Tiku.Application.Auth; using Tiku.Application.Growth; using Tiku.Domain.Commerce; using Tiku.Domain.Growth; using Tiku.Domain.Identity; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Auth; namespace Tiku.IntegrationTests.Api; public sealed class CommissionEndpointTests { [Fact] public async Task Anonymous_commission_request_returns_401() { await using var factory = new ApiTestFactory(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/commission/settings"); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } [Fact] public async Task Admin_can_generate_export_and_proof_commission_settlement() { await using var factory = new ApiTestFactory(); var seed = await SeedCommissionAsync(factory); using var client = factory.CreateClient(); await LoginAsync(client, seed.Admin); var settings = await client.PutAsJsonAsync( "/api/commission/settings", new UpdateCommissionSettingsDto { DefaultRate = 0.2m, MinSettlementCents = 1, SettlementCycle = "monthly" }); var sources = await client.GetAsync($"/api/commission/orders?referrerUserId={seed.Referrer.UserId}&startDate=2026-07-01&endDate=2026-07-31"); var summary = await client.GetAsync($"/api/commission/summary?referrerUserId={seed.Referrer.UserId}&startDate=2026-07-01&endDate=2026-07-31"); var generate = await client.PostAsJsonAsync( "/api/commission/settlements/generate", new GenerateCommissionSettlementDto { ReferrerUserId = seed.Referrer.UserId, StartDate = "2026-07-01", EndDate = "2026-07-31", Status = "pending_review" }); var settlement = await generate.Content.ReadFromJsonAsync(); var duplicate = await client.PostAsJsonAsync( "/api/commission/settlements/generate", new GenerateCommissionSettlementDto { ReferrerUserId = seed.Referrer.UserId, StartDate = "2026-07-01", EndDate = "2026-07-31" }); var status = await client.PostAsJsonAsync( "/api/commission/settlements/status", new UpdateCommissionSettlementStatusDto { SettlementId = settlement!.Id, Status = "paid", PaymentMethod = "bank", PaymentAccount = "****1234" }); var proof = await client.PostAsJsonAsync( "/api/commission/settlements/proofs", new CreateCommissionProofDto { SettlementId = settlement.Id, ProofType = "payment", ExternalUrl = "https://example.test/proof.png" }); var proofItem = await proof.Content.ReadFromJsonAsync(); var proofStatus = await client.PostAsJsonAsync( "/api/commission/settlements/proofs/status", new UpdateCommissionProofStatusDto { ProofId = proofItem!.Id, Status = "approved" }); var export = await client.GetAsync($"/api/commission/settlements/export?settlementId={settlement.Id}&format=csv"); var exportItem = await export.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, settings.StatusCode); Assert.Equal(HttpStatusCode.OK, sources.StatusCode); Assert.Equal(HttpStatusCode.OK, summary.StatusCode); Assert.Equal(HttpStatusCode.OK, generate.StatusCode); Assert.Equal(2, settlement.SourceCount); Assert.Equal(3000, settlement.CommissionAmountCents); Assert.Equal(HttpStatusCode.Conflict, duplicate.StatusCode); Assert.Equal(HttpStatusCode.OK, status.StatusCode); Assert.Equal(HttpStatusCode.OK, proof.StatusCode); Assert.Equal(HttpStatusCode.OK, proofStatus.StatusCode); Assert.Equal(HttpStatusCode.OK, export.StatusCode); Assert.Equal("csv", exportItem!.Format); Assert.False(string.IsNullOrWhiteSpace(exportItem.Sha256)); } private static async Task SeedCommissionAsync(ApiTestFactory factory) { var tenantId = Guid.NewGuid(); var admin = new LoginSeed(tenantId, Guid.NewGuid(), "13800003001"); var referrer = new LoginSeed(tenantId, Guid.NewGuid(), "13800003002"); var student = new LoginSeed(tenantId, Guid.NewGuid(), "13800003003"); var paidAt = new DateTimeOffset(2026, 7, 10, 12, 0, 0, TimeSpan.Zero); var batchId = Guid.NewGuid(); await factory.SeedAsync( new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "Commission Tenant" }, User(admin, "tenant_admin"), User(referrer, "sales"), User(student, "student"), Membership(admin, TenantRole.TenantAdmin), Membership(referrer, TenantRole.Sales), Membership(student, TenantRole.Student), new ReferralLead { TenantId = tenantId, StudentUserId = student.UserId, ReferrerUserId = referrer.UserId, RefCode = "COMM2026", Source = "manual" }, new Order { TenantId = tenantId, UserId = student.UserId, OrderNo = "C202607100001", Status = OrderStatus.Paid, AmountCents = 10000, ProductType = "svip", ProductName = "SVIP", PaidAt = paidAt }, new CodeBatch { Id = batchId, TenantId = tenantId, Name = "代理码", TotalCount = 1, DefaultUnitPriceCents = 5000, CommissionRate = 0.2m }, new ActivationCode { TenantId = tenantId, BatchId = batchId, AgentUserId = referrer.UserId, UsedBy = student.UserId, Code = "CODE2026", IsUsed = true, UnitPriceCents = 5000, UsedAt = paidAt }); return new CommissionSeed(tenantId, admin, referrer, student); } private static User User(LoginSeed seed, string role) => new User { Id = seed.UserId, Phone = seed.Phone, Name = role, PrimaryRole = role }.WithTestPassword(); private static TenantMembership Membership(LoginSeed seed, TenantRole role) => new() { TenantId = seed.TenantId, UserId = seed.UserId, Role = role, Status = MembershipStatus.Active }; private static async Task LoginAsync(HttpClient client, LoginSeed seed) { client.UseAccessToken(await client.LoginAsTenantAsync(seed.TenantId, seed.Phone)); } private sealed record LoginSeed(Guid TenantId, Guid UserId, string Phone); private sealed record CommissionSeed(Guid TenantId, LoginSeed Admin, LoginSeed Referrer, LoginSeed Student); }