campus-errand/server.tests/CommissionDistributionPropertyTests.cs
2026-03-12 18:12:10 +08:00

283 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using CampusErrand.Data;
using CampusErrand.Models;
using CampusErrand.Models.Dtos;
using FsCheck;
using FsCheck.Xunit;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace CampusErrand.Tests;
/// <summary>
/// Property 23: 佣金分销计算
/// 对任意跑腿佣金金额,平台分成应按匹配的佣金区间规则计算:
/// 百分比类型按比例计算,固定类型扣除固定金额。
/// 跑腿实得 = 原始佣金 - 平台分成。
/// **Feature: login-and-homepage, Property 23: 佣金分销计算**
///
/// </summary>
public class CommissionDistributionPropertyTests : IDisposable
{
private const string JwtSecret = "YourSuperSecretKeyForJwtTokenGeneration_AtLeast32Chars!";
private const string JwtIssuer = "CampusErrand";
private const string JwtAudience = "CampusErrandApp";
private readonly List<WebApplicationFactory<Program>> _factories = [];
private WebApplicationFactory<Program> CreateFactory(string dbName)
{
var factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var efDescriptors = services
.Where(d =>
d.ServiceType.FullName?.Contains("EntityFrameworkCore") == true
|| d.ServiceType == typeof(DbContextOptions<AppDbContext>)
|| d.ServiceType == typeof(DbContextOptions)
|| d.ImplementationType?.FullName?.Contains("EntityFrameworkCore") == true)
.ToList();
foreach (var d in efDescriptors) services.Remove(d);
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase(dbName));
});
});
_factories.Add(factory);
return factory;
}
private static string GenerateToken(int userId, string role = "User")
{
var key = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(
System.Text.Encoding.UTF8.GetBytes(JwtSecret));
var credentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(
key, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, userId.ToString()),
new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Role, role)
};
var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(
issuer: JwtIssuer, audience: JwtAudience, claims: claims,
expires: DateTime.UtcNow.AddHours(1), signingCredentials: credentials);
return new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler().WriteToken(token);
}
/// <summary>
/// 准备带佣金规则的已完成订单,确认后检查收益
/// </summary>
private (int ownerId, int runnerId, int orderId) SeedOrderWithRules(
WebApplicationFactory<Program> factory, string suffix, decimal commission,
CommissionRateType rateType, decimal rate, decimal ruleMin, decimal? ruleMax)
{
var ownerId = 100;
var runnerId = 200;
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Users.Add(new User
{
Id = ownerId, OpenId = $"owner_{suffix}", Phone = "13900000001",
Nickname = "单主", CreatedAt = DateTime.UtcNow
});
db.Users.Add(new User
{
Id = runnerId, OpenId = $"runner_{suffix}", Phone = "13900000002",
Nickname = "跑腿", Role = UserRole.Runner, CreatedAt = DateTime.UtcNow
});
db.RunnerCertifications.Add(new RunnerCertification
{
UserId = runnerId, RealName = "测试跑腿", Phone = "13900000002",
Status = CertificationStatus.Approved, CreatedAt = DateTime.UtcNow
});
// 添加佣金规则
db.CommissionRules.Add(new CommissionRule
{
MinAmount = ruleMin,
MaxAmount = ruleMax,
RateType = rateType,
Rate = rate
});
// 创建待确认订单
var order = new Order
{
OrderNo = $"ORD{suffix}"[..Math.Min(20, 3 + suffix.Length)],
OwnerId = ownerId, RunnerId = runnerId,
OrderType = OrderType.Pickup, Status = OrderStatus.WaitConfirm,
ItemName = "测试物品", DeliveryLocation = "测试地点",
Phone = "13800138000", Commission = commission, TotalAmount = commission,
CreatedAt = DateTime.UtcNow, AcceptedAt = DateTime.UtcNow,
CompletedAt = DateTime.UtcNow
};
db.Orders.Add(order);
db.SaveChanges();
return (ownerId, runnerId, order.Id);
}
/// <summary>
/// 属性:百分比类型佣金规则,平台分成 = 佣金 × 比例 / 100跑腿实得 = 佣金 - 平台分成
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
// 生成 1.0 ~ 50.0 的佣金
var commission = Math.Round(1.0m + (seed.Get % 490) * 0.1m, 1);
// 生成 1% ~ 30% 的抽成比例
var rate = (seed.Get % 30) + 1;
var dbName = $"comm_pct_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
var suffix = Guid.NewGuid().ToString("N");
var (ownerId, _, orderId) = SeedOrderWithRules(
factory, suffix, commission,
CommissionRateType.Percentage, rate, 0m, null);
// 单主确认订单
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(ownerId));
var resp = client.PostAsync($"/api/orders/{orderId}/confirm", null).Result;
if (resp.StatusCode != HttpStatusCode.OK) return false;
// 验证收益记录
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var earning = db.Earnings.FirstOrDefault(e => e.OrderId == orderId);
if (earning == null) return false;
var expectedPlatformFee = Math.Round(commission * rate / 100m, 2);
var expectedNetEarning = commission - expectedPlatformFee;
return earning.PlatformFee == expectedPlatformFee
&& earning.NetEarning == expectedNetEarning
&& earning.Commission == commission;
}
/// <summary>
/// 属性:固定金额佣金规则,平台分成 = 固定金额(不超过佣金),跑腿实得 = 佣金 - 平台分成
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
// 生成 1.0 ~ 50.0 的佣金
var commission = Math.Round(1.0m + (seed.Get % 490) * 0.1m, 1);
// 生成 0.5 ~ 5.0 的固定抽成
var fixedFee = Math.Round(0.5m + (seed.Get % 45) * 0.1m, 1);
var dbName = $"comm_fix_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
var suffix = Guid.NewGuid().ToString("N");
var (ownerId, _, orderId) = SeedOrderWithRules(
factory, suffix, commission,
CommissionRateType.Fixed, fixedFee, 0m, null);
// 单主确认订单
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(ownerId));
var resp = client.PostAsync($"/api/orders/{orderId}/confirm", null).Result;
if (resp.StatusCode != HttpStatusCode.OK) return false;
// 验证收益记录
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var earning = db.Earnings.FirstOrDefault(e => e.OrderId == orderId);
if (earning == null) return false;
var expectedPlatformFee = Math.Min(fixedFee, commission);
var expectedNetEarning = commission - expectedPlatformFee;
return earning.PlatformFee == expectedPlatformFee
&& earning.NetEarning == expectedNetEarning;
}
/// <summary>
/// 属性:无匹配佣金规则时,平台分成为 0跑腿实得等于全部佣金
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
var commission = Math.Round(1.0m + (seed.Get % 490) * 0.1m, 1);
var dbName = $"comm_none_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
var suffix = Guid.NewGuid().ToString("N");
// 不添加佣金规则,直接创建订单
var ownerId = 100;
var runnerId = 200;
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Users.Add(new User
{
Id = ownerId, OpenId = $"owner_{suffix}", Phone = "13900000001",
Nickname = "单主", CreatedAt = DateTime.UtcNow
});
db.Users.Add(new User
{
Id = runnerId, OpenId = $"runner_{suffix}", Phone = "13900000002",
Nickname = "跑腿", Role = UserRole.Runner, CreatedAt = DateTime.UtcNow
});
db.RunnerCertifications.Add(new RunnerCertification
{
UserId = runnerId, RealName = "测试跑腿", Phone = "13900000002",
Status = CertificationStatus.Approved, CreatedAt = DateTime.UtcNow
});
var order = new Order
{
OrderNo = $"ORD{suffix}"[..Math.Min(20, 3 + suffix.Length)],
OwnerId = ownerId, RunnerId = runnerId,
OrderType = OrderType.Pickup, Status = OrderStatus.WaitConfirm,
ItemName = "测试", DeliveryLocation = "测试",
Phone = "13800138000", Commission = commission, TotalAmount = commission,
CreatedAt = DateTime.UtcNow, AcceptedAt = DateTime.UtcNow,
CompletedAt = DateTime.UtcNow
};
db.Orders.Add(order);
db.SaveChanges();
}
// 单主确认订单
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(ownerId));
int orderId;
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
orderId = db.Orders.First().Id;
}
var resp = client.PostAsync($"/api/orders/{orderId}/confirm", null).Result;
if (resp.StatusCode != HttpStatusCode.OK) return false;
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var earning = db.Earnings.FirstOrDefault(e => e.OrderId == orderId);
if (earning == null) return false;
return earning.PlatformFee == 0m && earning.NetEarning == commission;
}
}
public void Dispose()
{
foreach (var f in _factories) f.Dispose();
_factories.Clear();
}
}