shoc-backend/SeaHaven.Services/Implementation/UpliftStatus.cs

96 lines
3.9 KiB
C#
Raw Normal View History

namespace SeaHaven.Services.Implementation
{
/// <summary>
/// Server-owned uplift state machine (SH-101). Canonical states are
/// Pending, Approved, Rejected, ChangesRequested, Withdrawn, Expired.
/// Legacy stored values "Denied" (-> Rejected) and "Cancelled" (-> Withdrawn)
/// are read as their canonical equivalents but never (re)written.
/// </summary>
public static class UpliftStatus
{
public const string Pending = "Pending";
public const string Approved = "Approved";
public const string Rejected = "Rejected";
public const string ChangesRequested = "ChangesRequested";
public const string Withdrawn = "Withdrawn";
public const string Expired = "Expired";
public const string NoApprovalRequired = "NoApprovalRequired";
public const string Revoked = "Revoked";
// Legacy aliases retained only for reads; never written by new code.
public const string LegacyDenied = "Denied";
public const string LegacyCancelled = "Cancelled";
private static readonly HashSet<string> TerminalStates =
new(StringComparer.Ordinal) { Approved, Rejected, Withdrawn, Expired, Revoked, NoApprovalRequired };
private static readonly HashSet<string> ActiveStates =
new(StringComparer.Ordinal) { Pending, ChangesRequested };
/// <summary>
/// Maps a stored status to its canonical DTO value. Legacy "Denied" is
/// read as "Rejected" and "Cancelled" as "Withdrawn".
/// </summary>
public static string ToCanonical(string? status) => status switch
{
LegacyDenied => Rejected,
LegacyCancelled => Withdrawn,
_ => status ?? Pending
};
/// <summary>True for terminal (immutable) states, including legacy aliases.</summary>
public static bool IsTerminal(string? status) =>
TerminalStates.Contains(ToCanonical(status));
/// <summary>True for actionable states that block a new active request.</summary>
public static bool IsActive(string? status) =>
ActiveStates.Contains(ToCanonical(status));
/// <summary>
/// Validates a server-owned transition. Only legal transitions return true.
/// Terminal states (incl. legacy aliases) never transition out.
/// </summary>
public static bool CanTransition(string? from, string to)
{
var canonicalFrom = ToCanonical(from);
if (TerminalStates.Contains(canonicalFrom))
return false;
return canonicalFrom switch
{
Pending => to is Approved or Rejected or ChangesRequested or Withdrawn or Expired,
ChangesRequested => to is Pending or Withdrawn or Expired,
_ => false
};
}
/// <summary>Snapshot-readable active states, used by data-service filters.</summary>
public static IReadOnlyCollection<string> ActiveStatuses => ActiveStates;
}
/// <summary>
/// Notification pipeline status (separate from the workflow <see cref="UpliftStatus"/>).
/// A notification failure never destroys an actionable request.
/// </summary>
public static class UpliftNotificationStatus
{
public const string Pending = "Pending";
public const string Sent = "Sent";
public const string Error = "Error";
}
/// <summary>
/// Vendor completion document purposes (SH-101). Completion is the legacy/default
/// behavior; UpliftEvidence is immutable supporting evidence for an uplift request.
/// </summary>
public static class VendorDocumentPurpose
{
public const string Completion = "Completion";
public const string UpliftEvidence = "UpliftEvidence";
public static bool IsValid(string? purpose) =>
purpose is Completion or UpliftEvidence;
}
}