# GB5 EIP System Reference

**Version:** 2026-06-20  
**Scope:** EIP Conversation Engine + DirectAction System  
**Audience:** Developers and architects extending or integrating with the EIP platform

---

## Table of Contents

1. [System Overview](#1-system-overview)
2. [Architecture Diagram](#2-architecture-diagram)
3. [EIP 6-Phase Pipeline](#3-eip-6-phase-pipeline)
4. [Session State](#4-session-state)
5. [Step Types — Complete Reference](#5-step-types--complete-reference)
6. [Variable Substitution](#6-variable-substitution)
7. [Channel Types](#7-channel-types)
8. [Response Payload — Extended Fields](#8-response-payload--extended-fields)
9. [GOP Qualifier Integration](#9-gop-qualifier-integration)
10. [Structured Data — EIPStructuredDataDTO](#10-structured-data--eipstructureddatadto)
11. [DirectAction System](#11-directaction-system)
12. [Key Database Tables](#12-key-database-tables)
13. [Key File Map](#13-key-file-map)
14. [Known Limitations / Future Work](#14-known-limitations--future-work)

---

## 1. System Overview

GB5 EIP consists of two independent but complementary platforms:

### EIP Conversation Engine

A 6-phase stateful pipeline for processing inbound messages across multiple channels (WhatsApp, Teams, Telegram, Slack, SMS, AppChat, Voice, PostMan). Flows are defined as JSON stored in `MEIPFLOWDEFINITION`. The engine resolves routing rules, restores session state, executes the flow step-by-step, and dispatches channel-specific responses.

Key characteristics:
- Stateful per `(userIdentifier, tenantId, channelType)` triple — the same user on WhatsApp and Teams runs independent sessions
- Flow definitions support 17 step types including conversational branching, GOP qualifier integration, external API calls, rich UI elements (QR, location, ratings), and structured data presentation
- Session TTL is 30 minutes from last activity
- Maximum 50 steps per request execution cycle to prevent infinite loops

### DirectAction System

A signed-token mechanism for embedding one-click approval/rejection buttons in email and WhatsApp notifications. Tokens are HMAC-SHA256 signed, have a 48-hour expiry, and are one-time-use. A separate flow handles WhatsApp button replies without tokens.

---

## 2. Architecture Diagram

```
External Channel (WhatsApp / Teams / Telegram / Slack / SMS / AppChat / Voice)
        │
        │  webhook / HTTP POST
        ▼
 FrameworkSL Endpoints
  ┌─────────────────────────────────────────┐
  │  POST /EIPConversation/Receive          │  ← ReceiveEIPConversation (AllowAnonymous)
  │  POST /EIPConversation/Request...       │  ← RequestEIPConversation (WhatsApp verify)
  └───────────────────┬─────────────────────┘
                      │
                      ▼
          EIPConversationBLL (thin orchestrator)
                      │
                      ▼
          EIPConversationEngine.ProcessIncomingMessageAsync()
          ┌───────────────────────────────────────────────┐
          │                                               │
          │  Phase 1 ── EIPChannelNormalizer              │
          │             builds EIPExecutionContextDTO     │
          │             extracts Location → _Location.*   │
          │                                               │
          │  Session ── EIPSessionDAL                     │
          │             MERGE upsert / restore            │
          │                                               │
          │  Phase 2 ── EIPRoutingEngine                  │
          │             MEIPROUTINGRULE lookup            │
          │             (skipped if FlowCode on session)  │
          │                                               │
          │  Phase 3 ── EIPFlowEngine                     │
          │             step handler dispatch             │
          │             GOP / Mapper integration          │
          │             → EIPFlowExecutionResultDTO       │
          │                                               │
          │  Phase 4 ── EIPCapabilityEngine               │
          │             risk + OTP (if CapabilityCode)    │
          │                                               │
          │  Phase 5 ── EIPActionEngine                   │
          │             CALL_API / SEND_NOTIFICATION /    │
          │             VALIDATION_ACTION                 │
          │                                               │
          │  Phase 6 ── EIPResponseEngine                 │
          │             rate-limit (2s window)            │
          │             handler lookup by channel name    │
          │             → HTTP delivery / SignalR push    │
          └───────────────────────────────────────────────┘
                      │
                      ▼
        Channel Handler (per EIPChannelType)
                      │
                      ▼
       External Channel / EIPChatHub (/hubs/eip-chat)
```

**DirectAction flow (separate path):**

```
Email / WhatsApp button click
        │
        ├─ GET /Action/Execute   (NeedsInput=0)  →  validate token → record → call API
        ├─ GET /Action/Submit    (NeedsInput=1 form) → (not yet wired to EIP engine)
        └─ WhatsApp button reply  →  DirectActionHandler (no token, ButtonPayloadPrefix match)
```

---

## 3. EIP 6-Phase Pipeline

All phases execute inside `EIPConversationEngine.ProcessIncomingMessageAsync(EIPConversationDTO, LoginDTO, CancellationToken)`.

### Phase 1 — Normalization

**Class:** `EIPChannelNormalizer`  
**File:** `FrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/ChannelNormalizer/EIPChannelNormalizer.cs`

Converts the raw inbound `EIPConversationDTO` into `EIPExecutionContextDTO`. Responsibilities:

- Normalizes the message text (trim, Unicode cleanup)
- Sets `UserIdentifier`, `TenantId`, `ChannelType`
- If `EIPConversationDTO.Location` is non-null, maps its fields into context Variables as:
  - `_Location.Lat` — latitude (string)
  - `_Location.Lng` — longitude (string)
  - `_Location.Address` — address string
  - `_Location.Accuracy` — accuracy (string)
  - `_Location.Name` — place name (string)

### Session Restore (between Phase 1 and Phase 2)

`EIPSessionDAL.GetActiveSessionAsync()` queries `TEIUSERSESSION` for the active session matching `(UserIdentifier, TenantId, ChannelType, STATUS=1)` within the 30-minute TTL window.

- Restores `FlowCode`, `CurrentStepCode`, and `CONTEXTJSON` (Variables dictionary) into the context
- `ConversationId` is derived: `Math.Abs((int)(sessionGuid.ToByteArray().Take(4) as int))` — first 4 bytes of the session GUID, treated as a non-negative `int`
- Session upsert uses `MERGE ... WITH (HOLDLOCK)` to prevent race conditions on reconnect

### Phase 2 — Routing

**Class:** `EIPRoutingEngine`  
**File:** `FrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/RoutingEngine/EIPRoutingEngine.cs`

Skipped if the session already has a `FlowCode` (i.e., mid-conversation).

Queries `MEIPROUTINGRULE` joined to `MEIPCHANNELENDPOINT` and `MEIPFLOWDEFINITION`, ordered by `PRIORITY ASC`. Evaluates each rule's `MATCHTYPE` against the normalized message:

| MATCHTYPE int | Behavior |
|---------------|----------|
| 1 | EXACT — case-insensitive full-string match |
| 2 | STARTS_WITH |
| 3 | CONTAINS |
| 4 | REGEX |
| null / empty `MATCHVALUE` | Catch-all — matches any input |

Tenant-specific flows (`TENANTID = @TenantId`) take priority over global flows (`TENANTID = -1`).

If `FlowCode` is supplied directly in the inbound `EIPConversationDTO`, routing is bypassed.

### Phase 3 — Flow Engine

**Class:** `EIPFlowEngine`  
**File:** `FrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/FlowEngine/EIPFlowEngine.cs` (1,379 lines)

Loads `MEIPFLOWDEFINITION.JSONDEFINITION`, which may be either:
- An array of step objects (positional format)
- An object with `{ "StartStepCode": "...", "Steps": { "code": {...} } }` (named format — preferred)

Step handlers are registered in `RegisterStepHandlers()` and dispatched by `StepType` (case-insensitive). Execution loops up to 50 steps per request. Returns `EIPFlowExecutionResultDTO`.

**ShouldPauseExecution logic:**
- `MESSAGE` / `PROMPT`: always pause (await next inbound message)
- `INPUT`: pause only when `ResponseMessage` is set (first visit, or re-prompt after validation error)
- `CHOICE`: always pause
- `DYNAMIC_CHOICE`: pause on first visit (buttons rendered); on reply, matched and stored — no pause
- `LOCATION`: pause until `_Location.Lat` appears in Variables
- All other step types: do not pause — loop continues to the next step in the same request cycle

### Phase 4 — Capability Engine

**Class:** `EIPCapabilityEngine`  
**File:** `FrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/CapabilityEngine/EIPCapabilityEngine.cs`

Only executed when `flowResult.CapabilityCode` is non-null (set by a `CAPABILITY` step). Performs risk assessment; OTP generation logs but does not yet persist or deliver.

### Phase 5 — Action Engine

**Class:** `EIPActionEngine`  
**File:** `FrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/ActionEngine/EIPActionEngine.cs`

Only executed when `flowResult.ActionCode` is non-null. Handles:
- `CALL_API` — internal GB5 API call
- `CALL_EXTERNAL_API` — external HTTP call
- `SEND_NOTIFICATION` — Dapr-based notification event
- `VALIDATION_ACTION` — validation-only action type

### Phase 6 — Response Engine

**Class:** `EIPResponseEngine`  
**File:** `FrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/ResponseEngine/EIPResponseEngine.cs`

Only executes when there is response content to deliver.

- **Rate limiting:** In-memory `ConcurrentDictionary<string, DateTime>` keyed on `"CHANNEL:RECIPIENT"`. Enforces 2-second minimum between sends. Entries evicted on each send. Note: in-memory only — does not survive restarts or span multiple instances.
- **Handler resolution:** Looks up `IEIPChannelHandler` by `ChannelName` string (e.g., `"WHATSAPP"`, `"APPCHAT"`)
- **AppChat delivery:** `AppChatChannelHandler` is a no-op HTTP handler; actual delivery is via `EIPChatHub` SignalR push on `/hubs/eip-chat`

---

## 4. Session State

**Table:** `TEIUSERSESSION`

| Column | Type | Notes |
|--------|------|-------|
| `SESSIONID` | GUID | PK |
| `USERIDENTIFIER` | string | Phone number, Teams UPN, etc. |
| `TENANTID` | int | |
| `CHANNELTYPE` | int | `EIPChannelType` enum value |
| `FLOWCODE` | string | Active flow |
| `CURRENTSTEPCODE` | string | Last paused step |
| `CONTEXTJSON` | string | Serialized `Dictionary<string, string>` (Variables) |
| `STARTEDAT` | datetime | |
| `LASTACTIVITYAT` | datetime | Updated on every interaction |
| `STATUS` | int | 1=active, 0=expired |

**TTL:** 30 minutes from `LASTACTIVITYAT`. Session is expired (STATUS=0) when the flow reaches an `END` step or when TTL elapses.

**Multi-channel isolation:** The unique key for an active session is `(USERIDENTIFIER, TENANTID, CHANNELTYPE, STATUS=1)`. The same user on WhatsApp (channel 1) and Teams (channel 2) has separate, independent sessions.

**Required index:**
```sql
CREATE INDEX IX_TEIUSERSESSION_LOOKUP
  ON TEIUSERSESSION (USERIDENTIFIER, TENANTID, CHANNELTYPE)
  WHERE STATUS = 1;
```

---

## 5. Step Types — Complete Reference

> **ℹ️** StepType strings are case-insensitive in the handler registry. All step handlers are registered in `EIPFlowEngine.RegisterStepHandlers()`.

| StepType | Pauses Flow | Key Config Fields | Notes |
|----------|:-----------:|-------------------|-------|
| `MESSAGE` | Always | `MessageTemplate`, `NextStepCode`, `QrCodeData?`, `VoiceHint?` | `QrCodeData` triggers QR rendering on the channel. `PROMPT` is an alias. |
| `PROMPT` | Always | Same as MESSAGE | Identical handler; legacy alias. |
| `INPUT` | First visit / validation error | `MessageTemplate`, `ContextKey`, `Validation` (`Required`, `Pattern`, `InputType`, `RatingMax`, `AllowedFileTypes`), `NextStepCode` | `InputType` values: `text` (default), `number`, `date`, `email`, `phone`, `qr_scan`, `file`, `rating`, `location`. Server-side validates the submitted value before advancing. On failure, re-prompts with the same step. |
| `CHOICE` | Always | `MessageTemplate`, `Choices` (`Dictionary<string, string>` label → NextStepCode) | User reply matched against keys case-insensitively. Renders as reply buttons on supporting channels. |
| `CONDITION` | Never | `ConditionExpression`, `NextStepCode` | Legacy. Checks if `NormalizedMessage` contains `ConditionExpression`. Use `CONDITIONAL` for new flows. |
| `CONDITIONAL` | Never | `ConditionKey`, `Conditions[]` (`Operator`/`Value`/`NextStepCode`), `DefaultNextStepCode` | Evaluates `Variables[ConditionKey]` against each condition in order. Operators: `EXISTS`, `NOT_EXISTS`, `EQUALS`, `NOT_EQUALS`, `IN` (Values list), `GT`, `LT`. Falls through to `DefaultNextStepCode` if no condition matches. |
| `ACTION` | Never | `ActionType` (`CALL_API`/`TRIGGER_MESSAGEHUB`/`DB_UPDATE`), `ApiConfig` (`Url`, `Method`, `Headers`, `BodyTemplate`; or `ServicePrefix`+`PathTemplate` for YARP-routed internal calls), `ContextKey`, `ResponseFormat?` | `CALL_API` result stored in `ContextKey`. `ResponseFormat` parses JSON response into `EIPStructuredDataDTO`. `ApiConfig.Parameters` supports structured param definitions (type: `FIXED`/`CONTEXT`/`INPUT` × placement: `body`/`query`/`header`/`path`). |
| `CAPABILITY` | Never | `CapabilityCode`, `MessageTemplate` | Triggers `EIPCapabilityEngine`. Risk assessment + OTP gating. OTP delivery is a stub (logged, not delivered). |
| `QUALIFY` | Never | `QualifyConfig` (`BoundToType`, `BoundToIdContextKey`, `InputKeys[]`, `Stage`, `Scope`, `OutputFactKey`, `MapperCode`, `OutputMapKey`, `OnValidationErrorStepCode`), `MessageTemplate`, `NextStepCode` | Calls `IQualifierFacadeImpl.ExecuteStageAsync`. Facts exported to `OutputFactKey`. If qualifier returns `Error` findings, flow redirects to `OnValidationErrorStepCode` and sets `_QualifyError` context variable. If `MapperCode` is set, `MappingEngineBLL.TransformAsync` is called on the result. |
| `ENRICH` | Never | `EnrichConfig` (`MapperCode`, `InputKey`, `OutputKey`), `NextStepCode` | Calls `IMappingEngineBLL.TransformAsync(mapperCode, Variables[InputKey])` directly. Stores result in `OutputKey`. |
| `DYNAMIC_CHOICE` | First visit | `DynamicChoiceConfig` (`SourceKey`, `DataPath`, `LabelField`, `ValueField`, `StoreSelectedKey`, `StoreItemKey`, `MaxChoices`), `MessageTemplate`, `NextStepCode` | First visit: renders buttons built from `Variables[SourceKey]` JSON (optionally navigated by `DataPath`). On reply: matches label and stores the selected value in `StoreSelectedKey` and the full item object in `StoreItemKey`. Dot-notation works for referencing: `{StoreSelectedKey.FieldName}`. |
| `LOCATION` | Until location received | `ContextKey`, `MessageTemplate`, `NextStepCode` | Sets `RequestLocation=true` on the response, prompting channel to show location-share UI. Waits for `_Location.Lat` to appear in Variables (populated by Phase 1 Normalizer from the next inbound message). Stores `{Lat, Lng, Address}` JSON in `ContextKey`. |
| `LINK` | Never | `LinkButtons[]` (`EIPLinkButtonDTO`: `Title`, `Url`), `ButtonType` (`REPLY`/`URL`/`CALL`/`QR`), `MessageTemplate`, `NextStepCode` | Emits buttons and routes immediately to `NextStepCode`. Does not await a user reply. |
| `PRESENT` | Never | `ContextKey`, `ResponseFormat` (`Type`, `Fields[]`, `MaxRows`, `TitleField`), `MessageTemplate`, `NextStepCode` | Reads `Variables[ContextKey]` and parses JSON into `EIPStructuredDataDTO`. AppChat renders natively. WhatsApp/SMS receive plain-text via `EIPTextFormatter`. |
| `TRANSFORM` | Never | `Metadata["Transforms"]` array (or single transform on step) | Each transform: `DATE_FORMAT` (InputFormat/OutputFormat), `VALUE_MAP` (Map dictionary), `DATE_DIFF` (SourceKey → SecondSourceKey, returns inclusive day count). Multiple transforms supported via array. |
| `EXTERNAL_ACTION` | Never | `DecisionMap` (`Dictionary<string, string>` decision → NextStepCode) | Legacy backward-compatibility for Mode 1 DirectAction flows. Matches decision key to next step. |
| `END` | — | `MessageTemplate` | Sends final message. Sets `IsCompleted=true`. Calls `EIPSessionDAL.ExpireSessionAsync()` (STATUS=0). |

---

## 6. Variable Substitution

Variables are stored in `TEIUSERSESSION.CONTEXTJSON` as `Dictionary<string, string>`.

**Simple substitution:**

```
{Key}  →  Variables["Key"]
```

In `MessageTemplate`, `ApiConfig.BodyTemplate`, and `ApiConfig.PathTemplate`.

**Dot-notation (nested JSON):**

```
{Key.SubKey}
```

If `Variables["Key"]` is a JSON string, `SubKey` is extracted from the JSON root using `JsonDocument`. Works one level deep — `{Key.A.B}` is not supported (only root properties).

**Built-in location variables** (set by Normalizer from inbound `Location` field):

| Variable | Content |
|----------|---------|
| `_Location.Lat` | Latitude as string |
| `_Location.Lng` | Longitude as string |
| `_Location.Address` | Address string |
| `_Location.Accuracy` | Accuracy as string |
| `_Location.Name` | Place name |

**Error variables** (set by step handlers on failure):

| Variable | Set by |
|----------|--------|
| `_QualifyError` | QUALIFY step when qualifier returns Error findings |

---

## 7. Channel Types

> **ℹ️** Numeric values are persisted in `TEIUSERSESSION.CHANNELTYPE`. Do not change them.

| Value | Name | Handler Class | ChannelName string | Notes |
|-------|------|--------------|-------------------|-------|
| 0 | `PostMan` | `PostmanChannelHandler` | `"POSTMAN"` | Simulation/echo only. No HTTP call. |
| 1 | `WhatsApp` | `WhatsAppChannelHandler` | `"WHATSAPP"` | Meta Graph v18 or Celitix (runtime switch via config). Polly 3-retry with exponential backoff. QR codes sent as image URL via `api.qrserver.com`. `EIPTextFormatter` fallback for structured data. |
| 2 | `Teams` | `TeamsChannelHandler` | `"TEAMS"` | Adaptive card rendering. |
| 3 | `Telegram` | `TelegramChannelHandler` | `"TELEGRAM"` | `sendPhoto` for QR codes. `ReplyKeyboardMarkup` for location sharing. CHOICE/LINK buttons currently text-only (inline keyboard not yet implemented). |
| 4 | `Slack` | `SlackChannelHandler` | `"SLACK"` | Block Kit formatting. |
| 5 | `SMS` | `SmsChannelHandler` | `"SMS"` | Plain text only. `EIPTextFormatter` fallback for structured data. |
| 6 | `AppChat` | `AppChatChannelHandler` | `"APPCHAT"` | No-op HTTP handler. Real delivery via `EIPChatHub` SignalR push on `/hubs/eip-chat`. Group: `appchat:user:{userId}:client:{clientId}`. |
| 7 | `Voice` | `VoiceChannelHandler` | `"VOICE"` | No outbound HTTP call. Returns `AdditionalData` with `TtsText`, `SsmlText`, `IsCompleted`. Consumes `VoiceHint` (SSML) from step definition. |

### WhatsApp Provider Switching

```csharp
// In MessageHubSettings (appsettings):
// "Celitix": { "Enable": "Y", "Key": "...", "WabaNumber": "..." }
// "WhatsApp": { "Enable": "Y", "BearerToken": "...", "PhoneNumberId": "..." }
```

The handler checks `Celitix:Enable = "Y"` first, then falls back to `WhatsApp:Enable = "Y"`. If neither is enabled, an exception is thrown at delivery time.

---

## 8. Response Payload — Extended Fields

These fields flow from the step handler through the full pipeline: `EIPFlowExecutionResultDTO` → `EIPExecutionResultDTO` → `EIPResponseContextDTO` → channel handler → `EIPChatMessageDTO` (AppChat).

| Field | Type | Set by | Consumed by |
|-------|------|--------|-------------|
| `StructuredData` | `EIPStructuredDataDTO?` | `PRESENT` step, `ACTION` with `ResponseFormat` | AppChat renders natively; WhatsApp/SMS converted via `EIPTextFormatter.FormatAsText()` |
| `QrCodeData` | `string?` | `MESSAGE`/`PROMPT` step `QrCodeData` field | AppChat: raw data string; WhatsApp/Telegram: image URL via qrserver.com |
| `InputHint` | `string?` | `INPUT` step `Validation.InputType` | AppChat: widget selection (`rating`, `qr_scan`, `file`, etc.) |
| `RatingMax` | `int?` | `INPUT` step `Validation.RatingMax` | AppChat: star rating maximum |
| `RequestLocation` | `bool` | `LOCATION` step (first visit) | Channel shows location-share UI |
| `VoiceHint` | `string?` | Step `VoiceHint` property (SSML string) | `VoiceChannelHandler` → `AdditionalData["SsmlText"]` |
| `AdditionalData` | `Dictionary<string, object>?` | Channel handlers | `VoiceChannelHandler` populates `TtsText`, `SsmlText`, `IsCompleted` |

### EIPChatMessageDTO (AppChat SignalR payload)

Pushed via `IEIPChatClient.ReceiveBotMessage(EIPChatMessageDTO)`:

```csharp
public class EIPChatMessageDTO
{
    public string Message { get; set; }
    public string CurrentStepCode { get; set; }
    public string NextStepCode { get; set; }
    public bool IsCompleted { get; set; }
    public List<EIPButtonDTO> Choices { get; set; }
    public EIPStructuredDataDTO? StructuredData { get; set; }
    public string? QrCodeData { get; set; }
    public string? InputHint { get; set; }
    public int? RatingMax { get; set; }
    public bool RequestLocation { get; set; }
    public DateTime SentAtUtc { get; set; }
}
```

---

## 9. GOP Qualifier Integration

### IQualifierFacadeImpl

**File:** `FrameworkBLL/GOP/QualifierFacadeImpl.cs`  
**Interface:** `IQualifierFacadeImpl` (distinct from `IQualifierFacade` in GB5Shared)

Called by the `QUALIFY` step handler via `ExecuteStageAsync(QualifierExecutionContext, LoginDTO, CancellationToken)`.

**QualifierExecutionContext fields:**

```csharp
public class QualifierExecutionContext
{
    public int ClientId { get; set; }
    public int EntityId { get; set; }           // MQUALIFIERDEFINITION FK
    public string EntityCode { get; set; }      // BoundToType (e.g., "PURCHASEORDER")
    public byte Stage { get; set; }             // 1=PreValidate, 2=PreWorkflow, 3=PrePersist
    public byte Scope { get; set; }             // effect depends on qualifier definition
    public JsonDocument Document { get; set; }  // the entity document to qualify
    public string CorrelationId { get; set; }
}
```

**QualifierResult:**

```csharp
public class QualifierResult
{
    public bool IsSuccess { get; set; }
    public List<QualifierFinding> Findings { get; set; }  // Severity, Code, Message
    public IFactBag Facts { get; set; }
}
```

`QualifierResult.Facts.ExportAll()` returns:

```json
{
  "Document": { },
  "Lines":    [ ],
  "Groups":   { }
}
```

**Qualifier execution modes** (registered as keyed services): 1=NCalc, 2=DataSource, 3=Service.

### IMappingEngineBLL

**File:** `FrameworkBLL/GOP/MappingEngineBLL.cs`

```csharp
Task<string> TransformAsync(string mapperCode, string sourcePayloadJson, LoginDTO login, CancellationToken ct);
```

Reads mapping nodes from `MMAPPINGDEFINITION` / `MMAPPINGVERSION` / `MMAPPINGNODE` via `IMappingDAL.GetActiveNodesByMapperCode`. Supports `[FactBag]` JSONPath prefix to read from qualifier facts (Lines/Groups/document).

Used by:
- `ENRICH` step: calls `TransformAsync` directly with `Variables[InputKey]` as source
- `QUALIFY` step: calls `TransformAsync` when `QualifyConfig.MapperCode` is set, after qualifier execution

**DI:** Both `IQualifierFacadeImpl` and `IMappingEngineBLL` are auto-registered via assembly scan — no manual DI needed.

---

## 10. Structured Data — EIPStructuredDataDTO

Produced by `PRESENT` and `ACTION` (with `ResponseFormat`) steps, consumed by AppChat and text-formatter fallback.

```csharp
public class EIPStructuredDataDTO
{
    public string Type { get; set; }                          // "TABLE" | "CARD" | "LIST" | "TEXT"
    public List<EIPFieldDefinitionDTO> Fields { get; set; }   // column/field definitions
    public List<Dictionary<string, string>> Rows { get; set; }
    public int TotalCount { get; set; }
    public bool IsTruncated { get; set; }                    // true if Rows.Count < TotalCount
}

public class EIPFieldDefinitionDTO
{
    public string Field { get; set; }    // property name in row dict
    public string Label { get; set; }    // display label
    public string? Format { get; set; } // optional format hint
}
```

### EIPTextFormatter.FormatAsText()

Used as fallback on channels that cannot render structured data natively (WhatsApp, SMS):

| Type | Output format |
|------|--------------|
| `TABLE` / `CARD` | `*Label*: value\n` blocks per field, rows separated by `---` |
| `LIST` | `1. value\n` numbered list (uses `TitleField` or first field) |
| `TEXT` | First row, first field value only |

Appends `(Showing N of M)` when `IsTruncated = true`.

---

## 11. DirectAction System

### Token Format

```
payload = "{actionCode}:{contextId}:{tenantId}:{assigneeUserId}:{expiresAtTicks}:{databaseName}"
token   = Base64Url(UTF8(payload)) + "." + Base64Url(HMAC-SHA256(payload, secret))
```

- **Signing:** HMAC-SHA256, key from `DirectAction:TokenSecret` config
- **Encoding:** Standard Base64Url (`+`→`-`, `/`→`_`, no padding)
- **Expiry:** Stored as `DateTime.Ticks` (long). Default: 48 hours from issuance
- **Comparison:** `CryptographicOperations.FixedTimeEquals` — constant-time to prevent timing attacks
- **`databaseName`:** May contain `:` — the tail (`parts[5..]`) is re-joined with `:` during parsing
- **One-time-use:** SHA256 hash of the token stored as `TOKENHASH` in `TDIRECTACTIONTOKEN`. On execution, `STATUS` is set to 1 (Used). Subsequent attempts are rejected

### Execution Flows

**NeedsInput = 0 (direct action):**

```
GET /Action/Execute?token={token}
  → DirectActionTokenService.ValidateTokenAsync()
  → Check TDIRECTACTIONTOKEN (not used, not expired)
  → GenericApiDirectActionHandler.ExecuteAsync()  ← calls MDIRECTACTIONDETAIL.ApiEndpoint
  → Mark TDIRECTACTIONTOKEN.STATUS = 1 (Used)
  → Return success page
```

**NeedsInput = 1 (remarks form):**

```
GET /Action/Execute?token={token}   →  show HTML remarks form
POST /Action/Submit                 →  validate token + record remarks + call API
```

> **⚠️** NeedsInput=1 is not yet wired to the EIP engine. The handler logs a warning. The form HTML is served but the conversational flow integration is pending.

**WhatsApp button replies (no token):**

```
Inbound webhook → DirectActionHandler
  → Match ButtonPayloadPrefix: "{BUTTONPAYLOADPREFIX}_{ContextId}"
  → No token validation, no one-time-use check
  → Call MDIRECTACTIONDETAIL.ApiEndpoint with ContextId
```

### Resend

`POST /Action/Resend` — re-publishes the `TOUTBOX` outbox event for a DirectAction. Existing tokens remain valid. One-time-use enforcement prevents double-execution even if the same token is clicked again.

---

## 12. Key Database Tables

> Column lists show key columns only. All tables include standard audit columns unless noted.

### EIP Tables

**MEIPFLOWDEFINITION**

| Column | Notes |
|--------|-------|
| `FLOWDEFINITIONID` | PK |
| `TENANTID` | -1 = global/shared (available to all tenants) |
| `FLOWCODE` | Unique per tenant + version |
| `FLOWVERSION` | Semantic version string |
| `FLOWSTATUS` | Active/inactive |
| `JSONDEFINITION` | JSON: array of steps, OR `{StartStepCode, Steps:{code:{...}}}` |
| `SOURCETYPE` | Origin identifier |
| `VERSION`, `STATUS`, `SORTORDER` | Standard framework columns |

DAL query prefers `TENANTID = @TenantId` over `TENANTID = -1` when both exist.

**MEIPROUTINGRULE**

| Column | Notes |
|--------|-------|
| `ROUTINGRULEID` | PK |
| `TENANTID` | |
| `CHANNELTYPE` | `EIPChannelType` int value |
| `FLOWDEFINITIONID` | FK → MEIPFLOWDEFINITION |
| `ENDPOINTID` | FK → MEIPCHANNELENDPOINT |
| `PRIORITY` | Evaluated ASC; lowest priority number = first match |
| `MATCHTYPE` | 1=EXACT, 2=STARTS_WITH, 3=CONTAINS, 4=REGEX |
| `MATCHVALUE` | Pattern; null/empty = catch-all |
| `STATUS` | 1=active |

**TEIUSERSESSION** — see [Section 4](#4-session-state)

### DirectAction Tables

**MDIRECTACTION**

| Column | Notes |
|--------|-------|
| `DIRECTACTIONID` | PK |
| `DIRECTACTIONCODE` | Unique identifier used in token payload |
| `DIRECTACTIONNAME` | Display name |
| `CONTEXTIDFIELD` | Field name in source entity that holds ContextId |
| `ASSIGNEEUSERIDFIELD` | Field name that holds AssigneeUserId |
| `TENANTID` | |

**MDIRECTACTIONDETAIL**

| Column | Notes |
|--------|-------|
| `DIRECTACTIONDETAILID` | PK |
| `DIRECTACTIONID` | FK → MDIRECTACTION |
| `ACTIONCODE` | e.g., `APPROVE`, `REJECT` |
| `ACTIONLABEL` | Button label text |
| `EMAILPLACEHOLDER` | Placeholder key in email template |
| `BUTTONPAYLOADPREFIX` | WhatsApp button payload prefix (for non-token flow) |
| `APIENDPOINT` | Target API URL called on execution |
| `HTTPMETHOD` | GET/POST/PUT |
| `PAYLOADTEMPLATE` | JSON template with `{ContextId}` etc. |
| `NEEDSINPUT` | 0=direct, 1=remarks form required |
| `FLOWID` | FK → MEIPFLOWDEFINITION (for NeedsInput=1, pending) |

**TDIRECTACTIONTOKEN**

| Column | Notes |
|--------|-------|
| `TOKENHASH` | SHA256 hash of the full token string (PK / unique) |
| `CONTEXTID` | Extracted from token payload |
| `ACTIONCODE` | |
| `ASSIGNEEUSERID` | |
| `TENANTID` | |
| `EXPIRESAT` | Stored as UTC datetime |
| `USEDAT` | Set on first use |
| `STATUS` | 0=Unused, 1=Used, 2=Revoked |
| `CREATEDON` | |

### Action Pipeline Tables

**TEVENTACTIONRUN**

| Column | Notes |
|--------|-------|
| `ACTIONRUNID` | PK |
| `ACTIONID` | FK → action definition |
| `RUNSTATUS` | Pending/Running/Completed/Failed |
| `PAYLOAD` | Input payload JSON |
| `ERRORMESSAGE` | Failure details |

**TACTIONOUTBOX**

| Column | Notes |
|--------|-------|
| `OUTBOXID` | PK |
| `DESTINATIONTOPIC` | Dapr pub/sub topic |
| `PAYLOAD` | Message payload JSON |
| `SENDSTATUS` | Pending/Sent/Failed |

---

## 13. Key File Map

### FrameworkBLL — Engine

| File | Responsibility |
|------|---------------|
| `EIPConversation/EIPHandlers/EIPConversationBLL/EIPConversationBLL.cs` | Thin BLL orchestrator; delegates to `IEIPConversationEngine` |
| `EIPConversation/EIPHandlers/EIPConversationEngine/EIPConversationEngine.cs` | 6-phase pipeline (`ProcessIncomingMessageAsync`) |
| `EIPConversation/EIPHandlers/EIPEngine/ChannelNormalizer/EIPChannelNormalizer.cs` | Phase 1: Normalization + Location extraction |
| `EIPConversation/EIPHandlers/EIPEngine/RoutingEngine/EIPRoutingEngine.cs` | Phase 2: MEIPROUTINGRULE resolution |
| `EIPConversation/EIPHandlers/EIPEngine/FlowEngine/EIPFlowEngine.cs` | Phase 3: step dispatch, GOP integration, QR/Location/InputType logic (1,379 lines) |
| `EIPConversation/EIPHandlers/EIPEngine/FlowEngine/EIPTextFormatter.cs` | TABLE/CARD/LIST → plain text (WhatsApp/SMS fallback) |
| `EIPConversation/EIPHandlers/EIPEngine/CapabilityEngine/EIPCapabilityEngine.cs` | Phase 4: risk + OTP (stub) |
| `EIPConversation/EIPHandlers/EIPEngine/ActionEngine/EIPActionEngine.cs` | Phase 5: CALL_API, SEND_NOTIFICATION, VALIDATION_ACTION |
| `EIPConversation/EIPHandlers/EIPEngine/ResponseEngine/EIPResponseEngine.cs` | Phase 6: rate-limit + handler dispatch |

### FrameworkBLL — Channel Handlers

| File | ChannelName | Notes |
|------|------------|-------|
| `ChannelHandler/PostmanChannelHandler.cs` | `"POSTMAN"` | Echo/simulation |
| `EIPConversation/EIPHandlers/ChannelHandler/WhatsAppChannelHandler.cs` | `"WHATSAPP"` | Celitix or Meta Graph; QR as image |
| `EIPConversation/EIPHandlers/ChannelHandler/TeamsChannelHandler.cs` | `"TEAMS"` | Adaptive cards |
| `EIPConversation/EIPHandlers/ChannelHandler/TelegramChannelHandler.cs` | `"TELEGRAM"` | sendPhoto, ReplyKeyboardMarkup |
| `EIPConversation/EIPHandlers/ChannelHandler/SlackChannelHandler.cs` | `"SLACK"` | Block Kit |
| `EIPConversation/EIPHandlers/ChannelHandler/SmsChannelHandler.cs` | `"SMS"` | Plain text |
| `EIPConversation/EIPHandlers/ChannelHandler/AppChatChannelHandler.cs` | `"APPCHAT"` | No-op; delivery via EIPChatHub |
| `EIPConversation/EIPHandlers/ChannelHandler/VoiceChannelHandler.cs` | `"VOICE"` | TTS/SSML; no HTTP call |

### FrameworkBLL — GOP

| File | Responsibility |
|------|---------------|
| `GOP/QualifierFacadeImpl.cs` | `IQualifierFacadeImpl` — DAG-executing qualifier engine |
| `GOP/MappingEngineBLL.cs` | `IMappingEngineBLL` — mapper engine (`TransformAsync`) |

### FrameworkBLL — DirectAction

| File | Responsibility |
|------|---------------|
| `DirectAction/DirectActionTokenService.cs` | Token generation, validation, one-time-use marking |
| `DirectAction/GenericApiDirectActionHandler.cs` | Executes `MDIRECTACTIONDETAIL.ApiEndpoint` |
| `DirectAction/DirectActionResult.cs` | Result DTO |

### FrameworkDAL — DTOs

All under `FrameworkDAL/DTO/EIPConversation/`:

| File | Key Contents |
|------|-------------|
| `EIPConversationDTO.cs` | Inbound payload (`Message`, `From`, `Platform`, `TenantId`, `ChannelType`, `UserIdentifier`, `FlowCode`, `ConversationId`, `Location`) |
| `EIPExecutionContextDTO.cs` | Execution context + `EIPChannelType` enum definition |
| `EIPFlowStepDTO.cs` | Full step definition: all config fields, `EIPStepValidation`, `EIPApiConfig`, `EIPStepCondition`, `EIPLinkButtonDTO` |
| `EIPButtonDTO.cs` | `Id`, `Title`, `Url`, `Payload`, `ButtonType` (default `"REPLY"`) |
| `EIPFlowExecutionResultDTO.cs` | Flow engine output including all extended fields |
| `EIPExecutionResultDTO.cs` | Propagated result returned to endpoint |
| `EIPResponseContextDTO.cs` | Assembled response context for channel handlers |
| `EIPChannelResponseDTO.cs` | Channel handler output (`IsSent`, `ExternalMessageId`, `AdditionalData`) |
| `EIPQualifyConfigDTO.cs` | QUALIFY step config |
| `EIPEnrichConfigDTO.cs` | ENRICH step config |
| `EIPDynamicChoiceConfigDTO.cs` | DYNAMIC_CHOICE step config |
| `EIPLocationDTO.cs` | `Latitude`, `Longitude`, `Address`, `Accuracy`, `Name` |
| `EIPResponseFormatDTO.cs` | `Type`, `Fields[]`, `MaxRows`, `TitleField` |
| `EIPStructuredDataDTO.cs` | Rendered structured data result |
| `EIPFieldDefinitionDTO.cs` | `Field`, `Label`, `Format?` |
| `EIPUserSessionDTO.cs` | Session row mapping |
| `EIPRoutingRuleDTO.cs` | Routing rule row mapping |
| `EIPParamDefinitionDTO.cs` | Structured API parameter definition |
| `EIPFlowDefinitionDTO.cs` | Flow definition row mapping |

### FrameworkDAL — Query Builders

| File | Tables |
|------|--------|
| `Query/EIPConversation/EIPFlow/EIPFlowQB.cs` | `MEIPFLOWDEFINITION` |
| `Query/EIPConversation/EIPRouting/EIPRoutingQB.cs` | `MEIPROUTINGRULE`, `MEIPCHANNELENDPOINT` |
| `Query/EIPConversation/EIPSession/EIPSessionQB.cs` | `TEIUSERSESSION` |
| `Query/DirectAction/DirectActionConfigQB.cs` | `MDIRECTACTION`, `MDIRECTACTIONDETAIL` |
| `Query/DirectAction/DirectActionTokenQB.cs` | `TDIRECTACTIONTOKEN` |

### FrameworkSL

| File | Responsibility |
|------|---------------|
| `Hubs/EIPChat/EIPChatHub.cs` | AppChat SignalR hub; route `/hubs/eip-chat`; group `appchat:user:{userId}:client:{clientId}` |
| `Hubs/EIPChat/IEIPChatClient.cs` | `ReceiveBotMessage(EIPChatMessageDTO)`, `ReceiveError(string)`, `ReceiveTypingIndicator(bool)` |
| `Endpoints/EIPConversation/ReceiveEIPConversation.cs` | `POST /EIPConversation/Receive` (AllowAnonymous) |
| `Endpoints/EIPConversation/RequestEIPConversation.cs` | `POST /EIPConversation/RequestEIPConversation` (WhatsApp webhook) |
| `Endpoints/Action/ResendDirectAction.cs` | `POST /Action/Resend` |

---

## 14. Known Limitations / Future Work

**This table was significantly stale as of 2026-08-13** — several rows described below were already
fixed in a prior session (2026-07-27, see backend memory `project_eip_capability_otp_fix.md`) and
several more were fixed live-verified on 2026-08-13 (see `project_eip_phase1_fixes` / DEPLOY_LOG.md
entry on GB5DEMO the same date). Corrected below; don't trust an unreviewed copy of this table again
without checking current code first.

| Area | Issue | Status |
|------|-------|--------|
| Telegram | `CHOICE`/`DYNAMIC_CHOICE`/`LINK` buttons rendered as inline keyboard (url + callback_data) | **Fixed 2026-08-13** |
| WhatsApp | `ButtonType=URL` renders as a distinct `cta_url` interactive message; `REPLY` buttons use the correct Meta `button`/`action.buttons[].reply` shape (previously malformed for ALL button types, not just URL) | **Fixed 2026-08-13** |
| Voice | `POST /EIPConversation/ReceiveVoice` now registered, routes to `VoiceChannelHandler` | **Fixed 2026-08-13** |
| CAPABILITY / OTP | Hashed, persisted, delivered via a real channel, 5-attempt lockout, live-verified full round trip | **Fixed & live-verified 2026-07-27** (this row was already stale before today) |
| DirectAction NeedsInput=1 | Both the static remarks form (`FlowId<=0`) and the conversational handoff (`FlowId>0`, `GET /Action/Conversation`) are wired to the EIP engine and live-verified | **Fixed & live-verified 2026-07-27** (this row was already stale before today) |
| EIPAdmin SaveFlow | `FlowStatus` was a string bound into a TINYINT column (SQL error on every call with a filter); `FLOWDEFINITIONID` insert never supplied a value (NULL-constraint violation on every insert) | **Fixed 2026-08-13** — byte-typed `FlowStatus`, AutoNumber-generated ID |
| INPUT step casing | Free-text answers were stored lowercased (`NormalizedMessage`) instead of preserving original casing (`OriginalMessage`) | **Fixed 2026-08-13** |
| `MessageKey` (i18n templates) | Only `MESSAGE`/`END` steps resolved `MEIPMESSAGETEMPLATE`; `INPUT`/`CHOICE`/`CAPABILITY` never did | **Fixed 2026-08-13**, live-verified via real template lookups |
| `TenantId` typing | `EIPConversationDTO`/`EIPExecutionContextDTO`/`EIPActionContextDTO`/routing repository+DAL all used `string` `TenantId` despite every `TENANTID` column in the schema being `INT` — caused a hard 400 the moment any real client (e.g. the AppChat frontend) sent a JSON number; also silently broke `SEND_NOTIFICATION` actions (`EIPActionContextDTO.TenantId` was never populated by either caller, so `NotificationActionHandler` always returned "Invalid TenantId") | **Fixed 2026-08-13**, converted to `int`/`int?` throughout |
| AppChat frontend (`features/gbeip`) | Fully-coded chat widget existed but was never wired into any running app (no route, no path alias, no menu) since it was scaffolded 2026-06-21; separately, its `EIPChatMessageDTO.choices` field name/shape didn't match what `EIPChatHub.SendMessage` actually sends (`Buttons`, not `Choices`), so buttons never rendered even once wired up; QR-scan was a `prompt()` placeholder | **Wired in + fixed 2026-08-13** — routed at `admin` project's `/dev/eip-appchat`, real `gb-qrscanner` component wired in, live-verified end-to-end on GB5DEMO (text, CHOICE buttons, click-to-advance, flow completion) |
| QUALIFY `Scope` | `Scope` byte parameter effect depends entirely on the qualifier definition in `MQUALIFIERDEFINITION` — the EIP step treats it as opaque | By design |
| Rate limiter | In-memory only — resets on restart, does not work across multiple EIP instances | Single-instance |
| EIP admin surface | Flow + Routing Rule CRUD exist; no admin CRUD for `MEIPCAPABILITY` (risk/OTP config) or `MEIPMESSAGETEMPLATE` — still SQL-seeded only | Deferred (explicit scope decision 2026-08-13) |
| EIP flow/capability authoring UI (frontend) | No FE screen for any of the above — deferred alongside the backend admin gap | Deferred (explicit scope decision 2026-08-13) |
| QR-scan | Real camera capture via `gb-qrscanner`/jsQR now wired (was a `prompt()` stub) — not live-tested against a real physical QR code in this session (no camera on the headless build server) | Code-complete, camera round-trip unverified |
| Rating / structured-data / QR-display widgets | Field names already matched the backend wire format (unlike `buttons`) and the underlying SignalR data pipeline is now proven correct via the CHOICE-button fix — plausible but not individually live-clicked | Unverified (lower risk given the pipeline fix) |
| TRANSFORM step | Not documented in the original EIP spec; added in the FlowEngine. `DATE_FORMAT`, `VALUE_MAP`, `DATE_DIFF` subtypes | Undocumented |
