forked from xiongyuxing/tiku-backend.net
148 lines
9.2 KiB
C#
148 lines
9.2 KiB
C#
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Tiku.Application.PlatformBilling;
|
|
using Tiku.Application.Security;
|
|
using Tiku.Domain.Operations;
|
|
using Tiku.Domain.Platform;
|
|
using Tiku.Infrastructure.Persistence;
|
|
using Tiku.Infrastructure.Messaging;
|
|
|
|
namespace Tiku.Infrastructure.PlatformBilling;
|
|
|
|
internal sealed class PlatformBillingAdminService(
|
|
ITenantExecutionScope tenantExecutionScope) : IPlatformBillingAdminService
|
|
{
|
|
public Task<IReadOnlyCollection<PlatformBillingOrder>> GetOrdersAsync(SaasCatalogActor actor, PlatformBillingAdminQuery query, CancellationToken cancellationToken = default) =>
|
|
ExecuteAsync<IReadOnlyCollection<PlatformBillingOrder>>("list SaaS orders", async (services, token) =>
|
|
{
|
|
var db = services.GetRequiredService<TikuDbContext>();
|
|
var values = db.PlatformBillingOrders.AsNoTracking();
|
|
if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
|
|
if (!string.IsNullOrWhiteSpace(query.Status)) values = values.Where(value => value.Status == Parse<PlatformBillingOrderStatus>(query.Status));
|
|
return await values.OrderByDescending(value => value.CreatedAt).Take(Limit(query.Limit)).ToArrayAsync(token);
|
|
}, cancellationToken);
|
|
|
|
public Task<IReadOnlyCollection<PlatformBillingPayment>> GetPaymentsAsync(SaasCatalogActor actor, PlatformBillingAdminQuery query, CancellationToken cancellationToken = default) =>
|
|
ExecuteAsync<IReadOnlyCollection<PlatformBillingPayment>>("list SaaS payments", async (services, token) =>
|
|
{
|
|
var db = services.GetRequiredService<TikuDbContext>();
|
|
var values = db.PlatformBillingPayments.AsNoTracking();
|
|
if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
|
|
if (!string.IsNullOrWhiteSpace(query.Status)) values = values.Where(value => value.Status == Parse<PlatformBillingPaymentStatus>(query.Status));
|
|
return await values.OrderByDescending(value => value.CreatedAt).Take(Limit(query.Limit)).ToArrayAsync(token);
|
|
}, cancellationToken);
|
|
|
|
public Task<IReadOnlyCollection<PlatformBillingRefund>> GetRefundsAsync(SaasCatalogActor actor, PlatformBillingAdminQuery query, CancellationToken cancellationToken = default) =>
|
|
ExecuteAsync<IReadOnlyCollection<PlatformBillingRefund>>("list SaaS refunds", async (services, token) =>
|
|
{
|
|
var db = services.GetRequiredService<TikuDbContext>();
|
|
var values = db.PlatformBillingRefunds.AsNoTracking();
|
|
if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
|
|
if (!string.IsNullOrWhiteSpace(query.Status)) values = values.Where(value => value.Status == Parse<PlatformBillingRefundStatus>(query.Status));
|
|
return await values.OrderByDescending(value => value.CreatedAt).Take(Limit(query.Limit)).ToArrayAsync(token);
|
|
}, cancellationToken);
|
|
|
|
public Task<IReadOnlyCollection<PlatformBillingInvoice>> GetInvoicesAsync(SaasCatalogActor actor, PlatformBillingAdminQuery query, CancellationToken cancellationToken = default) =>
|
|
ExecuteAsync<IReadOnlyCollection<PlatformBillingInvoice>>("list SaaS invoices", async (services, token) =>
|
|
{
|
|
var db = services.GetRequiredService<TikuDbContext>();
|
|
var values = db.PlatformBillingInvoices.AsNoTracking();
|
|
if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
|
|
if (!string.IsNullOrWhiteSpace(query.Status)) values = values.Where(value => value.Status == Parse<PlatformBillingInvoiceStatus>(query.Status));
|
|
return await values.OrderByDescending(value => value.CreatedAt).Take(Limit(query.Limit)).ToArrayAsync(token);
|
|
}, cancellationToken);
|
|
|
|
public Task<IReadOnlyCollection<TenantSaasSubscription>> GetSubscriptionsAsync(SaasCatalogActor actor, PlatformBillingAdminQuery query, CancellationToken cancellationToken = default) =>
|
|
ExecuteAsync<IReadOnlyCollection<TenantSaasSubscription>>("list SaaS subscriptions", async (services, token) =>
|
|
{
|
|
var db = services.GetRequiredService<TikuDbContext>();
|
|
var values = db.TenantSaasSubscriptions.AsNoTracking();
|
|
if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
|
|
if (!string.IsNullOrWhiteSpace(query.Status)) values = values.Where(value => value.Status == Parse<TenantSaasSubscriptionStatus>(query.Status));
|
|
return await values.OrderByDescending(value => value.UpdatedAt).Take(Limit(query.Limit)).ToArrayAsync(token);
|
|
}, cancellationToken);
|
|
|
|
public Task<PlatformBillingPayment> ConfirmManualPaymentAsync(
|
|
SaasCatalogActor actor,
|
|
ConfirmManualPaymentCommand command,
|
|
CancellationToken cancellationToken = default) =>
|
|
ExecuteAsync("confirm manual SaaS payment", async (services, token) =>
|
|
{
|
|
var db = services.GetRequiredService<TikuDbContext>();
|
|
var payment = await db.PlatformBillingPayments.AsNoTracking().SingleOrDefaultAsync(value => value.Id == command.PaymentId, token)
|
|
?? throw Error("Platform billing payment was not found.", "platform_billing_payment_not_found");
|
|
if (payment.Provider != "manual")
|
|
{
|
|
throw Error("Only a manual payment can be confirmed by an operator.", "platform_billing_manual_payment_required");
|
|
}
|
|
if (string.IsNullOrWhiteSpace(command.Reason))
|
|
{
|
|
throw Error("Manual payment confirmation reason is required.", "platform_billing_reason_required");
|
|
}
|
|
return await services.GetRequiredService<IPlatformBillingSettlementService>().MarkPaidAsync(
|
|
payment.Id,
|
|
$"manual-{payment.Id:N}",
|
|
"manual_payment_confirmed",
|
|
command.ProviderTradeNo,
|
|
command.PaidAt ?? DateTimeOffset.UtcNow,
|
|
JsonSerializer.SerializeToElement(new { command.Reason }),
|
|
actor.UserId,
|
|
token);
|
|
}, cancellationToken);
|
|
|
|
public Task<TenantFeatureOverride> UpsertFeatureOverrideAsync(
|
|
SaasCatalogActor actor,
|
|
UpsertTenantFeatureOverrideCommand command,
|
|
CancellationToken cancellationToken = default) =>
|
|
ExecuteAsync("upsert tenant feature override", async (services, token) =>
|
|
{
|
|
var db = services.GetRequiredService<TikuDbContext>();
|
|
var featureCode = command.FeatureCode.Trim().ToLowerInvariant();
|
|
if (string.IsNullOrWhiteSpace(command.Reason)) throw Error("Override reason is required.", "platform_billing_reason_required");
|
|
if (!await db.Tenants.AnyAsync(value => value.Id == command.TenantId, token)) throw Error("Tenant was not found.", "tenant_not_found");
|
|
if (!await db.SaasFeatures.AnyAsync(value => value.Code == featureCode && !value.IsCore, token)) throw Error("SaaS feature was not found.", "saas_feature_not_found");
|
|
var item = await db.TenantFeatureOverrides.SingleOrDefaultAsync(value => value.TenantId == command.TenantId && value.FeatureCode == featureCode, token);
|
|
if (item is null)
|
|
{
|
|
item = new TenantFeatureOverride { TenantId = command.TenantId, FeatureCode = featureCode };
|
|
db.TenantFeatureOverrides.Add(item);
|
|
}
|
|
item.Mode = command.Mode;
|
|
item.ExpiresAt = command.ExpiresAt;
|
|
item.Reason = command.Reason.Trim();
|
|
db.AuditLogs.Add(new AuditLog
|
|
{
|
|
TenantId = command.TenantId,
|
|
ActorUserId = actor.UserId,
|
|
Action = "platform.saas.feature_override.updated",
|
|
TargetType = "tenant_feature_overrides",
|
|
TargetId = item.Id.ToString(),
|
|
Details = JsonSerializer.SerializeToElement(new { item.FeatureCode, item.Mode, item.ExpiresAt, item.Reason })
|
|
});
|
|
await db.SaveChangesAsync(token);
|
|
await services.GetRequiredService<ISecurityEventPublisher>().CapabilityChangedAsync(
|
|
command.TenantId,
|
|
featureCode,
|
|
"tenant_feature_override_changed",
|
|
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
$"tenant-feature-override-{item.Id:N}",
|
|
token);
|
|
return item;
|
|
}, cancellationToken);
|
|
|
|
private Task<TResult> ExecuteAsync<TResult>(
|
|
string reason,
|
|
Func<IServiceProvider, CancellationToken, Task<TResult>> operation,
|
|
CancellationToken cancellationToken) =>
|
|
tenantExecutionScope.ExecuteAsync(
|
|
new SystemScopeRequest(null, SystemScopeCallerType.Platform, nameof(PlatformBillingAdminService), reason, Guid.NewGuid().ToString("N"), true),
|
|
operation,
|
|
cancellationToken);
|
|
|
|
private static T Parse<T>(string value) where T : struct, Enum =>
|
|
Enum.TryParse<T>(value, true, out var parsed) ? parsed : throw Error("Status is invalid.", "status_invalid");
|
|
private static int Limit(int value) => Math.Clamp(value, 1, 200);
|
|
private static PlatformBillingException Error(string message, string code) => new(message, code);
|
|
}
|