using Microsoft.EntityFrameworkCore; using Tiku.Application.PlatformAdmin; using Tiku.Application.Security; using Tiku.Domain.Platform; namespace Tiku.Infrastructure.PlatformAdmin; internal sealed partial class PlatformAdminService { public async Task GetBillingDunningChannelsAsync( PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default) { await AssertPlatformPermissionAsync(actor, BackendPermissions.PlatformBillingNotification, cancellationToken); return await ExecuteSystemAsync("platform dunning channel list", async dbContext => { var channels = dbContext.PlatformBillingDunningNotificationChannels.AsNoTracking(); if (!string.IsNullOrWhiteSpace(query.Search)) { var search = query.Search.Trim(); channels = channels.Where(channel => channel.ChannelCode.Contains(search) || channel.Name.Contains(search)); } if (!string.IsNullOrWhiteSpace(query.Status)) { var enabled = ParseEnabledStatus(query.Status); channels = channels.Where(channel => channel.Enabled == enabled); } return new PlatformBillingDunningChannelList(await channels .OrderByDescending(channel => channel.Enabled) .ThenBy(channel => channel.MinReminderLevel) .ThenBy(channel => channel.ChannelCode) .Take(Limit(query.Limit)) .Select(channel => ToDunningChannelItem(channel)) .ToArrayAsync(cancellationToken)); }, cancellationToken); } public async Task UpsertBillingDunningChannelAsync( PlatformAdminActor actor, UpsertPlatformBillingDunningChannelCommand command, CancellationToken cancellationToken = default) { await AssertPlatformPermissionAsync(actor, BackendPermissions.PlatformBillingNotification, cancellationToken); return await ExecuteSystemAsync("platform dunning channel upsert", async dbContext => { var code = NormalizeCode(command.ChannelCode); var channel = command.ChannelId.HasValue ? await dbContext.PlatformBillingDunningNotificationChannels.SingleOrDefaultAsync( item => item.Id == command.ChannelId.Value, cancellationToken) : await dbContext.PlatformBillingDunningNotificationChannels.SingleOrDefaultAsync( item => item.ChannelCode == code, cancellationToken); if (channel is null) { channel = new PlatformBillingDunningNotificationChannel { ChannelCode = code }; dbContext.PlatformBillingDunningNotificationChannels.Add(channel); } channel.ChannelCode = code; channel.Name = command.Name.Trim(); channel.Description = Normalize(command.Description); channel.Enabled = command.Enabled; channel.Provider = command.Provider; channel.WebhookUrl = command.WebhookUrl.Trim(); channel.SecretRef = Normalize(command.SecretRef); channel.ReminderTypes = NormalizeArray(command.ReminderTypes, ["overdue", "final_notice"]); channel.ReminderChannels = NormalizeArray(command.ReminderChannels, ["internal"]); channel.MinReminderLevel = Math.Clamp(command.MinReminderLevel, 1, 20); channel.TenantIds = command.TenantIds.Where(id => id != Guid.Empty).Distinct().ToArray(); channel.TimeoutSeconds = Math.Clamp(command.TimeoutSeconds, 1, 60); channel.Metadata = JsonObjectOrDefault(command.Metadata); AddAudit(dbContext, actor, "platform.billing_dunning_channel.upserted", channel.Id, new { channel.ChannelCode, channel.Name, channel.Enabled, channel.Provider, Webhook = MaskWebhook(channel.WebhookUrl), channel.SecretRef }); await dbContext.SaveChangesAsync(cancellationToken); return ToDunningChannelItem(channel); }, cancellationToken); } public async Task DisableBillingDunningChannelAsync( PlatformAdminActor actor, DisablePlatformBillingDunningChannelCommand command, CancellationToken cancellationToken = default) { await AssertPlatformPermissionAsync(actor, BackendPermissions.PlatformBillingNotification, cancellationToken); return await ExecuteSystemAsync("platform dunning channel disable", async dbContext => { var channel = await dbContext.PlatformBillingDunningNotificationChannels.SingleOrDefaultAsync( item => item.Id == command.ChannelId, cancellationToken) ?? throw new PlatformAdminException("Platform dunning channel was not found.", "dunning_channel_not_found"); channel.Enabled = false; AddAudit(dbContext, actor, "platform.billing_dunning_channel.disabled", channel.Id, new { channel.ChannelCode, command.Reason }); await dbContext.SaveChangesAsync(cancellationToken); return ToDunningChannelItem(channel); }, cancellationToken); } public async Task GetBillingDunningEventsAsync( PlatformAdminActor actor, PlatformAdminQuery query, CancellationToken cancellationToken = default) { await AssertPlatformPermissionAsync(actor, BackendPermissions.PlatformBillingNotification, cancellationToken); return await ExecuteSystemAsync("platform dunning event list", async dbContext => { var events = dbContext.PlatformBillingDunningNotificationEvents.AsNoTracking(); if (!string.IsNullOrWhiteSpace(query.Status)) events = events.Where(item => item.Status == ParseDunningEventStatus(query.Status)); return new PlatformBillingDunningEventList(await events .OrderByDescending(item => item.CreatedAt) .Take(Limit(query.Limit)) .Select(item => ToDunningEventItem(item)) .ToArrayAsync(cancellationToken)); }, cancellationToken); } public async Task GetBillingDunningEventDetailAsync( PlatformAdminActor actor, Guid eventId, CancellationToken cancellationToken = default) { await AssertPlatformPermissionAsync(actor, BackendPermissions.PlatformBillingNotification, cancellationToken); return await ExecuteSystemAsync("platform dunning event detail", async dbContext => { var item = await dbContext.PlatformBillingDunningNotificationEvents.AsNoTracking() .SingleOrDefaultAsync(item => item.Id == eventId, cancellationToken) ?? throw new PlatformAdminException("Platform dunning event was not found.", "dunning_event_not_found"); return ToDunningEventItem(item); }, cancellationToken); } public async Task RetryBillingDunningEventAsync( PlatformAdminActor actor, RetryPlatformBillingDunningEventCommand command, CancellationToken cancellationToken = default) { await AssertPlatformPermissionAsync(actor, BackendPermissions.PlatformBillingNotification, cancellationToken); return await ExecuteSystemAsync("platform dunning event retry", async dbContext => { var item = await dbContext.PlatformBillingDunningNotificationEvents.SingleOrDefaultAsync( item => item.Id == command.EventId, cancellationToken) ?? throw new PlatformAdminException("Platform dunning event was not found.", "dunning_event_not_found"); item.Status = PlatformBillingDunningNotificationStatus.Pending; item.NextAttemptAt = DateTimeOffset.UtcNow; item.LastError = null; AddAudit(dbContext, actor, "platform.billing_dunning_event.retry_requested", item.Id, new { item.TenantId, item.ChannelId, item.ReminderId, item.InvoiceId, command.Reason }); await dbContext.SaveChangesAsync(cancellationToken); return ToDunningEventItem(item); }, cancellationToken); } public Task AcknowledgeBillingDunningEventAsync( PlatformAdminActor actor, ResolvePlatformBillingDunningEventCommand command, CancellationToken cancellationToken = default) { return ResolveBillingDunningEventAsync(actor, command, PlatformBillingDunningNotificationStatus.Acknowledged, cancellationToken); } public Task IgnoreBillingDunningEventAsync( PlatformAdminActor actor, ResolvePlatformBillingDunningEventCommand command, CancellationToken cancellationToken = default) { return ResolveBillingDunningEventAsync(actor, command, PlatformBillingDunningNotificationStatus.Ignored, cancellationToken); } private async Task ResolveBillingDunningEventAsync( PlatformAdminActor actor, ResolvePlatformBillingDunningEventCommand command, PlatformBillingDunningNotificationStatus status, CancellationToken cancellationToken) { await AssertPlatformPermissionAsync(actor, BackendPermissions.PlatformBillingNotification, cancellationToken); return await ExecuteSystemAsync("platform dunning event resolve", async dbContext => { var item = await dbContext.PlatformBillingDunningNotificationEvents .SingleOrDefaultAsync(value => value.Id == command.EventId, cancellationToken) ?? throw new PlatformAdminException("Platform dunning event was not found.", "dunning_event_not_found"); if (item.Status == PlatformBillingDunningNotificationStatus.Processing || item.Status is PlatformBillingDunningNotificationStatus.Acknowledged or PlatformBillingDunningNotificationStatus.Ignored) throw new PlatformAdminException("Platform dunning event cannot be resolved from its current status.", "dunning_event_status_invalid"); var fromStatus = item.Status; item.Status = status; item.NextAttemptAt = null; AddAudit(dbContext, actor, status == PlatformBillingDunningNotificationStatus.Acknowledged ? "platform.billing_dunning_event.acknowledged" : "platform.billing_dunning_event.ignored", item.Id, new { item.TenantId, FromStatus = fromStatus, ToStatus = status, command.Reason }); await dbContext.SaveChangesAsync(cancellationToken); return ToDunningEventItem(item); }, cancellationToken); } }