FLS Developer Guide

API Reference, data model, integration patterns, and TMS deep-dive for backend developers and integrators
๐Ÿ”Œ Backend Developers  ยท  โš™๏ธ Integrators  ยท  ๐Ÿ— Architects

3-Layer Overview

FLS follows the project-wide 3-tier architecture: Service Layer (SL) handles HTTP endpoints and cache keys, Business Logic Layer (BLL) handles validation, orchestration, and event publishing, Data Access Layer (DAL) handles all Dapper queries and stored procedure calls.

SL

FLSSL โ€” FastEndpoints

  • All endpoints inherit BaseEndpoint<Params, ResponseStandardDTO<object>>
  • GetCacheKey() returns null for all FLS endpoints โ€” forms are dynamic per respondent, never cached
  • Request/response DTOs in FLSSL/Parameters/
  • SignalR Hub: FLSSL/Hubs/FlsMonitorHub.cs
BLL

FLSBLL โ€” Business Logic

  • Validation, orchestration, event publishing, cache invalidation
  • All save methods follow: GB5Trace.Step("validate") โ†’ validate โ†’ GB5Trace.Step("save") โ†’ DAL call โ†’ GB5Trace.Step("event-publish") โ†’ publish event
  • Catches exceptions: GB5Trace.MarkFailed(reason, ex) + _logger.LogError(...) + throw
  • Key implementations: FlsInstanceBLL, FlsSessionBLL, FlsRespondentBLL, SurveyInstrumentBLL, QuickRatingBLL, FlsDispatchBLL, FlsEventBLL
DAL

FLSDAL โ€” Dapper + SQL Server

  • All SQL in QueryBuilders: FlsQB.cs, SurveyQB.cs, QuickRatingQB.cs, FlsLifecycleQB.cs
  • Write operations use stored procedures (SPs): SAVE_REGISTRATION, SP_TMS_DISPATCHFEEDBACK, SAVEDRAFTANSWERS, etc.
  • Read operations use parameterised SELECT queries via IQueryExecutor
  • Bulk respondent add uses Table-Valued Parameter (TVP) for performance
  • All DAL methods accept and forward CancellationToken ct

Module Structure

GB5Solution/FLS/
โ”œโ”€โ”€ FLSSL/
โ”‚   โ”œโ”€โ”€ Endpoints/
โ”‚   โ”‚   โ”œโ”€โ”€ Instance/       CreateFlsInstance, GetFlsInstance, ControlFlsInstance,
โ”‚   โ”‚   โ”‚                   ScheduleFlsInstance, OpenFlsInstance
โ”‚   โ”‚   โ”œโ”€โ”€ Registration/   CreateFlsRegistration, GetFlsRegistration,
โ”‚   โ”‚   โ”‚                   SaveFlsAccessRule, DeleteFlsAccessRule
โ”‚   โ”‚   โ”œโ”€โ”€ Respondent/     GetFlsRespondents, AddFlsRespondents, NudgeFlsRespondent,
โ”‚   โ”‚   โ”‚                   FlsRespondentOptOut, GetFlsPendingForms
โ”‚   โ”‚   โ”œโ”€โ”€ Session/        OpenFlsSession, SaveFlsSessionDraft, SubmitFlsSession,
โ”‚   โ”‚   โ”‚                   MarkExternalSubmit
โ”‚   โ”‚   โ”œโ”€โ”€ Survey/         GetSurveyInstrument, SaveSurveyDraftAnswers,
โ”‚   โ”‚   โ”‚                   SubmitSurveyAnswers, GetSurveyResults, GetSurveyAnswers
โ”‚   โ”‚   โ””โ”€โ”€ QuickRating/    GetQuickRatingWidget, SubmitRating, GetEntityRatings, GetRatingTrend
โ”‚   โ”œโ”€โ”€ Hubs/
โ”‚   โ”‚   โ”œโ”€โ”€ FlsMonitorHub.cs
โ”‚   โ”‚   โ””โ”€โ”€ IFlsMonitorClient.cs
โ”‚   โ””โ”€โ”€ Program.cs
โ”œโ”€โ”€ FLSBLL/
โ”‚   โ”œโ”€โ”€ Implementations/    FlsInstanceBLL, FlsSessionBLL, FlsRespondentBLL,
โ”‚   โ”‚                       SurveyInstrumentBLL, QuickRatingBLL, FlsEventBLL, FlsDispatchBLL
โ”‚   โ””โ”€โ”€ Interfaces/
โ””โ”€โ”€ FLSDAL/
    โ”œโ”€โ”€ DTOs/               FlsRows.cs, SurveyRows.cs, QuickRatingRows.cs, FlsDtos.cs,
    โ”‚                       SurveyDtos.cs, QuickRatingDtos.cs, FlsRequests.cs
    โ”œโ”€โ”€ QueryBuilders/      FlsQB.cs, SurveyQB.cs, QuickRatingQB.cs, FlsLifecycleQB.cs
    โ””โ”€โ”€ Implementations/ + Interfaces/

Key Dependencies

DependencyUsed forInjected via
IQueryExecutorAll Dapper queries and SPsConstructor injection in every DAL class
IFlsEventBLLPublishing FormSubmitted, InstanceCompleted, etc. to LFLSEVENTLOGInjected into FlsSessionBLL, FlsInstanceBLL
IFlsDispatchBLLToken generation, EIP dispatch, LFLSDISPATCHLOG writesInjected into FlsInstanceBLL.OpenInstanceAsync
IHubContext<FlsMonitorHub, IFlsMonitorClient>Pushing real-time updates to incharges watching an instanceInjected into FlsSessionBLL
IKeyInvalidateCache invalidation after state changesInjected into FlsInstanceBLL (after open/pause/close)
IEventLogPublishBusiness audit trail publishing via DaprInjected into all BLL classes with write operations

Entity Relationship Summary

EntityParentLink ColumnPurpose
FLS Registration(root)โ€”Master config template (form type)
FLS InstanceRegistrationFlsRegistrationIdActive deployment to specific people/time
FLS Instance ScheduleInstanceFlsInstanceIdOpensFrom, ClosesOn, Quartz job IDs
FLS GroupInstanceFlsInstanceIdTarget audience sub-group
FLS RespondentGroupFlsInstanceId, GroupIdIndividual form assignee + token
FLS SessionRespondentFlsRespondentIdSession tracking per attempt
FLS Access RuleRegistrationFlsRegistrationIdView/manage permissions per user/role
FLS InchargeInstanceFlsInstanceIdAdmin users assigned to monitor instance
Survey Instrument ConfigRegistrationFlsRegistrationIdThe questionnaire for this registration
Survey SectionInstrument ConfigInstrumentConfigIdPage grouping of questions
Survey Question (bank)(reusable)โ€”Reusable question definitions
Survey Instance QuestionSectionInstrumentSectionIdQuestion assignment to section with overrides
Survey AnswerSessionFlsSessionId, InstQuestionIdIndividual respondent answer to a question
Survey SummaryInstance+GroupFlsInstanceId, GroupIdPre-aggregated stats per question
QuickRating Config(standalone)ObjectTypeId, BizTransactionTypeIdRating scale config for an entity type
QuickRatingConfigQuickRatingConfigIdIndividual rating submitted
QuickRating SummaryConfig+EntityObjectTypeId, ObjectIdAggregated rating stats per entity
FLS Event LogInstanceFlsInstanceIdEvent queue for cross-module notifications
FLS Bridge Config(standalone)ModuleIdModule-level integration rules
FLS Event SubscriptionBridge ConfigBridgeConfigIdWhich events trigger which handlers
FLS Dispatch LogRespondentFlsRespondentIdEIP notification dispatch tracking
FLS Reminder RuleRegistrationFlsRegistrationIdAutomated nudge/escalation schedule

FLS Core Tables

Master Tables (MFLS*)
TableKey Columns
MFLSREGISTRATIONFlsRegistrationId, RegistrationCode, ModuleId, CompletionMethod, AllowDraftResponse, AllowMultiAttempt, IsMandatory, TokenExpiryHours, DefaultEipTemplCode
MFLSREMINDERRULEReminderRuleId, FlsRegistrationId, DayOffset, StatusFilter, ReminderAction, EipTemplCode, MaxFireCount
MFLSBRIDGECONFIGBridgeConfigId, ModuleId, ModuleCode, IsDapr, DaprPubSubTopic, EventHandlerUrl, SummarySpName, ResponseVisibility, AllowRespondentAdd
MFLSEVENTSUBSCRIPTIONEventSubscriptionId, BridgeConfigId, FlsRegistrationId (-1=all), EventType, IsActive, MaxRetries, RetryBackoffMin
MFLSACCESSRULEAccessRuleId, FlsRegistrationId, PrincipalType, PrincipalId, AccessType (0/1/2), Scope, ScopeObjectId
MFLSINSTANCEFlsInstanceId, FlsRegistrationId, InstanceCode, InstanceName, InstanceStatus, EntityObjectId, PeriodId, CreatedBy, CreatedDate
MFLSINSTANCESCHEDULEFlsInstanceId, OpensFrom, ClosesOn, GlobalDeadlineDt, JobOpenId, JobCloseId, JobRemindId
MFLSINSTANCEGROUPGroupId, FlsInstanceId, GroupCode, GroupName, ContextObjectTypeId, ContextObjectId, StepCount, BlockingFlag, BlockingSp, RespondentCount, SubmittedCount, OpensFrom, ClosesOn, DeadlineDt
MFLSRESPONDENTFlsRespondentId, FlsInstanceId, GroupId, StepNo, RespondentType, RespondentRefId, RespondentName, RespondentMail, AccessToken (GUID), TokenExpiry, RespondentSts, FirstOpenedAt, LastSavedAt, SubmittedAt, NudgeCount, OptedOutAt
MFLSINCHARGEFlsInchargeId, FlsInstanceId, UserId, AccessType
Transaction Tables (TFLS*)
TableKey Columns
TFLSRESPONSESESSIONFlsSessionId, FlsRespondentId, FlsInstanceId, GroupId, StepNo, AttemptNo, SessionStatus, StartedAt, LastSavedAt, SubmittedAt, TimeTakenSeconds

Survey Tables

Survey Tables (MSURVEY*, TSURVEY*)
TableKey Columns
MSURVEYINSTRUMENTCONFIGInstrumentConfigId, FlsRegistrationId, InstrumentTitle, InstrumentTypeId, IsAnonymous, ShowProgressBar, LangDefault, IsNpsEnabled, IsLocked, VersionNo
MSURVEYINSTRUMENTSECTIONInstrumentSectionId, InstrumentConfigId, SectionCode, SectionName, Description, DisplayOrder, IsActive
MSURVEYQUESTIONBANKQuestionId, QuestionCode, QuestionText, QuestionTypeId (0-5), DefaultScaleMin, DefaultScaleMax, IsActive
MSURVEYQUESTIONOPTIONOptionId, QuestionId, OptionText, DisplayOrder, IsDefault
MSURVEYINSTQUESTIONInstQuestionId, InstrumentSectionId, QuestionId, DisplayOrder, IsRequiredOverride, Weight, BranchRulesJson, ScaleMin, ScaleMax
TSURVEYRESPONSEANSWERAnswerId, FlsSessionId, InstQuestionId, QuestionId, AnswerText, AnswerNumeric, AnswerOptionIds (CSV), IsDraft
TSURVEYRESPONSESUMMARYSummaryId, InstrumentConfigId, FlsInstanceId, GroupId, InstQuestionId, ResponseCount, SkippedCount, AvgRating, NpsPromoters, NpsPassives, NpsDetractors, NpsScore, OptionDistJson, FreeTextCount

QuickRating Tables

QuickRating Tables (MQUICKRATING*, TQUICKRATING*)
TableKey Columns
MQUICKRATINGCONFIGQuickRatingConfigId, ObjectTypeId, BizTransactionTypeId, ConfigName, ScaleMin, ScaleMax, IdentityMode, AllowMultiRating, AllowComment, CommentMaxLength, WidgetLabel, ThankyouLabel
TQUICKRATINGQuickRatingId, QuickRatingConfigId, ObjectTypeId, ObjectId, Rating (decimal), CommentText, RatingBy (user type), RatingByRefId, IsLatest, RatedOn
TQUICKRATINGSUMMARYSummaryId, QuickRatingConfigId, ObjectTypeId, ObjectId, RatingCount, AvgRating, MinRating, MaxRating, RatingDistJson, NpsPromoters, NpsPassives, NpsDetractors, NpsScore, LastUpdated

Log Tables

Log Tables (LFLS*)
TableKey Columns
LFLSEVENTLOGFlsEventLogId, FlsInstanceId, FlsRespondentId, BridgeConfigId, EventType (0โ€“6), EventStatus (0=Pending, 1=Success, 2=Failed, 3=Retry), PayloadJson (FlsEventPayload), CorrelationId, RetryCount, NextRetryOn, FailureReason
LFLSDISPATCHLOGDispatchLogId, FlsRespondentId, DispatchType (0=Initial, 1=Reminder, 2=Escalation), DispatchedAt, DeliveryStatus, EipJobId, FailureReason

Status Enums

InstanceStatus

ValueName
0Draft
1Scheduled
2Open
3Paused
4Closed
5Archived

RespondentSts

ValueName
0NotStarted
1Opened
2InProgress
3Submitted
4Expired
5OptedOut

Instance Endpoints

POST/fls/instancesCreate new instance. Body: CreateFlsInstanceRequest (FlsRegistrationId, InstanceCode, InstanceName, Groups[], EntityObjectId). Returns FlsInstanceId.
GET/fls/instances/{flsInstanceId}Get full instance details including groups, schedule, incharges.
GET/fls/instances/{flsInstanceId}/summaryGet FlsInstanceSummaryDto: completion %, stat counts by status, group breakdowns. Used by FLS Summary Widget component. Safe to poll frequently.
POST/fls/instances/{flsInstanceId}/scheduleSet OpensFrom, ClosesOn, GlobalDeadlineDt. Transitions instance to Scheduled.
POST/fls/instances/{flsInstanceId}/openOpen instance: generates tokens, triggers EIP dispatch, fires InstanceStatusChanged event. Instance must be in Draft or Scheduled.
POST/fls/instances/{flsInstanceId}/controlBody: { action: 0=Pause, 1=Resume, 2=Close, 3=Archive }. Fires InstanceStatusChanged event.

Registration Endpoints

POST/fls/registrationsCreate registration. Body: CreateFlsRegistrationRequest. Returns FlsRegistrationId.
GET/fls/registrations/{flsRegistrationId}Get registration details including reminder rules.
POST/fls/registrations/{id}/access-rulesGrant access to a user or role. Body: SaveFlsAccessRuleRequest (PrincipalType, PrincipalId, AccessType, Scope, ScopeObjectId).
GET/fls/registrations/{id}/access-rulesList all access rules for a registration.
DELETE/fls/access-rules/{accessRuleId}Remove a specific access rule.

Respondent Endpoints

GET/fls/instances/{id}/respondentsPaginated list. Params: GroupId (optional), StatusFilter (optional, 0-5), Page, PageSize. Returns RespondentRows with token status.
POST/fls/instances/{id}/respondentsAdd respondents. Body: List<RespondentRequest> submitted as TVP. Each: RespondentName, RespondentMail, RespondentRefId, GroupId, StepNo, RespondentType.
GET/fls/respondents/pendingList of pending forms for the logged-in user (match by userId). Returns pending form links with instance names.
GET/fls/respondents/{token}Look up respondent by GUID token. Validates expiry. Used by the Angular survey shell on load.
POST/fls/respondents/{flsRespondentId}/nudgeRe-dispatch token link to respondent. Increments NudgeCount. Creates new LFLSDISPATCHLOG row.
POST/fls/respondents/{flsRespondentId}/optoutMark respondent as OptedOut. Sets RespondentSts=5, OptedOutAt=now. No further reminders sent.

Session Endpoints

POST/fls/sessions/openOpen a session using a token. Body: { token: "guid" }. Validates token; sets RespondentSts=Opened. Returns FlsSessionContextDto: sessionId, instrumentDto, resumeDraftAnswers.
POST/fls/sessions/{flsSessionId}/draftSave draft answers without submitting. Body: Answers[{InstQuestionId, AnswerText/AnswerNumeric/AnswerOptionIds}]. Sets RespondentSts=InProgress.
POST/fls/sessions/{flsSessionId}/submitFinalise submission. Validates required questions answered. Sets RespondentSts=Submitted. Fires FormSubmitted event. Returns success message.
POST/fls/sessions/{flsSessionId}/external-submitUsed by external systems (CompletionMethod=External) to acknowledge form completion via webhook.

Survey Endpoints

GET/fls/surveys/{instrumentConfigId}/instrumentFull instrument definition: sections, questions, options, branch rules. Used to render the survey UI.
GET/fls/surveys/{instrumentConfigId}/previewPreview mode (no session required). For admin instrument design preview.
POST/fls/surveys/{flsSessionId}/answers/draftSave draft answers. Body: { answers: [{instQuestionId, answerText, answerNumeric, answerOptionIds}] }. Upserts TSURVEYRESPONSEANSWER with IsDraft=1.
POST/fls/surveys/{flsSessionId}/answers/submitSubmit final answers. Validates all IsRequired questions answered. Sets IsDraft=0. Triggers summary aggregation.
GET/fls/surveys/{flsInstanceId}/resultsAggregated results. Params: GroupId (optional, -1=all). Returns SurveyResultsDto with per-question summaries from TSURVEYRESPONSESUMMARY.
GET/fls/surveys/{flsRespondentId}/responsesIndividual respondent's answers. Requires AccessType=ViewResponses. Returns per-section Q&A pairs.
GET/fls/surveys/{flsInstanceId}/answersAll answers for export (paged). Used for CSV/Excel export.

QuickRating Endpoints

GET/quickrating/widgetParams: objectTypeId, objectId, bizTypeId, submitterType, submitterId. Returns QuickRatingWidgetDto: config + currentSummary + userExistingRating (if already rated).
POST/quickrating/rateSubmit rating. Body: SubmitQuickRatingRequest (quickRatingConfigId, objectTypeId, objectId, rating, comment, submitterType, submitterId). Returns QuickRatingSubmitResultDto with updatedSummary.
GET/quickrating/ratingsPaginated list of all ratings for an entity. Params: objectTypeId, objectId, page, pageSize.
GET/quickrating/trendMonthly AvgRating trend for an entity. Returns list of {month, avgRating, ratingCount}.

Token-based Access Flow

This is the critical flow every FLS integration depends on. Understand it before building anything.

  1. POST /fls/instances/{id}/open is called. FlsInstanceBLL transitions status to Open (2) and calls FlsDispatchBLL.DispatchAsync().

  2. FlsDispatchBLL generates one AccessToken (GUID) per respondent in MFLSRESPONDENT. Sets TokenExpiry = now + TokenExpiryHours.

  3. FlsDispatchBLL writes LFLSDISPATCHLOG rows (DispatchType=0, DeliveryStatus=Pending). Publishes EIP dispatch event via Dapr.

  4. EIP picks up the job. Renders the dispatch template (DefaultEipTemplCode) with the tokenised URL embedded. Sends personalised email to each RespondentMail. Updates LFLSDISPATCHLOG.DeliveryStatus.

  5. Respondent clicks link โ†’ Angular survey shell calls GET /fls/respondents/{token} to validate. If expired โ†’ shows Expired screen. If valid โ†’ calls POST /fls/sessions/open.

  6. POST /fls/sessions/open creates TFLSRESPONSESESSION row. Sets RespondentSts=Opened (1). Returns FlsSessionContextDto containing: FlsSessionId, survey instrument (SurveyInstrumentDto), any existing draft answers.

  7. Respondent fills form. Auto-save fires POST /fls/surveys/{sessionId}/answers/draft โ†’ sets RespondentSts=InProgress (2).

  8. Respondent submits โ†’ POST /fls/surveys/{sessionId}/answers/submit โ†’ validates required answers โ†’ sets RespondentSts=Submitted (3) โ†’ triggers summary aggregation โ†’ FlsSessionBLL fires FormSubmitted event to LFLSEVENTLOG โ†’ pushes SignalR update to incharges.

Multi-step Forms

Multi-step works by creating multiple respondent records with different StepNo values for the same instance. Step N respondents cannot open their form until Step N-1 is complete (enforced by BlockingFlag + BlockingSp).

-- How FlsDispatchBLL enforces multi-step:
-- Step 2 tokens are generated ONLY after StepCompleted event fires for Step 1

IF @StepNo > 1
BEGIN
  -- Check if previous step is complete for this group
  EXEC @IsBlocked = @BlockingSp @GroupId, @StepNo - 1
  IF @IsBlocked = 1
    RAISERROR('Previous step not complete', 16, 1)
END
RespondentType in multi-step

Use RespondentType to identify who fills each step: 0=Employee (Step 1), 1=Manager (Step 2), 2=HR (Step 3). This lets the SurveyResultsDto filter context โ€” managers see Step 1 answers alongside their Step 2 form.

Bridge Config Setup

Every module that needs to receive FLS events must have a row in MFLSBRIDGECONFIG. This is a migration-time setup โ€” run it once per module integration.

-- Insert Bridge Config for a new module (e.g., CRM = ModuleId 701)
INSERT INTO MFLSBRIDGECONFIG (
  ModuleId, ModuleCode, IsDapr, DaprPubSubTopic,
  EventHandlerUrl, SummarySpName,
  ResponseVisibility, AllowRespondentAdd, AllowManualClose
) VALUES (
  701, 'CRM',
  1,              -- 1=Dapr, 0=HTTP webhook
  'crm-fls-events',   -- Dapr pub/sub topic
  NULL,             -- not used when IsDapr=1
  'SP_CRM_FLS_UPDATESUMMARY',
  0,              -- 0=all can view, 1=incharge only, 2=HR only
  1,              -- allow adding respondents after open
  1               -- allow manual close
)

Event Subscriptions

After creating a Bridge Config, subscribe to the events you need:

-- Subscribe CRM to FormSubmitted and InstanceCompleted events
-- FlsRegistrationId = -1 means: all registrations linked to this bridge config
INSERT INTO MFLSEVENTSUBSCRIPTION (BridgeConfigId, FlsRegistrationId, EventType, IsActive, MaxRetries, RetryBackoffMin)
VALUES
  (@BridgeConfigId, -1, 0, 1, 3, 5),  -- FormSubmitted: 3 retries, 5 min backoff
  (@BridgeConfigId, -1, 2, 1, 3, 5)   -- InstanceCompleted: 3 retries, 5 min backoff

To subscribe for a specific registration only (e.g., only the "CRM Satisfaction Survey" registration):

INSERT INTO MFLSEVENTSUBSCRIPTION (BridgeConfigId, FlsRegistrationId, EventType, IsActive, MaxRetries, RetryBackoffMin)
VALUES (@BridgeConfigId, 12, 0, 1, 3, 5)  -- FlsRegistrationId=12 only

Event Retry Logic

LFLSEVENTLOG.EventStatus tracks the delivery state:

0 ยท Pending
Initial
โ†’
1 ยท Success
Delivered
0 ยท Pending
Initial
โ†’
2 ยท Failed
First failure
โ†’
3 ยท Retry
Queued for retry
โ†’
1 ยท Success
Recovered
3 ยท Retry (ร—N)
โ†’
2 ยท Failed (final)
Dead letter
ColumnPurpose
RetryCountNumber of delivery attempts made so far
MaxRetriesFrom MFLSEVENTSUBSCRIPTION.MaxRetries โ€” after this many failures, EventStatus stays at 2
NextRetryOnDateTime when the retry job should attempt delivery again. Set to now + (RetryCount ร— RetryBackoffMin) minutes (exponential backoff).
FailureReasonHTTP status code + response body, or exception message. Useful for diagnosing delivery failures.

Integrating a New Module โ€” Step-by-Step Checklist

Implement Event Handler (Dapr Subscriber Pattern)

// In your module SL โ€” Dapr subscriber endpoint
public class CrmFlsEventHandler : BaseEndpoint<FlsEventPayload, ResponseStandardDTO<object>>
{
    private readonly ICrmFeedbackBLL _bll;
    public CrmFlsEventHandler(ICrmFeedbackBLL bll) => _bll = bll;

    public override void Configure()
    {
        Post("/crm/webhooks/fls");  // matches EventHandlerUrl in MFLSBRIDGECONFIG
        AllowAnonymous();       // or validate Dapr header
    }

    protected override async Task<ResponseStandardDTO<object>> ExecuteAsync(
        FlsEventPayload req, LoginDTO login, CancellationToken ct)
    {
        return req.EventType switch {
            FlsEventType.FormSubmitted    => await _bll.HandleFormSubmittedAsync(req, ct),
            FlsEventType.InstanceCompleted => await _bll.HandleInstanceCompletedAsync(req, ct),
            _ => ResponseStandardDTO<object>.Success("Unhandled event type")
        };
    }
}
// FlsEventPayload DTO (from FLSDAL/DTOs/FlsDtos.cs)
public class FlsEventPayload
{
    public int    FlsEventLogId   { get; init; }
    public int    FlsInstanceId   { get; init; }
    public int    FlsRespondentId { get; init; }
    public FlsEventType EventType    { get; init; }
    public string CorrelationId   { get; init; } = string.Empty;
    public string? PayloadJson     { get; init; }
}

SP_TMS_DISPATCHFEEDBACK โ€” Spec

This SP is the single-call interface from TMS to FLS. It creates the full instance structure in one transaction.

SP Input Parameters
ParameterTypeExampleNotes
@BatchIdINT2047TMS training batch ID
@InstrumentConfigIdINT18Survey template to use
@BlockCertificateBIT11=gate certificate on submission
@EnrollmentTvpTVP (EnrollmentType)See belowTable-valued parameter: one row per learner
-- TVP type for enrollment list
CREATE TYPE EnrollmentType AS TABLE (
  EnrollmentId INT,
  EmployeeId   INT,
  EmployeeName NVARCHAR(200),
  EmployeeEmail NVARCHAR(254)
)

-- What the SP does internally:
-- 1. INSERT MFLSINSTANCE (creates instance for this batch)
-- 2. INSERT MFLSINSTANCEGROUP (one group, BlockingFlag=@BlockCertificate)
-- 3. INSERT MFLSRESPONDENT ร— N (one per enrollment, generates GUIDs)
-- 4. SELECT @FlsInstanceId, @RespondentCount as output
// BLL call pattern (TmsFeedbackBLL.cs)
GB5Trace.Step("dispatch-feedback", new { req.BatchId });

var result = await _dal.DispatchBatchFeedbackAsync(req, login, ct)
    .ConfigureAwait(false);

// Then open the instance to trigger token dispatch
await _flsInstance.OpenInstanceAsync(result.FlsInstanceId, login, ct)
    .ConfigureAwait(false);

SP_TMS_GETBATCHFEEDBACKSTATUS โ€” Spec

Returns per-learner feedback status for a batch. Called by TmsFeedbackBLL.GetBatchStatusAsync().

Output Columns (TmsFeedbackStatusRow)
ColumnTypeNotes
EnrollmentIdINTTMS enrollment record ID
EmployeeIdINTHR employee ID
EmployeeNameNVARCHAR(200)Display name
FlsRespondentIdINTCross-reference to MFLSRESPONDENT
RespondentStsINT0โ€“5: current status
DeliveryStatusINTLFLSDISPATCHLOG delivery status
FirstOpenedAtDATETIMENULL if not opened yet
SubmittedAtDATETIMENULL if not submitted
BlocksCertificateBIT1=certificate is blocked for this learner
FeedbackCompletedBIT1=RespondentSts=3 (Submitted)

Trainer Summary Aggregation

SP_TMS_FLS_UPDATESUMMARY is called by the TMS event handler after each FormSubmitted event. It aggregates answer values for specific question IDs into trainer-level KPIs.

-- SP aggregates survey answers into trainer KPI scores.
-- InstQuestionId mapping must match the Survey Instrument Config.
UPDATE TTRAINERFEEDBACKSUMMARY
SET
  AvgKnowledge  = (SELECT AVG(CAST(AnswerNumeric AS FLOAT))
                  FROM TSURVEYRESPONSEANSWER
                  WHERE InstQuestionId = 101   -- TMS-TRAINER-KNOWLEDGE question
                  AND   FlsInstanceId  = @FlsInstanceId),
  AvgClarity    = (SELECT AVG(CAST(AnswerNumeric AS FLOAT))
                  FROM TSURVEYRESPONSEANSWER
                  WHERE InstQuestionId = 102),  -- TMS-TRAINER-CLARITY
  AvgEngagement = (SELECT AVG(CAST(AnswerNumeric AS FLOAT))
                  FROM TSURVEYRESPONSEANSWER
                  WHERE InstQuestionId = 103),  -- TMS-TRAINER-ENGAGEMENT
  OverallAvg    = (AvgKnowledge + AvgClarity + AvgEngagement) / 3.0,
  LastUpdated   = GETUTCDATE()
WHERE  TrainerId = @TrainerId
InstQuestionId Hardcoding

The SP references specific InstQuestionIds (101, 102, 103). If the Survey Instrument Config is updated (e.g., questions reorganised with new IDs), the SP must be updated too. Consider storing the mapping in a config table (MTMSQUESTIONDIMENSION) rather than hardcoding IDs in the SP.

FlsMonitorHub Contract

// IFlsMonitorClient.cs โ€” strongly-typed client contract
public interface IFlsMonitorClient
{
    Task ReceiveSummaryUpdate(FlsInstanceSummaryDto summary);
    Task ReceiveError(string message);
}

// FlsMonitorHub.cs
public class FlsMonitorHub : Hub<IFlsMonitorClient>
{
    public async Task JoinInstanceMonitor(int flsInstanceId, CancellationToken ct)
    {
        var login = LoginDTO.FromHttpContext(Context.GetHttpContext()!);
        await Groups.AddToGroupAsync(
            Context.ConnectionId,
            $"fls:instance:{flsInstanceId}", ct);
    }

    public async Task LeaveInstanceMonitor(int flsInstanceId, CancellationToken ct)
    {
        await Groups.RemoveFromGroupAsync(
            Context.ConnectionId,
            $"fls:instance:{flsInstanceId}", ct);
    }
}

Pushing Updates from BLL

// In FlsSessionBLL.SubmitFormAsync โ€” after successful submission
var updatedSummary = await _dal.GetInstanceSummaryAsync(flsInstanceId, login, ct)
    .ConfigureAwait(false);

// Push live update to all incharges watching this instance
await _hubContext.Clients
    .Group($"fls:instance:{flsInstanceId}")
    .ReceiveSummaryUpdate(updatedSummary)
    .ConfigureAwait(false);
Hub vs IHubContext

The Hub class is transient โ€” one instance per method call. For BLL-initiated pushes (outside a Hub method), always inject IHubContext<FlsMonitorHub, IFlsMonitorClient> โ€” never try to hold a Hub reference. This pattern is used correctly in all GB5 BLL classes.

Widget Inputs / Outputs

<!-- Angular component inputs -->
<app-quick-rating-widget
  [objectTypeId]="objectTypeId()"  <!-- required: entity type -->
  [objectId]="objectId()"          <!-- required: entity ID -->
  [bizTypeId]="sessionId()"        <!-- optional: further scope -->
  [submitterType]="-1"             <!-- optional: caller type -->
  [submitterId]="-1"               <!-- optional: caller ID -->
  [showSummary]="true"             <!-- show avg badge after submit -->
/>

Backend Config Lookup

-- QuickRatingQB.GET_CONFIG โ€” how the widget loads its configuration
SELECT qrc.QuickRatingConfigId, qrc.ScaleMin, qrc.ScaleMax,
       qrc.AllowComment, qrc.CommentMaxLength,
       qrc.WidgetLabel, qrc.ThankyouLabel,
       qrc.AllowMultiRating, qrc.IdentityMode,
       qrs.RatingCount, qrs.AvgRating, qrs.RatingDistJson  -- summary
FROM  MQUICKRATINGCONFIG qrc
LEFT JOIN TQUICKRATINGSUMMARY qrs
       ON qrs.QuickRatingConfigId = qrc.QuickRatingConfigId
      AND qrs.ObjectId = @ObjectId
WHERE qrc.ObjectTypeId          = @ObjectTypeId
AND   qrc.BizTransactionTypeId  = @BizTransactionTypeId
AND   qrc.ClientId              = @ClientId  -- tenant filter

Event Retry Pattern

// FlsEventBLL.PublishAsync โ€” writes to LFLSEVENTLOG
public async Task PublishAsync(int flsInstanceId, int flsRespondentId,
    FlsEventType eventType, LoginDTO login, CancellationToken ct)
{
    GB5Trace.Step("publish-fls-event", new { flsInstanceId, eventType });

    var subscriptions = await _dal.GetActiveSubscriptionsAsync(
        flsInstanceId, eventType, login, ct)
        .ConfigureAwait(false);

    foreach (var sub in subscriptions)
    {
        await _dal.InsertEventLogAsync(new FlsEventLogRow
        {
            FlsInstanceId   = flsInstanceId,
            FlsRespondentId = flsRespondentId,
            BridgeConfigId  = sub.BridgeConfigId,
            EventType       = (int)eventType,
            EventStatus     = 0,  // Pending
            PayloadJson     = JsonSerializer.Serialize(new FlsEventPayload { ... }),
            CorrelationId   = Guid.NewGuid().ToString()
        }, login, ct).ConfigureAwait(false);
    }
}

// Background Quartz job (FlsEventDispatchJob) polls LFLSEVENTLOG:
// WHERE EventStatus IN (0, 3) AND (NextRetryOn IS NULL OR NextRetryOn <= GETUTCDATE())
// Dispatches via Dapr or HTTP; updates EventStatus to 1 (success) or 2/3 (fail/retry)

Dead Letter Handling

When RetryCount >= MaxRetries and delivery still fails, the event row stays at EventStatus=2 (Failed). There is no automatic dead-letter queue โ€” failed events must be handled manually or by alerting.

Recommended approach:

Testing Checklist

GoodBooks GB5 ยท FLS Developer Guide ยท 2026-07-01