Document ID: EAS-2025-001
Status: Binding
Scope: All entities, DTOs, handlers, controllers, middleware, migrations, and tests in this repository
This file is the authoritative implementation standard for tenant modeling, payment identifier naming, migration behavior, and test enforcement in this solution. Treat every checklist here as a gate, not guidance.
- Do not introduce new tenant-owned entities without
TenantIdFK ownership. - Do not introduce new payment identifiers outside the canonical vocabulary in this file.
- Do not expose
Tenant.IdorPaymentTraceIdin customer-facing DTOs. - Do not add tenant seed data anywhere except the baseline migration or an explicitly named seed migration.
- Do not rely on ambient tenant context in non-request code paths.
- Do not change these rules without an architecture review and an ADR.
The tenant model uses three keys for three distinct audiences.
| Property | Audience | Stability | Allowed surfaces |
|---|---|---|---|
Tenant.Id |
Database and EF only | Internal | FK columns, query filters, save stamping |
Tenant.ExternalId |
External integrations | Immutable | External API contracts, webhooks, third-party integrations |
Tenant.Code |
Operations and runtime resolution | Stable with notice | X-Tenant-Code, logs, admin tooling |
Current canonical entity shape:
public class Tenant
{
public int Id { get; set; }
public string ExternalId { get; set; } = string.Empty;
public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string TenantTier { get; set; } = "SharedPool"; // "SharedPool" | "Dedicated"
// ConnectionString removed (ADR-009): dedicated DB connections are resolved at runtime
// from IConfiguration["DedicatedTenantConnectionStrings:{Code}"], never persisted in the DB.
public int? CreatedBy { get; set; }
public DateTime? CreatedDate { get; set; }
public int? UpdatedBy { get; set; }
public DateTime? UpdatedDate { get; set; }
}Tenant statuses are operationally meaningful:
| Status | Meaning | Middleware result |
|---|---|---|
Active |
Tenant can use the system | Request continues |
Suspended |
Tenant exists but is blocked | HTTP 403 |
Decommissioned |
Tenant exists but is permanently closed | HTTP 403 |
Every inbound request follows exactly one of these outcomes:
- Missing
X-Tenant-Code-> HTTP 400 and stop immediately. - Unknown
X-Tenant-Code-> HTTP 400 and stop immediately. - Resolved tenant with
SuspendedorDecommissionedstatus -> HTTP 403 and stop immediately. - Resolved
Activetenant -> continue with canonical tenant context. - Approved headerless operational paths are
GET /api/v1/info/runtime-configuration,GET /health,GET /health/live, andGET /health/ready. - UI/browser code must acquire the active tenant code from API runtime configuration, never from UI-local configuration.
Required runtime contract:
public interface ITenantProvider
{
bool HasTenantContext { get; }
int TenantId { get; }
string TenantCode { get; }
string TenantExternalId { get; }
string? ConnectionString { get; }
bool IsSharedPool { get; }
}No null tenant context may flow downstream from middleware.
These are the only approved payment identifier names in the codebase.
| Identifier | Meaning | Allowed surfaces |
|---|---|---|
CustomerOrderId |
Business-visible order reference | UI, customer-facing responses, receipts |
AttemptOrderId |
Single payment-attempt/provider reference | Provider calls, callback reconciliation, technical diagnostics |
PaymentTraceId |
Internal correlation id | Structured logs and telemetry only |
Surface rules:
CustomerOrderIdis the only order identifier that may appear in customer-facing UI or response payloads.AttemptOrderIdis not a customer-facing field.PaymentTraceIdnever leaves the server in customer-facing APIs or UI.Tenant.ExternalIdis the only tenant identifier allowed in external contracts.Tenant.Idnever appears in API responses, logs intended for consumers, or UI.
Do not introduce these names in new code or new schema changes for payment and tenant modeling.
| Banned name | Use instead |
|---|---|
OrderId for payment attempt identity |
AttemptOrderId |
ReferenceNo |
AttemptOrderId or a provider-specific explicit name |
APINO1 |
OpenPayChargeId |
APINO2 |
OpenPayAuthorizationId |
TenantCode on tenant-owned rows |
TenantId FK only |
X-Tenant-Id |
X-Tenant-Code |
Existing legacy fields should be migrated toward the canonical names when touched.
Use this every time a new entity is created.
- Inherit from the correct base type:
public class MyEntity : BaseAuditableEntity { }
// or
public class MyEntity : BaseAuditableCreateEntity { }- Never redeclare
TenantIdon the derived type. - Use canonical payment/tenant identifiers only.
- Add EF configuration:
- FK to
Tenants - global query filter using
ITenantProvider.TenantId - composite indexes for every tenant-scoped lookup key
- FK to
- Ensure save stamping picks it up automatically.
- Add or update architecture and integration tests.
System entities include Tenant itself and any future system-wide control tables.
- Do not make them tenant-owned.
- Do not apply tenant query filters to them.
- Manage audit fields explicitly if needed.
- Identify whether the DTO is customer-facing, provider-facing, or internal.
- Customer-facing DTOs may include
CustomerOrderIdbut must not includeAttemptOrderIdunless it is explicitly a technical/admin surface. - Customer-facing DTOs must not expose
PaymentTraceId. - No DTO leaving the server may expose
Tenant.Id. - External tenant identity, if needed, must use
Tenant.ExternalId. - Customer-facing UI must not define its own active tenant source; it must consume the API-owned runtime configuration contract.
The DbContext is the enforcement point for tenant integrity.
Required rules:
- Every tenant-owned entity must have a non-nullable
TenantIdint FK. - Every tenant-owned entity must have a global query filter scoped to
ITenantProvider.TenantId. Tenantsmust be explicitly excluded from tenant query filters.- Save stamping applies only to tenant-owned base classes.
Tenantsand any other non-tenant-owned system tables are exempt from tenant stamping.
Mandatory composite indexes already standardized:
| Entity | Required composite indexes |
|---|---|
CardTransaction |
(TenantId, CustomerOrderId), (TenantId, AttemptOrderId) |
PayinLog |
(TenantId, AttemptOrderId) |
TransactionStatusHistory |
(TenantId, AttemptOrderId) |
- The repository uses a single clean baseline migration plus future incremental migrations.
- The current baseline migration is
RebaselineMultitenantPaymentSchema. TenantAandTenantBare seeded in the baseline migrationUp()method.- New tenant-owned tables must include:
- FK to
Tenants(Id) - required composite indexes
- no string tenant surrogate columns
- FK to
- After generating any migration, run a drift check by generating a second migration. It must be empty.
- Remove the temporary drift-check migration immediately after verification.
The following code paths must pass TenantId explicitly and must not depend on ambient middleware state:
DbInitializer- background jobs
- integration test fixtures
- any out-of-band scripts or import utilities
Rule: stamp tenant-owned entities, not everything.
DbInitializer.Initialize() accepts an optional IConfiguration parameter. After running Database.Migrate(), it:
- Seeds shared-pool sample data (Phase 1: TenantA, TenantB).
- Calls
SeedDedicatedTenants()(Phase 2) — for each Dedicated-tier tenant, reads the connection string fromIConfiguration["DedicatedTenantConnectionStrings:{Code}"], creates a scopedOrderProcessingSystemDbContextwithNullTenantProviderand runsDatabase.Migrate()+ sample data seeding on the dedicated DB. Connection strings are never stored in theTenantstable (ADR-009).
NullTenantProvider is a null-object ITenantProvider with HasTenantContext = false. It prevents query filter NullReferenceException in non-request contexts. The filter short-circuits to true (all rows visible), which is correct for dedicated-DB seeding where physical isolation replaces query-filter isolation.
Config pattern — each environment provides its own DedicatedTenantConnectionStrings section:
- Local:
sharedsettings.local.json - Azure: Key Vault references or App Settings
These tests form the compliance envelope for the standard.
Required concerns:
- migration drift
- required composite index presence
- tenant filter isolation
- identifier surface compliance
Current examples live in:
EfMigrationDriftTestsMultiTenantSchemaTests
Required concerns:
- HTTP 400 for missing
X-Tenant-Code - HTTP 400 for unknown
X-Tenant-Code - HTTP 403 for
SuspendedandDecommissionedtenants - per-class tenant isolation
- no reuse of global baseline tenant rows in test data setup
Required concerns:
- Dedicated + unprovisioned (null CS) + Active → 400 (fail-loud)
- Dedicated + provisioned + Active → 200 (routed to dedicated DB)
- Data written to dedicated tenant exists in dedicated DB (direct SQL verification)
- Dedicated tenant data not visible via direct query on shared-pool DB
- SharedPool tenant data not visible via direct query on dedicated DB
Current examples live in DedicatedTenantTests.
Required regression guards (both tenant tiers):
- Both CardTransaction rows (tokenization + charge) set
BillingCustomerId - OpenPay timestamps normalized to UTC
- Orphaned PaymentMethod deactivated on charge failure
- TSH state machine entries (callback + remote confirmation)
- TSH.CreatedBy uses
BillingCustomerId - 3DS ON path sets
IsThreeDSecureEnabled = trueandThreeDSecureStage = completed - 3DS OFF path sets
IsThreeDSecureEnabled = falseandThreeDSecureStage = not_applicable
Current examples live in ProcessPaymentHandlerTests and ConfirmPaymentStatusHandlerTests.
PaymentProvider.Use3DSecure is a per-tenant bool column — any combination is valid at runtime:
| Tenant | Tier | Use3DSecure | Supported | Mechanism |
|---|---|---|---|---|
| TenantA | SharedPool | true | ✅ | Default seed value |
| TenantA | SharedPool | false | ✅ | DB UPDATE at runtime |
| TenantB | SharedPool | true | ✅ | Default seed value |
| TenantB | SharedPool | false | ✅ | DB UPDATE at runtime |
| TenantC | Dedicated | true | ✅ | Default seed value |
| TenantC | Dedicated | false | ✅ | DB UPDATE at runtime |
| TenantD+ | Either | true/false | ✅ | Set in seed migration or DB UPDATE |
Rules:
Use3DSecureis a business rule per tenant, not a global infrastructure setting.- The property lives on
PaymentProvider, notOpenPayConfigor appsettings. DbInitializerseeds all tenants withUse3DSecure = trueby default.- Override per-tenant at runtime via DB UPDATE or by adding a seed-data migration.
ProcessPaymentCommandHandlerreads the value per-tenant viaAppMasterData.GetProviderByNameForTenant().AppMasterDatais scoped (per-request) and reads from the tenant-routed DbContext — no API restart needed for Use3DSecure changes.ConfirmPaymentStatusCommandHandlerdoes not consultPaymentProvider.Use3DSecure— it readsIsThreeDSecureEnabledfrom theCardTransactionrow (already stamped by ProcessPayment).
Unit test coverage:
| Path | Test | Verified assertion |
|---|---|---|
| 3DS ON (default) | HandleAsync_NewCustomer_BothCardTransactionsShouldSetBillingCustomerId |
Implicit — uses default use3DSecure: true |
| 3DS OFF | HandleAsync_3DSecureOff_ShouldSetNotApplicableStageOnChargeCardTransaction |
IsThreeDSecureEnabled = false, ThreeDSecureStage = not_applicable |
These topics are intentionally excluded from this standard and require separate architecture review before implementation:
- user-to-tenant membership
- authentication and claims-based tenant switching
- tenant administration UI
- remote environment data migration
- webhook/API versioning changes unrelated to tenant and payment identifier standards
The UI must bootstrap tenant context from API-owned runtime configuration.
Rules:
- The API owns the active tenant bootstrap contract.
- The approved UI bootstrap endpoint is
GET /api/v1/info/runtime-configuration. - That endpoint may be called without
X-Tenant-Code. - Operational health endpoints
GET /health,GET /health/live, andGET /health/readymay also bypass tenant middleware. - No other business endpoint may bypass tenant middleware for UI convenience.
- UI-local configuration such as
UI:DefaultTenantCodemust not be used as the runtime tenant source. - The runtime configuration payload may include non-sensitive bootstrap metadata only, such as
ActiveTenantCodeandTenantHeaderName.
| Decision point | Standard |
|---|---|
| Request header | X-Tenant-Code |
| Missing or unknown tenant code | HTTP 400 |
| Suspended or decommissioned tenant | HTTP 403 |
| Resolved active tenant | continue |
| Headerless bootstrap endpoint | GET /api/v1/info/runtime-configuration |
| Headerless operational endpoints | GET /health, GET /health/live, GET /health/ready |
| UI tenant source | API runtime configuration |
| Internal tenant key | Tenant.Id |
| External tenant key | Tenant.ExternalId |
| Ops/runtime tenant key | Tenant.Code |
| Business order id | CustomerOrderId |
| Attempt/provider order id | AttemptOrderId |
| Internal correlation id | PaymentTraceId |
| 3DS toggle location | PaymentProvider.Use3DSecure (per-tenant, not global config) |
| Baseline seed location | baseline migration Up() |
| Tenant stamping scope | tenant-owned base classes only |
| Tenant table filtering | no global query filter |
Binding rules for payment card data storage:
CardTransactionmust never store raw PAN or CVV. CVV must not be persisted under any circumstances.CardTransaction.MaskedCardNumberstores BIN (first 6) + masked middle + last 4, e.g.411111******1234.PayinLog.LastFourCardNbrstores only the last 4 digits for audit trail.- No DTO, log output, or error message may contain a full card number or CVV.
- Architecture tests must enforce these constraints to prevent regression.
Use these as the first reference points when applying the standard:
XYDataLabs.OrderProcessingSystem.SharedKernel/Multitenancy/ITenantProvider.csXYDataLabs.OrderProcessingSystem.SharedKernel/Multitenancy/TenantMiddleware.csXYDataLabs.OrderProcessingSystem.API/Controllers/InfoController.csXYDataLabs.OrderProcessingSystem.Domain/Entities/Tenant.csXYDataLabs.OrderProcessingSystem.Domain/Entities/BaseAuditableEntity.csXYDataLabs.OrderProcessingSystem.Domain/Entities/BaseAuditableCreateEntity.csXYDataLabs.OrderProcessingSystem.Infrastructure/DataContext/OrderProcessingSystemDbContext.csXYDataLabs.OrderProcessingSystem.Infrastructure/SeedData/DbInitializer.csXYDataLabs.OrderProcessingSystem.Infrastructure/Migrations/20260322112523_RebaselineMultitenantPaymentSchema.cstests/XYDataLabs.OrderProcessingSystem.Architecture.Tests/EfMigrationDriftTests.cstests/XYDataLabs.OrderProcessingSystem.Architecture.Tests/MultiTenantSchemaTests.cstests/XYDataLabs.OrderProcessingSystem.Integration.Tests/DedicatedTenantTests.cs
Any deviation from this file requires an ADR and explicit architecture approval.