Sprint E Architecture Notes Axlogis Design Decisions

Recorded 2026-06-01 · Calvin confirmed G-01, G-08, G-05 · Applies to audit logging, module structure, and TenantConfig

← Back to Project Dashboard
G-01 — Audit Log Granularity
G-01

AuditInterceptor scope — full JSON diff on all AuditableEntities

✓ Option A selected

The options

OptionWhat gets loggedTrade-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.
✓ Decision (Calvin, 2026-06-01): Option A — full JSON diff on ALL AuditableEntities.
Every Insert/Update/Delete on any entity that extends AuditableEntity is captured automatically with no per-entity configuration required.

Implementation

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.

  • Insert: OldValues = null, NewValues = full entity JSON
  • Update: OldValues = original DB values JSON, NewValues = new values JSON
  • Delete (soft): Captured as Update with IsDeleted: false → true transition
Excluded from audit: AuditLog 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.

AuditLog table schema

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
}
G-08 — Module Boundary (Axlogis.Common Scope)
G-08

Accept Common as shared service layer — no Axlogis.Contracts project

✓ Option A selected

The situation

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.

The options

OptionWhat it meansTrade-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.
✓ Decision (Calvin, 2026-06-01): Option A — accept Common as shared service layer.
G-08 is closed. Revisit when team splits or modules need independent deployment cadences.
When to revisit: If a future team owns Haulage independently and needs to deploy it without a Common change, extract Axlogis.Contracts at that point. Three similar lines is better than a premature abstraction.
G-05 — TenantConfig Initial Flag Set
G-05

TenantConfig endpoint — 4 HCC flags only in Phase 1

✓ Decided

What TenantConfig is

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.

✓ Decision (Calvin, 2026-06-01): Include only the 4 flags needed to unblock the HCC Vue page in Sprint F. Add more flags per feature as they are built — a flag nobody reads yet is dead config.

Phase 1 flag set

FlagKnownFeatureFlag constantControlsTrue 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

Vue usage (HCC page)

// 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>

API response shape

{
  "hasDnDCharges":  false,
  "hasCargoMove":   false,
  "isExilian":      false,
  "hasCteDropSeq":  false
}
Loaded at login: 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).
Sprint E Scope Summary
DeliverableDecision gateStatus
Hangfire install + OutboxProcessor recurring jobNone✓ Build now
OutboxInterceptor + AddDomainEvent() on AuditableEntityNone (decided 2026-05-30)✓ Build now
AuditInterceptor + AuditLog tableG-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 stateG-05✓ Build now
Dual-DB SQL lint in CI (grep MSSQL syntax in .cs)None✓ Build now
G-08 — no code change neededDecision = A (keep Common)✓ Closed by decision