Files
tiku-backend.net/Tiku.Application/PlatformAdmin/PlatformAdminModels.cs

432 lines
15 KiB
C#

using System.Text.Json;
using Tiku.Domain.Identity;
using Tiku.Domain.Platform;
using Tiku.Domain.Tenancy;
namespace Tiku.Application.PlatformAdmin;
public sealed record PlatformAdminActor(Guid UserId);
public sealed record PlatformAdminQuery(string? Status = null, string? Search = null, int? Limit = null);
public sealed record PlatformOverview(
int TenantCount,
int ActiveTenantCount,
int SuspendedTenantCount,
int OrderCount,
int PaidOrderCount,
int RevenueCents,
int QuestionBankCount,
int QuestionCount,
int LearningActiveUserCount);
public sealed record PlatformTenantList(IReadOnlyCollection<PlatformTenantItem> Items);
public sealed record PlatformTenantItem(
Guid Id,
string Slug,
string Name,
string? LegalName,
TenantStatus Status,
TenantMode Mode,
BillingStatus BillingStatus,
DateTimeOffset? SubscriptionExpiresAt,
int DomainCount,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt);
public sealed record PlatformTenantDetail(
PlatformTenantItem Tenant,
IReadOnlyCollection<PlatformTenantDomainItem> Domains,
IReadOnlyCollection<PlatformTenantSubscriptionItem> Subscriptions,
TenantBillingProfileItem? BillingProfile,
TenantBillingPolicyItem? BillingPolicy,
PlatformOwnerActivationStatus OwnerActivation);
public sealed record PlatformTenantDomainItem(
Guid Id,
Guid TenantId,
string Host,
TenantDomainType DomainType,
TenantDomainStatus Status,
bool IsPrimary,
DateTimeOffset? VerifiedAt,
DateTimeOffset? DnsVerifiedAt,
DateTimeOffset? TlsReadyAt,
DateTimeOffset? LastCheckedAt,
string? LastFailureReason,
string? VerificationRecordName,
string? VerificationToken,
string? CnameTarget);
public sealed record PlatformOwnerActivationStatus(
string Status,
Guid? ActivationId,
DateTimeOffset? ExpiresAt);
public sealed record PlatformOwnerActivationLinkResult(
Guid ActivationId,
string? ActivationUrl,
DateTimeOffset ExpiresAt,
bool IsReplay);
public sealed record IssuePlatformOwnerActivationLinkCommand(
Guid TenantId,
string IdempotencyKey,
string Reason,
bool ReplaceExisting);
public sealed record ReplacePlatformPrimaryDomainCommand(Guid TenantId, string Host, string Reason);
public sealed record PlatformTenantSubscriptionItem(
Guid Id,
Guid TenantId,
Guid BaseOfferingVersionId,
TenantSaasSubscriptionStatus Status,
DateTimeOffset StartsAt,
DateTimeOffset CurrentPeriodStart,
DateTimeOffset CurrentPeriodEnd,
bool CancelAtPeriodEnd,
JsonElement Metadata);
public sealed record TenantBillingProfileItem(
Guid TenantId,
string? BillingName,
string? TaxId,
string? ContactName,
string? ContactPhoneMasked,
string? ContactEmail,
string? BillingAddress,
string? InvoiceTitle,
TenantBillingInvoiceTitleType? InvoiceType,
string? BankName,
string? BankAccountMasked,
JsonElement Metadata);
public sealed record CreatePlatformTenantCommand(
string Slug,
string Name,
string? LegalName,
TenantStatus Status,
BillingStatus BillingStatus,
JsonElement Metadata,
string PrimaryDomainHost,
string? OwnerEmail,
string? OwnerPhone,
string OwnerName,
Guid? InitialOfferingVersionId,
int? TrialDays,
TenantBillingCollectionMode CollectionMode,
string DefaultPaymentProvider,
bool AutoGenerateRenewal,
int RenewalLeadDays,
string IdempotencyKey);
public sealed record PlatformTenantProvisioningResult(
PlatformTenantItem Tenant,
Guid OwnerUserId,
string OwnerIdentifier,
bool MustChangePassword,
PlatformTenantDomainItem PrimaryDomain,
PlatformOwnerActivationStatus OwnerActivation,
bool IsReplay);
public sealed class TenantProvisioningOptions
{
public const string SectionName = "TenantProvisioning";
public string DefaultBaseOfferingCode { get; set; } = "starter";
public int DefaultTrialDays { get; set; } = 14;
public int OwnerActivationMinutes { get; set; } = 30;
public string OwnerActivationUrlTemplate { get; set; } = "https://{host}";
public string? DevelopmentLocalhostOwnerActivationUrlTemplate { get; set; }
}
public static class TenantOwnerActivationUrlPolicy
{
public static string Build(
TenantProvisioningOptions options,
string host,
Guid activationId,
string token)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentException.ThrowIfNullOrWhiteSpace(host);
ArgumentException.ThrowIfNullOrWhiteSpace(token);
var normalizedHost = host.Trim().TrimEnd('.').ToLowerInvariant();
var useDevelopmentLocalhost = normalizedHost.EndsWith(".localhost", StringComparison.Ordinal) &&
!string.IsNullOrWhiteSpace(options
.DevelopmentLocalhostOwnerActivationUrlTemplate);
var template = useDevelopmentLocalhost
? options.DevelopmentLocalhostOwnerActivationUrlTemplate!
: options.OwnerActivationUrlTemplate;
var origin = template.Replace("{host}", normalizedHost, StringComparison.Ordinal).TrimEnd('/');
if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri) ||
(!useDevelopmentLocalhost && uri.Scheme != Uri.UriSchemeHttps) ||
(useDevelopmentLocalhost &&
uri.Scheme != Uri.UriSchemeHttp &&
uri.Scheme != Uri.UriSchemeHttps))
throw new InvalidOperationException(
"Owner activation URLs must use HTTPS except for an explicitly configured Development .localhost origin.");
return $"{origin}/activate/{activationId}#token={token}";
}
}
public sealed record TenantBillingPolicyItem(
Guid TenantId,
TenantBillingCollectionMode CollectionMode,
string DefaultPaymentProvider,
bool AutoGenerateRenewal,
int RenewalLeadDays,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt);
public sealed record UpsertTenantBillingPolicyCommand(
Guid TenantId,
TenantBillingCollectionMode CollectionMode,
string DefaultPaymentProvider,
bool AutoGenerateRenewal,
int RenewalLeadDays,
string Reason);
public sealed record UpdatePlatformTenantStatusCommand(
Guid TenantId,
TenantStatus Status,
string? Reason);
public sealed record UpsertPlatformTenantBillingProfileCommand(
Guid TenantId,
string? BillingName,
string? TaxId,
string? ContactName,
string? ContactPhone,
string? ContactEmail,
string? BillingAddress,
string? InvoiceTitle,
TenantBillingInvoiceTitleType? InvoiceType,
string? BankName,
string? BankAccountMasked,
JsonElement Metadata);
public sealed record PlatformDomainList(IReadOnlyCollection<PlatformTenantDomainItem> Items);
public sealed record PlatformDomainRecheckResult(
Guid DomainId,
TenantDomainStatus Status,
DateTimeOffset LastCheckedAt);
public sealed record PlatformStaffList(IReadOnlyCollection<PlatformStaffItem> Items);
public sealed record PlatformStaffItem(
Guid UserId,
string? Name,
string? PhoneMasked,
string? Email,
UserStatus Status,
IReadOnlyCollection<string> RoleCodes,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt);
public sealed record UpsertPlatformStaffCommand(
Guid? UserId,
string? Email,
string? Phone,
string? Name,
UserStatus Status,
IReadOnlyCollection<Guid> RoleIds);
public sealed record UpdatePlatformStaffStatusCommand(Guid UserId, UserStatus Status, string? Reason);
public sealed record PlatformAuditLogList(IReadOnlyCollection<PlatformAuditLogItem> Items);
public sealed record PlatformAuditLogItem(
Guid Id,
Guid? TenantId,
Guid? ActorUserId,
string Action,
string? TargetType,
string? TargetId,
JsonElement Details,
string? IpAddress,
string? UserAgent,
DateTimeOffset CreatedAt);
public sealed record PlatformAuditAlertList(IReadOnlyCollection<PlatformAuditAlert> Items);
public sealed record UpdatePlatformAuditAlertStatusCommand(
Guid AlertId,
PlatformAuditAlertStatus Status,
string? ResolutionNote);
public sealed record PlatformBillingDunningChannelList(IReadOnlyCollection<PlatformBillingDunningChannelItem> Items);
public sealed record PlatformBillingDunningChannelItem(
Guid Id,
string ChannelCode,
string Name,
string? Description,
bool Enabled,
PlatformBillingDunningProvider Provider,
string WebhookUrlMasked,
string? SecretRef,
IReadOnlyCollection<string> ReminderTypes,
IReadOnlyCollection<string> ReminderChannels,
int MinReminderLevel,
IReadOnlyCollection<Guid> TenantIds,
int TimeoutSeconds,
JsonElement Metadata,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt);
public sealed record UpsertPlatformBillingDunningChannelCommand(
Guid? ChannelId,
string ChannelCode,
string Name,
string? Description,
bool Enabled,
PlatformBillingDunningProvider Provider,
string WebhookUrl,
string? SecretRef,
IReadOnlyCollection<string> ReminderTypes,
IReadOnlyCollection<string> ReminderChannels,
int MinReminderLevel,
IReadOnlyCollection<Guid> TenantIds,
int TimeoutSeconds,
JsonElement Metadata);
public sealed record DisablePlatformBillingDunningChannelCommand(Guid ChannelId, string? Reason);
public sealed record PlatformBillingDunningEventList(IReadOnlyCollection<PlatformBillingDunningEventItem> Items);
public sealed record PlatformBillingDunningEventItem(
Guid Id,
Guid TenantId,
Guid ChannelId,
Guid ReminderId,
Guid InvoiceId,
PlatformBillingDunningProvider Provider,
PlatformBillingDunningNotificationStatus Status,
int Attempts,
DateTimeOffset ScheduledAt,
DateTimeOffset? NextAttemptAt,
DateTimeOffset? LastAttemptAt,
DateTimeOffset? SentAt,
string? LastError,
int? LastHttpCode,
string? LastResponseSummary,
JsonElement RequestPayload,
JsonElement Metadata,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt);
public sealed record RetryPlatformBillingDunningEventCommand(Guid EventId, string? Reason);
public sealed record ResolvePlatformBillingDunningEventCommand(Guid EventId, string Reason);
public interface IPlatformDashboardService
{
Task<PlatformOverview> GetOverviewAsync(PlatformAdminActor actor, CancellationToken cancellationToken = default);
}
public interface ITenantProvisioningAdministrationService
{
Task<PlatformTenantList> GetTenantsAsync(PlatformAdminActor actor, PlatformAdminQuery query,
CancellationToken cancellationToken = default);
Task<PlatformTenantDetail> GetTenantDetailAsync(PlatformAdminActor actor, Guid tenantId,
CancellationToken cancellationToken = default);
Task<PlatformTenantProvisioningResult> CreateTenantAsync(PlatformAdminActor actor,
CreatePlatformTenantCommand command, CancellationToken cancellationToken = default);
Task<PlatformTenantDomainItem> ReplacePrimaryDomainAsync(PlatformAdminActor actor,
ReplacePlatformPrimaryDomainCommand command, CancellationToken cancellationToken = default);
Task<PlatformOwnerActivationLinkResult> IssueOwnerActivationLinkAsync(PlatformAdminActor actor,
IssuePlatformOwnerActivationLinkCommand command, CancellationToken cancellationToken = default);
Task<PlatformTenantItem> UpdateTenantStatusAsync(PlatformAdminActor actor,
UpdatePlatformTenantStatusCommand command, CancellationToken cancellationToken = default);
Task<TenantBillingProfileItem> UpsertTenantBillingProfileAsync(PlatformAdminActor actor,
UpsertPlatformTenantBillingProfileCommand command, CancellationToken cancellationToken = default);
Task<TenantBillingPolicyItem> GetTenantBillingPolicyAsync(PlatformAdminActor actor, Guid tenantId,
CancellationToken cancellationToken = default);
Task<TenantBillingPolicyItem> UpsertTenantBillingPolicyAsync(PlatformAdminActor actor,
UpsertTenantBillingPolicyCommand command, CancellationToken cancellationToken = default);
}
public interface IPlatformTenantDomainService
{
Task<PlatformDomainList> GetDomainsAsync(PlatformAdminActor actor, PlatformAdminQuery query,
CancellationToken cancellationToken = default);
Task<PlatformDomainRecheckResult> RecheckDomainAsync(PlatformAdminActor actor, Guid domainId,
CancellationToken cancellationToken = default);
}
public interface IPlatformStaffAccessService
{
Task<PlatformStaffList> GetStaffAsync(PlatformAdminActor actor, PlatformAdminQuery query,
CancellationToken cancellationToken = default);
Task<PlatformStaffItem> UpsertStaffAsync(PlatformAdminActor actor, UpsertPlatformStaffCommand command,
CancellationToken cancellationToken = default);
Task<PlatformStaffItem> UpdateStaffStatusAsync(PlatformAdminActor actor, UpdatePlatformStaffStatusCommand command,
CancellationToken cancellationToken = default);
}
public interface IPlatformAuditAlertService
{
Task<PlatformAuditLogList> GetAuditLogsAsync(PlatformAdminActor actor, PlatformAdminQuery query,
CancellationToken cancellationToken = default);
Task<PlatformAuditAlertList> GetAuditAlertsAsync(PlatformAdminActor actor, PlatformAdminQuery query,
CancellationToken cancellationToken = default);
Task<PlatformAuditAlert> UpdateAuditAlertStatusAsync(PlatformAdminActor actor,
UpdatePlatformAuditAlertStatusCommand command, CancellationToken cancellationToken = default);
}
public interface IPlatformDunningService
{
Task<PlatformBillingDunningChannelList> GetBillingDunningChannelsAsync(PlatformAdminActor actor,
PlatformAdminQuery query, CancellationToken cancellationToken = default);
Task<PlatformBillingDunningChannelItem> UpsertBillingDunningChannelAsync(PlatformAdminActor actor,
UpsertPlatformBillingDunningChannelCommand command, CancellationToken cancellationToken = default);
Task<PlatformBillingDunningChannelItem> DisableBillingDunningChannelAsync(PlatformAdminActor actor,
DisablePlatformBillingDunningChannelCommand command, CancellationToken cancellationToken = default);
Task<PlatformBillingDunningEventList> GetBillingDunningEventsAsync(PlatformAdminActor actor,
PlatformAdminQuery query, CancellationToken cancellationToken = default);
Task<PlatformBillingDunningEventItem> GetBillingDunningEventDetailAsync(PlatformAdminActor actor, Guid eventId,
CancellationToken cancellationToken = default);
Task<PlatformBillingDunningEventItem> RetryBillingDunningEventAsync(PlatformAdminActor actor,
RetryPlatformBillingDunningEventCommand command, CancellationToken cancellationToken = default);
Task<PlatformBillingDunningEventItem> AcknowledgeBillingDunningEventAsync(PlatformAdminActor actor,
ResolvePlatformBillingDunningEventCommand command, CancellationToken cancellationToken = default);
Task<PlatformBillingDunningEventItem> IgnoreBillingDunningEventAsync(PlatformAdminActor actor,
ResolvePlatformBillingDunningEventCommand command, CancellationToken cancellationToken = default);
}
public sealed class PlatformAdminException(string message, string code) : Exception(message)
{
public string Code { get; } = code;
}