229 lines
11 KiB
C#
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);
|
|
}
|
|
}
|