Skip to main content

Adding a PSA provider

This app is built around one seam: IPsaDataProvider. Halo is the first implementation, not the only one. Adding a new PSA means implementing one interface and registering it, not rewriting the worker.

The interface​

public interface IPsaDataProvider
{
Task<PsaSnapshot> GetSnapshotAsync(CancellationToken cancellationToken);
}

One method. Whatever your PSA's API looks like, a new provider's job is to turn its ticket state into a PsaSnapshot.

What PsaSnapshot needs​

  • OpenTicketCount: total open tickets.
  • PriorityCounts: ticket counts keyed by priority rank, where 1 is the most urgent tier your PSA defines and higher numbers are progressively less urgent. Use a numeric rank rather than your PSA's own label (Halo calls its tiers "Critical", "Urgent", "High"), so the contract stays usable by any future provider regardless of what it calls its own tiers.
  • PriorityNames (optional): a display ready name per rank, if your PSA has one to offer, for example Halo's own tier names. This is purely a display enrichment kept separate from PriorityCounts. Leave a rank out, or leave the whole dictionary null, and the display falls back to a generic "P1", "P2" style label. If your PSA has no such concept, leave this null.
  • SlaRiskTickets: every ticket with a known SLA timer, not filtered by any risk threshold, including tickets with days left and already breached tickets (a negative MinutesRemaining). Do not filter by threshold in your provider: PriorityEngine is the single place that applies the SLA risk threshold, so filtering here would duplicate, and risk diverging from, that logic.
  • UnassignedTicketCount: open tickets with nobody assigned.
  • VipTickets: open tickets belonging to a customer your PSA flags as VIP. There is no separate escalation concept; an open VIP customer ticket is the escalation.
  • OrganizationName (optional): a display ready organisation or portal name, if your PSA can supply one, for example Halo's portal_title. Leave it null if your PSA has no such concept, or the lookup fails; the display falls back to the configured Dashboard:HeaderText.

Registering the provider​

Program.cs selects which provider runs based on the Psa:Provider configuration value:

var psaProviderName = builder.Configuration.GetSection(PsaOptions.SectionName)["Provider"];
if (string.Equals(psaProviderName, "Halo", StringComparison.OrdinalIgnoreCase))
{
builder.Services.AddHttpClient<IPsaDataProvider, HaloPsaDataProvider>((provider, client) =>
{
var options = provider.GetRequiredService<IOptions<HaloOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
client.Timeout = TimeSpan.FromSeconds(30);
});
}
else
{
throw new InvalidOperationException($"Unknown Psa:Provider '{psaProviderName}'. Supported: Halo.");
}

Add a new branch alongside the Halo one, registering your implementation against IPsaDataProvider, then select it by setting Psa:Provider to your provider's name.

Authentication is your provider's own concern​

HaloAuthClient's OAuth2 client credentials flow is specific to Halo, not part of the IPsaDataProvider contract. Your provider authenticates however its PSA requires, whether that is OAuth2, an API key, or something else, and keeps that logic to itself.

Gorelo provider notes​

Gorelo's public API is considerably thinner than Halo's, which shapes a few PsaSnapshot fields when Psa:Provider=Gorelo:

  • VipTickets is always empty: Gorelo's ticket/client schema has no VIP or customer-tier concept.
  • OrganizationName is always null: Gorelo has no organization-profile/portal-name endpoint, so the dashboard always falls back to Dashboard:HeaderText.
  • SLA risk reflects first-response only: Gorelo exposes a single sla.firstResponse.minutes timer (no overall resolution-SLA timer like Halo's). Its exact semantics aren't documented in Gorelo's API spec; treated as "minutes remaining until first-response breach" by inference, not confirmed behavior.
  • On-hold/paused tickets are not excluded: unlike Halo (which excludes on-hold tickets from every count so a paused ticket can't drive a P1/SLA/VIP/unassigned signal), Gorelo has no reliable on-hold/paused signal in its ticket schema: its baseStatusId taxonomy is undocumented and tenant-customizable, so it isn't safe to key filtering off. A paused Gorelo ticket therefore still counts toward OpenTicketCount, PriorityCounts, SLA risk, and unassigned counts.

See the Gorelo PSA provider design doc for the full rationale behind these decisions.