diff --git a/api-v3/src/database/migrations/1790046000000-phase-f4-document-numbering.ts b/api-v3/src/database/migrations/1790046000000-phase-f4-document-numbering.ts new file mode 100644 index 0000000..b60df3c --- /dev/null +++ b/api-v3/src/database/migrations/1790046000000-phase-f4-document-numbering.ts @@ -0,0 +1,144 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class PhaseF4DocumentNumbering1790046000000 implements MigrationInterface { + name = 'PhaseF4DocumentNumbering1790046000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM inspection_acts WHERE act_number > 99999) THEN + RAISE EXCEPTION 'F4 numbering requires inspection act numbers <= 99999'; + END IF; + IF EXISTS ( + SELECT 1 + FROM inspection_visits + WHERE code ~ '^INS-[0-9]{4}-[0-9]{6}$' + AND RIGHT(code, 6)::integer > 99999 + ) THEN + RAISE EXCEPTION 'F4 numbering requires inspection visit numbers <= 99999'; + END IF; + END $$ + `); + + await queryRunner.query(` + UPDATE inspection_visits + SET code = 'INSP-' + || LPAD(RIGHT(code, 6)::integer::text, 5, '0') + || '-' + || TO_CHAR( + COALESCE(planned_start_at, created_at) AT TIME ZONE 'America/Argentina/Mendoza', + 'DD-MM-YY' + ), + title = CASE + WHEN title = inspection_visits.code THEN 'INSP-' + || LPAD(RIGHT(inspection_visits.code, 6)::integer::text, 5, '0') + || '-' + || TO_CHAR( + COALESCE(planned_start_at, created_at) AT TIME ZONE 'America/Argentina/Mendoza', + 'DD-MM-YY' + ) + ELSE title + END + WHERE code ~ '^INS-[0-9]{4}-[0-9]{6}$' + `); + + await queryRunner.query(` + UPDATE inspection_acts + SET code = 'ACT-' + || LPAD(act_number::text, 5, '0') + || '-' + || TO_CHAR(occurred_at AT TIME ZONE 'America/Argentina/Mendoza', 'DD-MM-YY') + `); + + await queryRunner.query(`DROP INDEX IF EXISTS uq_inspection_reports_year_number`); + await queryRunner.query(` + UPDATE inspection_reports report + SET report_year = act.act_year, + report_number = act.act_number, + code = 'INF-' + || LPAD(act.act_number::text, 5, '0') + || '-' + || TO_CHAR(act.occurred_at AT TIME ZONE 'America/Argentina/Mendoza', 'DD-MM-YY') + FROM inspection_acts act + WHERE act.id = report.act_id + `); + await queryRunner.query(` + CREATE UNIQUE INDEX uq_inspection_reports_year_number + ON inspection_reports(report_year, report_number) + `); + + await queryRunner.query(` + CREATE OR REPLACE FUNCTION dhv2_f4_set_act_code() + RETURNS trigger + LANGUAGE plpgsql + AS $$ + BEGIN + IF NEW.act_number > 99999 THEN + RAISE EXCEPTION 'Inspection Act sequence exhausted for F4 institutional format'; + END IF; + NEW.code := 'ACT-' + || LPAD(NEW.act_number::text, 5, '0') + || '-' + || TO_CHAR(NEW.occurred_at AT TIME ZONE 'America/Argentina/Mendoza', 'DD-MM-YY'); + RETURN NEW; + END; + $$ + `); + await queryRunner.query(` + DROP TRIGGER IF EXISTS trg_dhv2_f4_set_act_code ON inspection_acts + `); + await queryRunner.query(` + CREATE TRIGGER trg_dhv2_f4_set_act_code + BEFORE INSERT OR UPDATE OF act_number, occurred_at + ON inspection_acts + FOR EACH ROW + EXECUTE FUNCTION dhv2_f4_set_act_code() + `); + + await queryRunner.query(` + CREATE OR REPLACE FUNCTION dhv2_f4_set_report_code() + RETURNS trigger + LANGUAGE plpgsql + AS $$ + DECLARE + source_act inspection_acts%ROWTYPE; + BEGIN + SELECT * INTO source_act + FROM inspection_acts + WHERE id = NEW.act_id; + IF NOT FOUND THEN + RAISE EXCEPTION 'Inspection Act not found for report %', NEW.act_id; + END IF; + IF source_act.act_number > 99999 THEN + RAISE EXCEPTION 'Inspection Report sequence exhausted for F4 institutional format'; + END IF; + NEW.report_year := source_act.act_year; + NEW.report_number := source_act.act_number; + NEW.code := 'INF-' + || LPAD(source_act.act_number::text, 5, '0') + || '-' + || TO_CHAR(source_act.occurred_at AT TIME ZONE 'America/Argentina/Mendoza', 'DD-MM-YY'); + RETURN NEW; + END; + $$ + `); + await queryRunner.query(` + DROP TRIGGER IF EXISTS trg_dhv2_f4_set_report_code ON inspection_reports + `); + await queryRunner.query(` + CREATE TRIGGER trg_dhv2_f4_set_report_code + BEFORE INSERT OR UPDATE OF act_id + ON inspection_reports + FOR EACH ROW + EXECUTE FUNCTION dhv2_f4_set_report_code() + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TRIGGER IF EXISTS trg_dhv2_f4_set_report_code ON inspection_reports`); + await queryRunner.query(`DROP FUNCTION IF EXISTS dhv2_f4_set_report_code()`); + await queryRunner.query(`DROP TRIGGER IF EXISTS trg_dhv2_f4_set_act_code ON inspection_acts`); + await queryRunner.query(`DROP FUNCTION IF EXISTS dhv2_f4_set_act_code()`); + } +} diff --git a/api-v3/test/unit/phase-f4-document-numbering.test.ts b/api-v3/test/unit/phase-f4-document-numbering.test.ts new file mode 100644 index 0000000..80cb607 --- /dev/null +++ b/api-v3/test/unit/phase-f4-document-numbering.test.ts @@ -0,0 +1,24 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; + +const root = process.cwd(); +const read = (relative: string) => fs.readFileSync(path.join(root, relative), 'utf8'); + +test('F4 institutional numbering keeps Inspection, Act and INF date-readable and coherent', () => { + const migration = read('src/database/migrations/1790046000000-phase-f4-document-numbering.ts'); + assert.match(migration, /INSP-/); + assert.match(migration, /ACT-/); + assert.match(migration, /INF-/); + assert.match(migration, /DD-MM-YY/); + assert.match(migration, /LPAD\(act_number::text, 5, '0'\)/); +}); + +test('F4 gives one INF the same annual correlativo as its one Act', () => { + const migration = read('src/database/migrations/1790046000000-phase-f4-document-numbering.ts'); + assert.match(migration, /report_number = act\.act_number/); + assert.match(migration, /NEW\.report_number := source_act\.act_number/); + assert.match(migration, /NEW\.report_year := source_act\.act_year/); + assert.match(migration, /trg_dhv2_f4_set_report_code/); +});