432 lines
15 KiB
C#
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;
|
|
}
|