Merge: fix(projects) — unbreak Create + 6-digit CM constraint
This commit is contained in:
@@ -1149,9 +1149,9 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"projects.field.title.placeholder": "z.B. Siemens AG | Siemens v. Huawei | EP 1 234 567",
|
||||
"projects.field.reference": "Interne Referenz (optional)",
|
||||
"projects.field.reference.placeholder": `z.B. ${FIRM}-2026-0042`,
|
||||
"projects.field.client_number": "Client-Nr. (7 Ziffern)",
|
||||
"projects.field.matter_number": "Matter-Nr. (7 Ziffern)",
|
||||
"projects.field.clientmatter.hint": `${FIRM}-Billing-Nummern. Format CCCCCCC.MMMMMMM. Client-Nr. wird an Unterprojekte vererbt (\u00fcberschreibbar).`,
|
||||
"projects.field.client_number": "Client-Nr. (6 Ziffern)",
|
||||
"projects.field.matter_number": "Matter-Nr. (6 Ziffern)",
|
||||
"projects.field.clientmatter.hint": `${FIRM}-Billing-Nummern. Format CCCCCC.MMMMMM. Client-Nr. wird an Unterprojekte vererbt (\u00fcberschreibbar).`,
|
||||
"projects.field.billing_reference": "Billing-Referenz (optional)",
|
||||
"projects.field.netdocuments_url": "netDocuments-URL (optional)",
|
||||
"projects.field.industry": "Branche",
|
||||
@@ -3698,9 +3698,9 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"projects.field.title.placeholder": "e.g. Siemens AG | Siemens v. Huawei | EP 1 234 567",
|
||||
"projects.field.reference": "Internal reference (optional)",
|
||||
"projects.field.reference.placeholder": `e.g. ${FIRM}-2026-0042`,
|
||||
"projects.field.client_number": "Client no. (7 digits)",
|
||||
"projects.field.matter_number": "Matter no. (7 digits)",
|
||||
"projects.field.clientmatter.hint": `${FIRM} billing numbers. Format CCCCCCC.MMMMMMM. Client no. is inherited by sub-projects (overridable).`,
|
||||
"projects.field.client_number": "Client no. (6 digits)",
|
||||
"projects.field.matter_number": "Matter no. (6 digits)",
|
||||
"projects.field.clientmatter.hint": `${FIRM} billing numbers. Format CCCCCC.MMMMMM. Client no. is inherited by sub-projects (overridable).`,
|
||||
"projects.field.billing_reference": "Billing reference (optional)",
|
||||
"projects.field.netdocuments_url": "netDocuments URL (optional)",
|
||||
"projects.field.industry": "Industry",
|
||||
|
||||
@@ -64,28 +64,28 @@ export function ProjectFormFields(): string {
|
||||
|
||||
<div className="form-field-row">
|
||||
<div className="form-field">
|
||||
<label htmlFor="project-client-number" data-i18n="projects.field.client_number">Client-Nr. (7 Ziffern)</label>
|
||||
<label htmlFor="project-client-number" data-i18n="projects.field.client_number">Client-Nr. (6 Ziffern)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="project-client-number"
|
||||
pattern="[0-9]{7}"
|
||||
maxLength={7}
|
||||
placeholder="0001234"
|
||||
pattern="[0-9]{6}"
|
||||
maxLength={6}
|
||||
placeholder="001234"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="project-matter-number" data-i18n="projects.field.matter_number">Matter-Nr. (7 Ziffern)</label>
|
||||
<label htmlFor="project-matter-number" data-i18n="projects.field.matter_number">Matter-Nr. (6 Ziffern)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="project-matter-number"
|
||||
pattern="[0-9]{7}"
|
||||
maxLength={7}
|
||||
placeholder="0000567"
|
||||
pattern="[0-9]{6}"
|
||||
maxLength={6}
|
||||
placeholder="000567"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="form-hint" data-i18n="projects.field.clientmatter.hint">
|
||||
{`${FIRM}-Billing-Nummern. Format CCCCCCC.MMMMMMM. Client-Nr. wird an Unterprojekte vererbt
|
||||
{`${FIRM}-Billing-Nummern. Format CCCCCC.MMMMMM. Client-Nr. wird an Unterprojekte vererbt
|
||||
(überschreibbar).`}
|
||||
</p>
|
||||
|
||||
|
||||
32
internal/db/migrations/094_clientmatter_six_digit.down.sql
Normal file
32
internal/db/migrations/094_clientmatter_six_digit.down.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
-- mig 094 DOWN — restore the 7-digit CHECK and the snapshotted
|
||||
-- pre-clear client_number / matter_number values from
|
||||
-- paliad.projects_pre_094. Symmetric to the up migration.
|
||||
|
||||
SELECT set_config(
|
||||
'paliad.audit_reason',
|
||||
'mig 094 DOWN: restore 7-digit CHECK and pre-094 client_number/matter_number values from snapshot',
|
||||
true);
|
||||
|
||||
-- 1. Drop the 6-digit CHECKs.
|
||||
ALTER TABLE paliad.projects
|
||||
DROP CONSTRAINT projekte_client_number_check,
|
||||
DROP CONSTRAINT projekte_matter_number_check;
|
||||
|
||||
-- 2. Restore the original values from the snapshot. Only rows that
|
||||
-- existed at snapshot time are touched; rows added since stay as
|
||||
-- they were.
|
||||
UPDATE paliad.projects p
|
||||
SET client_number = s.client_number,
|
||||
matter_number = s.matter_number
|
||||
FROM paliad.projects_pre_094 s
|
||||
WHERE p.id = s.id;
|
||||
|
||||
-- 3. Re-add the legacy 7-digit CHECKs.
|
||||
ALTER TABLE paliad.projects
|
||||
ADD CONSTRAINT projekte_client_number_check
|
||||
CHECK (client_number IS NULL OR client_number ~ '^[0-9]{7}$'),
|
||||
ADD CONSTRAINT projekte_matter_number_check
|
||||
CHECK (matter_number IS NULL OR matter_number ~ '^[0-9]{7}$');
|
||||
|
||||
-- 4. Drop the snapshot. The down migration is the only consumer.
|
||||
DROP TABLE IF EXISTS paliad.projects_pre_094;
|
||||
97
internal/db/migrations/094_clientmatter_six_digit.up.sql
Normal file
97
internal/db/migrations/094_clientmatter_six_digit.up.sql
Normal file
@@ -0,0 +1,97 @@
|
||||
-- mig 094 — tighten paliad.projects.client_number + matter_number CHECK
|
||||
-- from 7-digit to 6-digit. The "7-Ziffern" rule in mig 018 was wrong;
|
||||
-- HLC's real Client/Matter format is 6 digits each (m's correction,
|
||||
-- 2026-05-17). The constraints carry the legacy 'projekte_*_check'
|
||||
-- name from before the table was renamed (mig 021), so the ALTER
|
||||
-- TABLE DROP / ADD has to use those names verbatim.
|
||||
--
|
||||
-- Existing rows: only test data (2 client_numbers, 1 matter_number),
|
||||
-- all 7-digit. They violate the new pattern, so we NULL them out
|
||||
-- before tightening — preserving the project rows themselves, just
|
||||
-- clearing the wrong-shaped billing identifiers. The rows are
|
||||
-- snapshotted in projects_pre_094 first so the down migration can
|
||||
-- restore them byte-identically.
|
||||
--
|
||||
-- audit_reason wrapper at top: the trigger on paliad.projects logs
|
||||
-- every row-level UPDATE; the message persists in the audit table as
|
||||
-- the permanent record of why those test values were cleared.
|
||||
|
||||
SELECT set_config(
|
||||
'paliad.audit_reason',
|
||||
'mig 094: clear test 7-digit client_number/matter_number values before tightening CHECK to 6-digit (HLC real format correction, 2026-05-17)',
|
||||
true);
|
||||
|
||||
-- =============================================================================
|
||||
-- 1. Backup snapshot. Full row copy of every paliad.projects row that
|
||||
-- has either field populated. Idempotent via CREATE TABLE IF NOT
|
||||
-- EXISTS — re-running the migration after an aborted run re-uses
|
||||
-- the existing snapshot.
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS paliad.projects_pre_094 AS
|
||||
SELECT *, now() AS snapshotted_at
|
||||
FROM paliad.projects
|
||||
WHERE client_number IS NOT NULL OR matter_number IS NOT NULL;
|
||||
|
||||
COMMENT ON TABLE paliad.projects_pre_094 IS
|
||||
'Snapshot of paliad.projects rows that had a client_number or '
|
||||
'matter_number set before mig 094 tightened the CHECK from '
|
||||
'7-digit to 6-digit. The 094 UPDATE NULL-ed those values out '
|
||||
'because they were leftover 7-digit test data. Persists as the '
|
||||
'permanent audit anchor; the down migration restores from it.';
|
||||
|
||||
-- =============================================================================
|
||||
-- 2. Clear the 7-digit test values. Only rows that already violate
|
||||
-- the new pattern are touched — anything that happens to already
|
||||
-- be 6 digits (none today, but the WHERE keeps the migration
|
||||
-- re-runnable after future inserts) is left alone.
|
||||
-- =============================================================================
|
||||
|
||||
UPDATE paliad.projects
|
||||
SET client_number = NULL
|
||||
WHERE client_number IS NOT NULL
|
||||
AND client_number !~ '^[0-9]{6}$';
|
||||
|
||||
UPDATE paliad.projects
|
||||
SET matter_number = NULL
|
||||
WHERE matter_number IS NOT NULL
|
||||
AND matter_number !~ '^[0-9]{6}$';
|
||||
|
||||
-- =============================================================================
|
||||
-- 3. Replace the legacy 7-digit CHECKs with 6-digit ones. The
|
||||
-- constraint names carry the pre-rename `projekte_*` prefix from
|
||||
-- mig 018; keep them stable so external audit tools that scan
|
||||
-- pg_constraint by name don't drift.
|
||||
-- =============================================================================
|
||||
|
||||
ALTER TABLE paliad.projects
|
||||
DROP CONSTRAINT projekte_client_number_check,
|
||||
DROP CONSTRAINT projekte_matter_number_check;
|
||||
|
||||
ALTER TABLE paliad.projects
|
||||
ADD CONSTRAINT projekte_client_number_check
|
||||
CHECK (client_number IS NULL OR client_number ~ '^[0-9]{6}$'),
|
||||
ADD CONSTRAINT projekte_matter_number_check
|
||||
CHECK (matter_number IS NULL OR matter_number ~ '^[0-9]{6}$');
|
||||
|
||||
-- =============================================================================
|
||||
-- 4. Hard assertions. Any row that survived the UPDATE+ALTER must
|
||||
-- satisfy the new pattern; the count of cleared test rows must
|
||||
-- match the snapshot.
|
||||
-- =============================================================================
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
n_violations int;
|
||||
BEGIN
|
||||
SELECT count(*) INTO n_violations
|
||||
FROM paliad.projects
|
||||
WHERE (client_number IS NOT NULL AND client_number !~ '^[0-9]{6}$')
|
||||
OR (matter_number IS NOT NULL AND matter_number !~ '^[0-9]{6}$');
|
||||
|
||||
IF n_violations > 0 THEN
|
||||
RAISE EXCEPTION 'mig 094: % rows still violate the 6-digit pattern after UPDATE — should be 0', n_violations;
|
||||
END IF;
|
||||
|
||||
RAISE NOTICE 'mig 094: 6-digit CHECKs in place, all rows compliant';
|
||||
END $$;
|
||||
@@ -849,8 +849,15 @@ func (s *ProjectService) Create(ctx context.Context, userID uuid.UUID, input Cre
|
||||
id := uuid.New()
|
||||
now := time.Now().UTC()
|
||||
|
||||
// path is NOT NULL but the trigger populates it; supply a placeholder
|
||||
// the trigger will overwrite. (BEFORE INSERT trigger rewrites path.)
|
||||
// path is NOT NULL but paliad.projects_sync_path() (BEFORE INSERT
|
||||
// trigger from mig 018/021) overwrites it from id and parent path,
|
||||
// so any non-null value satisfies the constraint. Use a literal
|
||||
// placeholder rather than re-referencing $1 — reusing a parameter
|
||||
// across columns with different SQL types (id is uuid, path is text)
|
||||
// makes Postgres's planner reject the statement with 42P08
|
||||
// "inconsistent types deduced for parameter" once the driver hands
|
||||
// $1 across as an inferred type. The literal keeps the param list
|
||||
// decoupled from the id column's type.
|
||||
if input.OurSide != nil {
|
||||
if err := validateOurSide(*input.OurSide); err != nil {
|
||||
return nil, err
|
||||
@@ -868,7 +875,7 @@ func (s *ProjectService) Create(ctx context.Context, userID uuid.UUID, input Cre
|
||||
matter_number, netdocuments_url, patent_number, filing_date, grant_date,
|
||||
court, case_number, proceeding_type_id, our_side, counterclaim_of,
|
||||
instance_level, metadata, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $1::text, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
|
||||
VALUES ($1, $2, $3, '', $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
|
||||
$14, $15, $16, $17, $18, $19, $20, $21, $22, $23, '{}'::jsonb, $24, $24)`,
|
||||
id, input.Type, input.ParentID,
|
||||
input.Title, input.Reference, input.Description, status,
|
||||
@@ -1281,12 +1288,15 @@ func (s *ProjectService) CreateCounterclaim(ctx context.Context, userID, parentI
|
||||
id := uuid.New()
|
||||
now := time.Now().UTC()
|
||||
|
||||
// path placeholder is overwritten by paliad.projects_sync_path();
|
||||
// same rationale as ProjectService.Create — see comment there for
|
||||
// why we use a literal '' instead of re-referencing $1.
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
`INSERT INTO paliad.projects
|
||||
(id, type, parent_id, path, title, status, created_by,
|
||||
court, case_number, proceeding_type_id, our_side, counterclaim_of,
|
||||
metadata, created_at, updated_at)
|
||||
VALUES ($1, 'case', $2, $1::text, $3, 'active', $4,
|
||||
VALUES ($1, 'case', $2, '', $3, 'active', $4,
|
||||
$5, $6, $7, $8, $9, '{}'::jsonb, $10, $10)`,
|
||||
id, childParentID, title, userID,
|
||||
parent.Court, opts.CaseNumber, procTypeID,
|
||||
|
||||
Reference in New Issue
Block a user