Recorded 2026-06-01 · Calvin confirmed G-01, G-08, G-05 · Applies to audit logging, module structure, and TenantConfig
| Option | What gets logged | Trade-off |
|---|---|---|
| A — Full JSON diff on all AuditableEntities | Every field change on every Job, CargoReceipt, ContainerRequest, etc. Before/after JSON stored in AuditLog table. |
✓ Maximum auditability. Easy to build investigation tools later. Captures exactly what changed and who changed it. |
| B — Header entities only | Audit only Job and ConsignmentNote, skip line items (JobProcess, JobEntry, etc.) | ✗ Line-item changes invisible. Compliance gaps for movement and container operations. |
C — Opt-in via [Audit] attribute |
Only explicitly marked entities logged | ✗ Developer must remember to add attribute. Easy to forget on new entities. Silently missing audit coverage. |
AuditableEntity is captured automatically with no per-entity configuration required.
AuditInterceptor : SaveChangesInterceptor fires in SavingChangesAsync — after the existing field-stamping (CreatedAt/UpdatedAt/soft-delete) but before the actual DB write. It captures OriginalValues (DB state) and CurrentValues (new state) as JSON.
IsDeleted: false → true transitionAuditLog itself (no self-audit recursion), OutboxMessage, JobSequence (counter-only, no business value in diff).
[AuditIgnore] attribute: Apply to sensitive properties (e.g. PasswordHash) to exclude from JSON capture. No properties use it in Phase 1, but the attribute exists for Phase 2+ use.
public class AuditLog { public Guid Id { get; init; } // PK public string Enterprise { get; set; } // tenant context public string Entity { get; set; } public string UserId { get; set; } // from JWT uid claim public string TableName { get; set; } // e.g. "Jobs" public string EntityKey { get; set; } // e.g. "I26060001" public string Operation { get; set; } // Insert / Update / Delete public string? OldValues { get; set; } // JSON — null for Insert public string? NewValues { get; set; } // JSON — null for Delete public DateTime Timestamp { get; init; } // UTC }
Several interfaces that are conceptually Freight or Haulage domain ended up in Axlogis.Common so other modules could inject them without circular dependencies: IJobEventService, IJobEntryService, IMovementService, IContainerTrackingService, IImportCargoCostService.
| Option | What it means | Trade-off |
|---|---|---|
| A — Keep Common as shared service layer | Status quo. Common holds interfaces needed by multiple modules. | ✓ Zero disruption. No extra assembly. Appropriate for current 1-team, 1-deployment scale. G-08 gap closes without any code change. |
B — Create Axlogis.Contracts project |
Move shared interfaces to a dedicated contracts assembly. | ✗ Extra project, extra ceremony. No runtime benefit unless modules are independently deployed. Premature abstraction for current scale. |
Axlogis.Contracts at that point. Three similar lines is better than a premature abstraction.
GET /api/v1/identity/config returns a JSON object loaded into Pinia at login. It tells the Vue frontend which columns, tabs, and features to show for the current tenant. Without it, the HCC (Haulage Command Centre) cannot know which optional columns (D&D dates, CargoMove status, Planner flag) to render.
| Flag | KnownFeatureFlag constant | Controls | True when |
|---|---|---|---|
hasDnDCharges |
KnownFeatureFlags.DnDCharges |
HCC D&D date columns (4 cols) | Tenant has all 4 D&D charge codes configured in TenantFeatureFlags |
hasCargoMove |
KnownFeatureFlags.CargoMoveTracking |
HCC CargoMove status column | Tenant has CargoMoveTracking feature flag enabled |
isExilian |
KnownFeatureFlags.ExilianPlanner |
HCC Planner column | Tenant has ExilianPlanner feature flag enabled |
hasCteDropSeq |
KnownFeatureFlags.CteDropSequenceEdit |
HCC inline DropSeq edit | Tenant has CteDropSequenceEdit feature flag enabled |
// In HCC Vue component — v-if driven by Pinia TenantConfig <th v-if="tenant.config.hasDnDCharges">Port Storage Last Day</th> <th v-if="tenant.config.isExilian">Planned</th> <th v-if="tenant.config.hasCargoMove">CargoMove</th> <th v-if="tenant.config.hasCteDropSeq">Drop Seq</th>
{
"hasDnDCharges": false,
"hasCargoMove": false,
"isExilian": false,
"hasCteDropSeq": false
}
auth.ts login action calls tenant.fetchConfig() immediately after the JWT is stored. Config is stored in Pinia (useTenantStore().config) and persists until logout. If the endpoint returns 401 (unauthenticated call), the config remains at default (all false).
| Deliverable | Decision gate | Status |
|---|---|---|
Hangfire install + OutboxProcessor recurring job | None | ✓ Build now |
OutboxInterceptor + AddDomainEvent() on AuditableEntity | None (decided 2026-05-30) | ✓ Build now |
AuditInterceptor + AuditLog table | G-01 = A (full diff, all entities) | ✓ Build now |
| GET /api/v1/identity/config (TenantConfig endpoint) | G-05 = 4 HCC flags | ✓ Build now |
Vue tenant.ts fetchConfig + Pinia state | G-05 | ✓ Build now |
| Dual-DB SQL lint in CI (grep MSSQL syntax in .cs) | None | ✓ Build now |
| G-08 — no code change needed | Decision = A (keep Common) | ✓ Closed by decision |