forked from gongxuegit/tiku-backend.net
feat: add commission settlement endpoints
This commit is contained in:
179
Tiku.IntegrationTests/Api/CommissionEndpointTests.cs
Normal file
179
Tiku.IntegrationTests/Api/CommissionEndpointTests.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
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<CommissionSettlementItemDto>();
|
||||
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<CommissionProofItem>();
|
||||
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<CommissionExportItem>();
|
||||
|
||||
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<CommissionSeed> 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"),
|
||||
Identity(admin),
|
||||
Identity(referrer),
|
||||
Identity(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() { Id = seed.UserId, Phone = seed.Phone, Name = role, PrimaryRole = role };
|
||||
private static TenantMembership Membership(LoginSeed seed, TenantRole role) => new() { TenantId = seed.TenantId, UserId = seed.UserId, Role = role, Status = MembershipStatus.Active };
|
||||
private static UserIdentity Identity(LoginSeed seed) => new() { UserId = seed.UserId, Provider = "password", ProviderSubject = seed.Phone, Phone = seed.Phone, SecretPayload = CreateSecretPayload(new PasswordHasher().Hash("passw0rd!")) };
|
||||
|
||||
private static async Task LoginAsync(HttpClient client, LoginSeed seed)
|
||||
{
|
||||
var loginResponse = await client.PostAsJsonAsync("/api/auth/login/password", new PasswordLoginDto { TenantId = seed.TenantId, Phone = seed.Phone, Password = "passw0rd!" });
|
||||
loginResponse.EnsureSuccessStatusCode();
|
||||
using var loginJson = await JsonDocument.ParseAsync(await loginResponse.Content.ReadAsStreamAsync());
|
||||
client.DefaultRequestHeaders.Authorization = new("Bearer", loginJson.RootElement.GetProperty("tokens").GetProperty("accessToken").GetString());
|
||||
}
|
||||
|
||||
private static JsonElement CreateSecretPayload(string passwordHash)
|
||||
{
|
||||
using var document = JsonDocument.Parse($$"""{"passwordHash":{{JsonSerializer.Serialize(passwordHash)}}}""");
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
|
||||
private sealed record LoginSeed(Guid TenantId, Guid UserId, string Phone);
|
||||
private sealed record CommissionSeed(Guid TenantId, LoginSeed Admin, LoginSeed Referrer, LoginSeed Student);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public sealed class PersistenceModelTests
|
||||
.Select(entity => entity.GetTableName())
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
Assert.Equal(131, tableNames.Count);
|
||||
Assert.Equal(133, tableNames.Count);
|
||||
Assert.Contains("tenants", tableNames);
|
||||
Assert.Contains("tenant_settings", tableNames);
|
||||
Assert.Contains("content_entries", tableNames);
|
||||
@@ -116,6 +116,8 @@ public sealed class PersistenceModelTests
|
||||
Assert.Contains("tenant_commission_settings", tableNames);
|
||||
Assert.Contains("commission_settlements", tableNames);
|
||||
Assert.Contains("commission_settlement_items", tableNames);
|
||||
Assert.Contains("commission_settlement_proofs", tableNames);
|
||||
Assert.Contains("commission_settlement_export_events", tableNames);
|
||||
Assert.Contains("banners", tableNames);
|
||||
Assert.Contains("faqs", tableNames);
|
||||
Assert.Contains("announcements", tableNames);
|
||||
@@ -684,6 +686,8 @@ public sealed class PersistenceModelTests
|
||||
[InlineData(typeof(TenantCommissionSetting), nameof(TenantCommissionSetting.Config), "'{}'::jsonb")]
|
||||
[InlineData(typeof(CommissionSettlement), nameof(CommissionSettlement.Metadata), "'{}'::jsonb")]
|
||||
[InlineData(typeof(CommissionSettlementItem), nameof(CommissionSettlementItem.Metadata), "'{}'::jsonb")]
|
||||
[InlineData(typeof(CommissionSettlementProof), nameof(CommissionSettlementProof.Metadata), "'{}'::jsonb")]
|
||||
[InlineData(typeof(CommissionSettlementExportEvent), nameof(CommissionSettlementExportEvent.Metadata), "'{}'::jsonb")]
|
||||
public void Growth_json_properties_are_mapped_to_jsonb(
|
||||
Type entityType,
|
||||
string propertyName,
|
||||
|
||||
Reference in New Issue
Block a user