Files
tiku-backend.net/Tiku.Application/PlatformBilling/PlatformBillingContracts.cs
xiong c497a3ca8d
Some checks failed
ci / release-gate (push) Has been cancelled
清理代码
2026-08-03 12:31:39 +08:00

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;
}