forked from gongxuegit/tiku-backend.net
feat(saas): implement marketplace and tenant onboarding
This commit is contained in:
@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Npgsql;
|
||||
using System.Text.Json;
|
||||
using Tiku.Application.Commerce;
|
||||
using Tiku.Application.PlatformBilling;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Growth;
|
||||
using Tiku.Application.Storage;
|
||||
@@ -29,6 +30,7 @@ public sealed class ApiTestFactory(
|
||||
IObjectStorageService? objectStorageService = null,
|
||||
IReferralQrcodeGenerator? referralQrcodeGenerator = null,
|
||||
IPaymentProviderGateway? paymentProviderGateway = null,
|
||||
IPlatformBillingPaymentGateway? platformBillingPaymentGateway = null,
|
||||
IDomainOwnershipVerifier? domainOwnershipVerifier = null,
|
||||
IDomainGatewayProvisioner? domainGatewayProvisioner = null,
|
||||
ISmsProvider? smsProvider = null,
|
||||
@@ -110,6 +112,12 @@ public sealed class ApiTestFactory(
|
||||
services.AddSingleton(paymentProviderGateway);
|
||||
}
|
||||
|
||||
if (platformBillingPaymentGateway is not null)
|
||||
{
|
||||
services.RemoveAll<IPlatformBillingPaymentGateway>();
|
||||
services.AddSingleton(platformBillingPaymentGateway);
|
||||
}
|
||||
|
||||
if (domainOwnershipVerifier is not null)
|
||||
{
|
||||
services.RemoveAll<IDomainOwnershipVerifier>();
|
||||
@@ -138,54 +146,160 @@ public sealed class ApiTestFactory(
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
var tenants = entities.OfType<Tenant>().Where(tenant => tenant.Mode == TenantMode.Saas).ToArray();
|
||||
var hasExplicitCapabilitySetup = entities.Any(entity =>
|
||||
entity is PlatformSaasPlan or ProductModule or PlanModuleEntitlement or TenantSubscription);
|
||||
entity is SaasOffering or SaasOfferingVersion or TenantSaasSubscription);
|
||||
if (tenants.Length > 0 && !hasExplicitCapabilitySetup)
|
||||
{
|
||||
const string integrationPlanCode = "integration-full-access";
|
||||
if (!await dbContext.PlatformSaasPlans.AnyAsync(plan => plan.Code == integrationPlanCode))
|
||||
{
|
||||
dbContext.PlatformSaasPlans.Add(new PlatformSaasPlan
|
||||
const string integrationOfferingCode = "integration-full-access";
|
||||
var existingFeatures = await dbContext.SaasFeatures.Select(feature => feature.Code).ToArrayAsync();
|
||||
dbContext.SaasFeatures.AddRange(SaasFeatureCatalog.All
|
||||
.Except(existingFeatures, StringComparer.Ordinal)
|
||||
.Select((code, index) => new SaasFeature
|
||||
{
|
||||
Code = integrationPlanCode,
|
||||
Name = "Integration Full Access"
|
||||
});
|
||||
}
|
||||
|
||||
var existingModules = await dbContext.ProductModules
|
||||
.Select(module => module.Code)
|
||||
.ToArrayAsync();
|
||||
foreach (var module in ProductModuleCatalog.All.Where(module => !existingModules.Contains(module.Key)))
|
||||
Code = code,
|
||||
Name = code,
|
||||
Category = code.Split('.')[0],
|
||||
IsCore = code == SaasFeatureCatalog.CoreBackoffice,
|
||||
Status = SaasFeatureStatus.Active,
|
||||
SortOrder = index * 10
|
||||
}));
|
||||
var offering = await dbContext.SaasOfferings.SingleOrDefaultAsync(value => value.Code == integrationOfferingCode);
|
||||
if (offering is null)
|
||||
{
|
||||
dbContext.ProductModules.Add(new ProductModule { Code = module.Key, Name = module.Value });
|
||||
offering = new SaasOffering
|
||||
{
|
||||
Code = integrationOfferingCode,
|
||||
Name = "Integration Full Access",
|
||||
Type = SaasOfferingType.BasePlan,
|
||||
Status = SaasOfferingStatus.Active
|
||||
};
|
||||
dbContext.SaasOfferings.Add(offering);
|
||||
}
|
||||
var version = await dbContext.SaasOfferingVersions.SingleOrDefaultAsync(value =>
|
||||
value.OfferingId == offering.Id && value.Version == 1);
|
||||
if (version is null)
|
||||
{
|
||||
version = new SaasOfferingVersion
|
||||
{
|
||||
OfferingId = offering.Id,
|
||||
Version = 1,
|
||||
Status = SaasOfferingVersionStatus.Draft,
|
||||
OriginalAmountCents = 100,
|
||||
AmountCents = 100,
|
||||
EffectiveAt = DateTimeOffset.UtcNow.AddDays(-1)
|
||||
};
|
||||
dbContext.SaasOfferingVersions.Add(version);
|
||||
}
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
var entitledModules = await dbContext.PlanModuleEntitlements
|
||||
.Where(entitlement => entitlement.PlanCode == integrationPlanCode)
|
||||
.Select(entitlement => entitlement.ModuleCode)
|
||||
var entitledFeatures = await dbContext.SaasOfferingVersionFeatures
|
||||
.Where(entitlement => entitlement.OfferingVersionId == version.Id)
|
||||
.Select(entitlement => entitlement.FeatureCode)
|
||||
.ToArrayAsync();
|
||||
foreach (var moduleCode in ProductModuleCatalog.All.Keys.Except(entitledModules, StringComparer.Ordinal))
|
||||
{
|
||||
dbContext.PlanModuleEntitlements.Add(new PlanModuleEntitlement
|
||||
dbContext.SaasOfferingVersionFeatures.AddRange(SaasFeatureCatalog.All
|
||||
.Where(code => code != SaasFeatureCatalog.CoreBackoffice)
|
||||
.Except(entitledFeatures, StringComparer.Ordinal)
|
||||
.Select(code => new SaasOfferingVersionFeature
|
||||
{
|
||||
PlanCode = integrationPlanCode,
|
||||
ModuleCode = moduleCode
|
||||
});
|
||||
}
|
||||
OfferingVersionId = version.Id,
|
||||
FeatureCode = code
|
||||
}));
|
||||
await dbContext.SaveChangesAsync();
|
||||
if (version.Status == SaasOfferingVersionStatus.Draft)
|
||||
{
|
||||
version.Status = SaasOfferingVersionStatus.Published;
|
||||
version.PublishedAt = DateTimeOffset.UtcNow.AddDays(-1);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
entities = entities.Concat(tenants.Select(tenant => new TenantSubscription
|
||||
var subscriptionGraphs = tenants.SelectMany(tenant =>
|
||||
{
|
||||
TenantId = tenant.Id,
|
||||
PlanCode = integrationPlanCode,
|
||||
Status = TenantSubscriptionStatus.Active,
|
||||
StartsAt = now.AddDays(-1),
|
||||
ExpiresAt = now.AddYears(1)
|
||||
})).ToArray();
|
||||
var subscription = new TenantSaasSubscription
|
||||
{
|
||||
TenantId = tenant.Id,
|
||||
BaseOfferingVersionId = version.Id,
|
||||
Status = TenantSaasSubscriptionStatus.Active,
|
||||
StartsAt = now.AddDays(-1),
|
||||
CurrentPeriodStart = now.AddDays(-1),
|
||||
CurrentPeriodEnd = now.AddYears(1)
|
||||
};
|
||||
return new object[]
|
||||
{
|
||||
subscription,
|
||||
new TenantSaasSubscriptionItem
|
||||
{
|
||||
TenantId = tenant.Id,
|
||||
SubscriptionId = subscription.Id,
|
||||
OfferingVersionId = version.Id,
|
||||
ItemType = TenantSaasSubscriptionItemType.BasePlan,
|
||||
Status = TenantSaasSubscriptionItemStatus.Active,
|
||||
StartsAt = subscription.CurrentPeriodStart,
|
||||
EndsAt = subscription.CurrentPeriodEnd
|
||||
}
|
||||
};
|
||||
});
|
||||
entities = entities.Concat(subscriptionGraphs).ToArray();
|
||||
}
|
||||
var explicitPermissions = entities.OfType<BackendPermission>().ToArray();
|
||||
if (explicitPermissions.Length > 0)
|
||||
{
|
||||
var moduleCodes = explicitPermissions.Select(value => value.PermissionModuleCode)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
var existingModuleCodes = await dbContext.PermissionModules
|
||||
.Where(value => moduleCodes.Contains(value.Code))
|
||||
.Select(value => value.Code)
|
||||
.ToArrayAsync();
|
||||
var suppliedModuleCodes = entities.OfType<PermissionModule>().Select(value => value.Code).ToArray();
|
||||
var modules = moduleCodes
|
||||
.Except(existingModuleCodes, StringComparer.Ordinal)
|
||||
.Except(suppliedModuleCodes, StringComparer.Ordinal)
|
||||
.Select(code => new PermissionModule
|
||||
{
|
||||
Code = code,
|
||||
Name = code,
|
||||
Area = explicitPermissions.First(value => value.PermissionModuleCode == code).Area,
|
||||
RequiredFeatureCode = PermissionModuleCatalog.RequiredFeatures.GetValueOrDefault(code)
|
||||
})
|
||||
.ToArray();
|
||||
var requiredFeatureCodes = modules.Select(value => value.RequiredFeatureCode)
|
||||
.Where(value => value is not null)
|
||||
.Cast<string>()
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
var existingRequiredFeatures = await dbContext.SaasFeatures
|
||||
.Where(value => requiredFeatureCodes.Contains(value.Code))
|
||||
.Select(value => value.Code)
|
||||
.ToArrayAsync();
|
||||
var suppliedFeatureCodes = entities.OfType<SaasFeature>().Select(value => value.Code).ToArray();
|
||||
dbContext.SaasFeatures.AddRange(requiredFeatureCodes
|
||||
.Except(existingRequiredFeatures, StringComparer.Ordinal)
|
||||
.Except(suppliedFeatureCodes, StringComparer.Ordinal)
|
||||
.Select(code => new SaasFeature
|
||||
{
|
||||
Code = code,
|
||||
Name = code,
|
||||
Category = code.Split('.')[0],
|
||||
Status = SaasFeatureStatus.Active
|
||||
}));
|
||||
dbContext.PermissionModules.AddRange(modules);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
var offeringVersionsToFinalize = entities.OfType<SaasOfferingVersion>()
|
||||
.Where(value => value.Status != SaasOfferingVersionStatus.Draft)
|
||||
.Select(value => new { Version = value, TargetStatus = value.Status })
|
||||
.ToArray();
|
||||
foreach (var item in offeringVersionsToFinalize)
|
||||
{
|
||||
item.Version.Status = SaasOfferingVersionStatus.Draft;
|
||||
}
|
||||
|
||||
dbContext.AddRange(entities);
|
||||
await dbContext.SaveChangesAsync();
|
||||
foreach (var item in offeringVersionsToFinalize)
|
||||
{
|
||||
item.Version.Status = item.TargetStatus;
|
||||
}
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
var backendMembers = entities
|
||||
.OfType<TenantMembership>()
|
||||
@@ -206,6 +320,34 @@ public sealed class ApiTestFactory(
|
||||
IEnumerable<(Guid TenantId, Guid UserId)> members)
|
||||
{
|
||||
var permissionCodes = BackendPermissions.Tenant.Order(StringComparer.Ordinal).ToArray();
|
||||
var featureCodes = PermissionModuleCatalog.RequiredFeatures.Values
|
||||
.Where(value => value is not null)
|
||||
.Cast<string>()
|
||||
.Append(SaasFeatureCatalog.CoreBackoffice)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
var existingFeatureCodes = await dbContext.SaasFeatures
|
||||
.Where(value => featureCodes.Contains(value.Code))
|
||||
.Select(value => value.Code)
|
||||
.ToArrayAsync();
|
||||
dbContext.SaasFeatures.AddRange(featureCodes.Except(existingFeatureCodes, StringComparer.Ordinal).Select(code => new SaasFeature
|
||||
{
|
||||
Code = code,
|
||||
Name = code,
|
||||
Category = code.Split('.')[0],
|
||||
IsCore = code == SaasFeatureCatalog.CoreBackoffice,
|
||||
Status = SaasFeatureStatus.Active
|
||||
}));
|
||||
var moduleCodes = permissionCodes.Select(PermissionModuleCatalog.ResolvePermissionModuleCode).Distinct(StringComparer.Ordinal).ToArray();
|
||||
var existingModuleCodes = await dbContext.PermissionModules.Where(value => moduleCodes.Contains(value.Code)).Select(value => value.Code).ToArrayAsync();
|
||||
dbContext.PermissionModules.AddRange(moduleCodes.Except(existingModuleCodes, StringComparer.Ordinal).Select(code => new PermissionModule
|
||||
{
|
||||
Code = code,
|
||||
Name = code,
|
||||
Area = BackendPermissionArea.Tenant,
|
||||
RequiredFeatureCode = PermissionModuleCatalog.RequiredFeatures[code]
|
||||
}));
|
||||
await dbContext.SaveChangesAsync();
|
||||
var existingPermissionCodes = await dbContext.BackendPermissions
|
||||
.Where(permission => permissionCodes.Contains(permission.Code))
|
||||
.Select(permission => permission.Code)
|
||||
@@ -217,7 +359,7 @@ public sealed class ApiTestFactory(
|
||||
Code = permissionCode,
|
||||
Name = permissionCode,
|
||||
Area = BackendPermissionArea.Tenant,
|
||||
Module = permissionCode.Split(':')[1],
|
||||
PermissionModuleCode = PermissionModuleCatalog.ResolvePermissionModuleCode(permissionCode),
|
||||
IsSystem = true
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user