Appearance
FreshDesk Inbox Automation — Spec
Change set: FreshDeskInbox-v1 (this file is the only member so far — SQL follows as -v1-02-… etc.) Status: draft for review. Only the webhook receiver (§3.6) is built (UserStory1909); everything else is proposed. Date: 2026-08-20
FreshDesk becomes the shared inbox for insurer email. Every inbound ticket fires a FreshDesk webhook at WebAPI, which reads the ticket, matches it against a rule, and writes back to FreshDesk (priority, tags, responder, notes) and to Prime (status, case manager).
1. What the business list actually asks for
~40 rows across six task groups (Triages, Quote Approval, Quote Amendment, Quote Declined/Cash Settled, Communication, Misc) reduce to 7 atomic actions. Everything else is configuration data, so the rules belong in a table, not in 40 code paths.
| # | Action | What it does |
|---|---|---|
| A1 | Set ticket priority | Rule's priority (1/2/3) to FreshDesk priority |
| A2 | Assign existing case manager | Extract claim/client reference, find Prime job, read CaseManagerId, map to FreshDesk agent, set responder |
| A3 | Round-robin assign new case manager | Next agent from a pool, set FreshDesk responder and set the case manager on Prime |
| A4 | Change Prime job status | Two targets only (see below) |
| A5 | Add FreshDesk tag | Category tag, plus conditional make safe tag on body match |
| A6 | Assign to "system owner" | Weekly rotating owner (Christine maintains), fallback Christine |
| A7 | Auto-forward | Complaints / escalations to Laura Westelynck |
Two composites:
- Existing-or-new (A&G Make Safe, A&G Restoration, Repairline Specialist Report, Repairline Make Safe): try A2; if no Prime job matches the reference, fall back to A3 on both Prime and FreshDesk.
- Admin-team round robin (A&G Quote Only – Admin, "Ethos Building has been requested to quote – Admin"): A3 against a separate pool (admin team, not case managers).
Rule matching
sender address + subject keyword -> rule. Only four senders:
| Sender | Client |
|---|---|
noreply@mail.claims.autogeneral.com.au | A&G |
enquiries@endataclaims.com | Allianz and CHU — needs a second discriminator |
no-reply@repairline.com.au | Sedgwick |
ethos.mailer, microsoft, unmatched | Misc |
2. What we already have (verified against the codebase)
Prime API — CommonPrimeAPI
| Need | Existing | Verdict |
|---|---|---|
| A4 set job status | IPrimeJobStatusService.PatchJobStatus(jobId, newStatus[, version], callingFunction) | Ready |
| A3 set case manager on Prime | IPrimeJobService.UpdateJob(jobId, PrimeJobUpdate, …) — PrimeJobUpdateAttributes.caseManagerId exists, version is required | Ready, read-modify-write: GetJobFromPrime for version first |
| A2 read case manager | PrimeJobModel…attributes.caseManagerId | Ready |
| Job by GUID or job number | GetJobFromPrime(string jobIdentifier, …), GetJobByIdentifier | Ready |
| Job by claim / client reference | nothing — no reference search anywhere in PrimeJobService | GAP — build it (see 3.5) |
| Add a Prime note | IPrimeNotificationService.CreateInternalNote / CreateClientContactNote | Ready — useful for audit trail |
| Filter tags on a Prime job | IPrimeJobService.AddJobFilterTags / Remove / Patch | Ready if any tag must land on Prime as well as FreshDesk |
| Allocations | IPrimeAllocationService.CreateAllocation (AssignedContactId, AllocationType e.g. "Make Safe") | Available — only needed if a Make Safe row must be created, which the list does not ask for |
| Job creation | CreateJobFromPayloadAsync(...) | Available — see open question Q1 |
| HTTP plumbing, retry, 429 handling, rate-limit logging | PrimeApiClient.SendAsync / SendAllPagesAsync | Ready — reuse, do not hand-roll |
Mirror DB
primeJobsholdsClientReference,AdditionalReference,BrokerReference,StrataReference,AdjusterReference,JobNumber,CaseManagerId,StatusId,Version. So reference to job is a mirror query, not a Prime API call.primeUsers(id,fullName,email,status) —emailis the natural join key to a FreshDesk agent, so the Prime/FreshDesk mapping can be email-based with an override table for mismatches.primeStatuses(id,name,description) — status names resolvable, no hardcoding needed.ethPrimeJobReference(UserStory1274) maps Prime's numeric jobId to the job GUID. Note it is not yet in thesql-ontologyexport; confirm its shape before relying on it.- No round-robin allocator exists — nothing in the DB does this today. The closest precedent is
makesafeUser+makesafeUserSchedule(UserID,FromDate,ToDate,IsActive), which is also the right shape for the A6 weekly system-owner roster.
Webhook receiver pattern
PrimeWebhookController + ethPrimeWebhookLog + PrimeWebhookQueueService is the pattern to copy:
- endpoint authenticates, promotes a few fields from the body, stores raw, returns 200;
- a queue service does the work, with retries and an attempt cap;
- unhandled event shapes are marked "no handler" rather than left queued forever;
- route excluded from
BasicAuthMiddlewareinProgram.cswhen the sender uses its own header.
Doing FreshDesk work inline would repeat the exact failure the Prime receiver was built to avoid: a slow outbound call times out, FreshDesk retries, and the same ticket is actioned twice.
Prime status GUIDs — already known
Both target statuses are already constants in WebAPI/Services/BookingJobStatus.cs:
| Status | GUID |
|---|---|
| Initial contact made – awaiting further info | FAFF6EF9-44AF-4169-B57C-B9ED332386C8 |
| Desktop quote required | 98C6A1FC-5CAF-4074-B505-E879C36C771D |
Move these to a shared constants class rather than duplicating them.
Not present
- FreshDesk client: exists now, see the correction note at the start of §6. interface, not an HTTP client. A new
CommonFreshDesklibrary is required.
3. Proposed build
3.1 CommonFreshDesk (new library)
Modelled on CommonPrimeAPI: typed HttpClient plus a FreshDeskApiClient with the same retry / 429 / logging behaviour, service-per-concern, interfaces in Interfaces/, models in Models/.
| Service | Operations |
|---|---|
IFreshDeskTicketService | get ticket, update priority, update responder, update status, update custom fields |
IFreshDeskTagService | add / remove tags (FreshDesk replaces the whole tag array — read-modify-write) |
IFreshDeskNoteService | add private / public note |
IFreshDeskAgentService | list agents, resolve agent by email (cached) |
IFreshDeskForwardService | dropped: A7 is an email plus a private note (§6.6) |
3.2 Rule store (SQL)
ethFreshDeskRule — business-editable, so adding a keyword needs no deploy:
ID, TaskGroup, SenderAddress, SubjectKeyword, ClientName, Priority, CategoryTag, SetPrimeStatusId (null), AssignMode (Existing | RoundRobin | ExistingThenRoundRobin | SystemOwner), AgentPoolID (null), ForwardToAgentEmail (null), BodyKeyword (null), BodyKeywordTag (null), SortOrder, IsActive
BodyKeyword / BodyKeywordTag cover the one conditional in the list ("make safe required" gives a make safe tag on the Allianz Request for Service rule). SortOrder decides which rule wins when two keywords both match; first match by SortOrder wins and the decision is logged.
Built (AB#2120): ethFreshDeskRule (migration 0067) with the §6.4 match and action columns, except that the pool is a text AgentPool (CaseManagers | AdminTeam, required for the two round-robin modes) until the Admin Team pool table exists, and there is no ethFreshDeskPriority table (SLA text lives in FreshDesk). ClientContactId is the client filter; ClientName is only a label, so a seeded row with a name but no contact applies to any client. Seeded from the business spreadsheet (49 rules: each "Allianz & CHU" sheet row is one rule per client). A&G rules are linked to Auto & General Holdings Pty Ltd. Allianz rules are linked to Allianz Australia Insurance Limited and CHU rules to CHU Underwriting Agencies Pty Ltd, and Sedgwick rules to the Prime contact "Sedgwick - Allianz". "Job not found (Prime)" is two rules, one per Prime sender address (ethos.mailer@primeeco.tech, ethos.job@primeeco.tech). Managed in Admin Portal > Settings > FreshDesk Rules (roles Admin and ClaimsTeamManager): list, search, add, edit, delete, enable, and a Preview dialog (paste sender, subject, body and optionally the job's client to see the winning rule and the values it sets; matching lives in FreshDeskRuleMatcher, which the engine will reuse). The body keyword only adds BodyKeywordTag; it is not a match condition. Procs spEthFreshDeskRuleList / Upsert / Delete. Nothing reads the table yet; matching, the Preview and Trace dialogs and the other tabs in §6.4 are still to build with the engine.
3.3 Round-robin allocator (SQL)
Built (AB#2116) for the Case Managers pool. The roster is ethCaseManager (PrimeUserID PK, IsEnabled, LastAssignedAt) plus ethCaseManagerLeave (many rows per person: FromDate, ToDate, Reason; annual or sick leave, inclusive), added by hand in Admin Portal > Settings > Case Managers (roles Admin and ClaimsTeamManager). Candidates are Active Prime users who are primeJobs.CaseManagerId on at least one job. There is no separate pointer table: the last case manager selected is the row with the latest LastAssignedAt.
spEthCaseManagerGetNext returns the next available case manager after that one in name order, wrapping to the top, and stamps LastAssignedAt, under UPDLOCK, HOLDLOCK so two tickets cannot get the same person. Available = enabled, Active in Prime, and not on leave today (Sydney). The portal hides past leave unless asked. No rows when nobody is available. Disabled people are skipped without losing the rotation's place; removing the most recently picked person restarts from the top, so pause by disabling instead.
Built (AB#2117) for the A6 system owner. Same shape: ethClaimsTeamOwner (PrimeUserID, IsEnabled, Priority default 1, LastAssignedAt) plus ethClaimsTeamOwnerLeave (many date periods per owner, with a reason), managed in Admin Portal > Settings > Claims Team Owners (roles Admin and ClaimsTeamManager). Any Active Prime user can be added. spEthClaimsTeamOwnerGetNext ("Next Team Owner") considers only available owners (enabled, Active in Prime, not on leave today), takes the lowest priority number present, and round-robins by name ASC within it; a lone owner in that tier is always picked. No rows when nobody is available, so the caller falls back to Christine. Only this one allocation mode exists; add a mode switch when a second is defined. Calling it from A6 is still to build (IClaimsTeamOwnerData.GetNext()).
Still to build with the rule engine: the Admin Team pool, the Prime user to FreshDesk agent mapping, and calling ICaseManagerData.GetNext() from A3 (set the Prime case manager, then the FreshDesk responder). The original generic pool design below is superseded for Case Managers and is the starting point for the Admin Team pool:
ethFreshDeskAgentPool—ID, PoolName(Case Managers,Admin Team)ethFreshDeskAgentPoolMember—PoolID, PrimeUserID, FreshDeskAgentID, IsActive, SortOrderethFreshDeskAgentPoolPointer—PoolID, LastAssignedMemberID, LastAssignedAtspEthFreshDeskGetNextPoolMember @PoolID— advances the pointer and returns the member in one atomic statement (UPDATE … OUTPUTunderUPDLOCK), so two concurrent tickets cannot get the same agent. SkipsIsActive = 0.
3.4 System-owner roster (SQL)
ethFreshDeskSystemOwner — ID, FreshDeskAgentID, FromDate, ToDate, IsActive, same shape as makesafeUserSchedule. spEthFreshDeskGetSystemOwner @AtDate returns the current owner or falls back to a configured default (Christine). Maintained from an AdminPortal page.
Decided (Q14): a reference table plus an editable roster. Admins add, change and end-date weekly rows on the Admin Portal's System owner tab (§6.4); the owner is picked from synced primeUsers, mapped to a FreshDesk agent by email. Overlapping date ranges are rejected on save. The fallback default (Christine) is a row flagged as default, not a config value, so it is editable too.
3.5 Reference lookup (SQL)
spPrimeFindJobByReference @Reference — matches ClientReference first, then AdditionalReference / BrokerReference / StrataReference / AdjusterReference / JobNumber, returning JobId, JobNumber, StatusId, CaseManagerId, MatchedOn and flagging multiple matches rather than silently picking one. Ambiguity routes to the system owner with a note; it does not guess.
Reference extraction from subject/body is per-client (A&G, Endata and Repairline all format differently) and lives in code as one regex per client, unit-tested against real sample emails.
3.6 WebAPI — receiver plus queue
Built (UserStory1909) — the receiver and its log, nothing that acts on the rows:
FreshDeskWebhookController(POST api/freshdesk/webhook?event=created|updated): checks theAPI-Keyheader againstFreshDesk:WebhookApiKey(blank config or a mismatch is 401; the route is excluded fromBasicAuthMiddlewareinProgram.cs), readsticket_idfrom the body (insidefreshdesk_webhookfor Simple content), stores the raw body viaspFreshDeskInsertWebhookLog, returns 200. A failed insert returns 500 so FreshDesk retries. TheAPI-Keyheader is stored as(redacted).ethFreshDeskWebhookLog(migration0052):id, receivedUTC, eventType, ticketId, payload, headers, remoteIP, processedUTC, processAttempts, processStatus— theethPrimeWebhookLogshape. Theprocess*columns are unused until the queue service below exists.FreshDesk setup (Admin › Automations), two rules, each with a Trigger webhook action: POST, JSON encoding, Simple content, custom header
{"API-Key": "<FreshDesk:WebhookApiKey>"}.- Ticket Creation rule →
https://<webapi>/api/freshdesk/webhook?event=created - Ticket Updates rule, on: status changed, priority changed, agent or group changed, note added, reply sent →
…?event=updated
The event is on the URL because Simple content cannot add a field of our own to the body.
- Ticket Creation rule →
Also built: the same call can be received by WebhookReceiver (api/freshdesk/webhook), which queues it on Service Bus instead of writing the table — see WebhookReceiver-Spec.md. The WebAPI receiver above stays as a duplicate until FreshDesk is repointed and a queue consumer feeds ethFreshDeskWebhookLog.
Prime job note (AB#2033): for each created ticket on the Claims instance (never updated, never IT; AB#2063), FreshDeskJobNoteService fetches the ticket from the FreshDesk API, CommonFreshDesk.JobReferenceExtractor pulls candidate references from the subject and body (sender profiles, noise list and ranking per the quotes-mailbox reference spec; profiles live in code), and spPrimeFindJobsByReference matches them against primeJobs (JobNumber, ClientReference, AdditionalReference, AdjusterReference, BrokerReference, StrataReference). The first candidate that resolves to exactly one job wins, open jobs before closed; several jobs, or several J numbers in the subject, are listed with none chosen. A match adds a private note: job number, status, address, Prime link, and an AI Job Summary link (/view/jobsummary/{jobId}, any signed-in user) when a summary exists. No match, noise, or no candidates adds nothing. Settings: FreshDesk:Claims:Domain, FreshDesk:Claims:ApiKey, FreshDesk:AdminPortalBaseUrl, and FreshDesk:JobNoteEnabled (default false — it writes to live tickets). The step runs in FreshDeskWebhookConsumer after the log insert; a failure is logged, not retried (a retry would insert the row again), and a FreshDesk retry of the same created event can add a second note.
Still proposed:
- Promote sender / subject and add
MatchedRuleID,ActionsAppliedcolumns when rules exist. FreshDeskWebhookQueueService: match rule, execute its ordered action list, writeActionsApplied(JSON) for audit, mark processed. Retries with an attempt cap; a ticket that fails every attempt is assigned to the system owner with a note saying automation failed, so nothing goes silently unworked.
Idempotency: key on TicketID + MatchedRuleID. A FreshDesk retry must not re-run a round robin and burn a second agent.
3.7 AdminPortal
One page: rule list (CRUD), agent pools (membership plus active toggle), system-owner roster, and a webhook log viewer with replay — mirroring PrimeWebhookLogController.
4. Action to capability matrix
| Action | Prime side | FreshDesk side | Build needed |
|---|---|---|---|
| A1 priority | — | update ticket | CommonFreshDesk |
| A2 existing CM | spPrimeFindJobByReference (new) plus caseManagerId from mirror/Prime | resolve agent by email, set responder | proc, reference regexes, agent map |
| A3 round robin | UpdateJob with caseManagerId (ready) | set responder | allocator proc, pool tables |
| A4 status | PatchJobStatus (ready) | — | status constants only |
| A5 tags | AddJobFilterTags if needed (ready) | add tag | CommonFreshDesk |
| A6 system owner | — | set responder | roster proc |
| A7 forward | — | private note on ticket; email sent via the existing email queue | CommonFreshDesk (note) |
Net: the Prime side is almost entirely in place. The new work is the FreshDesk client, the rule / pool / roster tables, the reference lookup proc, the reference regexes, and the receiver plus queue.
5. Open questions
- Job creation. The Make Safe / Restoration rules imply the Prime job may not exist yet. Do we create it via
CreateJobFromPayloadAsync, or only allocate against an existing job and route the rest to the system owner? Creating jobs from email has a much bigger blast radius — recommend not in v1. - Round-robin scope. One shared pointer, or one per client / per task group? The spec above assumes one pointer per pool.
- Allianz vs CHU share
enquiries@endataclaims.com— answered (AB#2101): they are clients; the reference / job number resolves the Prime job, and the job's client decides (6.4). - "Tag with specified priority" — the FreshDesk priority field, a literal tag, or both? The spec assumes both (priority field plus category tag).
- Prime case manager field.
caseManagerIdonPrimeJobUpdateis assumed correct for "assign case manager on Prime". Confirm against a real job rather thanassignedIdorsupervisorId. - Fairness vs load. Round robin is positional, not workload-aware. Is strict rotation acceptable, or should it skip agents over a job count / on leave?
- Quarantine row has no priority and IT is removing it from the inbox — answered (AB#2120): keep it, the emails arrive regardless. Seeded with the lowest priority (3) and assigned to the system owner.
- Reference ambiguity. Proposed behaviour on multiple Prime matches is to route to the system owner. Please confirm that is what the business wants.
(Questions 9 to 15 are added by §6 below.)
6. Requirements analysis of "Inbox Tasks.xlsx" (AB#2101)
Status: proposed, for review. Analysis only: nothing here is built beyond §3.6 "Built". Source: the spreadsheet attached to AB#2101 (6 task groups, about 36 rows, plus a priority/SLA table). It confirms §1's seven actions and adds what §1 did not cover: the priority SLA table, the Allianz Works to Commence rule, and the Misc "system" rows (voicemail, job not found, quarantine, complaints).
Correction to §2: "No FreshDesk client at all" is out of date.
CommonFreshDeskexists (FreshDeskClient: get ticket, latest conversation, add private note, set subject, retry/429, Claims and IT tenants). It still lacks: update priority / agent / type / tags / custom fields, agent lookup, forward.
6.1 Prime data lookups
| # | Lookup | Used by | Status |
|---|---|---|---|
| L1 | Reference to job: JobNumber, Client / Additional / Adjuster / Broker / Strata reference | A2 and the existing-or-new rows | Built: spPrimeFindJobsByReference (open before closed) |
| L2 | The job's current case manager (primeJobs.CaseManagerId to primeUsers.email / fullName) | every "existing case manager" row (about 20 of 36) | Gap: L1 does not return it. Add CaseManagerId, CaseManagerEmail, StatusId, and the job's client (ClientId to primeContacts.name, Q3) in a -v2 of the same proc |
| L3 | Prime user to FreshDesk agent | assign agent | Gap: join on primeUsers.email = agent email, with an override column for mismatches |
| L4 | Pool members: active Prime users with a given role | round robin | Not derivable (Q10, answered): the synced Prime user data cannot give the required list, so pool membership is a reference table (6.5). Prime is still used to resolve each member's name and email |
| L5 | Open-job count per case manager | pool display, least-jobs option | Derivable: primeJobs.CaseManagerId + primeStatuses.statusType = 'Open' |
| L6 | Status ids | A4 | Both already constants in BookingJobStatus.cs (Initial contact made, Desktop quote required) |
Writes to Prime (all ready): PatchJobStatus (A4), UpdateJob with caseManagerId (A3; GET first for version), CreateInternalNote (audit note of what the automation did). Reads come from the mirror (primeJobs); only a write needs a live GET.
6.2 Fields to add to FreshDesk (Claims instance)
| Field | Type | Filled by |
|---|---|---|
| Prime Job Number | single-line text | the rule engine, on a unique match |
| Prime Job ID | single-line text | same; lets later webhooks skip the lookup |
| Client Reference | single-line text | JobReferenceExtractor |
| Client | dropdown: A&G, Allianz, CHU, Sedgwick, Other | the matched Prime job's client (primeJobs.ClientId), else the sender's client from the rule (A&G, Sedgwick); blank when unknown |
| Automation Result | dropdown: Matched existing job, New claim, Ambiguous, Not found, CM not in roster, Failed | the engine, so a ticket view can list "needs a human" |
Standard fields used: Priority, Type, Agent, Tags. The Claims instance is new and uses FreshDesk's standard priorities (Low, Medium, High, Urgent), so the mapping is P1 = Urgent, P2 = High, P3 = Medium; Low is left unused and is the default for tickets no rule matches (Q9, answered). The helpdesk is on the Growth plan, which allows basic custom ticket fields (Q11, answered), so the five fields above (text and dropdown only) are created by hand in Admin > Ticket Fields before the engine is switched on.
6.3 Ticket types and SLA
Type is single-select, so it holds the spreadsheet's task group; finer detail goes in tags:
| Ticket Type | Spreadsheet group | Tags set by rule (examples) |
|---|---|---|
| Claim Triage | Task: Triages | ag-assess-repair, ag-make-safe, repairline-quote-request, make-safe (conditional) |
| Quote Approval | Quote Approval | approved, variation-approved |
| Quote Amendment | Quote Amendment | amendment, resubmission |
| Quote Declined / Cash Settled | Quote Declined/Cash Settled | declined, cash-settled, cancelled, finalised |
| Communication | Communication Tasks | new-communication, action-required |
| System / Admin | Misc | voicemail, job-not-found, quarantine, complaint |
A per-client tag (ag, allianz, chu, sedgwick) is always added with the category tag.
Priority SLA (right-hand table of the sheet, business hours, so FreshDesk's business-hours calendar must be set):
| Priority | First response | Resolution |
|---|---|---|
| 1 | 1 business hour | 24 hours |
| 2 | 4 business hours | 48 business hours |
| 3 | 24 business hours | 72 business hours |
The sheet's "Next Action Target" column is out of scope (Q12, answered) and is not modelled: FreshDesk has no native field for it. First response and resolution map onto FreshDesk SLA policies, which are configured in FreshDesk keyed on Priority, so the engine only sets Priority and does not compute due dates.
6.4 Editable rule table (AdminPortal)
Keep the §3.2 table and extend it. One rule is when plus then:
ethFreshDeskRule: RuleID, TaskGroup, SenderAddress (null = any), SubjectKeyword, BodyKeyword (null), ClientContactId (null = any client; soft ref to primeContacts.id), Priority, TicketType, CategoryTag, BodyKeywordTag, SetPrimeStatusId (null), AssignMode (Existing | RoundRobin | ExistingThenRoundRobin | SystemOwner), PoolID (null), ForwardToEmail (null), SortOrder, IsEnabled, EffectiveFrom/To, Notes, audit columns. ethFreshDeskPriority holds the three priority rows (FreshDesk priority id plus the SLA text above, for display).
Matching: sender equals (or null), subject contains the keyword (case-insensitive), optional body keyword. Among matches the longest keyword wins, then lowest SortOrder. XXXX in the sheet (Reference Number XXXX has been finalized) means "any reference": store Reference Number % with % as the only wildcard, never regex, so business users can edit rules safely. No match falls to a fallback rule (system owner, no priority) and an "unmatched" log row, never silence.
Client discriminator (Q3 answered). Allianz and CHU are clients on the Prime job, so the engine does not read them from the email. After the reference lookup (step 2 in 6.6) the matched job's ClientId is compared with the rule's ClientContactId: rules for another client drop out, rules with a null client stay. This is how the two Request for Service rows (CHU: assign new case manager; Allianz: status change plus make safe body tag) are told apart. If no job resolves, the client is unknown, client-specific rules cannot match, and the ticket is handled as an unresolved ticket (Q17 answered, 6.6).
AdminPortal page freshdeskrules, built like ClientWIPSettings: tabs Rules, Agent pools, System owner, Priority & SLA, Webhook log; the ClientWIPRuleEdit dialog shape for editing; a Preview dialog (paste sender, subject and body to see the rule that would win and the actions it would take, like ClientWIPRulePreview); a Trace dialog per ticket reading ActionsApplied (like ClientWIPTrace). Admin-role gated, every save audited.
6.5 Case manager selection
What can be derived from data, and what cannot
| Need | Derivable? |
|---|---|
| Who is eligible (case managers, admin team) | No (Q10, answered): not enough is synced from Prime to build the list (primeUsers.roles is one value per user), so membership is a reference table maintained in the Admin Portal |
| Who is on leave / skipped | No: needs an IsActive override per member |
| Who is next in a rotation | No: a pointer is state, so round robin needs ethFreshDeskAgentPoolPointer regardless |
| Who has the fewest open jobs | Yes, stateless: count primeJobs per CaseManagerId where primeStatuses.statusType = 'Open' |
| Prime user to FreshDesk agent | Yes, by email, with an override column |
Round robin vs least open jobs
| Round robin | Least open jobs | |
|---|---|---|
| State | pointer table | none |
| Fairness | equal new assignments, ignores existing load | equal load |
| Weakness | an agent already swamped still gets the next job | restoration jobs stay open for months, so the count shows history, not capacity; a new starter or someone back from leave is flooded until they catch up |
| Matches the sheet | yes: every row says "round robin basis" | no |
Recommendation: build round robin as the sheet states. Pool membership is the reference table ethFreshDeskAgentPoolMember (PoolID, PrimeUserID, FreshDeskAgentID, IsActive, SortOrder; the user is picked from synced primeUsers, which supplies name and email), with IsActive doubling as the on-leave switch, plus the pointer. Store Strategy (RoundRobin | LeastOpenJobs) on the pool and show each member's open-job count on the pool tab, so the business can see whether rotation skews load and flip the setting without a deploy. spEthFreshDeskGetNextPoolMember advances the pointer atomically (UPDATE ... OUTPUT under UPDLOCK); for LeastOpenJobs it takes the lowest count, ties broken by LastAssignedAt. Do not combine the two in v1.
Two pools: Case Managers (new claim, and the existing-or-new fall-through) and Admin team (A&G Quote Only - Admin, Ethos Building has been requested to quote - Admin).
6.6 FreshDesk Claims webhook: processing design
The receiver, log and Prime job note step are built (§3.6). The rule engine slots in behind them:
FreshDesk "Ticket Creation" automation -> WebhookReceiver / WebAPI -> ethFreshDeskWebhookLog
-> consumer (Claims tenant, event=created only)
1. fetch ticket (FreshDeskClient.GetTicketAsync)
2. extract references + spPrimeFindJobsByReference <- the AB#2033 step, reused not repeated
3. match rule (sender + subject + body) -> MatchedRuleID
4. run the rule's actions in order, appending each result to ActionsApplied (JSON)
5. one FreshDesk update (priority, type, tags, agent, custom fields) + private note
6. mark processedcreatedonly. The engine's own writes fireupdated; acting on it would loop.updatedstays logged and ignored.- Idempotency on
TicketID + MatchedRuleID: a FreshDesk retry must not burn a second round-robin turn. The pointer advance and theActionsAppliedwrite share one transaction. - Assignment order: pick agent, set the Prime case manager (GET then PUT), then set the FreshDesk agent. If Prime fails FreshDesk is untouched, so the two never disagree; a retry resumes at the failed step from
ActionsApplied. - No Prime job reference is a blocker (Q17 answered). An email may carry no job reference because it is unrelated to a job, or because it asks Ethos to create one. For now both are treated the same: no resolved Prime job blocks every action that needs a job, i.e. existing case manager, client-specific rules, Prime status change, and round robin / setting the Prime case manager (including the
ExistingThenRoundRobinfall-through and the "new claim" wording in the sheet's A&G Make Safe / Restoration and Repairline rows). What still runs, because it needs only the sender and subject: priority, ticket Type, category and client tags (client from the sender only where the sender implies it, A&G or Sedgwick), the MiscSystemOwnerrows and the A7 email. The ticket is then assigned to the current system owner, Automation Result isNot found, and a private note says which actions were blocked and why, so a person finishes it. The log row is marked blocked (not failed) and keeps the matched rule, so it can be re-evaluated. Several matches behave the same way withAmbiguous, never a guess. - Future (not built): richer lookups (address, policy, attachment text) and delayed actions (retry the lookup after the job is created, or create the job) would re-run the blocked tickets through the same engine. v1 only has to keep the blocked state queryable: do not discard the rule match or the extracted references.
- "Tag the case manager already assigned" (Q15 answered) means assign the FreshDesk ticket to the case manager currently set on the Prime job, nothing more (no Prime note). The engine reads
primeJobs.CaseManagerId, finds that user in the Case Managers roster (ethFreshDeskAgentPoolMember) and sets the mapped FreshDesk agent. - Case manager escalation. If the Prime case manager is blank, is not on the roster, or maps to a FreshDesk agent that no longer exists or is deactivated, the engine does not guess or round-robin (the job already has an owner in Prime). It assigns the ticket to the current system owner, sets Automation Result to
CM not in roster, adds a private note naming the Prime case manager and the reason, and queues the same style of email as the A7 forward to the system owner, so someone fixes the roster or the Prime assignment. The ticket still gets its priority and tags, so SLA clocks run. Proposed, not yet confirmed by the business: a roster member flagged on leave (IsActive = 0) still receives tickets for jobs they already own;IsActiveonly removes them from round robin. - Failure: attempt cap, then system owner plus a note "automation failed", so nothing sits unworked.
- Safety switches:
FreshDesk:RulesEnabled(default false) andFreshDesk:RulesDryRun(default true: log what it would do, write nothing), the same pattern asITProvisioning. - Forward (A7), Q13 answered: "forward" means email the person and record it on the ticket, not FreshDesk's forward feature. The engine queues an HTML notification email to the rule's
ForwardToEmail(Laura Westelynck for Complaints / Escalations) with the ticket subject, sender, a snippet and a FreshDesk ticket link, using the PDP-style email template andIEmailMessageData.InsertHtmlMessage, then adds a private note to the ticket ("Forwarded to <name> on <date>"). That note is the history entry. No FreshDesk forward API is needed, so noIFreshDeskForwardService. The ticket is also assigned to the system owner as before.
6.7 Spreadsheet rows to behaviour
| Group | Rows | Behaviour | Priority |
|---|---|---|---|
| Triages | 12 | 4 Existing / ExistingThenRoundRobin; 2 Admin pool + status Desktop quote required; 4 status Initial contact made + next case manager; 2 plain round robin | 1 or 2 |
| Quote Approval | 5 | Existing | 2 |
| Quote Amendment | 5 | Existing | 1 |
| Declined / Cash Settled | 7 | Existing | 3 |
| Communication | 4 | Existing | 2 |
| Misc | 5 | SystemOwner (plus forward for Complaints); Works to Commence is Existing | 1 to 3 |
6.8 New open questions
Priority mapping — answered: new instance with the standard Low / Medium / High / Urgent priorities; P1 = Urgent, P2 = High, P3 = Medium (6.2).
Roles — answered: not enough is synced from Prime to derive the pools, so membership is a reference table (6.5).
Custom fields / Type list — answered: Growth plan, basic custom fields can be added (6.2).
"Next Action Target" SLA — answered: ignored; only first response and resolution (6.3).
Forward — answered: email the person plus a private history note on the ticket (6.6).
System owner roster — answered: reference table plus an editable roster page (§3.4).
"Tag the case manager already assigned" — answered: assign the FreshDesk ticket to the Prime case manager; if they are not on the roster, escalate to the system owner (6.6).
Case manager escalation: who receives it, and what happens when the case manager is on leave.TO BE DECIDED — discuss with the request owner. Two parts, both about tickets for a job that already has a case manager in Prime (the "existing case manager" rows, about 20 of 36).
16a. Who handles a ticket when the Prime case manager cannot be assigned? This happens when the Prime case manager is blank, is not on the FreshDesk Case Managers roster, or maps to a FreshDesk agent that is missing or deactivated (6.6). Options:
- (i) The current system owner (the weekly rotating owner, §3.4). Proposed default: the role already exists for "nobody else owns this", and it is the fallback for every other failure.
- (ii) A fixed escalation person or group (e.g. a team lead), independent of the weekly rota.
- (iii) The Admin team round-robin pool.
16b. Should a roster member who is on leave (
IsActive = 0) still be assigned tickets for jobs they already own?IsActiveis the on-leave switch and removes someone from round robin for new jobs; this question is about existing jobs. Options:- (i) Yes, assign to them anyway. Proposed default: the job is theirs, the ticket sits in their queue, and the SLA clock runs while they are away, so a breach is possible.
- (ii) No, treat on leave like "not on roster" and escalate per 16a. Protects SLAs but moves work away from the person who knows the job.
- (iii) Assign to them and notify the 16a recipient so cover can be arranged.
Needed from the owner: the choice for 16a and 16b, and who owns keeping the roster and on-leave flags current. Impact of deciding late: none on the design; the engine reads an escalation recipient and an on-leave rule from data. The proposed defaults (16a-i, 16b-i) are what would be built if no decision is made, and the build is dry-run first (6.6), so nothing reaches live tickets before this is settled.
Emails with no Prime job reference — answered: a missing job reference blocks the job-dependent actions and the ticket goes to the system owner; richer lookups and delayed actions are future work (6.6). Consequence to confirm with the business: the sheet's "if no claim exists, assign a new case manager" rows do not auto-assign in v1.

