Appearance
BusinessOwners Service
The BusinessOwners service is the multi-tenant administration portal for Microtec ERP. It provides a dedicated API for platform administrators and tenant owners to manage tenant lifecycle, subscriptions, licensing, and system-level configuration — all separated from the ERP business modules.
Overview
| Property | Value |
|---|---|
| Solution | Microtec.BusinessOwner.sln |
| Service | BusinessOwners.Apis |
| CAE placement | Private CAE |
| Gateway route prefix | /bo-apis/ |
| Auth realm | businessowner (separate Keycloak realm) |
| Source | Platforms/Src/BusinessOwners/ |
| Database | Shared admin database (not per-tenant) |
Separate Keycloak Realm
BusinessOwners uses the businessowner realm, distinct from the microtec realm used by ERP modules. This means BO admins and ERP users have completely separate authentication sessions.
Solution Structure — 11 Projects
The BusinessOwner solution is composed of 11 projects to enforce strict separation of concerns:
Platforms/Src/BusinessOwners/
├── BusinessOwners.Apis/ # REST controllers, startup, Dockerfile
├── BusinessOwners.Application/ # CQRS handlers, DTOs, validators
│ ├── Tenants/
│ │ ├── Commands/
│ │ │ ├── AddTenant/
│ │ │ ├── EditTenant/
│ │ │ ├── ProvisionTenant/
│ │ │ ├── SuspendTenant/
│ │ │ └── ActivateTenant/
│ │ └── Queries/
│ │ ├── GetByIdTenant/
│ │ ├── GetAllTenants/
│ │ └── GetDropdownTenant/
│ ├── Subscriptions/
│ ├── Modules/
│ ├── Users/
│ └── SystemConfig/
├── BusinessOwners.Domain/ # Admin domain entities
│ ├── Tenant.cs
│ ├── Subscription.cs
│ ├── Module.cs
│ └── TenantModule.cs
├── BusinessOwners.Infrastructure/ # Admin DB context, EF migrations
│ ├── Persistence/
│ │ ├── AdminDbContext.cs
│ │ └── Migrations/
│ └── ...
├── BusinessOwners.Contracts/ # Shared DTOs exposed to other services
├── BusinessOwners.IntegrationEvents/ # ASB event definitions (tenant provisioned, etc.)
├── BusinessOwners.IntegrationHandlers/# ASB event consumers
├── BusinessOwners.Jobs/ # Background job definitions
├── BusinessOwners.Services/ # Domain services (provisioning orchestration)
├── BusinessOwners.PublicApi/ # IBusinessOwnersPublicApi typed HTTP client
└── BusinessOwners.Tests/ # Unit and integration testsDomain Model
Tenant Lifecycle
Provisioning Flow
Tenant provisioning is a multi-step orchestrated process:
Database-per-Tenant
Each tenant gets its own SQL Server database. The connection string is stored encrypted in the admin database and resolved at runtime via ITenantContextManager. Never store tenant connection strings in app config files.
API Endpoints
Tenant Management
| Method | Route | Operation | CQRS Handler |
|---|---|---|---|
GET | /Tenants | List all tenants (paged) | GetAllTenantsQuery |
GET | /Tenants/{Id} | Get tenant by ID | GetByIdTenantQuery |
GET | /Tenants/DropDown | Tenant select list | GetDropdownTenantQuery |
POST | /Tenants | Create tenant record | AddTenantCommand |
PUT | /Tenants | Update tenant info | EditTenantCommand |
POST | /Tenants/Provision/{Id} | Run provisioning pipeline | ProvisionTenantCommand |
POST | /Tenants/Suspend/{Id} | Suspend tenant access | SuspendTenantCommand |
POST | /Tenants/Activate/{Id} | Restore tenant access | ActivateTenantCommand |
Subscription Management
| Method | Route | Operation | CQRS Handler |
|---|---|---|---|
GET | /Subscriptions | List subscriptions | GetAllSubscriptionsQuery |
POST | /Subscriptions | Create subscription | AddSubscriptionCommand |
PUT | /Subscriptions/Renew | Renew subscription | RenewSubscriptionCommand |
PUT | /Subscriptions/Upgrade | Upgrade plan | UpgradeSubscriptionCommand |
Module Activation
| Method | Route | Operation | CQRS Handler |
|---|---|---|---|
GET | /TenantModules/{TenantId} | List enabled modules | GetTenantModulesQuery |
POST | /TenantModules/Enable | Enable a module for tenant | EnableTenantModuleCommand |
POST | /TenantModules/Disable | Disable a module | DisableTenantModuleCommand |
ZATCA Onboarding
| Method | Route | Operation |
|---|---|---|
POST | /Zatca/Onboard | Initiate ZATCA device onboarding |
GET | /Zatca/Status/{TenantId} | Check onboarding status |
IAdminUnitOfWork
BusinessOwners handlers use IAdminUnitOfWork (not IUnitOfWork<TContext>), which always connects to the shared admin database:
csharp
// Tenant provision handler excerpt
public class ProvisionTenantCommandHandler(
IAdminUnitOfWork adminUnitOfWork,
IKeycloakProvisioningService keycloak,
ITenantDatabaseProvisioner dbProvisioner,
IPublisher publisher) : IRequestHandler<ProvisionTenantCommand, ApiResponse>
{
public async Task<ApiResponse> Handle(ProvisionTenantCommand request, CancellationToken ct)
{
var tenant = await adminUnitOfWork.Repository<Tenant>()
.GetByIdAsync(request.TenantId)
?? throw new NotFoundException(nameof(Tenant), request.TenantId);
tenant.SetStatus(TenantStatus.Provisioning);
await adminUnitOfWork.SaveChangesAsync(ct);
// Step 1 — Keycloak realm
var realmConfig = await keycloak.CreateRealmAsync(tenant.Code, ct);
// Step 2 — SQL database
var connString = await dbProvisioner.CreateDatabaseAsync(tenant.Code, ct);
tenant.SetConnectionString(connString);
await adminUnitOfWork.SaveChangesAsync(ct);
// Step 3 — Publish event for downstream consumers
await publisher.Publish(new TenantProvisionedEvent(tenant.Id, tenant.Code), ct);
return ApiResponse.Success();
}
}Integration Events
BusinessOwners publishes events to Azure Service Bus that other services react to:
| Event | Published When | Consumers |
|---|---|---|
TenantProvisionedEvent | Provisioning completes | PlatformsWorker (seed data) |
TenantSuspendedEvent | Tenant suspended | Notification (alert email) |
TenantActivatedEvent | Tenant re-activated | PlatformsWorker (restore jobs) |
SubscriptionRenewedEvent | Subscription renewed | Notification (confirmation email) |
SubscriptionExpiredEvent | Subscription past end date | PlatformsWorker (auto-suspend) |
IBusinessOwnersPublicApi
Other services call BusinessOwners via the typed HTTP client:
csharp
// Defined in BusinessOwners.PublicApi
public interface IBusinessOwnersPublicApi
{
Task<TenantDto?> GetTenantByCodeAsync(string code, CancellationToken ct = default);
Task<bool> IsTenantActiveAsync(int tenantId, CancellationToken ct = default);
Task<List<EnabledModuleDto>> GetEnabledModulesAsync(int tenantId, CancellationToken ct = default);
}Register in consuming services:
csharp
// Program.cs in any consuming service
builder.Services.AddHttpClient<IBusinessOwnersPublicApi, BusinessOwnersPublicApiClient>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["Services:BusinessOwners"]!);
});Configuration
json
// appsettings.json (BusinessOwners.Apis)
{
"ConnectionStrings": {
"AdminConnection": "Server=...;Database=MicrotecAdmin;..."
},
"Keycloak": {
"Authority": "https://<keycloak-host>/realms/businessowner",
"Audience": "account",
"AdminClientId": "admin-cli",
"AdminClientSecret": "..."
},
"ServiceBus": {
"ConnectionString": "Endpoint=sb://...",
"TopicName": "microtec-events"
},
"Services": {
"Notification": "https://mic-erp-be-notification.internal.<env>.azurecontainerapps.io",
"Integration": "https://mic-erp-be-integration.internal.<env>.azurecontainerapps.io"
}
}Local Build
bash
cd Platforms
# Build BusinessOwner solution
dotnet build Microtec.BusinessOwner.sln
# Run BusinessOwners API
dotnet run --project Src/BusinessOwners/BusinessOwners.Apis/BusinessOwners.Apis.csprojDeployment
BusinessOwners is listed in the pipeline service config:
Devops/azure/config/container-backend/services-config.json → "businessowners"It is deployed to the Private CAE alongside all other backend microservices. Only the Gateway is internet-facing; BO API calls are routed via /bo-apis/ on the Gateway.