Files
tiku-backend.net/Tiku.Infrastructure/PlatformAdmin/Dunning/PlatformDunningService.cs

229 lines
11 KiB
C#

using Microsoft.EntityFrameworkCore;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.Security;
using Tiku.Domain.Platform;
namespace Tiku.Infrastructure.PlatformAdmin;
internal sealed class PlatformDunningService(PlatformAdministrationDependencies dependencies)
: PlatformAdministrationServiceBase(dependencies), IPlatformDunningService
{
public async Task<PlatformBillingDunningChannelList> 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<PlatformBillingDunningChannelItem> 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<PlatformBillingDunningChannelItem> 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<PlatformBillingDunningEventList> 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<PlatformBillingDunningEventItem> 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<PlatformBillingDunningEventItem> 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<PlatformBillingDunningEventItem> AcknowledgeBillingDunningEventAsync(
PlatformAdminActor actor,
ResolvePlatformBillingDunningEventCommand command,
CancellationToken cancellationToken = default)
{
return ResolveBillingDunningEventAsync(actor, command, PlatformBillingDunningNotificationStatus.Acknowledged,
cancellationToken);
}
public Task<PlatformBillingDunningEventItem> IgnoreBillingDunningEventAsync(
PlatformAdminActor actor,
ResolvePlatformBillingDunningEventCommand command,
CancellationToken cancellationToken = default)
{
return ResolveBillingDunningEventAsync(actor, command, PlatformBillingDunningNotificationStatus.Ignored,
cancellationToken);
}
private async Task<PlatformBillingDunningEventItem> 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);
}
}