forked from xiongyuxing/tiku-backend.net
feat(platform): add CRM SMS and payment settings
This commit is contained in:
@@ -7,6 +7,7 @@ using Tiku.Api.Contracts;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Growth;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.Platform;
|
||||
@@ -182,6 +183,134 @@ public sealed class PlatformAdminEndpointTests
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Platform_admin_can_manage_crm_sms_and_payment_settings()
|
||||
{
|
||||
await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary<string, string?>
|
||||
{
|
||||
["Tenancy:Resolution:PlatformHosts:0"] = "localhost"
|
||||
});
|
||||
var platform = await SeedPlatformAdminAsync(factory);
|
||||
var tenantA = Guid.NewGuid();
|
||||
var tenantB = Guid.NewGuid();
|
||||
var failedQueueId = Guid.NewGuid();
|
||||
var otherQueueId = Guid.NewGuid();
|
||||
await factory.SeedAsync(
|
||||
new Tenant { Id = tenantA, Slug = "capability-a", Name = "Capability A", Status = TenantStatus.Active, BillingStatus = BillingStatus.Active },
|
||||
new Tenant { Id = tenantB, Slug = "capability-b", Name = "Capability B", Status = TenantStatus.Active, BillingStatus = BillingStatus.Active },
|
||||
new CrmWebhookQueueItem { Id = failedQueueId, TenantId = tenantA, RecordId = "lead-a", Source = "tenant.student.crm_push", Status = CrmWebhookQueueStatus.Failed, Attempts = 2, IdempotencyKey = "platform-capability-lead-a", LastError = "timeout" },
|
||||
new CrmWebhookQueueItem { Id = otherQueueId, TenantId = tenantB, RecordId = "lead-b", Source = "tenant.student.crm_push", Status = CrmWebhookQueueStatus.Failed, Attempts = 1, IdempotencyKey = "platform-capability-lead-b", LastError = "still failed" });
|
||||
using var client = factory.CreateClient();
|
||||
client.UseAccessToken(await client.LoginAsPlatformAsync(platform.Email));
|
||||
|
||||
var crm = await client.PutAsJsonAsync("/api/platform-admin/tenant-capabilities/crm/configs", new UpsertPlatformCrmConfigDto
|
||||
{
|
||||
TenantId = tenantA,
|
||||
Enabled = true,
|
||||
Url = "https://crm.example.test/webhook",
|
||||
SecretRef = "tenant_secret:crm:redacted",
|
||||
AssignmentMode = "round_robin",
|
||||
AssignmentPool = JsonSerializer.SerializeToElement(new[] { "sales-a" }),
|
||||
AssignmentConfig = JsonSerializer.SerializeToElement(new { retry = 3 })
|
||||
});
|
||||
var retry = await client.PostAsJsonAsync("/api/platform-admin/tenant-capabilities/crm/leads/retry", new RetryPlatformCrmLeadDto { QueueId = failedQueueId, Note = "retry" });
|
||||
var smsChannel = await client.PutAsJsonAsync("/api/platform-admin/tenant-capabilities/sms/channels", new UpsertPlatformSmsChannelDto
|
||||
{
|
||||
TenantId = tenantA,
|
||||
Provider = "aliyun",
|
||||
Name = "阿里云短信",
|
||||
Signature = "题库测试",
|
||||
Scene = "login",
|
||||
Status = TenantExternalProviderStatus.Active,
|
||||
SecretRef = "tenant_secret:sms:aliyun:redacted",
|
||||
MonthlyQuota = 1000
|
||||
});
|
||||
var smsJson = JsonDocument.Parse(await smsChannel.Content.ReadAsStringAsync());
|
||||
var channelId = smsJson.RootElement.GetProperty("id").GetGuid();
|
||||
var template = await client.PutAsJsonAsync("/api/platform-admin/tenant-capabilities/sms/templates", new UpsertPlatformSmsTemplateDto
|
||||
{
|
||||
TenantId = tenantA,
|
||||
ChannelId = channelId,
|
||||
Code = "login_code",
|
||||
Name = "登录验证码",
|
||||
Type = SmsTemplateType.VerificationCode,
|
||||
AuditStatus = SmsTemplateAuditStatus.Draft,
|
||||
Status = SmsTemplateStatus.Active,
|
||||
Content = "验证码 ${code}"
|
||||
});
|
||||
var paymentApp = await client.PutAsJsonAsync("/api/platform-admin/payment-settings/apps", new UpsertPlatformPaymentAppDto
|
||||
{
|
||||
AppCode = "platform_collect_test",
|
||||
AppName = "平台收款测试",
|
||||
Status = PlatformPaymentAppStatus.Active,
|
||||
SettlementMode = "PlatformCollect"
|
||||
});
|
||||
var tenantPayment = await client.PutAsJsonAsync("/api/platform-admin/tenant-capabilities/payments/apps", new UpsertPlatformTenantPaymentAppDto
|
||||
{
|
||||
TenantId = tenantA,
|
||||
Provider = "manual",
|
||||
Status = TenantExternalProviderStatus.Active,
|
||||
DisplayName = "线下收款",
|
||||
SecretRef = "tenant_payment:manual:redacted"
|
||||
});
|
||||
var reads = await Task.WhenAll(
|
||||
client.GetAsync("/api/platform-admin/tenant-capabilities/crm/configs"),
|
||||
client.GetAsync("/api/platform-admin/tenant-capabilities/crm/leads"),
|
||||
client.GetAsync("/api/platform-admin/tenant-capabilities/sms/channels"),
|
||||
client.GetAsync("/api/platform-admin/tenant-capabilities/sms/templates"),
|
||||
client.GetAsync("/api/platform-admin/payment-settings/apps"),
|
||||
client.GetAsync("/api/platform-admin/tenant-capabilities/payments/apps"));
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, crm.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, retry.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, smsChannel.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, template.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, paymentApp.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, tenantPayment.StatusCode);
|
||||
Assert.All(reads, response => Assert.Equal(HttpStatusCode.OK, response.StatusCode));
|
||||
|
||||
using var scope = factory.CreateSystemScope("Verify platform capability endpoints");
|
||||
var db = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
Assert.Equal(CrmWebhookQueueStatus.Retrying, await db.CrmWebhookQueue.Where(item => item.Id == failedQueueId).Select(item => item.Status).SingleAsync());
|
||||
Assert.Equal(CrmWebhookQueueStatus.Failed, await db.CrmWebhookQueue.Where(item => item.Id == otherQueueId).Select(item => item.Status).SingleAsync());
|
||||
Assert.True(await db.AuditLogs.AnyAsync(item => item.ActorUserId == platform.UserId && item.Action == "platform.crm.lead.retry"));
|
||||
Assert.True(await db.AuditLogs.AnyAsync(item => item.ActorUserId == platform.UserId && item.Action == "platform.sms.channel.upserted"));
|
||||
Assert.True(await db.AuditLogs.AnyAsync(item => item.ActorUserId == platform.UserId && item.Action == "platform.payment.app.upserted"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Platform_capability_read_permissions_cannot_write()
|
||||
{
|
||||
await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary<string, string?>
|
||||
{
|
||||
["Tenancy:Resolution:PlatformHosts:0"] = "localhost"
|
||||
});
|
||||
var platform = await SeedPlatformUserAsync(factory,
|
||||
[
|
||||
BackendPermissions.PlatformCrmRead,
|
||||
BackendPermissions.PlatformSmsRead,
|
||||
BackendPermissions.PlatformPaymentRead
|
||||
]);
|
||||
var tenantId = Guid.NewGuid();
|
||||
await factory.SeedAsync(new Tenant { Id = tenantId, Slug = "readonly-capability", Name = "Readonly Capability" });
|
||||
using var client = factory.CreateClient();
|
||||
client.UseAccessToken(await client.LoginAsPlatformAsync(platform.Email));
|
||||
|
||||
var read = await client.GetAsync("/api/platform-admin/tenant-capabilities/sms/channels");
|
||||
var write = await client.PutAsJsonAsync("/api/platform-admin/tenant-capabilities/sms/channels", new UpsertPlatformSmsChannelDto
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Provider = "aliyun",
|
||||
Name = "只读不应写入",
|
||||
Signature = "题库",
|
||||
Scene = "login",
|
||||
Status = TenantExternalProviderStatus.Active
|
||||
});
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, read.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.Forbidden, write.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Platform_admin_can_manage_dunning_channels_and_retry_events()
|
||||
{
|
||||
@@ -301,11 +430,19 @@ public sealed class PlatformAdminEndpointTests
|
||||
}
|
||||
|
||||
private static async Task<(Guid UserId, string Email)> SeedPlatformAdminAsync(ApiTestFactory factory)
|
||||
{
|
||||
return await SeedPlatformUserAsync(factory, BackendPermissions.Platform);
|
||||
}
|
||||
|
||||
private static async Task<(Guid UserId, string Email)> SeedPlatformUserAsync(
|
||||
ApiTestFactory factory,
|
||||
IEnumerable<string> platformPermissions)
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var roleId = Guid.NewGuid();
|
||||
var email = $"platform-{Guid.NewGuid():N}@example.test";
|
||||
var modules = BackendPermissions.Platform
|
||||
var permissionCodes = platformPermissions.Distinct(StringComparer.Ordinal).ToArray();
|
||||
var modules = permissionCodes
|
||||
.Select(PermissionModuleCatalog.ResolvePermissionModuleCode)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Select(code => new PermissionModule
|
||||
@@ -317,7 +454,7 @@ public sealed class PlatformAdminEndpointTests
|
||||
})
|
||||
.Cast<object>()
|
||||
.ToList();
|
||||
var permissions = BackendPermissions.Platform.Select(code => new BackendPermission
|
||||
var permissions = permissionCodes.Select(code => new BackendPermission
|
||||
{
|
||||
Code = code,
|
||||
Name = code,
|
||||
@@ -348,7 +485,7 @@ public sealed class PlatformAdminEndpointTests
|
||||
Status = BackendRoleStatus.Active,
|
||||
IsSystem = true
|
||||
},
|
||||
..BackendPermissions.Platform.Select(code => new PlatformBackendRolePermission
|
||||
..permissionCodes.Select(code => new PlatformBackendRolePermission
|
||||
{
|
||||
RoleId = roleId,
|
||||
PermissionCode = code
|
||||
|
||||
Reference in New Issue
Block a user