Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-215Done

4.1.3 `sprintsService` — sprint CRUD + `assertSprintTransition` state-machine guard + DTOs/errors

Estimate: 30m · Depends on: 4.1.2

The business-logic layer for the sprint ENTITY + the state-machine RULES (Story 4.4 composes the rules into its start/complete flows — they are NOT implemented here).

sprintsService (lib/services/sprintsService.ts) — one method = one transaction, owns DTO mapping (lib/mappers/sprintMappers.tslib/dto/sprints.ts), throws typed errors (lib/sprints/errors.ts) the route layer maps to status codes, and enforces the finding-#26 application-layer workspaceId gate on every read/write:

  • createSprint(projectId, { name?, goal?, startDate?, endDate? }) — creates a planned sprint; default-names it "Sprint <maxSequence+1>" when name is omitted; validates the date window (endDatestartDate when both given). Does NOT start it.
  • updateSprint(id, patch) — rename / edit goal / adjust the planned window. Date/name validation; rejects editing a complete sprint.
  • deleteSprint(id) — deletes a planned (or complete) sprint; its issues fall back to the backlog via the onDelete: SetNull FK (their backlog_rank already exists, so they re-appear in rank order). Rejects deleting the active sprint (that goes through 4.4's complete flow).
  • assertSprintTransition(from: SprintState, to: SprintState) — the PURE state-machine guard: allows planned→active and active→complete; throws InvalidSprintTransitionError for skips (planned→complete), reopens (complete→active, active→planned), and self-transitions. Exported as a pure function so Story 4.4's start/complete flows + the one-active guard call it without re-deriving the rules. (4.1 ships + tests the guard; 4.4 owns the orchestration that consumes it — scope-lock, carry-over, report.)
  • Mappers return a SprintDto (id, name, goal, state, startDate, endDate, completedAt, sequence, issueCount) — never a raw Prisma model.

Typed errors (lib/sprints/errors.ts): SprintNotFoundError, InvalidSprintTransitionError, SprintWindowInvalidError, CannotModifyCompletedSprintError, CannotDeleteActiveSprintError — distinct codes so the (future) route layer maps them to 404/409/422.

Routes are minimal here (CRUD endpoints POST/PATCH/DELETE /api/sprints) — HTTP-only, one service call each, error→status mapping; the rich sprint-planning surface is Story 4.2.

Acceptance criteria

  • sprintsService exposes createSprint (planned, default-named, window-validated), updateSprint, deleteSprint (issues fall to backlog; active rejected), and the pure exported assertSprintTransition; each write is one transaction; reads/writes enforce the finding-#26 workspaceId gate; methods return SprintDtos.
  • assertSprintTransition allows planned→active + active→complete and throws InvalidSprintTransitionError for every skip/reopen/self transition; it is a pure function (no I/O) Story 4.4 can import.
  • Typed errors live in lib/sprints/errors.ts; the CRUD routes are HTTP-only (one service call + error mapping each).
  • pnpm test:coverage keeps the new service file ≥90% branch/fn/line (the coverage gate); start/complete ORCHESTRATION is explicitly absent (deferred to Story 4.4).

Context refs

  • lib/services/boardsService.ts (3.1/3.7) — the service shape to mirror (one-tx-per-method, DTO mapping, workspaceId gate); lib/mappers/*, lib/dto/*, lib/<domain>/errors.ts layout
  • Story 4.4 (sprint lifecycle) — the consumer of assertSprintTransition + the one-active guard; Story 4.2 (backlog UI) — the consumer of the CRUD + DTOs
  • motir-core/CLAUDE.md (service layer: transactions, DTOs, typed errors) + motir-core-coverage-gate