Interface ISupportCaseService
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
teamKeystringcaseIdstringbodystringattachmentsIReadOnlyList<SupportAttachmentUpload>cancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringbodystringbodyHtmlstringattachmentsIReadOnlyList<SupportAttachmentUpload>cancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringbodystringcancellationTokenCancellationToken
Returns
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
caseIdstringteamKeystringcancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringcancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringcancellationTokenCancellationToken
Returns
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
teamKeystringcancellationTokenCancellationToken
Returns
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
cancellationTokenCancellationToken
Returns
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
cancellationTokenCancellationToken
Returns
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
teamKeystringcancellationTokenCancellationToken
Returns
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
cancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringcancellationTokenCancellationToken
Returns
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
cursorstringpageSizeintcancellationTokenCancellationToken
Returns
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
teamKeystringcursorstringpageSizeintcancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringcursorstringpageSizeintcancellationTokenCancellationToken
Returns
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
teamKeystringcancellationTokenCancellationToken
Returns
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
teamKeystringcursorstringpageSizeintcancellationTokenCancellationToken
Returns
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
teamKeystringcancellationTokenCancellationToken
Returns
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
cancellationTokenCancellationToken
Returns
Remarks
Authorized as SetMyStaffStatusAsync(SupportStaffStatus, CancellationToken) is.
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
teamKeystringcancellationTokenCancellationToken
Returns
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
teamKeystringcancellationTokenCancellationToken
Returns
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
cursorstringpageSizeintcancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringcancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringcancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringsequenceintattachmentIdstringcancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringbodystringbodyHtmlstringcancellationTokenCancellationToken
Returns
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
teamKeystringsubjectstringbodystringassistanceSupportAssistancecancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringsequenceintratingSupportAnswerRatingcancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringcancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringbodystringattachmentsIReadOnlyList<SupportAttachmentUpload>cancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringbodystringbodyHtmlstringattachmentsIReadOnlyList<SupportAttachmentUpload>cancellationTokenCancellationToken
Returns
ReplyToCaseAsync(string, string, string, CancellationToken)
Appends a reply to an open case.
Task ReplyToCaseAsync(string teamKey, string caseId, string body, CancellationToken cancellationToken = default)
Parameters
teamKeystringcaseIdstringbodystringcancellationTokenCancellationToken
Returns
ReportStaffActivityAsync(CancellationToken)
Says the caller is at work on support right now, so their Online keeps counting.
Task ReportStaffActivityAsync(CancellationToken cancellationToken = default)
Parameters
cancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringasSupportboolcancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringcancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringsequenceintcancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringprogressIProgress<SupportAssistantProgress>cancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringcancellationTokenCancellationToken
Returns
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
teamKeystringcaseIdstringvisibilitySupportCaseVisibilitycancellationTokenCancellationToken
Returns
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
statusSupportStaffStatuscancellationTokenCancellationToken
Returns
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.