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> GetOrdersAsync(SaasCatalogActor actor, PlatformBillingAdminQuery query, CancellationToken cancellationToken = default) => ExecuteAsync>("list SaaS orders", async (services, token) => { var db = services.GetRequiredService(); 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(query.Status)); return await values.OrderByDescending(value => value.CreatedAt).Take(Limit(query.Limit)).ToArrayAsync(token); }, cancellationToken); public Task> GetPaymentsAsync(SaasCatalogActor actor, PlatformBillingAdminQuery query, CancellationToken cancellationToken = default) => ExecuteAsync>("list SaaS payments", async (services, token) => { var db = services.GetRequiredService(); 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(query.Status)); return await values.OrderByDescending(value => value.CreatedAt).Take(Limit(query.Limit)).ToArrayAsync(token); }, cancellationToken); public Task> GetRefundsAsync(SaasCatalogActor actor, PlatformBillingAdminQuery query, CancellationToken cancellationToken = default) => ExecuteAsync>("list SaaS refunds", async (services, token) => { var db = services.GetRequiredService(); 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(query.Status)); return await values.OrderByDescending(value => value.CreatedAt).Take(Limit(query.Limit)).ToArrayAsync(token); }, cancellationToken); public Task> GetInvoicesAsync(SaasCatalogActor actor, PlatformBillingAdminQuery query, CancellationToken cancellationToken = default) => ExecuteAsync>("list SaaS invoices", async (services, token) => { var db = services.GetRequiredService(); 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(query.Status)); return await values.OrderByDescending(value => value.CreatedAt).Take(Limit(query.Limit)).ToArrayAsync(token); }, cancellationToken); public Task> GetSubscriptionsAsync(SaasCatalogActor actor, PlatformBillingAdminQuery query, CancellationToken cancellationToken = default) => ExecuteAsync>("list SaaS subscriptions", async (services, token) => { var db = services.GetRequiredService(); 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(query.Status)); return await values.OrderByDescending(value => value.UpdatedAt).Take(Limit(query.Limit)).ToArrayAsync(token); }, cancellationToken); public Task ConfirmManualPaymentAsync( SaasCatalogActor actor, ConfirmManualPaymentCommand command, CancellationToken cancellationToken = default) => ExecuteAsync("confirm manual SaaS payment", async (services, token) => { var db = services.GetRequiredService(); 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().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 UpsertFeatureOverrideAsync( SaasCatalogActor actor, UpsertTenantFeatureOverrideCommand command, CancellationToken cancellationToken = default) => ExecuteAsync("upsert tenant feature override", async (services, token) => { var db = services.GetRequiredService(); 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().CapabilityChangedAsync( command.TenantId, featureCode, "tenant_feature_override_changed", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), $"tenant-feature-override-{item.Id:N}", token); return item; }, cancellationToken); private Task ExecuteAsync( string reason, Func> operation, CancellationToken cancellationToken) => tenantExecutionScope.ExecuteAsync( new SystemScopeRequest(null, SystemScopeCallerType.Platform, nameof(PlatformBillingAdminService), reason, Guid.NewGuid().ToString("N"), true), operation, cancellationToken); private static T Parse(string value) where T : struct, Enum => Enum.TryParse(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); }