feat(projects): t-paliad-222 — Client Role + auto-derived project codes
Implements m/paliad#47 (Client Role rework) + m/paliad#50 (auto-derived project codes from the ancestor tree) in one shift. Migrations: - mig 112_client_role_rework: widen paliad.projects.our_side CHECK to seven sub-roles (claimant / defendant / applicant / appellant / respondent / third_party / other); drop legacy 'court' / 'both' and backfill rows to NULL (no-op on prod, defensive on staging). - mig 113_projects_opponent_code: add paliad.projects.opponent_code text on litigation rows (slug pattern [A-Z0-9-]{1,16}); used as the middle segment when assembling auto-derived project codes. Backend: - internal/services/project_code.go — new package-level helpers BuildProjectCode (single row) + PopulateProjectCodes (bulk, one CTE-based round-trip). Walks the existing paliad.projects.path ltree; custom paliad.projects.reference on the target wins. - Wired into ProjectService.List, GetByID, ListAncestors, GetTree, LoadCounterclaimChildrenVisible, BuildTreeWithOptions — every service entry-point that returns []models.Project / *models.Project populates .Code before returning. - Models: Project.OurSide doc widened; new Project.OpponentCode (db:"opponent_code") and Project.Code (db:"-", projection-only). - CreateProjectInput / UpdateProjectInput accept OpponentCode; validateOpponentCode + nullableOpponentCode mirror our_side helpers. - validateOurSide widens to the seven sub-roles; legacy 'court' / 'both' rejected at the service layer with a clear error before the DB CHECK fires. - derivedCounterclaimOurSide CCR flip widened: applicant ↔ respondent, appellant → respondent; third_party / other / NULL pass through. - submission_vars: project.code added to the placeholder bag. ourSideDE / ourSideEN now use the gender-neutral "-Seite" / "-Partei" suffix shape (Klägerseite / Antragstellerseite / ...); better legal-prose default for a B2B patent practice, matches the form labels which already used this shape (cf. head's soft-note on Q4). Frontend: - ProjectFormFields: opponent_code on a new projekt-fields-litigation block (hidden by default, shown when type=litigation); our_side moved into projekt-fields-case and re-labelled "Client Role" / "Mandantenrolle" with three <optgroup>s + seven options. - project-form.ts: showFieldsForType toggles the new litigation block; readPayload / prefillForm wire opponent_code; our_side is now only emitted for type=case. - fristenrechner: ourSideToPerspective widened to the seven sub-roles (Active→claimant, Reactive→defendant, Other→null). ProjectOption type literal updated. - i18n.ts: new projects.field.client_role.* and projects.field.opponent_code.* keys (DE+EN). Legacy projects.field.our_side.* keys stay one release for cached bundles + Verlauf event-history rendering of the new sub-roles. Tests: - TestProjectCodeSegment, TestAssembleProjectCode, TestPatentLast3, TestSanitizeClientShort, TestProceedingTail, TestValidateOpponentCode, TestValidateOurSideSubRoles pin the new pure helpers. - TestOurSideTranslations widened to the seven sub-roles + new prose shape; 'court'/'both' arms now return "" (legacy rejected). - TestDerivedCounterclaimOurSide widened to the new flip map. Migration slot history (this branch was rebumped twice on 2026-05-20): mig 110 was claimed by m/paliad#51 (project_type_other, euler); mig 111 was claimed by m/paliad#48 (project_admin_and_select, gauss). Final slots 112 / 113. go build && go test ./internal/... && cd frontend && bun run build all clean.
This commit is contained in:
@@ -144,10 +144,10 @@ Existing `'court'`/`'both'` switch arms get deleted (no live rows; if a
|
||||
stale `our_side='court'` slipped through somehow, the function returns
|
||||
`""` — same fallback as today for unknown values).
|
||||
|
||||
### §2.3 Migration `111_client_role_rework`
|
||||
### §2.3 Migration `112_client_role_rework`
|
||||
|
||||
```sql
|
||||
-- 111_client_role_rework.up.sql (renumbered 2026-05-20 — mig 110 was claimed by m/paliad#51 project_type_other)
|
||||
-- 112_client_role_rework.up.sql (renumbered 2026-05-20 — mig 110 was claimed by m/paliad#51, mig 111 by m/paliad#48)
|
||||
-- t-paliad-222 / m/paliad#47.
|
||||
-- Widens projects.our_side CHECK to seven sub-role values and drops
|
||||
-- the legacy 'court' / 'both' entries. Backfill is a no-op on the
|
||||
@@ -442,10 +442,10 @@ projects), introduce a materialised view
|
||||
`paliad.projects_derived_codes(project_id, derived_code)` refreshed by
|
||||
trigger on `projects` writes. Don't pre-optimise.
|
||||
|
||||
### §3.3 Migration `112_projects_opponent_code`
|
||||
### §3.3 Migration `113_projects_opponent_code`
|
||||
|
||||
```sql
|
||||
-- 112_projects_opponent_code.up.sql (renumbered 2026-05-20)
|
||||
-- 113_projects_opponent_code.up.sql (renumbered 2026-05-20)
|
||||
-- t-paliad-222 / m/paliad#50.
|
||||
-- Add an opponent-code field on litigation projects. Used as the
|
||||
-- middle segment when assembling auto-derived project codes from the
|
||||
@@ -643,8 +643,8 @@ material pushes back. Coder shift only after head signs off.)
|
||||
|
||||
## §5 Implementation order (coder phase)
|
||||
|
||||
1. **Mig 111** (client role widen + backfill) → mig 112 (opponent_code).
|
||||
*Renumbered 2026-05-20 — mig 110 was claimed by m/paliad#51 project_type_other; boltzmann's gap-tolerant runner hard-fails on collisions so this is a strict rebump.*
|
||||
1. **Mig 112** (client role widen + backfill) → mig 113 (opponent_code).
|
||||
*Renumbered twice on 2026-05-20 — mig 110 claimed by m/paliad#51 project_type_other; mig 111 claimed by m/paliad#48 project_admin_and_select; boltzmann's gap-tolerant runner hard-fails on collisions so this is a strict rebump.*
|
||||
Run `ls internal/db/migrations/ | tail` first to verify slot
|
||||
availability (boltzmann's gap-tolerant runner means 110 is fine
|
||||
even if 109 was the last applied).
|
||||
|
||||
@@ -2163,6 +2163,19 @@ export type I18nKey =
|
||||
| "projects.field.billing_reference"
|
||||
| "projects.field.case_number"
|
||||
| "projects.field.client_number"
|
||||
| "projects.field.client_role"
|
||||
| "projects.field.client_role.appellant"
|
||||
| "projects.field.client_role.applicant"
|
||||
| "projects.field.client_role.claimant"
|
||||
| "projects.field.client_role.defendant"
|
||||
| "projects.field.client_role.group.active"
|
||||
| "projects.field.client_role.group.other"
|
||||
| "projects.field.client_role.group.reactive"
|
||||
| "projects.field.client_role.hint"
|
||||
| "projects.field.client_role.other"
|
||||
| "projects.field.client_role.respondent"
|
||||
| "projects.field.client_role.third_party"
|
||||
| "projects.field.client_role.unset"
|
||||
| "projects.field.clientmatter.hint"
|
||||
| "projects.field.collaborators"
|
||||
| "projects.field.collaborators.hint"
|
||||
@@ -2180,13 +2193,21 @@ export type I18nKey =
|
||||
| "projects.field.matter_number"
|
||||
| "projects.field.netdocuments_url"
|
||||
| "projects.field.office"
|
||||
| "projects.field.opponent_code"
|
||||
| "projects.field.opponent_code.hint"
|
||||
| "projects.field.opponent_code.placeholder"
|
||||
| "projects.field.our_side"
|
||||
| "projects.field.our_side.appellant"
|
||||
| "projects.field.our_side.applicant"
|
||||
| "projects.field.our_side.both"
|
||||
| "projects.field.our_side.claimant"
|
||||
| "projects.field.our_side.court"
|
||||
| "projects.field.our_side.defendant"
|
||||
| "projects.field.our_side.hint"
|
||||
| "projects.field.our_side.none"
|
||||
| "projects.field.our_side.other"
|
||||
| "projects.field.our_side.respondent"
|
||||
| "projects.field.our_side.third_party"
|
||||
| "projects.field.our_side.unset"
|
||||
| "projects.field.parent"
|
||||
| "projects.field.parent.hint"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- Down migration for 111_client_role_rework.
|
||||
-- Down migration for 112_client_role_rework.
|
||||
--
|
||||
-- Restores the original 4-value CHECK ('claimant','defendant',
|
||||
-- 'court','both', NULL) and backfills any rows that landed on a new
|
||||
@@ -1,4 +1,4 @@
|
||||
-- mig 111 — t-paliad-222 / m/paliad#47 — Client Role rework.
|
||||
-- mig 112 — t-paliad-222 / m/paliad#47 — Client Role rework.
|
||||
--
|
||||
-- Widens paliad.projects.our_side CHECK to seven sub-role values and
|
||||
-- drops the legacy 'court' / 'both' entries. The DB column name stays
|
||||
@@ -1,4 +1,4 @@
|
||||
-- Down migration for 112_projects_opponent_code.
|
||||
-- Down migration for 113_projects_opponent_code.
|
||||
|
||||
BEGIN;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- mig 112 — t-paliad-222 / m/paliad#50 — auto-derived project codes.
|
||||
-- mig 113 — t-paliad-222 / m/paliad#50 — auto-derived project codes.
|
||||
--
|
||||
-- Adds an opponent-code slug field on litigation projects. Used as
|
||||
-- the middle segment when BuildProjectCode assembles an auto-derived
|
||||
@@ -161,7 +161,7 @@ type Project struct {
|
||||
// chip from the project context (t-paliad-164). NULL = unknown /
|
||||
// not set; Determinator falls back to free-pick.
|
||||
//
|
||||
// Allowed sub-roles (mig 111, t-paliad-222):
|
||||
// Allowed sub-roles (mig 112, t-paliad-222):
|
||||
// Active : claimant, applicant, appellant
|
||||
// Reactive : defendant, respondent
|
||||
// Other : third_party, other
|
||||
@@ -177,7 +177,7 @@ type Project struct {
|
||||
// assembles an auto-derived project code from the ancestor tree —
|
||||
// e.g. EXMPL.OPNT.567.INF.CFI (t-paliad-222 / m/paliad#50). NULL
|
||||
// → segment skipped silently. Only meaningful on type='litigation'
|
||||
// rows; CHECK constraint (mig 112) enforces the pairing.
|
||||
// rows; CHECK constraint (mig 113) enforces the pairing.
|
||||
OpponentCode *string `db:"opponent_code" json:"opponent_code,omitempty"`
|
||||
|
||||
// Code is the auto-derived (or override) project code, computed at
|
||||
|
||||
@@ -351,7 +351,7 @@ func TestValidateOpponentCode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOurSideSubRoles pins the widened allowlist (mig 111).
|
||||
// TestValidateOurSideSubRoles pins the widened allowlist (mig 112).
|
||||
func TestValidateOurSideSubRoles(t *testing.T) {
|
||||
valid := []string{
|
||||
"", "claimant", "defendant", "applicant", "appellant",
|
||||
|
||||
@@ -1437,7 +1437,7 @@ func insertCounterclaimEvent(ctx context.Context, tx *sqlx.Tx, projectID, userID
|
||||
// Third Party / Other (third_party, other) and NULL pass through
|
||||
// unchanged — the flip is meaningless without a clear active / reactive
|
||||
// posture. Legacy 'court' / 'both' no longer exist in the column
|
||||
// (mig 111) so they have no case arm; if a stale value sneaks in via a
|
||||
// (mig 112) so they have no case arm; if a stale value sneaks in via a
|
||||
// pre-migration in-memory row it falls through to the default branch
|
||||
// and passes through unchanged, preserving previous behaviour.
|
||||
//
|
||||
@@ -1979,7 +1979,7 @@ func validateProjectStatus(s string) error {
|
||||
// (t-paliad-164, widened in t-paliad-222 / m/paliad#47). Empty string
|
||||
// is the explicit "clear" sentinel — callers pass the value as-is
|
||||
// from the form payload, and the helper accepts it so an Update can
|
||||
// null the column. The DB-level CHECK constraint (mig 111) enforces
|
||||
// null the column. The DB-level CHECK constraint (mig 112) enforces
|
||||
// the same set; this validation gives a clearer error than relying
|
||||
// on the constraint to fire.
|
||||
//
|
||||
@@ -1988,7 +1988,7 @@ func validateProjectStatus(s string) error {
|
||||
// Reactive (we defend) : defendant, respondent
|
||||
// Third Party / Other : third_party, other
|
||||
//
|
||||
// Legacy 'court' / 'both' are no longer accepted (mig 111 backfills
|
||||
// Legacy 'court' / 'both' are no longer accepted (mig 112 backfills
|
||||
// existing rows to NULL); callers that still send them get a clear
|
||||
// validation error rather than a constraint violation.
|
||||
func validateOurSide(s string) error {
|
||||
@@ -2049,7 +2049,7 @@ func nullableOurSide(p *string) any {
|
||||
}
|
||||
|
||||
// opponentCodePattern matches the slug shape enforced by the
|
||||
// projects_opponent_code_check constraint (mig 112): uppercase letters,
|
||||
// projects_opponent_code_check constraint (mig 113): uppercase letters,
|
||||
// digits, dashes, 1-16 chars. The DB CHECK is the source of truth; this
|
||||
// helper surfaces a friendlier ErrInvalidInput error before the write.
|
||||
var opponentCodePattern = regexp.MustCompile(`^[A-Z0-9-]{1,16}$`)
|
||||
|
||||
@@ -276,7 +276,7 @@ func TestLegalSourcePretty(t *testing.T) {
|
||||
// mapping used by addProjectVars. Post t-paliad-222: seven sub-role
|
||||
// values + the gender-neutral "-Seite" / "-Partei" suffix shape on
|
||||
// DE. Legacy 'court' / 'both' yield "" (the column no longer accepts
|
||||
// them after mig 111, but the function defensively handles stale
|
||||
// them after mig 112, but the function defensively handles stale
|
||||
// in-memory values from older callers).
|
||||
func TestOurSideTranslations(t *testing.T) {
|
||||
cases := []struct {
|
||||
|
||||
Reference in New Issue
Block a user