---
id: w6/m12/t002
title: "Backend: Service.ChangePlan verb with downgrade guards"
worker: worker6
status: todo
estimate: 45m
depends_on: [w6/m12/t001]
---

## Objective

Add a `ChangePlan` verb to the workspaces service: `can_manage`-scoped, plan validated via `store.NormalizePlan`, and downgrades refused when the workspace would violate the target plan's caps — member count vs `MaxMembers`, service count vs `MaxServices`, and the per-user cap (`MaxWorkspacesPerUser`, e.g. a 6th Hobby workspace via downgrade).

## Context

- Verb inventory today: `lego/backend/internal/workspaces/service.go` has only `Create`/`Rename`/`Delete`; `Create` already shows the guard pattern (`store.LimitsFor(plan)`, `CountWorkspacesForSubjectPlan`).
- Plan catalog + limits: `lego/backend/internal/store/plans.go` (hobby/pro/scale/enterprise; Hobby 25 services / 1 member / 5 per user). Service-count check precedent: `internal/store/api.go` (`MaxServices`).
- Error shape: `core.ErrBadRequest`-wrapped messages, matching `Create`'s cap error; guard copy from t001's capture.

## Steps

1. Add `ChangePlan(ctx, id, plan string) (WorkspaceView, error)` to `internal/workspaces/service.go` — authorize `can_manage`, normalize the plan, no-op if unchanged.
2. On downgrade, count members and services against `LimitsFor(target)` and the caller's per-user workspace count against `MaxWorkspacesPerUser`; refuse with the decided copy.
3. Add the store update (`UpdateWorkspacePlan` or extend the existing tenant-update path) + audit-event emission consistent with rename/delete.

## Files

- `lego/backend/internal/workspaces/service.go`
- `lego/backend/internal/store/` (plan-update + count queries as needed)

## Acceptance criteria

- [ ] hobby→pro succeeds; pro→hobby with 2 members or 26 services is refused with the guard error
- [ ] Downgrade creating a 6th hobby workspace for the same user is refused
- [ ] Non-admin caller gets a 403-shaped error; `go test ./...` green

## Out of scope

- GraphQL/dashboard exposure (t003/t005); role-set gating (t004)
