Skip to content

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

PropertyValue
SolutionMicrotec.BusinessOwner.sln
ServiceBusinessOwners.Apis
CAE placementPrivate CAE
Gateway route prefix/bo-apis/
Auth realmbusinessowner (separate Keycloak realm)
SourcePlatforms/Src/BusinessOwners/
DatabaseShared 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 tests

Domain 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

MethodRouteOperationCQRS Handler
GET/TenantsList all tenants (paged)GetAllTenantsQuery
GET/Tenants/{Id}Get tenant by IDGetByIdTenantQuery
GET/Tenants/DropDownTenant select listGetDropdownTenantQuery
POST/TenantsCreate tenant recordAddTenantCommand
PUT/TenantsUpdate tenant infoEditTenantCommand
POST/Tenants/Provision/{Id}Run provisioning pipelineProvisionTenantCommand
POST/Tenants/Suspend/{Id}Suspend tenant accessSuspendTenantCommand
POST/Tenants/Activate/{Id}Restore tenant accessActivateTenantCommand

Subscription Management

MethodRouteOperationCQRS Handler
GET/SubscriptionsList subscriptionsGetAllSubscriptionsQuery
POST/SubscriptionsCreate subscriptionAddSubscriptionCommand
PUT/Subscriptions/RenewRenew subscriptionRenewSubscriptionCommand
PUT/Subscriptions/UpgradeUpgrade planUpgradeSubscriptionCommand

Module Activation

MethodRouteOperationCQRS Handler
GET/TenantModules/{TenantId}List enabled modulesGetTenantModulesQuery
POST/TenantModules/EnableEnable a module for tenantEnableTenantModuleCommand
POST/TenantModules/DisableDisable a moduleDisableTenantModuleCommand

ZATCA Onboarding

MethodRouteOperation
POST/Zatca/OnboardInitiate 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:

EventPublished WhenConsumers
TenantProvisionedEventProvisioning completesPlatformsWorker (seed data)
TenantSuspendedEventTenant suspendedNotification (alert email)
TenantActivatedEventTenant re-activatedPlatformsWorker (restore jobs)
SubscriptionRenewedEventSubscription renewedNotification (confirmation email)
SubscriptionExpiredEventSubscription past end datePlatformsWorker (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.csproj

Deployment

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.

Internal Documentation — Microtec Platform Team