admin-sla-configrun.md00_intake/stub.mdSLA exists only as notification logic (packages/services/src/notifications/sla/) — there is no
stored, editable SLA configuration. The dashboard's SlaConfigCard and the /admin/sla table/page
show dummy stage timers. To measure delivery and trigger escalations, the admin needs real,
per-tenant, per-stage SLA definitions. This is the one section the user agreed warrants a dedicated
model (rather than tenant-settings) because its shape is structured.
/admin/sla page table to the real stage rows./admin/sla table renders the tenant's real SLA rows (no empty mock array).slaConfigured. Decide whether default timers live as a tenant-setting or on the model.touches: packages/services/src/db/models (new SLA model), apps/web/app/(app)/admin/sla/page.tsx,
apps/web/components/admin/dashboard/sla-config-card.tsx.01_define/output/spec.mdSLA exists only as notification logic (packages/services/src/notifications/sla/ — a single
notifySlaBreach helper) with no stored, editable SLA configuration. On the admin "Platform setup"
dashboard the SlaConfigCard and the (not-yet-built) /admin/sla page read dummy values from
apps/web/lib/mock/admin.ts (slaConfig: { status, stagesCovered, totalStages, defaultTimersSet, stages: [] }), so every tenant sees the same fictional state and an admin cannot actually define
delivery timers. This is feature 3 of 7 in the admin-dashboard-data epic, advancing Build the
Bridge → Q2 2026 Objective 3 (Validate Technical Infrastructure & Payout Flow) by making each
dashboard section operational from real, per-tenant data. Per the epic's storage decision, SLA is the
one section that warrants a dedicated model (rather than the shared TenantSetting key/value
store its siblings use) because its shape is structured — one configurable definition per service
stage. The data foundation (admin-data-foundation, #438) already established the per-tenant server
loader loadAdminDashboardData(); this feature plugs its real SLA datapoint into that seam.
packages/services/src/db/models/sla-definition.ts, one row per
service stage, exported from db/models/index.ts and using the established model conventions
(schemaPlugin, softDeletePlugin, tenantPlugin; collection sla_definitions; unique index on
{ tenantId, stage }). Fields per row:stage — one of the canonical service stages: Request | BRD | Bidding | Delivery | Billing | CSAT (the same vocabulary the service-model card and business/service-journey use, so
admin-readiness-gaps can later derive slaConfigured from it).ownerRole — the persona that owns the stage (admin | csm | sdm | expert | vendor | customer).slaType — Response | Resolution.targetDays, warningDays, breachDays — SLA thresholds in business days (non-negative
integers; expected ordering target ≤ warning ≤ breach, validated on save).impact — Low | Medium | High | Critical.escalationEnabled — boolean (records intent only; no escalation is fired this round).packages/services/src/db/services/sla/ (mirroring the
tenant-setting service shape: index.ts + instance.ts) exposing:getSlaDefinitions(tenantId) → the tenant's saved stage rows (empty array when none).upsertSlaDefinition(tenantId, stage, { ownerRole, slaType, targetDays, warningDays, breachDays, impact, escalationEnabled }) → create/update one stage row.deleteSlaDefinition(tenantId, stage) → remove (soft-delete) a stage's definition.getSlaConfigSummary(tenantId) → the card slice { status, stagesCovered, totalStages, defaultTimersSet } (see status rules below).totalStages = 6. stagesCovered = number of the 6 stages with a saved
definition. status = Missing when 0 covered, Partial when 1–5, Complete when all 6.
defaultTimersSet = whether the tenant has saved at least one SLA row (stagesCovered > 0). A
brand-new tenant therefore reads Missing / no timers, never a mock value — consistent with how
the sibling escalation card derives its empty state.loadAdminDashboardData() calls getSlaConfigSummary(tenantId) and
returns the real slaConfig slice; app/(app)/admin/dashboard/page.tsx passes it into
SlaConfigCard instead of d.slaConfig from the mock. The SlaConfigCard component already takes
exactly these props (status, stagesCovered, totalStages, defaultTimersSet) and needs no
shape change./admin/sla page (new, server component scoped to the authenticated admin's tenant
via the established resolveActionContext({ allowedRoles: ["admin"] }) pattern, with the same
graceful no-access fallback the other admin pages use):/admin/escalation and
/admin/settings actions precedent) so the admin can define or edit each stage's timers, owner,
SLA type, impact, and escalation flag, and save per tenant. Saving revalidatePath("/admin") so
the dashboard card reflects the new coverage.slaConfig block from lib/mock/admin.ts (and any now
unused fields) so nothing reads dummy SLA state./admin/sla
(owner role, SLA type, target/warning/breach business days, impact, escalation toggle), and the
values persist per tenant in the new sla_definitions collection.{ tenantId, stage } and validates timer values
as non-negative integers ordered target ≤ warning ≤ breach.getSlaDefinitions, upsertSlaDefinition, deleteSlaDefinition, and getSlaConfigSummary
exist on the new SLA service; getSlaDefinitions returns an empty array when nothing is stored.stagesCovered / totalStages and a
derived status: Missing at 0 covered, Partial at 1–5, Complete at all 6 — fed from the live data,
not the mock.defaultTimersSet reflects real state (true once at least one stage is saved); a tenant that has
never configured SLA renders Missing with no timers set./admin/sla table renders the tenant's real stage rows (no empty mock array); saving a stage
updates the dashboard card after revalidation.Request, BRD, Bidding, Delivery, Billing, CSAT, matching the
service-model card and business/service-journey, so the same vocabulary is reusable by
admin-readiness-gaps.slaConfig mock block is removed from lib/mock/admin.ts and no code reads it.notifications/sla wiring stays as-is;
escalationEnabled records intent only.admin-escalation-config) — separate concern.slaConfigured readiness tile and config-gap computation (admin-readiness-gaps).02_build/output/notes.mdpackages/services/src/db/models/sla-definition.ts (new): per-tenant SLA model, one row per
service stage. Canonical stage/owner/type/impact vocabularies are exported as const arrays
(SLA_STAGES, SLA_OWNER_ROLES, SLA_TYPES, SLA_IMPACTS) so the UI and validation reuse one
source. Collection sla_definitions; unique partial index on { tenantId, stage } (non-deleted
only, so a stage can be reconfigured after a soft delete); per-field non-negative-integer
validation on the day timers; schemaPlugin + softDeletePlugin + tenantPlugin.packages/services/src/db/services/sla/{index,instance}.ts (new): SlaConfigService mirroring the
tenant-setting service shape. getSlaDefinitions (canonical-order, [] when none),
upsertSlaDefinition (ordering check target ≤ warning ≤ breach, upsert on { tenantId, stage }),
deleteSlaDefinition (soft delete via archive), and getSlaConfigSummary (coverage-derived
{ status, stagesCovered, totalStages, defaultTimersSet }).packages/services/src/db/models/index.ts + services/index.ts: export the new model + service
(flows through @sustentus/services/server).apps/web/lib/admin-dashboard-data.ts: loader now calls getSlaConfigSummary(tenantId) and returns
a real sla slice on AdminDashboardData.apps/web/app/(app)/admin/dashboard/page.tsx: SlaConfigCard is fed the real sla slice instead
of d.slaConfig.apps/web/app/(app)/admin/sla/ (new): server page.tsx (admin-scoped via resolveActionContext,
graceful no-access fallback), actions.ts (saveSlaDefinition / removeSlaDefinition server
actions with zod validation + revalidatePath), and _components/sla-config-manager.tsx (client
table of all 6 stages + per-stage editor). Server-only union types reach the client via
import type; option lists are passed as props so no server runtime leaks into the client bundle.apps/web/lib/mock/admin.ts: removed the slaConfig block (no apps/web code reads it anymore)./admin/sla and it persists per tenant —
/admin/sla editor → saveSlaDefinition → upsertSlaDefinition into sla_definitions.{ tenantId, stage } and validates non-negative integers
ordered target ≤ warning ≤ breach — unique partial index + per-field validators + ordering
check in the service and the action's zod refine.getSlaDefinitions / upsertSlaDefinition / deleteSlaDefinition / getSlaConfigSummary
exist; getSlaDefinitions returns [] when nothing is stored.stagesCovered / totalStages + derived status (Missing 0, Partial
1–5, Complete 6) — from the loader's sla slice.defaultTimersSet reflects real state (true once ≥1 stage saved); never-configured tenant reads
Missing / no timers./admin/sla table renders real rows; saving revalidates /admin/dashboard + /admin so the
card updates.Request, BRD, Bidding, Delivery, Billing, CSAT (SLA_STAGES).slaConfig mock block removed from apps/web/lib/mock/admin.ts; nothing in apps/web reads it.gh pr checks.apps/dashboards/ keeps its own standalone slaConfig mock + SLA page — that app is self-contained
and out of this feature's scope (the spec's touches: is apps/web only).findOneAndUpdate./admin/sla editor selects a stage (table row click or the stage dropdown) and edits it in
place; Remove soft-deletes a configured stage. Coverage on the dashboard card updates on save.03_ship/output/changelog.mdPersona: admin
Admins can now set up service-level agreements from the platform setup dashboard:
This is part of making your platform setup dashboard reflect your organisation's real configuration.
03_ship/output/investor-update.mdWe've made service-level agreements configurable per organisation — admins can define the delivery timers for each stage of the service journey, and the platform setup dashboard now reflects real SLA coverage instead of placeholder content. It continues turning the admin control surface into a working control panel, and puts the delivery commitments that govern how we measure service under explicit, per-organisation control.
This advances the Build the Bridge initiative and our Q2 2026 objective to validate technical infrastructure & payout flow — establishing the per-organisation SLA definitions that delivery measurement and escalation will build on.
03_ship/output/pr.mdapps/docs/technical page enumerates admin routes, DB models, or service methods; a new model + service + admin route under existing packages doesn't change documented architecture (mirrors the escalation/commercial/people siblings' determination).service-journey step or feature-role-matrix entity; platform-overview already describes per-tenant SLA timers generically, and this feature realises part of that without changing what the page describes.03_ship/output/investor-update.md) + changelog entry published to apps/help/app/changelog/page.mdx./code-review (high) over the diff vs origin/main — 6 candidates, 2 fixed, 4 accepted:
$or + $exists:false (copied from the csat
precedent), which is ambiguous across MongoDB versions. Simplified to
partialFilterExpression: { isDeleted: false } — unambiguously valid and, since the soft-delete
plugin defaults isDeleted:false on every insert, it covers every live row in this new collection./admin/sla editor coerced blank timer inputs to 0 via Number(""), silently saving
a 0-day SLA and marking the stage covered. Added a non-empty guard in handleSave (explicit 0
still allowed).getSlaConfigSummary loads + sorts + de-dupes documents to produce a count; a
countDocuments would be cheaper, but it's ≤6 docs once per dashboard render and countDocuments
bypasses the soft-delete pre(/^find/) hook, so the current form is safer. Not worth the risk.fieldErrors mapping) and the
service (backstop); minor duplication, deliberate.isDuplicateKeyError / Archiveable*Model mirror the established sibling-service
pattern (9 services); a shared helper is a repo-wide refactor, out of scope here./admin/sla (owner, type, target/warning/breach business days, impact, escalation) and it persists per tenant in sla_definitions — page + form + saveSlaDefinition → upsertSlaDefinition.{ tenantId, stage } and validates non-negative-integer timers ordered target ≤ warning ≤ breach — unique partial index + per-field validators + service ordering check + action check.getSlaDefinitions / upsertSlaDefinition / deleteSlaDefinition / getSlaConfigSummary exist; getSlaDefinitions returns [] when none stored.stagesCovered / totalStages + derived status (Missing 0, Partial 1–5, Complete 6).defaultTimersSet reflects real state; never-configured tenant reads Missing / no timers./admin/sla table renders real rows; saving revalidates /admin/dashboard + /admin.SLA_STAGES).slaConfig mock block removed from apps/web/lib/mock/admin.ts; nothing in apps/web reads it./admin/sla registered in route-policies.ts so the deny-by-default proxy no longer bounces it to /admin.