417 lines
15 KiB
C#
417 lines
15 KiB
C#
using System.Text.Json;
|
|
using Tiku.Application.Commerce;
|
|
using Tiku.Application.Security;
|
|
using Tiku.Domain.Platform;
|
|
|
|
namespace Tiku.Application.PlatformBilling;
|
|
|
|
public sealed record SaasCatalogActor(Guid UserId);
|
|
|
|
public sealed record TenantBillingActor(Guid UserId, Guid TenantId);
|
|
|
|
public sealed record UpsertSaasFeatureCommand(
|
|
Guid? Id,
|
|
string Code,
|
|
string Name,
|
|
string Category,
|
|
string? Description,
|
|
int ReferencePriceCents,
|
|
string Currency,
|
|
SaasFeatureStatus Status,
|
|
int SortOrder);
|
|
|
|
public sealed record UpsertSaasOfferingCommand(
|
|
Guid? Id,
|
|
string Code,
|
|
string Name,
|
|
SaasOfferingType Type,
|
|
SaasOfferingStatus Status,
|
|
string? Description,
|
|
int SortOrder);
|
|
|
|
public sealed record UpsertSaasFeatureLimitCommand(
|
|
Guid? Id,
|
|
string MetricCode,
|
|
string FeatureCode,
|
|
string Name,
|
|
string Unit,
|
|
SaasFeatureLimitKind Kind,
|
|
int WarningPercent,
|
|
bool IsHardLimit);
|
|
|
|
public sealed record UpsertSaasOfferingVersionCommand(
|
|
Guid? Id,
|
|
Guid OfferingId,
|
|
PlatformBillingCycle BillingCycle,
|
|
int OriginalAmountCents,
|
|
int AmountCents,
|
|
string Currency,
|
|
DateTimeOffset? EffectiveAt,
|
|
IReadOnlyCollection<string> FeatureCodes,
|
|
IReadOnlyDictionary<string, long> Limits,
|
|
JsonElement Metadata);
|
|
|
|
public sealed record SaasOfferingVersionItem(
|
|
Guid Id,
|
|
Guid OfferingId,
|
|
string OfferingCode,
|
|
string OfferingName,
|
|
SaasOfferingType OfferingType,
|
|
int Version,
|
|
SaasOfferingVersionStatus Status,
|
|
PlatformBillingCycle BillingCycle,
|
|
int OriginalAmountCents,
|
|
int AmountCents,
|
|
string Currency,
|
|
DateTimeOffset? EffectiveAt,
|
|
DateTimeOffset? PublishedAt,
|
|
IReadOnlyCollection<string> FeatureCodes,
|
|
IReadOnlyDictionary<string, long> Limits);
|
|
|
|
public sealed record SaasCatalogSnapshot(
|
|
IReadOnlyCollection<SaasFeature> Features,
|
|
IReadOnlyCollection<SaasFeatureLimitDefinition> LimitDefinitions,
|
|
IReadOnlyCollection<SaasOffering> Offerings,
|
|
IReadOnlyCollection<SaasOfferingVersionItem> Versions);
|
|
|
|
public interface ISaasCatalogAdminService
|
|
{
|
|
Task<SaasCatalogSnapshot> GetCatalogAsync(SaasCatalogActor actor, CancellationToken cancellationToken = default);
|
|
|
|
Task<SaasFeature> UpsertFeatureAsync(SaasCatalogActor actor, UpsertSaasFeatureCommand command,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<SaasFeatureLimitDefinition> UpsertLimitDefinitionAsync(SaasCatalogActor actor,
|
|
UpsertSaasFeatureLimitCommand command, CancellationToken cancellationToken = default);
|
|
|
|
Task<SaasOffering> UpsertOfferingAsync(SaasCatalogActor actor, UpsertSaasOfferingCommand command,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<SaasOfferingVersionItem> UpsertDraftVersionAsync(SaasCatalogActor actor,
|
|
UpsertSaasOfferingVersionCommand command, CancellationToken cancellationToken = default);
|
|
|
|
Task<SaasOfferingVersionItem> PublishVersionAsync(SaasCatalogActor actor, Guid versionId,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<SaasOfferingVersionItem> CloneVersionAsync(SaasCatalogActor actor, Guid versionId,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<SaasOfferingVersionItem> RetireVersionAsync(SaasCatalogActor actor, Guid versionId,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public sealed record TenantBillingCatalog(
|
|
IReadOnlyCollection<SaasFeature> Features,
|
|
IReadOnlyCollection<SaasOfferingVersionItem> BasePlans,
|
|
IReadOnlyCollection<SaasOfferingVersionItem> AddOns);
|
|
|
|
public sealed record CreatePlatformBillingQuoteCommand(
|
|
Guid BaseOfferingVersionId,
|
|
IReadOnlyCollection<Guid> AddOnOfferingVersionIds,
|
|
PlatformBillingOrderPurpose Purpose,
|
|
string IdempotencyKey);
|
|
|
|
public sealed record PlatformBillingQuoteItemView(
|
|
Guid OfferingVersionId,
|
|
string OfferingCode,
|
|
string OfferingName,
|
|
PlatformBillingItemType ItemType,
|
|
int UnitAmountCents,
|
|
int AmountCents);
|
|
|
|
public sealed record PlatformBillingQuoteView(
|
|
Guid Id,
|
|
string QuoteNo,
|
|
PlatformBillingQuoteStatus Status,
|
|
PlatformBillingOrderPurpose Purpose,
|
|
int OriginalAmountCents,
|
|
int DiscountAmountCents,
|
|
int TotalAmountCents,
|
|
string Currency,
|
|
DateTimeOffset ExpiresAt,
|
|
IReadOnlyCollection<PlatformBillingQuoteItemView> Items,
|
|
IReadOnlyCollection<string> FeatureCodes,
|
|
IReadOnlyDictionary<string, long> Limits);
|
|
|
|
public sealed record CreatePlatformBillingOrderCommand(Guid QuoteId, string IdempotencyKey);
|
|
|
|
public sealed record CreatePlatformBillingPaymentCommand(
|
|
string OrderNo,
|
|
string Provider,
|
|
string Method,
|
|
string IdempotencyKey,
|
|
string? OpenId,
|
|
string? ReturnUrl,
|
|
string? QuitUrl);
|
|
|
|
public sealed record PlatformBillingOrderView(
|
|
Guid Id,
|
|
string OrderNo,
|
|
PlatformBillingOrderPurpose Purpose,
|
|
PlatformBillingOrderStatus Status,
|
|
int TotalAmountCents,
|
|
string Currency,
|
|
DateTimeOffset ExpiresAt,
|
|
DateTimeOffset? PaidAt,
|
|
IReadOnlyCollection<PlatformBillingQuoteItemView> Items);
|
|
|
|
public sealed record PlatformBillingPaymentView(
|
|
Guid Id,
|
|
string PaymentNo,
|
|
string Provider,
|
|
string Method,
|
|
PlatformBillingPaymentStatus Status,
|
|
int AmountCents,
|
|
JsonElement ClientPayload);
|
|
|
|
public sealed record TenantSubscriptionView(
|
|
Guid Id,
|
|
TenantSaasSubscriptionStatus Status,
|
|
DateTimeOffset StartsAt,
|
|
DateTimeOffset CurrentPeriodStart,
|
|
DateTimeOffset CurrentPeriodEnd,
|
|
bool CancelAtPeriodEnd,
|
|
Guid BaseOfferingVersionId,
|
|
Guid? ScheduledBaseOfferingVersionId,
|
|
IReadOnlyCollection<string> FeatureCodes);
|
|
|
|
public sealed record ChangeTenantSubscriptionCommand(
|
|
Guid BaseOfferingVersionId,
|
|
IReadOnlyCollection<Guid> AddOnOfferingVersionIds);
|
|
|
|
public interface ITenantBillingService
|
|
{
|
|
Task<TenantBillingCatalog> GetCatalogAsync(TenantBillingActor actor, CancellationToken cancellationToken = default);
|
|
|
|
Task<PlatformBillingQuoteView> CreateQuoteAsync(TenantBillingActor actor, CreatePlatformBillingQuoteCommand command,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<PlatformBillingOrderView> CreateOrderAsync(TenantBillingActor actor, CreatePlatformBillingOrderCommand command,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<PlatformBillingPaymentView> CreatePaymentAsync(TenantBillingActor actor,
|
|
CreatePlatformBillingPaymentCommand command, CancellationToken cancellationToken = default);
|
|
|
|
Task<IReadOnlyCollection<PlatformBillingOrderView>> GetOrdersAsync(TenantBillingActor actor, int limit,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<PlatformBillingOrderView> GetOrderAsync(TenantBillingActor actor, string orderNo,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<TenantSubscriptionView?> GetSubscriptionAsync(TenantBillingActor actor,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<PlatformBillingOrderView> ChangeSubscriptionAsync(TenantBillingActor actor,
|
|
ChangeTenantSubscriptionCommand command, string idempotencyKey, CancellationToken cancellationToken = default);
|
|
|
|
Task<PlatformBillingOrderView> RenewSubscriptionAsync(TenantBillingActor actor, string idempotencyKey,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<TenantSubscriptionView> CancelSubscriptionAsync(TenantBillingActor actor,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<IReadOnlyCollection<FeatureQuotaSnapshot>> GetUsageAsync(TenantBillingActor actor,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<IReadOnlyCollection<PlatformBillingInvoice>> GetInvoicesAsync(TenantBillingActor actor, int limit,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<IReadOnlyCollection<TenantReceivableView>> GetReceivablesAsync(TenantBillingActor actor, int limit,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<PlatformBillingOrderView> CancelOrderAsync(TenantBillingActor actor, string orderNo,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<IReadOnlyCollection<PlatformBillingRefund>> GetRefundsAsync(TenantBillingActor actor, int limit,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public sealed record PlatformBillingAdminQuery(Guid? TenantId, string? Status, int Limit = 100);
|
|
|
|
public sealed record ConfirmManualPaymentCommand(
|
|
Guid PaymentId,
|
|
string? ProviderTradeNo,
|
|
DateTimeOffset? PaidAt,
|
|
string Reason);
|
|
|
|
public sealed record UpsertTenantFeatureOverrideCommand(
|
|
Guid TenantId,
|
|
string FeatureCode,
|
|
TenantFeatureOverrideMode Mode,
|
|
DateTimeOffset? ExpiresAt,
|
|
string Reason);
|
|
|
|
public sealed record GrantTenantTrialCommand(
|
|
Guid TenantId,
|
|
Guid BaseOfferingVersionId,
|
|
int TrialDays,
|
|
string IdempotencyKey,
|
|
string Reason);
|
|
|
|
public sealed record ChangePlatformSubscriptionCommand(Guid SubscriptionId, string Reason, int? ExtendDays = null);
|
|
|
|
public sealed record RequestPlatformRefundCommand(
|
|
Guid PaymentId,
|
|
int AmountCents,
|
|
string Reason,
|
|
string IdempotencyKey,
|
|
PlatformBillingRefundSubscriptionEffect SubscriptionEffect);
|
|
|
|
public sealed record ReviewPlatformRefundCommand(Guid RefundId, string Reason);
|
|
|
|
public sealed record CommercialMetrics(
|
|
int ActiveSubscriptions,
|
|
int TrialSubscriptions,
|
|
int PastDueSubscriptions,
|
|
int MrrCents,
|
|
int ArrCents,
|
|
int CollectedCents,
|
|
int OutstandingCents,
|
|
int OverdueCents,
|
|
int RefundedCents,
|
|
int ChurnedSubscriptions);
|
|
|
|
public sealed record TenantReceivableView(
|
|
Guid Id,
|
|
Guid? OrderId,
|
|
string InvoiceNo,
|
|
PlatformBillingInvoiceStatus Status,
|
|
int TotalAmountCents,
|
|
string Currency,
|
|
DateOnly? DueDate,
|
|
DateTimeOffset? IssuedAt,
|
|
DateTimeOffset? PaidAt);
|
|
|
|
public interface IPlatformBillingAdminService
|
|
{
|
|
Task<IReadOnlyCollection<PlatformBillingOrder>> GetOrdersAsync(SaasCatalogActor actor,
|
|
PlatformBillingAdminQuery query, CancellationToken cancellationToken = default);
|
|
|
|
Task<IReadOnlyCollection<PlatformBillingPayment>> GetPaymentsAsync(SaasCatalogActor actor,
|
|
PlatformBillingAdminQuery query, CancellationToken cancellationToken = default);
|
|
|
|
Task<IReadOnlyCollection<PlatformBillingRefund>> GetRefundsAsync(SaasCatalogActor actor,
|
|
PlatformBillingAdminQuery query, CancellationToken cancellationToken = default);
|
|
|
|
Task<IReadOnlyCollection<PlatformBillingInvoice>> GetInvoicesAsync(SaasCatalogActor actor,
|
|
PlatformBillingAdminQuery query, CancellationToken cancellationToken = default);
|
|
|
|
Task<IReadOnlyCollection<TenantFeatureUsage>> GetUsageAsync(SaasCatalogActor actor, PlatformBillingAdminQuery query,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<IReadOnlyCollection<PlatformBillingInvoiceReminder>> GetInvoiceRemindersAsync(SaasCatalogActor actor,
|
|
PlatformBillingAdminQuery query, CancellationToken cancellationToken = default);
|
|
|
|
Task<IReadOnlyCollection<TenantSaasSubscription>> GetSubscriptionsAsync(SaasCatalogActor actor,
|
|
PlatformBillingAdminQuery query, CancellationToken cancellationToken = default);
|
|
|
|
Task<PlatformBillingPayment> ConfirmManualPaymentAsync(SaasCatalogActor actor, ConfirmManualPaymentCommand command,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<TenantFeatureOverride> UpsertFeatureOverrideAsync(SaasCatalogActor actor,
|
|
UpsertTenantFeatureOverrideCommand command, CancellationToken cancellationToken = default);
|
|
|
|
Task<TenantSaasSubscription> GrantTrialAsync(SaasCatalogActor actor, GrantTenantTrialCommand command,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<TenantSaasSubscription> SuspendSubscriptionAsync(SaasCatalogActor actor,
|
|
ChangePlatformSubscriptionCommand command, CancellationToken cancellationToken = default);
|
|
|
|
Task<TenantSaasSubscription> ResumeSubscriptionAsync(SaasCatalogActor actor,
|
|
ChangePlatformSubscriptionCommand command, CancellationToken cancellationToken = default);
|
|
|
|
Task<TenantSaasSubscription> CancelSubscriptionAsync(SaasCatalogActor actor,
|
|
ChangePlatformSubscriptionCommand command, CancellationToken cancellationToken = default);
|
|
|
|
Task<TenantSaasSubscription> ExtendSubscriptionAsync(SaasCatalogActor actor,
|
|
ChangePlatformSubscriptionCommand command, CancellationToken cancellationToken = default);
|
|
|
|
Task<PlatformBillingRefund> RequestRefundAsync(SaasCatalogActor actor, RequestPlatformRefundCommand command,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<PlatformBillingRefund> ApproveRefundAsync(SaasCatalogActor actor, ReviewPlatformRefundCommand command,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<PlatformBillingRefund> RejectRefundAsync(SaasCatalogActor actor, ReviewPlatformRefundCommand command,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<PlatformBillingRefund> RetryRefundAsync(SaasCatalogActor actor, ReviewPlatformRefundCommand command,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<CommercialMetrics> GetCommercialMetricsAsync(SaasCatalogActor actor,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public interface IPlatformBillingPaymentGateway
|
|
{
|
|
Task<CreatePaymentProviderResult> CreatePaymentAsync(string provider, CreatePaymentProviderRequest request,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<PaymentNotificationResult> ParseNotificationAsync(string provider, PaymentNotificationRequest request,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<CreateRefundProviderResult> CreateRefundAsync(
|
|
string provider,
|
|
CreateRefundProviderRequest request,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return Task.FromException<CreateRefundProviderResult>(new PlatformBillingException(
|
|
"Platform refund gateway is not configured.",
|
|
"platform_billing_refund_provider_not_supported"));
|
|
}
|
|
}
|
|
|
|
public interface ICommercialBillingProcessor
|
|
{
|
|
Task<int> ProcessDueAsync(CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public sealed class CommercialBillingOptions
|
|
{
|
|
public bool Enabled { get; set; } = true;
|
|
public int BatchSize { get; set; } = 100;
|
|
public string[] AllowedWebhookHosts { get; set; } = [];
|
|
}
|
|
|
|
public sealed record PlatformBillingNotification(
|
|
string Provider,
|
|
IReadOnlyDictionary<string, string> Headers,
|
|
string RawBody,
|
|
JsonElement Body);
|
|
|
|
public interface IPlatformBillingNotificationService
|
|
{
|
|
Task ProcessAsync(PlatformBillingNotification notification, CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public interface IPlatformBillingSettlementService
|
|
{
|
|
Task<PlatformBillingPayment> MarkPaidAsync(
|
|
Guid paymentId,
|
|
string providerEventId,
|
|
string eventType,
|
|
string? providerTradeNo,
|
|
DateTimeOffset paidAt,
|
|
JsonElement payload,
|
|
Guid? actorUserId,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public sealed class SaasSubscriptionLifecycleOptions
|
|
{
|
|
public bool Enabled { get; set; } = true;
|
|
public int BatchSize { get; set; } = 100;
|
|
public int PastDueGraceDays { get; set; } = 7;
|
|
}
|
|
|
|
public interface ISaasSubscriptionLifecycleService
|
|
{
|
|
Task<int> ProcessDueAsync(
|
|
DateTimeOffset? asOf = null,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public sealed class PlatformBillingException(string message, string code) : Exception(message)
|
|
{
|
|
public string Code { get; } = code;
|
|
} |