refactor(architecture): harden module boundaries
Some checks failed
ci / release-gate (push) Has been cancelled
Some checks failed
ci / release-gate (push) Has been cancelled
This commit is contained in:
@@ -21,7 +21,7 @@ using Tiku.Infrastructure.Persistence;
|
||||
namespace Tiku.Infrastructure.PlatformBilling;
|
||||
|
||||
internal sealed class CommercialBillingProcessor(
|
||||
TikuDbContext directoryDbContext,
|
||||
IPlatformBillingPersistence directoryDbContext,
|
||||
ITenantContext tenantContext,
|
||||
ITenantExecutionScope tenantExecutionScope,
|
||||
IOptions<CommercialBillingOptions> options,
|
||||
@@ -70,7 +70,7 @@ internal sealed class CommercialBillingProcessor(
|
||||
Scope(candidate.TenantId, "Generate subscription renewal receivable", candidate.SubscriptionId),
|
||||
async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var subscription =
|
||||
await db.TenantSaasSubscriptions.SingleAsync(value => value.Id == candidate.SubscriptionId,
|
||||
token);
|
||||
@@ -144,7 +144,7 @@ internal sealed class CommercialBillingProcessor(
|
||||
Scope(candidate.TenantId, "Generate billing reminder", candidate.InvoiceId),
|
||||
async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var invoice =
|
||||
await db.PlatformBillingInvoices.SingleAsync(value => value.Id == candidate.InvoiceId, token);
|
||||
var dueDate = invoice.DueDate!.Value;
|
||||
@@ -237,7 +237,7 @@ internal sealed class CommercialBillingProcessor(
|
||||
Scope(candidate.TenantId, "Execute approved SaaS refund", candidate.RefundId),
|
||||
async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var refund =
|
||||
await db.PlatformBillingRefunds.SingleAsync(value => value.Id == candidate.RefundId, token);
|
||||
var payment =
|
||||
@@ -284,7 +284,7 @@ internal sealed class CommercialBillingProcessor(
|
||||
TargetType = "platform_billing_refunds",
|
||||
TargetId = refund.Id.ToString(),
|
||||
Details = JsonSerializer.SerializeToElement(new
|
||||
{ refund.AmountCents, refund.SubscriptionEffect })
|
||||
{ refund.AmountCents, refund.SubscriptionEffect })
|
||||
});
|
||||
}
|
||||
catch (Exception exception)
|
||||
@@ -318,7 +318,7 @@ internal sealed class CommercialBillingProcessor(
|
||||
Scope(candidate.TenantId, "Dispatch billing dunning notification", candidate.EventId),
|
||||
async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var item = await db.PlatformBillingDunningNotificationEvents.SingleAsync(
|
||||
value => value.Id == candidate.EventId, token);
|
||||
var channel = await db.PlatformBillingDunningNotificationChannels.AsNoTracking()
|
||||
@@ -385,7 +385,7 @@ internal sealed class CommercialBillingProcessor(
|
||||
return candidates.Length;
|
||||
}
|
||||
|
||||
private async Task ApplyRefundEffectAsync(TikuDbContext db, PlatformBillingRefund refund,
|
||||
private async Task ApplyRefundEffectAsync(IPlatformBillingPersistence db, PlatformBillingRefund refund,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (refund.SubscriptionEffect == PlatformBillingRefundSubscriptionEffect.KeepService) return;
|
||||
@@ -478,7 +478,7 @@ internal sealed class CommercialBillingProcessor(
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<JsonElement> BillingProfileSnapshotAsync(TikuDbContext db, Guid tenantId,
|
||||
private static async Task<JsonElement> BillingProfileSnapshotAsync(IPlatformBillingPersistence db, Guid tenantId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var profile = await db.TenantBillingProfiles.AsNoTracking()
|
||||
|
||||
@@ -20,7 +20,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
{
|
||||
return ExecuteAsync<IReadOnlyCollection<PlatformBillingOrder>>("list SaaS orders", async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var values = db.PlatformBillingOrders.AsNoTracking();
|
||||
if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
|
||||
if (!string.IsNullOrWhiteSpace(query.Status))
|
||||
@@ -36,7 +36,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
return ExecuteAsync<IReadOnlyCollection<PlatformBillingPayment>>("list SaaS payments",
|
||||
async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var values = db.PlatformBillingPayments.AsNoTracking();
|
||||
if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
|
||||
if (!string.IsNullOrWhiteSpace(query.Status))
|
||||
@@ -51,7 +51,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
{
|
||||
return ExecuteAsync<IReadOnlyCollection<PlatformBillingRefund>>("list SaaS refunds", async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var values = db.PlatformBillingRefunds.AsNoTracking();
|
||||
if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
|
||||
if (!string.IsNullOrWhiteSpace(query.Status))
|
||||
@@ -67,7 +67,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
return ExecuteAsync<IReadOnlyCollection<PlatformBillingInvoice>>("list SaaS invoices",
|
||||
async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var values = db.PlatformBillingInvoices.AsNoTracking();
|
||||
if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
|
||||
if (!string.IsNullOrWhiteSpace(query.Status))
|
||||
@@ -82,7 +82,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
{
|
||||
return ExecuteAsync<IReadOnlyCollection<TenantFeatureUsage>>("list SaaS usage", async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var values = db.TenantFeatureUsages.AsNoTracking();
|
||||
if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
|
||||
return await values.OrderByDescending(value => value.PeriodStart).ThenBy(value => value.MetricCode)
|
||||
@@ -96,7 +96,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
return ExecuteAsync<IReadOnlyCollection<PlatformBillingInvoiceReminder>>("list SaaS invoice reminders",
|
||||
async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var values = db.PlatformBillingInvoiceReminders.AsNoTracking();
|
||||
if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
|
||||
if (!string.IsNullOrWhiteSpace(query.Status))
|
||||
@@ -113,7 +113,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
return ExecuteAsync<IReadOnlyCollection<TenantSaasSubscription>>("list SaaS subscriptions",
|
||||
async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var values = db.TenantSaasSubscriptions.AsNoTracking();
|
||||
if (query.TenantId.HasValue) values = values.Where(value => value.TenantId == query.TenantId);
|
||||
if (!string.IsNullOrWhiteSpace(query.Status))
|
||||
@@ -130,7 +130,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
{
|
||||
return ExecuteAsync("confirm manual SaaS payment", async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var payment = await db.PlatformBillingPayments.AsNoTracking()
|
||||
.SingleOrDefaultAsync(value => value.Id == command.PaymentId, token)
|
||||
?? throw Error("Platform billing payment was not found.",
|
||||
@@ -161,7 +161,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
{
|
||||
return ExecuteAsync("upsert tenant feature override", async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var featureCode = command.FeatureCode.Trim().ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(command.Reason))
|
||||
throw Error("Override reason is required.", "platform_billing_reason_required");
|
||||
@@ -188,7 +188,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
TargetType = "tenant_feature_overrides",
|
||||
TargetId = item.Id.ToString(),
|
||||
Details = JsonSerializer.SerializeToElement(new
|
||||
{ item.FeatureCode, item.Mode, item.ExpiresAt, item.Reason })
|
||||
{ item.FeatureCode, item.Mode, item.ExpiresAt, item.Reason })
|
||||
});
|
||||
await db.SaveChangesAsync(token);
|
||||
await services.GetRequiredService<ITenantFeatureCacheInvalidator>()
|
||||
@@ -204,7 +204,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
{
|
||||
return ExecuteAsync("grant tenant SaaS trial", async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var key = Required(command.IdempotencyKey, "idempotencyKey");
|
||||
var existingRequest = await db.PlatformOperationIdempotencies.AsNoTracking()
|
||||
.SingleOrDefaultAsync(value => value.ActorUserId == actor.UserId &&
|
||||
@@ -308,7 +308,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
{
|
||||
return ExecuteAsync("request SaaS refund", async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var key = Required(command.IdempotencyKey, "idempotencyKey");
|
||||
var payment = await db.PlatformBillingPayments.AsNoTracking()
|
||||
.SingleOrDefaultAsync(value => value.Id == command.PaymentId, token)
|
||||
@@ -375,7 +375,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
{
|
||||
return ExecuteAsync("retry SaaS refund", async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var refund =
|
||||
await db.PlatformBillingRefunds.SingleOrDefaultAsync(value => value.Id == command.RefundId, token)
|
||||
?? throw Error("Refund was not found.", "platform_billing_refund_not_found");
|
||||
@@ -395,7 +395,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
{
|
||||
return ExecuteAsync<CommercialMetrics>("get SaaS commercial metrics", async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var periodStart = new DateTimeOffset(now.Year, now.Month, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
var subscriptions = await (
|
||||
@@ -449,7 +449,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
{
|
||||
return ExecuteAsync($"{action} SaaS subscription", async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var subscription =
|
||||
await db.TenantSaasSubscriptions.SingleOrDefaultAsync(value => value.Id == command.SubscriptionId,
|
||||
token)
|
||||
@@ -525,7 +525,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
{
|
||||
return ExecuteAsync(approve ? "approve SaaS refund" : "reject SaaS refund", async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var refund =
|
||||
await db.PlatformBillingRefunds.SingleOrDefaultAsync(value => value.Id == command.RefundId, token)
|
||||
?? throw Error("Refund was not found.", "platform_billing_refund_not_found");
|
||||
@@ -545,7 +545,7 @@ internal sealed class PlatformBillingAdminService(
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private static void AddAudit(TikuDbContext db, Guid actorUserId, Guid tenantId, string action, Guid targetId,
|
||||
private static void AddAudit(IPlatformBillingPersistence db, Guid actorUserId, Guid tenantId, string action, Guid targetId,
|
||||
string? reason)
|
||||
{
|
||||
db.AuditLogs.Add(new AuditLog
|
||||
|
||||
@@ -27,7 +27,7 @@ internal sealed class PlatformBillingNotificationService(
|
||||
"Settle platform billing notification", parsed.EventId, true),
|
||||
async (services, token) =>
|
||||
{
|
||||
var db = services.GetRequiredService<TikuDbContext>();
|
||||
var db = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var order = await db.PlatformBillingOrders.AsNoTracking()
|
||||
.SingleOrDefaultAsync(value => value.OrderNo == parsed.OrderNo, token)
|
||||
?? throw Error("Platform billing order was not found.", "platform_billing_order_not_found");
|
||||
|
||||
@@ -56,7 +56,7 @@ internal sealed class PlatformBillingPaymentGateway(
|
||||
true),
|
||||
async (services, token) =>
|
||||
{
|
||||
var dbContext = services.GetRequiredService<TikuDbContext>();
|
||||
var dbContext = services.GetRequiredService<IPlatformBillingPersistence>();
|
||||
var platformTenantId = await dbContext.Tenants.AsNoTracking()
|
||||
.Where(value => value.Mode == TenantMode.PlatformOwned)
|
||||
.Select(value => value.Id)
|
||||
|
||||
@@ -11,7 +11,10 @@ using Tiku.Infrastructure.Persistence;
|
||||
namespace Tiku.Infrastructure.PlatformBilling;
|
||||
|
||||
internal sealed class PlatformBillingSettlementService(
|
||||
TikuDbContext dbContext,
|
||||
IPlatformControlPlanePersistence platformControlPlanePersistence,
|
||||
ITenancyPersistence tenancyPersistence,
|
||||
ITenantAdministrationPersistence tenantAdministrationPersistence,
|
||||
IJobsOperationsPersistence jobsOperationsPersistence,
|
||||
ITenantFeatureCacheInvalidator featureCacheInvalidator) : IPlatformBillingSettlementService
|
||||
{
|
||||
public async Task<PlatformBillingPayment> MarkPaidAsync(
|
||||
@@ -25,21 +28,21 @@ internal sealed class PlatformBillingSettlementService(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var payment =
|
||||
await dbContext.PlatformBillingPayments.SingleOrDefaultAsync(value => value.Id == paymentId,
|
||||
await platformControlPlanePersistence.PlatformBillingPayments.SingleOrDefaultAsync(value => value.Id == paymentId,
|
||||
cancellationToken)
|
||||
?? throw Error("Platform billing payment was not found.", "platform_billing_payment_not_found");
|
||||
if (await dbContext.PlatformBillingPaymentEvents.AnyAsync(value =>
|
||||
if (await platformControlPlanePersistence.PlatformBillingPaymentEvents.AnyAsync(value =>
|
||||
value.TenantId == payment.TenantId &&
|
||||
value.Provider == payment.Provider &&
|
||||
value.ProviderEventId == providerEventId, cancellationToken))
|
||||
return payment;
|
||||
|
||||
var order = await dbContext.PlatformBillingOrders.SingleAsync(value =>
|
||||
var order = await platformControlPlanePersistence.PlatformBillingOrders.SingleAsync(value =>
|
||||
value.TenantId == payment.TenantId && value.Id == payment.OrderId, cancellationToken);
|
||||
if (payment.AmountCents != order.TotalAmountCents)
|
||||
throw Error("Payment amount does not match the order.", "platform_billing_payment_amount_mismatch");
|
||||
|
||||
dbContext.PlatformBillingPaymentEvents.Add(new PlatformBillingPaymentEvent
|
||||
platformControlPlanePersistence.PlatformBillingPaymentEvents.Add(new PlatformBillingPaymentEvent
|
||||
{
|
||||
TenantId = payment.TenantId,
|
||||
PaymentId = payment.Id,
|
||||
@@ -50,7 +53,7 @@ internal sealed class PlatformBillingSettlementService(
|
||||
});
|
||||
if (payment.Status == PlatformBillingPaymentStatus.Succeeded)
|
||||
{
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await platformControlPlanePersistence.SaveChangesAsync(cancellationToken);
|
||||
return payment;
|
||||
}
|
||||
|
||||
@@ -64,17 +67,17 @@ internal sealed class PlatformBillingSettlementService(
|
||||
payment.PaidAt = paidAt;
|
||||
order.Status = PlatformBillingOrderStatus.Paid;
|
||||
order.PaidAt = paidAt;
|
||||
var tenant = await dbContext.Tenants.SingleAsync(value => value.Id == order.TenantId, cancellationToken);
|
||||
var tenant = await tenancyPersistence.Tenants.SingleAsync(value => value.Id == order.TenantId, cancellationToken);
|
||||
tenant.BillingStatus = BillingStatus.Active;
|
||||
|
||||
var orderItems = await dbContext.PlatformBillingOrderItems.AsNoTracking()
|
||||
var orderItems = await platformControlPlanePersistence.PlatformBillingOrderItems.AsNoTracking()
|
||||
.Where(value => value.TenantId == order.TenantId && value.OrderId == order.Id)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var baseItem = orderItems.SingleOrDefault(value => value.ItemType == PlatformBillingItemType.BasePlan)
|
||||
?? throw Error("Order base plan item is missing.", "platform_billing_base_plan_missing");
|
||||
var baseVersion = await dbContext.SaasOfferingVersions.AsNoTracking()
|
||||
var baseVersion = await platformControlPlanePersistence.SaasOfferingVersions.AsNoTracking()
|
||||
.SingleAsync(value => value.Id == baseItem.OfferingVersionId, cancellationToken);
|
||||
var subscription = await dbContext.TenantSaasSubscriptions
|
||||
var subscription = await platformControlPlanePersistence.TenantSaasSubscriptions
|
||||
.OrderByDescending(value => value.UpdatedAt)
|
||||
.FirstOrDefaultAsync(value => value.TenantId == order.TenantId, cancellationToken);
|
||||
var now = paidAt;
|
||||
@@ -89,7 +92,7 @@ internal sealed class PlatformBillingSettlementService(
|
||||
CurrentPeriodStart = now,
|
||||
CurrentPeriodEnd = AddCycle(now, baseVersion.BillingCycle)
|
||||
};
|
||||
dbContext.TenantSaasSubscriptions.Add(subscription);
|
||||
platformControlPlanePersistence.TenantSaasSubscriptions.Add(subscription);
|
||||
}
|
||||
else if (order.Purpose == PlatformBillingOrderPurpose.Renewal)
|
||||
{
|
||||
@@ -122,7 +125,7 @@ internal sealed class PlatformBillingSettlementService(
|
||||
|
||||
if (order.Purpose != PlatformBillingOrderPurpose.Downgrade)
|
||||
{
|
||||
var existingItems = await dbContext.TenantSaasSubscriptionItems
|
||||
var existingItems = await platformControlPlanePersistence.TenantSaasSubscriptionItems
|
||||
.Where(value => value.TenantId == order.TenantId && value.SubscriptionId == subscription.Id &&
|
||||
value.Status == TenantSaasSubscriptionItemStatus.Active)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
@@ -132,7 +135,7 @@ internal sealed class PlatformBillingSettlementService(
|
||||
existing.EndsAt = now > existing.StartsAt ? now : existing.StartsAt.AddTicks(1);
|
||||
}
|
||||
|
||||
dbContext.TenantSaasSubscriptionItems.AddRange(orderItems.Select(item => new TenantSaasSubscriptionItem
|
||||
platformControlPlanePersistence.TenantSaasSubscriptionItems.AddRange(orderItems.Select(item => new TenantSaasSubscriptionItem
|
||||
{
|
||||
TenantId = order.TenantId,
|
||||
SubscriptionId = subscription.Id,
|
||||
@@ -148,7 +151,7 @@ internal sealed class PlatformBillingSettlementService(
|
||||
}
|
||||
else
|
||||
{
|
||||
var existingScheduledItems = await dbContext.TenantSaasSubscriptionItems
|
||||
var existingScheduledItems = await platformControlPlanePersistence.TenantSaasSubscriptionItems
|
||||
.Where(value => value.TenantId == order.TenantId &&
|
||||
value.SubscriptionId == subscription.Id &&
|
||||
value.Status == TenantSaasSubscriptionItemStatus.Scheduled)
|
||||
@@ -159,7 +162,7 @@ internal sealed class PlatformBillingSettlementService(
|
||||
existing.EndsAt = now > existing.StartsAt ? now : existing.StartsAt.AddTicks(1);
|
||||
}
|
||||
|
||||
dbContext.TenantSaasSubscriptionItems.AddRange(orderItems.Select(item => new TenantSaasSubscriptionItem
|
||||
platformControlPlanePersistence.TenantSaasSubscriptionItems.AddRange(orderItems.Select(item => new TenantSaasSubscriptionItem
|
||||
{
|
||||
TenantId = order.TenantId,
|
||||
SubscriptionId = subscription.Id,
|
||||
@@ -174,7 +177,7 @@ internal sealed class PlatformBillingSettlementService(
|
||||
}));
|
||||
}
|
||||
|
||||
var invoice = await dbContext.PlatformBillingInvoices.SingleOrDefaultAsync(value =>
|
||||
var invoice = await platformControlPlanePersistence.PlatformBillingInvoices.SingleOrDefaultAsync(value =>
|
||||
value.TenantId == order.TenantId && value.OrderId == order.Id, cancellationToken);
|
||||
if (invoice is null)
|
||||
{
|
||||
@@ -188,12 +191,12 @@ internal sealed class PlatformBillingSettlementService(
|
||||
IssuedAt = now,
|
||||
BillingProfileSnapshot = await LoadBillingProfileSnapshotAsync(order.TenantId, cancellationToken)
|
||||
};
|
||||
dbContext.PlatformBillingInvoices.Add(invoice);
|
||||
platformControlPlanePersistence.PlatformBillingInvoices.Add(invoice);
|
||||
}
|
||||
|
||||
invoice.Status = PlatformBillingInvoiceStatus.Paid;
|
||||
invoice.PaidAt = now;
|
||||
dbContext.AuditLogs.Add(new AuditLog
|
||||
jobsOperationsPersistence.AuditLogs.Add(new AuditLog
|
||||
{
|
||||
TenantId = order.TenantId,
|
||||
ActorUserId = actorUserId,
|
||||
@@ -201,9 +204,9 @@ internal sealed class PlatformBillingSettlementService(
|
||||
TargetType = "platform_billing_orders",
|
||||
TargetId = order.Id.ToString(),
|
||||
Details = JsonSerializer.SerializeToElement(new
|
||||
{ order.OrderNo, payment.PaymentNo, payment.Provider, order.Purpose })
|
||||
{ order.OrderNo, payment.PaymentNo, payment.Provider, order.Purpose })
|
||||
});
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await platformControlPlanePersistence.SaveChangesAsync(cancellationToken);
|
||||
await featureCacheInvalidator.InvalidateAsync(order.TenantId, cancellationToken);
|
||||
|
||||
return payment;
|
||||
@@ -211,7 +214,7 @@ internal sealed class PlatformBillingSettlementService(
|
||||
|
||||
private async Task<JsonElement> LoadBillingProfileSnapshotAsync(Guid tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var profile = await dbContext.TenantBillingProfiles.AsNoTracking()
|
||||
var profile = await tenantAdministrationPersistence.TenantBillingProfiles.AsNoTracking()
|
||||
.SingleOrDefaultAsync(value => value.TenantId == tenantId, cancellationToken);
|
||||
return profile is null
|
||||
? JsonDefaults.Object()
|
||||
|
||||
@@ -9,7 +9,7 @@ using Tiku.Infrastructure.Persistence;
|
||||
namespace Tiku.Infrastructure.PlatformBilling;
|
||||
|
||||
internal sealed class SaasCatalogAdminService(
|
||||
TikuDbContext dbContext,
|
||||
IPlatformControlPlanePersistence dbContext,
|
||||
IOperationAuditService auditService) : ISaasCatalogAdminService
|
||||
{
|
||||
public async Task<SaasCatalogSnapshot> GetCatalogAsync(
|
||||
@@ -297,8 +297,8 @@ internal sealed class SaasCatalogAdminService(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = from version in dbContext.SaasOfferingVersions.AsNoTracking()
|
||||
join offering in dbContext.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
|
||||
select new { Version = version, Offering = offering };
|
||||
join offering in dbContext.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
|
||||
select new { Version = version, Offering = offering };
|
||||
if (versionId.HasValue) query = query.Where(value => value.Version.Id == versionId);
|
||||
var rows = await query.OrderBy(value => value.Offering.SortOrder).ThenBy(value => value.Offering.Code)
|
||||
.ThenByDescending(value => value.Version.Version).ToArrayAsync(cancellationToken);
|
||||
|
||||
@@ -12,7 +12,7 @@ using Tiku.Infrastructure.Persistence;
|
||||
namespace Tiku.Infrastructure.PlatformBilling;
|
||||
|
||||
internal sealed class SaasSubscriptionLifecycleService(
|
||||
TikuDbContext directoryDbContext,
|
||||
IPlatformControlPlanePersistence directoryPersistence,
|
||||
ITenantContext tenantContext,
|
||||
ITenantExecutionScope tenantExecutionScope,
|
||||
IOptions<SaasSubscriptionLifecycleOptions> options) : ISaasSubscriptionLifecycleService
|
||||
@@ -29,7 +29,7 @@ internal sealed class SaasSubscriptionLifecycleService(
|
||||
"SaaS subscription lifecycle discovery requires a global system context.");
|
||||
|
||||
var effectiveAt = asOf ?? DateTimeOffset.UtcNow;
|
||||
var tenantIds = await directoryDbContext.TenantSaasSubscriptions.AsNoTracking()
|
||||
var tenantIds = await directoryPersistence.TenantSaasSubscriptions.AsNoTracking()
|
||||
.Where(subscription =>
|
||||
subscription.CurrentPeriodEnd <= effectiveAt &&
|
||||
(subscription.Status == TenantSaasSubscriptionStatus.Trial ||
|
||||
@@ -66,7 +66,9 @@ internal sealed class SaasSubscriptionLifecycleService(
|
||||
DateTimeOffset asOf,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var dbContext = services.GetRequiredService<TikuDbContext>();
|
||||
var dbContext = services.GetRequiredService<IPlatformControlPlanePersistence>();
|
||||
var tenancyPersistence = services.GetRequiredService<ITenancyPersistence>();
|
||||
var jobsOperationsPersistence = services.GetRequiredService<IJobsOperationsPersistence>();
|
||||
var subscription = await dbContext.TenantSaasSubscriptions
|
||||
.OrderByDescending(value => value.UpdatedAt)
|
||||
.FirstOrDefaultAsync(value =>
|
||||
@@ -83,7 +85,7 @@ internal sealed class SaasSubscriptionLifecycleService(
|
||||
var items = await dbContext.TenantSaasSubscriptionItems
|
||||
.Where(value => value.TenantId == tenantId && value.SubscriptionId == subscription.Id)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var tenant = await dbContext.Tenants.SingleAsync(value => value.Id == tenantId, cancellationToken);
|
||||
var tenant = await tenancyPersistence.Tenants.SingleAsync(value => value.Id == tenantId, cancellationToken);
|
||||
|
||||
string transition;
|
||||
if (subscription.CancelAtPeriodEnd)
|
||||
@@ -167,7 +169,7 @@ internal sealed class SaasSubscriptionLifecycleService(
|
||||
}
|
||||
|
||||
subscription.LifecycleVersion++;
|
||||
dbContext.AuditLogs.Add(new AuditLog
|
||||
jobsOperationsPersistence.AuditLogs.Add(new AuditLog
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Action = $"platform_billing.subscription.{transition}",
|
||||
@@ -217,4 +219,4 @@ internal sealed class SaasSubscriptionLifecycleService(
|
||||
item.Status = TenantSaasSubscriptionItemStatus.Cancelled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ using Tiku.Infrastructure.Persistence;
|
||||
namespace Tiku.Infrastructure.PlatformBilling;
|
||||
|
||||
internal sealed class TenantBillingService(
|
||||
TikuDbContext dbContext,
|
||||
IPlatformControlPlanePersistence platformControlPlanePersistence,
|
||||
IJobsOperationsPersistence jobsOperationsPersistence,
|
||||
IPlatformBillingPaymentGateway paymentGateway,
|
||||
IPlatformBillingSettlementService settlementService,
|
||||
IFeatureAccessService featureAccessService,
|
||||
@@ -26,7 +27,7 @@ internal sealed class TenantBillingService(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var features = await dbContext.SaasFeatures.AsNoTracking()
|
||||
var features = await platformControlPlanePersistence.SaasFeatures.AsNoTracking()
|
||||
.Where(value => value.Status == SaasFeatureStatus.Active && !value.IsCore)
|
||||
.OrderBy(value => value.SortOrder).ThenBy(value => value.Code)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
@@ -43,7 +44,7 @@ internal sealed class TenantBillingService(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var idempotencyKey = Required(command.IdempotencyKey, "idempotencyKey");
|
||||
var existingQuote = await dbContext.PlatformBillingQuotes.AsNoTracking()
|
||||
var existingQuote = await platformControlPlanePersistence.PlatformBillingQuotes.AsNoTracking()
|
||||
.SingleOrDefaultAsync(value => value.TenantId == actor.TenantId && value.IdempotencyKey == idempotencyKey,
|
||||
cancellationToken);
|
||||
if (existingQuote is not null) return await LoadQuoteAsync(actor.TenantId, existingQuote.Id, cancellationToken);
|
||||
@@ -54,8 +55,8 @@ internal sealed class TenantBillingService(
|
||||
.ToArray();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var versions = await (
|
||||
from version in dbContext.SaasOfferingVersions.AsNoTracking()
|
||||
join offering in dbContext.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
|
||||
from version in platformControlPlanePersistence.SaasOfferingVersions.AsNoTracking()
|
||||
join offering in platformControlPlanePersistence.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
|
||||
where requestedIds.Contains(version.Id) &&
|
||||
version.Status == SaasOfferingVersionStatus.Published &&
|
||||
offering.Status == SaasOfferingStatus.Active &&
|
||||
@@ -71,10 +72,10 @@ internal sealed class TenantBillingService(
|
||||
if (versions.Select(value => value.Version.Currency).Distinct(StringComparer.Ordinal).Count() != 1)
|
||||
throw Error("All quote items must use the same currency.", "platform_billing_currency_mismatch");
|
||||
|
||||
var featureRows = await dbContext.SaasOfferingVersionFeatures.AsNoTracking()
|
||||
var featureRows = await platformControlPlanePersistence.SaasOfferingVersionFeatures.AsNoTracking()
|
||||
.Where(value => requestedIds.Contains(value.OfferingVersionId))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var limitRows = await dbContext.SaasOfferingVersionLimits.AsNoTracking()
|
||||
var limitRows = await platformControlPlanePersistence.SaasOfferingVersionLimits.AsNoTracking()
|
||||
.Where(value => requestedIds.Contains(value.OfferingVersionId))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var featureCodes = featureRows.Select(value => value.FeatureCode).Distinct(StringComparer.Ordinal)
|
||||
@@ -97,8 +98,8 @@ internal sealed class TenantBillingService(
|
||||
FeatureSnapshot = JsonSerializer.SerializeToElement(featureCodes),
|
||||
LimitSnapshot = JsonSerializer.SerializeToElement(limits)
|
||||
};
|
||||
dbContext.PlatformBillingQuotes.Add(quote);
|
||||
dbContext.PlatformBillingQuoteItems.AddRange(versions.Select(value => new PlatformBillingQuoteItem
|
||||
platformControlPlanePersistence.PlatformBillingQuotes.Add(quote);
|
||||
platformControlPlanePersistence.PlatformBillingQuoteItems.AddRange(versions.Select(value => new PlatformBillingQuoteItem
|
||||
{
|
||||
TenantId = actor.TenantId,
|
||||
QuoteId = quote.Id,
|
||||
@@ -123,7 +124,7 @@ internal sealed class TenantBillingService(
|
||||
.ToDictionary(limit => limit.MetricCode, limit => limit.LimitValue)
|
||||
})
|
||||
}));
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await platformControlPlanePersistence.SaveChangesAsync(cancellationToken);
|
||||
return await LoadQuoteAsync(actor.TenantId, quote.Id, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -133,23 +134,23 @@ internal sealed class TenantBillingService(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = Required(command.IdempotencyKey, "idempotencyKey");
|
||||
var existing = await dbContext.PlatformBillingOrders.AsNoTracking()
|
||||
var existing = await platformControlPlanePersistence.PlatformBillingOrders.AsNoTracking()
|
||||
.SingleOrDefaultAsync(value => value.TenantId == actor.TenantId && value.IdempotencyKey == key,
|
||||
cancellationToken);
|
||||
if (existing is not null) return await LoadOrderAsync(actor.TenantId, existing.OrderNo, cancellationToken);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var quote = await dbContext.PlatformBillingQuotes.SingleOrDefaultAsync(value =>
|
||||
var quote = await platformControlPlanePersistence.PlatformBillingQuotes.SingleOrDefaultAsync(value =>
|
||||
value.TenantId == actor.TenantId && value.Id == command.QuoteId, cancellationToken)
|
||||
?? throw Error("Quote was not found.", "platform_billing_quote_not_found");
|
||||
if (quote.Status != PlatformBillingQuoteStatus.Active || quote.ExpiresAt <= now)
|
||||
{
|
||||
quote.Status = quote.ExpiresAt <= now ? PlatformBillingQuoteStatus.Expired : quote.Status;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await platformControlPlanePersistence.SaveChangesAsync(cancellationToken);
|
||||
throw Error("Quote is no longer active.", "platform_billing_quote_expired");
|
||||
}
|
||||
|
||||
var quoteItems = await dbContext.PlatformBillingQuoteItems.AsNoTracking()
|
||||
var quoteItems = await platformControlPlanePersistence.PlatformBillingQuoteItems.AsNoTracking()
|
||||
.Where(value => value.TenantId == actor.TenantId && value.QuoteId == quote.Id)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var order = new PlatformBillingOrder
|
||||
@@ -171,8 +172,8 @@ internal sealed class TenantBillingService(
|
||||
quote.LimitSnapshot
|
||||
})
|
||||
};
|
||||
dbContext.PlatformBillingOrders.Add(order);
|
||||
dbContext.PlatformBillingOrderItems.AddRange(quoteItems.Select(value => new PlatformBillingOrderItem
|
||||
platformControlPlanePersistence.PlatformBillingOrders.Add(order);
|
||||
platformControlPlanePersistence.PlatformBillingOrderItems.AddRange(quoteItems.Select(value => new PlatformBillingOrderItem
|
||||
{
|
||||
TenantId = actor.TenantId,
|
||||
OrderId = order.Id,
|
||||
@@ -184,7 +185,7 @@ internal sealed class TenantBillingService(
|
||||
Snapshot = value.Snapshot
|
||||
}));
|
||||
quote.Status = PlatformBillingQuoteStatus.Converted;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await platformControlPlanePersistence.SaveChangesAsync(cancellationToken);
|
||||
return await LoadOrderAsync(actor.TenantId, order.OrderNo, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -194,12 +195,12 @@ internal sealed class TenantBillingService(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = Required(command.IdempotencyKey, "idempotencyKey");
|
||||
var existing = await dbContext.PlatformBillingPayments.AsNoTracking()
|
||||
var existing = await platformControlPlanePersistence.PlatformBillingPayments.AsNoTracking()
|
||||
.SingleOrDefaultAsync(value => value.TenantId == actor.TenantId && value.IdempotencyKey == key,
|
||||
cancellationToken);
|
||||
if (existing is not null) return ToPaymentView(existing);
|
||||
|
||||
var order = await dbContext.PlatformBillingOrders.SingleOrDefaultAsync(value =>
|
||||
var order = await platformControlPlanePersistence.PlatformBillingOrders.SingleOrDefaultAsync(value =>
|
||||
value.TenantId == actor.TenantId && value.OrderNo == command.OrderNo, cancellationToken)
|
||||
?? throw Error("Order was not found.", "platform_billing_order_not_found");
|
||||
if (order.Status != PlatformBillingOrderStatus.PendingPayment || order.ExpiresAt <= DateTimeOffset.UtcNow)
|
||||
@@ -207,7 +208,7 @@ internal sealed class TenantBillingService(
|
||||
if (order.ExpiresAt <= DateTimeOffset.UtcNow && order.Status == PlatformBillingOrderStatus.PendingPayment)
|
||||
{
|
||||
order.Status = PlatformBillingOrderStatus.Expired;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await platformControlPlanePersistence.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
throw Error("Order does not allow a new payment.", "platform_billing_order_status_invalid");
|
||||
@@ -224,8 +225,8 @@ internal sealed class TenantBillingService(
|
||||
Method = Required(command.Method, "method"),
|
||||
AmountCents = order.TotalAmountCents
|
||||
};
|
||||
dbContext.PlatformBillingPayments.Add(payment);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
platformControlPlanePersistence.PlatformBillingPayments.Add(payment);
|
||||
await platformControlPlanePersistence.SaveChangesAsync(cancellationToken);
|
||||
|
||||
if (order.TotalAmountCents == 0)
|
||||
{
|
||||
@@ -259,13 +260,13 @@ internal sealed class TenantBillingService(
|
||||
cancellationToken);
|
||||
payment.ProviderTradeNo = result.ProviderTradeNo;
|
||||
payment.ClientPayload = result.ClientPayload;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await platformControlPlanePersistence.SaveChangesAsync(cancellationToken);
|
||||
return ToPaymentView(payment);
|
||||
}
|
||||
catch
|
||||
{
|
||||
payment.Status = PlatformBillingPaymentStatus.Failed;
|
||||
await dbContext.SaveChangesAsync(CancellationToken.None);
|
||||
await platformControlPlanePersistence.SaveChangesAsync(CancellationToken.None);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -275,7 +276,7 @@ internal sealed class TenantBillingService(
|
||||
int limit,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var orderNos = await dbContext.PlatformBillingOrders.AsNoTracking()
|
||||
var orderNos = await platformControlPlanePersistence.PlatformBillingOrders.AsNoTracking()
|
||||
.Where(value => value.TenantId == actor.TenantId)
|
||||
.OrderByDescending(value => value.CreatedAt)
|
||||
.Take(Math.Clamp(limit, 1, 200))
|
||||
@@ -296,7 +297,7 @@ internal sealed class TenantBillingService(
|
||||
TenantBillingActor actor,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var subscription = await dbContext.TenantSaasSubscriptions.AsNoTracking()
|
||||
var subscription = await platformControlPlanePersistence.TenantSaasSubscriptions.AsNoTracking()
|
||||
.Where(value => value.TenantId == actor.TenantId)
|
||||
.OrderByDescending(value => value.UpdatedAt)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -309,16 +310,16 @@ internal sealed class TenantBillingService(
|
||||
string idempotencyKey,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var current = await dbContext.TenantSaasSubscriptions.AsNoTracking()
|
||||
var current = await platformControlPlanePersistence.TenantSaasSubscriptions.AsNoTracking()
|
||||
.Where(value => value.TenantId == actor.TenantId)
|
||||
.OrderByDescending(value => value.UpdatedAt)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
var currentAmount = current is null
|
||||
? 0
|
||||
: await dbContext.SaasOfferingVersions.AsNoTracking()
|
||||
: await platformControlPlanePersistence.SaasOfferingVersions.AsNoTracking()
|
||||
.Where(value => value.Id == current.BaseOfferingVersionId).Select(value => value.AmountCents)
|
||||
.SingleAsync(cancellationToken);
|
||||
var requestedAmount = await dbContext.SaasOfferingVersions.AsNoTracking()
|
||||
var requestedAmount = await platformControlPlanePersistence.SaasOfferingVersions.AsNoTracking()
|
||||
.Where(value => value.Id == command.BaseOfferingVersionId)
|
||||
.Select(value => (int?)value.AmountCents).SingleOrDefaultAsync(cancellationToken)
|
||||
?? throw Error("Offering version was not found.", "saas_offering_version_not_found");
|
||||
@@ -339,12 +340,12 @@ internal sealed class TenantBillingService(
|
||||
string idempotencyKey,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var subscription = await dbContext.TenantSaasSubscriptions.AsNoTracking()
|
||||
var subscription = await platformControlPlanePersistence.TenantSaasSubscriptions.AsNoTracking()
|
||||
.Where(value => value.TenantId == actor.TenantId)
|
||||
.OrderByDescending(value => value.UpdatedAt)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
?? throw Error("Subscription was not found.", "tenant_saas_subscription_not_found");
|
||||
var addOns = await dbContext.TenantSaasSubscriptionItems.AsNoTracking()
|
||||
var addOns = await platformControlPlanePersistence.TenantSaasSubscriptionItems.AsNoTracking()
|
||||
.Where(value => value.TenantId == actor.TenantId && value.SubscriptionId == subscription.Id &&
|
||||
value.ItemType == TenantSaasSubscriptionItemType.AddOn &&
|
||||
value.Status == TenantSaasSubscriptionItemStatus.Active)
|
||||
@@ -361,14 +362,14 @@ internal sealed class TenantBillingService(
|
||||
TenantBillingActor actor,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var subscription = await dbContext.TenantSaasSubscriptions
|
||||
var subscription = await platformControlPlanePersistence.TenantSaasSubscriptions
|
||||
.Where(value => value.TenantId == actor.TenantId)
|
||||
.OrderByDescending(value => value.UpdatedAt)
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
?? throw Error("Subscription was not found.", "tenant_saas_subscription_not_found");
|
||||
subscription.CancelAtPeriodEnd = true;
|
||||
subscription.CancelledAt = DateTimeOffset.UtcNow;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await platformControlPlanePersistence.SaveChangesAsync(cancellationToken);
|
||||
return await ToSubscriptionViewAsync(subscription, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -383,7 +384,7 @@ internal sealed class TenantBillingService(
|
||||
int limit,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await dbContext.PlatformBillingInvoices.AsNoTracking()
|
||||
return await platformControlPlanePersistence.PlatformBillingInvoices.AsNoTracking()
|
||||
.Where(value => value.TenantId == actor.TenantId)
|
||||
.OrderByDescending(value => value.CreatedAt)
|
||||
.Take(Math.Clamp(limit, 1, 200))
|
||||
@@ -395,7 +396,7 @@ internal sealed class TenantBillingService(
|
||||
int limit,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await dbContext.PlatformBillingInvoices.AsNoTracking()
|
||||
return await platformControlPlanePersistence.PlatformBillingInvoices.AsNoTracking()
|
||||
.Where(value => value.TenantId == actor.TenantId)
|
||||
.OrderByDescending(value => value.CreatedAt)
|
||||
.Take(Math.Clamp(limit, 1, 200))
|
||||
@@ -418,20 +419,20 @@ internal sealed class TenantBillingService(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalized = Required(orderNo, "orderNo");
|
||||
var order = await dbContext.PlatformBillingOrders.SingleOrDefaultAsync(value =>
|
||||
var order = await platformControlPlanePersistence.PlatformBillingOrders.SingleOrDefaultAsync(value =>
|
||||
value.TenantId == actor.TenantId && value.OrderNo == normalized,
|
||||
cancellationToken) ?? throw Error("Order was not found.", "platform_billing_order_not_found");
|
||||
if (order.Status != PlatformBillingOrderStatus.PendingPayment)
|
||||
throw Error("Only a pending order can be cancelled.", "platform_billing_order_not_cancellable");
|
||||
order.Status = PlatformBillingOrderStatus.Cancelled;
|
||||
order.CancelledAt = DateTimeOffset.UtcNow;
|
||||
var receivables = await dbContext.PlatformBillingInvoices
|
||||
var receivables = await platformControlPlanePersistence.PlatformBillingInvoices
|
||||
.Where(value => value.TenantId == actor.TenantId && value.OrderId == order.Id &&
|
||||
(value.Status == PlatformBillingInvoiceStatus.Draft ||
|
||||
value.Status == PlatformBillingInvoiceStatus.Issued))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
foreach (var receivable in receivables) receivable.Status = PlatformBillingInvoiceStatus.Void;
|
||||
dbContext.AuditLogs.Add(new AuditLog
|
||||
jobsOperationsPersistence.AuditLogs.Add(new AuditLog
|
||||
{
|
||||
TenantId = actor.TenantId,
|
||||
ActorUserId = actor.UserId,
|
||||
@@ -439,7 +440,7 @@ internal sealed class TenantBillingService(
|
||||
TargetType = "platform_billing_orders",
|
||||
TargetId = order.Id.ToString()
|
||||
});
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await platformControlPlanePersistence.SaveChangesAsync(cancellationToken);
|
||||
return await LoadOrderAsync(actor.TenantId, normalized, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -448,7 +449,7 @@ internal sealed class TenantBillingService(
|
||||
int limit,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await dbContext.PlatformBillingRefunds.AsNoTracking()
|
||||
return await platformControlPlanePersistence.PlatformBillingRefunds.AsNoTracking()
|
||||
.Where(value => value.TenantId == actor.TenantId)
|
||||
.OrderByDescending(value => value.CreatedAt)
|
||||
.Take(Math.Clamp(limit, 1, 200))
|
||||
@@ -458,7 +459,7 @@ internal sealed class TenantBillingService(
|
||||
private async Task<PlatformBillingQuoteView> LoadQuoteAsync(Guid tenantId, Guid quoteId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var quote = await dbContext.PlatformBillingQuotes.AsNoTracking()
|
||||
var quote = await platformControlPlanePersistence.PlatformBillingQuotes.AsNoTracking()
|
||||
.SingleAsync(value => value.TenantId == tenantId && value.Id == quoteId, cancellationToken);
|
||||
var items = await LoadQuoteItemsAsync(tenantId, quote.Id, cancellationToken);
|
||||
return new PlatformBillingQuoteView(
|
||||
@@ -480,15 +481,15 @@ internal sealed class TenantBillingService(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var normalized = Required(orderNo, "orderNo");
|
||||
var order = await dbContext.PlatformBillingOrders.AsNoTracking()
|
||||
var order = await platformControlPlanePersistence.PlatformBillingOrders.AsNoTracking()
|
||||
.SingleOrDefaultAsync(value => value.TenantId == tenantId && value.OrderNo == normalized,
|
||||
cancellationToken)
|
||||
?? throw Error("Order was not found.", "platform_billing_order_not_found");
|
||||
var items = await (
|
||||
from item in dbContext.PlatformBillingOrderItems.AsNoTracking()
|
||||
join version in dbContext.SaasOfferingVersions.AsNoTracking() on item.OfferingVersionId equals version
|
||||
from item in platformControlPlanePersistence.PlatformBillingOrderItems.AsNoTracking()
|
||||
join version in platformControlPlanePersistence.SaasOfferingVersions.AsNoTracking() on item.OfferingVersionId equals version
|
||||
.Id
|
||||
join offering in dbContext.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
|
||||
join offering in platformControlPlanePersistence.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
|
||||
where item.TenantId == tenantId && item.OrderId == order.Id
|
||||
orderby item.ItemType, offering.Code
|
||||
select new PlatformBillingQuoteItemView(item.OfferingVersionId, offering.Code, offering.Name,
|
||||
@@ -502,10 +503,10 @@ internal sealed class TenantBillingService(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await (
|
||||
from item in dbContext.PlatformBillingQuoteItems.AsNoTracking()
|
||||
join version in dbContext.SaasOfferingVersions.AsNoTracking() on item.OfferingVersionId equals version
|
||||
from item in platformControlPlanePersistence.PlatformBillingQuoteItems.AsNoTracking()
|
||||
join version in platformControlPlanePersistence.SaasOfferingVersions.AsNoTracking() on item.OfferingVersionId equals version
|
||||
.Id
|
||||
join offering in dbContext.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
|
||||
join offering in platformControlPlanePersistence.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
|
||||
where item.TenantId == tenantId && item.QuoteId == quoteId
|
||||
orderby item.ItemType, offering.Code
|
||||
select new PlatformBillingQuoteItemView(item.OfferingVersionId, offering.Code, offering.Name,
|
||||
@@ -516,14 +517,14 @@ internal sealed class TenantBillingService(
|
||||
private async Task<TenantSubscriptionView> ToSubscriptionViewAsync(TenantSaasSubscription subscription,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var versionIds = await dbContext.TenantSaasSubscriptionItems.AsNoTracking()
|
||||
var versionIds = await platformControlPlanePersistence.TenantSaasSubscriptionItems.AsNoTracking()
|
||||
.Where(value => value.TenantId == subscription.TenantId && value.SubscriptionId == subscription.Id &&
|
||||
value.Status == TenantSaasSubscriptionItemStatus.Active)
|
||||
.Select(value => value.OfferingVersionId)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
if (!versionIds.Contains(subscription.BaseOfferingVersionId))
|
||||
versionIds = [.. versionIds, subscription.BaseOfferingVersionId];
|
||||
var features = await dbContext.SaasOfferingVersionFeatures.AsNoTracking()
|
||||
var features = await platformControlPlanePersistence.SaasOfferingVersionFeatures.AsNoTracking()
|
||||
.Where(value => versionIds.Contains(value.OfferingVersionId))
|
||||
.Select(value => value.FeatureCode)
|
||||
.Distinct()
|
||||
@@ -545,8 +546,8 @@ internal sealed class TenantBillingService(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var rows = await (
|
||||
from version in dbContext.SaasOfferingVersions.AsNoTracking()
|
||||
join offering in dbContext.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
|
||||
from version in platformControlPlanePersistence.SaasOfferingVersions.AsNoTracking()
|
||||
join offering in platformControlPlanePersistence.SaasOfferings.AsNoTracking() on version.OfferingId equals offering.Id
|
||||
where version.Status == SaasOfferingVersionStatus.Published &&
|
||||
offering.Status == SaasOfferingStatus.Active &&
|
||||
(version.EffectiveAt == null || version.EffectiveAt <= now)
|
||||
@@ -555,9 +556,9 @@ internal sealed class TenantBillingService(
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var latest = rows.GroupBy(value => value.Offering.Id).Select(group => group.First()).ToArray();
|
||||
var ids = latest.Select(value => value.Version.Id).ToArray();
|
||||
var features = await dbContext.SaasOfferingVersionFeatures.AsNoTracking()
|
||||
var features = await platformControlPlanePersistence.SaasOfferingVersionFeatures.AsNoTracking()
|
||||
.Where(value => ids.Contains(value.OfferingVersionId)).ToArrayAsync(cancellationToken);
|
||||
var limits = await dbContext.SaasOfferingVersionLimits.AsNoTracking()
|
||||
var limits = await platformControlPlanePersistence.SaasOfferingVersionLimits.AsNoTracking()
|
||||
.Where(value => ids.Contains(value.OfferingVersionId)).ToArrayAsync(cancellationToken);
|
||||
return latest.Select(row => new SaasOfferingVersionItem(
|
||||
row.Version.Id, row.Offering.Id, row.Offering.Code, row.Offering.Name, row.Offering.Type,
|
||||
@@ -620,4 +621,4 @@ internal sealed class TenantBillingService(
|
||||
return value.EnumerateObject().Where(property => property.Value.TryGetInt64(out _))
|
||||
.ToDictionary(property => property.Name, property => property.Value.GetInt64(), StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user