Table of Contents

Interface ISupportCaseService

Namespace
Tharga.Team.Support.Cases
Assembly
Tharga.Team.dll

Support-case operations. The surface a host, a component or a channel adapter calls.

public interface ISupportCaseService

Remarks

Operations, not CRUD. There is no UpdateCase: raising, replying and closing are three distinct facts, each separately authorizable and separately auditable. A general update would be none of those, because its legitimacy would depend on which of the three it was really doing.

Every method names its team. A case id alone never identifies a case — the store requires the team too — so a caller cannot reach another tenant's case by holding an id, and the authorization decorator has a team to check against on every call.

Authorization is applied by the decorator over this interface and nowhere else. Implementations of this interface enforce nothing, and a component that renders a button still has its call checked.

Methods

AnswerCaseAsync(string, string, string, IReadOnlyList<SupportAttachmentUpload>, CancellationToken)

Answers a case from the support side with files, authorized exactly as an answer without files.

Task AnswerCaseAsync(string teamKey, string caseId, string body, IReadOnlyList<SupportAttachmentUpload> attachments, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
body string
attachments IReadOnlyList<SupportAttachmentUpload>
cancellationToken CancellationToken

Returns

Task

AnswerCaseAsync(string, string, string, string, IReadOnlyList<SupportAttachmentUpload>, CancellationToken)

Answers a case from the support side with formatted text and any files, authorized as a plain answer and sanitized and derived as the formatted reply is.

Task AnswerCaseAsync(string teamKey, string caseId, string body, string bodyHtml, IReadOnlyList<SupportAttachmentUpload> attachments, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
body string
bodyHtml string
attachments IReadOnlyList<SupportAttachmentUpload>
cancellationToken CancellationToken

Returns

Task

AnswerCaseAsync(string, string, string, CancellationToken)

Answers a case from the support side. The answer counts as support's whoever writes it, so it takes the case out of the waiting count and is unread for the person who raised it until they open it.

Task AnswerCaseAsync(string teamKey, string caseId, string body, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
body string
cancellationToken CancellationToken

Returns

Task

Remarks

Needs a grant to answer on behalf of support — AllManage, support:manage on the case's team, or for a case with no team Manage. Having raised the case confers nothing here.

AssignCaseAsync(string, string, CancellationToken)

Gives an unassigned case to a team.

Task<bool> AssignCaseAsync(string caseId, string teamKey, CancellationToken cancellationToken = default)

Parameters

caseId string
teamKey string
cancellationToken CancellationToken

Returns

Task<bool>

Remarks

The only way a case gains a team after the fact, and an operation rather than a property set because it is one authorizable, auditable fact: it decides which tenant a case and its entire transcript become part of.

Authorized by Manage, not by a scope on the receiving team. A member of one team must not be able to pull a case that may concern another into their own.

Only an unassigned case can be assigned. Moving a case between tenants moves its transcript with it, so it is refused rather than treated as a correction — if it is genuinely wrong, that is a decision somebody should have to make deliberately, and there is no evidence yet that anybody needs to.

CloseCaseAsync(string, string, CancellationToken)

Closes a case and records who closed it in its transcript.

Task CloseCaseAsync(string teamKey, string caseId, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
cancellationToken CancellationToken

Returns

Task

GetAnswerRouteAsync(string, string, CancellationToken)

Where support's answer to the case goes — the address it is mailed to and the mailbox it is sent from, and what this environment's outbound mail policy does with it — or that it is shown on the site; null when the team has no such case.

Task<SupportAnswerRoute> GetAnswerRouteAsync(string teamKey, string caseId, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
cancellationToken CancellationToken

Returns

Task<SupportAnswerRoute>

Remarks

Authorized exactly as AnswerCaseAsync(string, string, string, CancellationToken) is: the address is shown to whoever may answer.

GetAssistantReadinessAsync(string, CancellationToken)

Whether the assistant can answer for this team, and if not, why.

Task<SupportAssistantReadiness> GetAssistantReadinessAsync(string teamKey, CancellationToken cancellationToken = default)

Parameters

teamKey string
cancellationToken CancellationToken

Returns

Task<SupportAssistantReadiness>

Remarks

Ask this before offering the assistant. A deployment can give each team its own model or none, so whether there is an assistant is a question about a team, not about the host. Raising a case that asks for the assistant applies the same answer: a team that is not ready gets a person.

Authorized by membership, as raising a case is. A responder that cannot tell, because its settings could not be read, reads as NotConfigured rather than an error.

GetAttachmentLimitsAsync(CancellationToken)

How many files, and how large, a reply or answer may carry, so a component offers what the service accepts.

Task<SupportAttachmentLimits> GetAttachmentLimitsAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Returns

Task<SupportAttachmentLimits>

GetAwaitingSupportCountAcrossTeamsAsync(CancellationToken)

How many open cases, in every team, are waiting on an answer. What a support-staff indicator shows.

Task<int> GetAwaitingSupportCountAcrossTeamsAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Returns

Task<int>

Remarks

Authorized by AllRead or AllManage, the grant that lists the same cases. Leaves out the unassigned queue, which GetAwaitingUnassignedCountAsync(CancellationToken) counts under its own grant.

GetAwaitingSupportCountAsync(string, CancellationToken)

How many open cases in the team are waiting on an answer — their newest entry came from the person who raised them. What a support-side indicator shows.

Task<int> GetAwaitingSupportCountAsync(string teamKey, CancellationToken cancellationToken = default)

Parameters

teamKey string
cancellationToken CancellationToken

Returns

Task<int>

Remarks

Counts across everybody's cases, so it is exactly as privileged as reading them and requires support:read.

GetAwaitingUnassignedCountAsync(CancellationToken)

How many open cases that belong to no team are waiting on an answer.

Task<int> GetAwaitingUnassignedCountAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Returns

Task<int>

Remarks

Authorized by Read or Manage; the cross-team grant confers nothing here, as it confers nothing over the unassigned queue itself.

GetCaseAsync(string, string, CancellationToken)

One case, or null when the team has no such case.

Task<SupportCase> GetCaseAsync(string teamKey, string caseId, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
cancellationToken CancellationToken

Returns

Task<SupportCase>

GetCasesAcrossTeamsAsync(string, int, CancellationToken)

Cases across every team, for the product's own support staff.

Task<SupportCasePage> GetCasesAcrossTeamsAsync(string cursor = null, int pageSize = 20, CancellationToken cancellationToken = default)

Parameters

cursor string
pageSize int
cancellationToken CancellationToken

Returns

Task<SupportCasePage>

Remarks

Authorized by AllRead. The companion to that grant: without a listing, a holder can answer a case only once somebody hands them its id, which is not a queue.

Does not include the unassigned queue — that is GetUnassignedCasesAsync(string, int, CancellationToken) and Read. Holding one must never confer the other.

GetCasesAsync(string, string, int, CancellationToken)

Every case in a team, newest first.

Task<SupportCasePage> GetCasesAsync(string teamKey, string cursor = null, int pageSize = 20, CancellationToken cancellationToken = default)

Parameters

teamKey string
cursor string
pageSize int
cancellationToken CancellationToken

Returns

Task<SupportCasePage>

GetMessagesAsync(string, string, string, int, CancellationToken)

A case's transcript, oldest first.

Task<SupportMessagePage> GetMessagesAsync(string teamKey, string caseId, string cursor = null, int pageSize = 50, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
cursor string
pageSize int
cancellationToken CancellationToken

Returns

Task<SupportMessagePage>

GetMyAnsweredCountAsync(string, CancellationToken)

How many of the caller's own open cases support answered last — read or not, they wait for the caller to reply or to close them.

Task<int> GetMyAnsweredCountAsync(string teamKey, CancellationToken cancellationToken = default)

Parameters

teamKey string
cancellationToken CancellationToken

Returns

Task<int>

GetMyCasesAsync(string, string, int, CancellationToken)

The caller's own cases in a team, newest first.

Task<SupportCasePage> GetMyCasesAsync(string teamKey, string cursor = null, int pageSize = 20, CancellationToken cancellationToken = default)

Parameters

teamKey string
cursor string
pageSize int
cancellationToken CancellationToken

Returns

Task<SupportCasePage>

GetMyOpenCountAsync(string, CancellationToken)

How many of the caller's own cases are open, conversations with the assistant aside.

Task<int> GetMyOpenCountAsync(string teamKey, CancellationToken cancellationToken = default)

Parameters

teamKey string
cancellationToken CancellationToken

Returns

Task<int>

GetMyStaffStatusAsync(CancellationToken)

The caller's status on the support desk, or null when they have set none since the application started.

Task<SupportStaffStatus?> GetMyStaffStatusAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Returns

Task<SupportStaffStatus?>

Remarks

GetMyUnreadByCaseAsync(string, CancellationToken)

The caller's own cases that hold entries they have not read, each with how many — so a list can mark where to look rather than only that something is waiting.

Task<SupportCaseUnread[]> GetMyUnreadByCaseAsync(string teamKey, CancellationToken cancellationToken = default)

Parameters

teamKey string
cancellationToken CancellationToken

Returns

Task<SupportCaseUnread[]>

GetMyUnreadCountAsync(string, CancellationToken)

How many of the caller's own cases hold entries they have not read. What a per-user indicator shows.

Task<int> GetMyUnreadCountAsync(string teamKey, CancellationToken cancellationToken = default)

Parameters

teamKey string
cancellationToken CancellationToken

Returns

Task<int>

GetUnassignedCasesAsync(string, int, CancellationToken)

Cases belonging to no team, for whoever triages them.

Task<SupportCasePage> GetUnassignedCasesAsync(string cursor = null, int pageSize = 20, CancellationToken cancellationToken = default)

Parameters

cursor string
pageSize int
cancellationToken CancellationToken

Returns

Task<SupportCasePage>

Remarks

Authorized by Read. Separate from GetCasesAsync(string, string, int, CancellationToken) because that one answers "what is happening in this team" and this one answers "what has arrived that nobody owns" — different questions, different grants.

GetVisibilityChoiceAsync(string, string, CancellationToken)

What the caller may choose about who reads the case — so a component offers exactly what SetCaseVisibilityAsync(string, string, SupportCaseVisibility, CancellationToken) would accept, without knowing who is asking.

Task<SupportCaseVisibilityChoice> GetVisibilityChoiceAsync(string teamKey, string caseId, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
cancellationToken CancellationToken

Returns

Task<SupportCaseVisibilityChoice>

MarkReadAsync(string, string, CancellationToken)

Records that the caller has read this case up to its newest entry.

Task MarkReadAsync(string teamKey, string caseId, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
cancellationToken CancellationToken

Returns

Task

Remarks

Authorized exactly as reading the case is — anything weaker would let somebody write to a case they cannot see.

OpenAttachmentAsync(string, string, int, string, CancellationToken)

Opens a file carried by one entry of a case, authorized exactly as reading its transcript, or returns null when that entry carries no attachment with this id; the caller disposes it and serves it as ServedAs(string) says.

Task<SupportAttachmentContent> OpenAttachmentAsync(string teamKey, string caseId, int sequence, string attachmentId, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
sequence int
attachmentId string
cancellationToken CancellationToken

Returns

Task<SupportAttachmentContent>

PreviewAnswerAsync(string, string, string, string, CancellationToken)

A draft answer as the person who raised the case would receive it by mail, in the host's template and with the conversation so far quoted, rendered without sending or storing anything; null when the answer would not be mailed or the team has no such case.

Task<MailPreview> PreviewAnswerAsync(string teamKey, string caseId, string body, string bodyHtml, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
body string
bodyHtml string
cancellationToken CancellationToken

Returns

Task<MailPreview>

Remarks

Authorized exactly as answering; the draft is sanitized and derived as an answer is. Files are not rendered.

RaiseCaseAsync(string, string, string, SupportAssistance, CancellationToken)

Raises a case for a team, with its opening message.

Task<SupportCase> RaiseCaseAsync(string teamKey, string subject, string body, SupportAssistance assistance = SupportAssistance.None, CancellationToken cancellationToken = default)

Parameters

teamKey string
subject string
body string
assistance SupportAssistance
cancellationToken CancellationToken

Returns

Task<SupportCase>

Remarks

assistance is the customer's choice of who answers, and it is the only place that choice is made. Asking for an assistant when the host has registered none is not an error: the case is raised and a person answers it, which is what the customer would have got anyway.

RateAnswerAsync(string, string, int, SupportAnswerRating, CancellationToken)

Rates one of the assistant's answers in this case, or takes a rating back with None.

Task RateAnswerAsync(string teamKey, string caseId, int sequence, SupportAnswerRating rating, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
sequence int
rating SupportAnswerRating
cancellationToken CancellationToken

Returns

Task

Remarks

Only the person who raised the case rates, because it is them the assistant answered; anybody else is refused. Only an answer by the assistant can be rated. The case counts its ratings, for the people who build the assistant.

ReopenCaseAsync(string, string, CancellationToken)

Opens a closed case again, keeping its history.

Task ReopenCaseAsync(string teamKey, string caseId, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
cancellationToken CancellationToken

Returns

Task

Remarks

Authorized exactly as replying is — the member who raised the case, or a caller holding support:read or support:manage on the team. Somebody who could not answer a case has no business changing its state.

This is what makes closing safe to do. Without it, closing is a decision somebody has to be sure about, and the safe move is to leave cases open forever — which is how a case list stops being read. A case that closed too early costs one click to bring back, and it brings the conversation with it rather than starting a second case that explains nothing.

Reopening an already-open case does nothing and is not an error.

ReplyToCaseAsync(string, string, string, IReadOnlyList<SupportAttachmentUpload>, CancellationToken)

Appends a reply carrying files to an open case, authorized exactly as a reply without files.

Task ReplyToCaseAsync(string teamKey, string caseId, string body, IReadOnlyList<SupportAttachmentUpload> attachments, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
body string
attachments IReadOnlyList<SupportAttachmentUpload>
cancellationToken CancellationToken

Returns

Task

ReplyToCaseAsync(string, string, string, string, IReadOnlyList<SupportAttachmentUpload>, CancellationToken)

Appends a formatted reply with any files, authorized as a plain reply: bodyHtml is sanitized, its images become their alt text, the text is derived from it, and body is used only when it holds nothing readable.

Task ReplyToCaseAsync(string teamKey, string caseId, string body, string bodyHtml, IReadOnlyList<SupportAttachmentUpload> attachments, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
body string
bodyHtml string
attachments IReadOnlyList<SupportAttachmentUpload>
cancellationToken CancellationToken

Returns

Task

ReplyToCaseAsync(string, string, string, CancellationToken)

Appends a reply to an open case.

Task ReplyToCaseAsync(string teamKey, string caseId, string body, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
body string
cancellationToken CancellationToken

Returns

Task

ReportStaffActivityAsync(CancellationToken)

Says the caller is at work on support right now, so their Online keeps counting.

Task ReportStaffActivityAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Returns

Task

Remarks

Authorized as SetMyStaffStatusAsync(SupportStaffStatus, CancellationToken) is. Answering a case and writing as support count as well, without calling this. Online counts as away once the caller has done nothing in support for the host's idle time, ten minutes by default, and counts again with the next thing they do.

ReportTypingAsync(string, string, bool, CancellationToken)

Says the caller is writing in this case right now, so whoever is reading it on the other side sees an answer coming.

Task ReportTypingAsync(string teamKey, string caseId, bool asSupport, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
asSupport bool
cancellationToken CancellationToken

Returns

Task

Remarks

Raised through ISupportTypingNotifier and stored nowhere, so it is neither audited nor counted as activity. Authorized exactly as writing is: replying for a member, answering when asSupport. Call it again every few seconds while writing; the signal lapses when no further one follows.

RequestHumanAsync(string, string, CancellationToken)

Stops the assistant answering a case, so a person takes over.

Task RequestHumanAsync(string teamKey, string caseId, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
cancellationToken CancellationToken

Returns

Task

Remarks

Same case, same transcript. Handing over is not a second case: whoever picks it up reads everything that was already said, which is the whole point of offering an assistant first.

Authorized exactly as replying is. On a case with no assistant this does nothing and is not an error, so a component can offer it without first working out whether it applies.

ResendAsync(string, string, int, CancellationToken)

Sends one transcript entry to the case's channels again, and returns where it stands now.

Task<SupportMessageDelivery> ResendAsync(string teamKey, string caseId, int sequence, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
sequence int
cancellationToken CancellationToken

Returns

Task<SupportMessageDelivery>

Remarks

Only an entry that is Failed or Pending is sent; any other is left alone and its status returned, so pressing the button again after it went through sends nothing twice. Authorized exactly as AnswerCaseAsync(string, string, string, CancellationToken) is.

RunAssistantAsync(string, string, IProgress<SupportAssistantProgress>, CancellationToken)

Runs the assistant as RunAssistantAsync(string, string, CancellationToken) does, reporting to progress how far it has got: the round, the tools running, and the answer as written so far.

Task<bool> RunAssistantAsync(string teamKey, string caseId, IProgress<SupportAssistantProgress> progress, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
progress IProgress<SupportAssistantProgress>
cancellationToken CancellationToken

Returns

Task<bool>

True when an answer was appended. When false, whatever was reported was not kept.

Remarks

Only the caller sees the answer before it is kept. Reports go to progress and nowhere else, never through ISupportCaseNotifier, which carries no text and no authorization. Everybody else reads the answer once it is appended. Each report is the whole state, the text so far included; a responder that cannot stream reports nothing and answers in one piece. A report may arrive on another thread, so marshal before touching component state.

RunAssistantAsync(string, string, CancellationToken)

Has the assistant answer the newest message on a case, and appends what it says.

Task<bool> RunAssistantAsync(string teamKey, string caseId, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
cancellationToken CancellationToken

Returns

Task<bool>

True when an answer was appended.

Remarks

A separate operation rather than something raising and replying do for you. A model can take tens of seconds, and burying that inside a write would make reporting a problem feel broken. Raising stays fast, and the caller decides when to ask for the answer — which also lets a UI say that the assistant is thinking.

Does nothing and returns false when the case has no assistant, when the customer has already asked for a person, when the host registered no responder, or when the caller is not the person who raised the case: the assistant's tools read with the caller's rights, so it answers only its author. Every one of those is an ordinary state rather than an error, so a caller can call this unconditionally.

SetCaseVisibilityAsync(string, string, SupportCaseVisibility, CancellationToken)

Shares a case with the team's support readers, or takes it back.

Task SetCaseVisibilityAsync(string teamKey, string caseId, SupportCaseVisibility visibility, CancellationToken cancellationToken = default)

Parameters

teamKey string
caseId string
visibility SupportCaseVisibility
cancellationToken CancellationToken

Returns

Task

Remarks

Only the case's author, only while it is open, and only when the host enables per-case visibility — otherwise an InvalidOperationException. Nobody else may decide who reads somebody's conversation, the product's own support staff included.

SetMyStaffStatusAsync(SupportStaffStatus, CancellationToken)

Sets whether the caller, on the support desk, is shown to members as there to answer.

Task SetMyStaffStatusAsync(SupportStaffStatus status, CancellationToken cancellationToken = default)

Parameters

status SupportStaffStatus
cancellationToken CancellationToken

Returns

Task

Remarks

For the product's support staff: a caller who may answer across teams or the queue of cases with no team; anybody else is refused. Setting it counts as acting in support. Kept in-process and stored nowhere, so it is not audited, and it is forgotten when the application restarts. Members see the outcome through ISupportPresence.