Table of Contents

Class SupportCase

Namespace
Tharga.Team
Assembly
Tharga.Team.dll

A support conversation belonging to a team.

public record SupportCase : IEquatable<SupportCase>
Inheritance
SupportCase
Implements
Inherited Members

Remarks

The team owns the case; the author is a link that may dangle. A case is raised by a person but is about the team's account, and the team is what still exists a year later. Deleting a user runs ITeamService.RemoveUserFromAllTeamsAsync, which strips that person's membership from every team — so a case keyed on a member would be orphaned by design. Authorization keys on TeamKey alone, and no check requires the author to still resolve.

The transcript is not carried here. A case can accumulate years of messages, so history is read separately and paged; see GetMessagesAsync(string, string, string, int, CancellationToken). This record is the header.

Properties

AnswersRatedDown

How many of the assistant's answers in this case the person it answered found unhelpful.

public int AnswersRatedDown { get; init; }

Property Value

int

AnswersRatedUp

How many of the assistant's answers in this case the person it answered found helpful.

public int AnswersRatedUp { get; init; }

Property Value

int

AssistantGaps

What the assistant was asked in this case and had no way to do, oldest first, in its own words.

public string[] AssistantGaps { get; init; }

Property Value

string[]

Remarks

For the people who build the assistant, so they can see what to add. Empty on every case it could help with, and on every case raised before this existed.

AssistantState

Whether an assistant is answering this case.

public SupportAssistantState AssistantState { get; init; }

Property Value

SupportAssistantState

Remarks

Defaults to None, which is what every case raised before assistants existed deserializes as — so nothing already stored changes meaning.

AuthorIdentity

The stable authentication subject of whoever raised the case. May no longer resolve to a user or a member, which is expected rather than exceptional.

public required string AuthorIdentity { get; init; }

Property Value

string

AuthorName

The author's display name as it was when the case was raised.

public required string AuthorName { get; init; }

Property Value

string

AwaitingSupport

Whether the case is open and its newest entry is from the one who raised it — waiting on support.

public bool AwaitingSupport { get; init; }

Property Value

bool

Bindings

External projections. Empty until the channel work lands.

public SupportChannelBinding[] Bindings { get; init; }

Property Value

SupportChannelBinding[]

ClosedAt

public DateTime? ClosedAt { get; init; }

Property Value

DateTime?

ClosedBy

The stable subject of whoever closed the case, or null while it is open.

public string ClosedBy { get; init; }

Property Value

string

Remarks

May be AutoClose, when the case closed itself rather than being closed by a person.

ClosedReason

Why the case is closed, or null while it is open.

public SupportCaseClosureReason? ClosedReason { get; }

Property Value

SupportCaseClosureReason?

Remarks

Derived from ClosedBy rather than stored, and deliberately so. The store already records who closed a case; adding a reason to CloseCaseAsync(string, string, DateTime, string, SupportMessage, CancellationToken) would change a signature that hosts implement for their own storage, which is a compile-time break in somebody else's repository for a value that can be read from what is already there.

It follows that the reason is exactly as trustworthy as the actor, which is the right coupling: a case closed by the sweeper is closed for inactivity by definition, not by a flag that could disagree with who closed it.

CreatedAt

public required DateTime CreatedAt { get; init; }

Property Value

DateTime

ExternalContact

Who wrote, when the case was raised through a public form by somebody who was not signed in; null for every other case.

public ExternalSupportContact ExternalContact { get; init; }

Property Value

ExternalSupportContact

Remarks

What the visitor typed, unverified, kept so staff see the phone number, the organisation and whether consent was given. A case raised before this existed reads null.

Id

public required string Id { get; init; }

Property Value

string

LastMessageAt

When the newest entry was written, so a list can put the liveliest conversation first.

public DateTime? LastMessageAt { get; init; }

Property Value

DateTime?

MessageCount

Number of entries in the transcript, so a list can be rendered without reading it.

public required int MessageCount { get; init; }

Property Value

int

Status

public required SupportCaseStatus Status { get; init; }

Property Value

SupportCaseStatus

Subject

public required string Subject { get; init; }

Property Value

string

SuggestedTeams

Teams an unassigned case may belong to, found from the sender's address when it arrived by mail; empty when nothing matched or the case opened in a team.

public SupportCaseTeam[] SuggestedTeams { get; init; }

Property Value

SupportCaseTeam[]

Remarks

A suggestion, never an assignment. Staff decide, and giving the case one of these goes through AssignCaseAsync(string, string, CancellationToken) exactly as choosing any other team does.

SuggestionReason

Why SuggestedTeams are suggestions rather than the case's team; null when there are none.

public SupportTeamSuggestionReason? SuggestionReason { get; init; }

Property Value

SupportTeamSuggestionReason?

TeamKey

The owning team, or null when the case is not assigned to one.

public string TeamKey { get; init; }

Property Value

string

Remarks

Unassigned is a durable state, not a staging area. A case that arrived by mail from somebody who belongs to several teams — or to none the toolkit can see — belongs to no team until a support agent says which, and it can be answered, closed and reopened meanwhile. The person who knows which tenant a problem concerns is a human reading it, not an algorithm reading a From: header.

The invariant it replaces still holds where there is a team. A case with one is loaded through it and authorized by membership exactly as before, so holding a case id from another tenant still gains nothing. What changed is that "no team" became expressible, not that team scoping became optional.

An unassigned case is governed system-wide, because a team scope has no team to be held against. See AssignCaseAsync(string, string, CancellationToken).

The one mail that opens in a team is one whose sender the transport verified and whose address belongs to a single account that is an active member of a single team. A From header alone never places a case; anything short of that opens here with SuggestedTeams instead.

TeamName

The team's name as it was when the case was raised, for wording a message about it; null when it could not be read, and for a case that arrived with no team.

public string TeamName { get; init; }

Property Value

string

Visibility

Whether the team's support readers may read this case, when the host lets each case decide.

public SupportCaseVisibility? Visibility { get; init; }

Property Value

SupportCaseVisibility?

Remarks

null when nobody chose — every case under team-wide visibility, and any case written before visibility existed. Under per-case visibility a null reads as Private, because nobody ever shared it.