F4.0 · numeración INSP y lifecycle explícito
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
name: F4 Document Flow CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'feature/f4*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'api-v3/**'
|
||||
- 'web-v2/**'
|
||||
- 'android-app/**'
|
||||
- 'scripts/**'
|
||||
- '.github/workflows/f4-ci.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
api:
|
||||
name: API · F4
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
defaults:
|
||||
run:
|
||||
working-directory: api-v3
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
cache-dependency-path: api-v3/package-lock.json
|
||||
- run: npm ci
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
- name: Tests
|
||||
run: npm test
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
web:
|
||||
name: WEB · regression
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web-v2
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
cache-dependency-path: web-v2/package-lock.json
|
||||
- run: npm ci
|
||||
- run: npm run typecheck
|
||||
- name: F3.1 structural WEB contract
|
||||
run: bash ../scripts/check-f3-1-web-contract.sh
|
||||
- run: npm run build
|
||||
|
||||
deploy-preflight:
|
||||
name: VPS-equivalent preflight / Docker
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
needs: [api, web]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Validate shell scripts
|
||||
run: |
|
||||
while IFS= read -r -d '' script; do
|
||||
bash -n "$script"
|
||||
done < <(find scripts -type f -name '*.sh' -print0)
|
||||
- name: Validate Compose
|
||||
run: docker compose --env-file .env.example config >/dev/null
|
||||
- name: VPS-equivalent isolated API tests
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
image="dhv2-api:f4-preflight-${GITHUB_SHA::12}"
|
||||
docker build --target builder -t "$image" api-v3
|
||||
docker run --rm \
|
||||
-v "$PWD/api-v3/test:/app/test:ro" \
|
||||
-v "$PWD/api-v3/tsconfig.test.json:/app/tsconfig.test.json:ro" \
|
||||
"$image" npm test
|
||||
docker image rm "$image" >/dev/null 2>&1 || true
|
||||
- name: Build production images
|
||||
run: docker compose --env-file .env.example build api migrate web
|
||||
@@ -4,14 +4,15 @@ export async function nextInspectionVisitCode(
|
||||
manager: EntityManager,
|
||||
plannedStartAt: Date,
|
||||
): Promise<string> {
|
||||
const [yearRow] = (await manager.query(`
|
||||
SELECT EXTRACT(
|
||||
YEAR FROM $1::timestamptz AT TIME ZONE 'America/Argentina/Mendoza'
|
||||
)::integer AS year
|
||||
`, [plannedStartAt])) as Array<{ year: number }>;
|
||||
const year = Number(yearRow?.year);
|
||||
if (!Number.isInteger(year) || year < 2000 || year > 9999) {
|
||||
throw new Error('Unable to derive inspection visit year');
|
||||
const [dateRow] = (await manager.query(`
|
||||
SELECT
|
||||
EXTRACT(YEAR FROM $1::timestamptz AT TIME ZONE 'America/Argentina/Mendoza')::integer AS year,
|
||||
TO_CHAR($1::timestamptz AT TIME ZONE 'America/Argentina/Mendoza', 'DD-MM-YY') AS "datePart"
|
||||
`, [plannedStartAt])) as Array<{ year: number; datePart: string }>;
|
||||
const year = Number(dateRow?.year);
|
||||
const datePart = dateRow?.datePart;
|
||||
if (!Number.isInteger(year) || year < 2000 || year > 9999 || !datePart) {
|
||||
throw new Error('Unable to derive inspection visit date');
|
||||
}
|
||||
|
||||
await manager.query(
|
||||
@@ -20,15 +21,25 @@ export async function nextInspectionVisitCode(
|
||||
);
|
||||
|
||||
const [sequenceRow] = (await manager.query(`
|
||||
SELECT COALESCE(MAX(RIGHT(code, 6)::integer), 0)::integer AS sequence
|
||||
FROM inspection_visits
|
||||
WHERE code LIKE $1
|
||||
AND code ~ ('^INS-' || $2::text || '-[0-9]{6}$')
|
||||
`, [`INS-${year}-%`, year])) as Array<{ sequence: number }>;
|
||||
SELECT COALESCE(MAX(candidate.sequence), 0)::integer AS sequence
|
||||
FROM (
|
||||
SELECT SUBSTRING(code FROM '^INSP-([0-9]{5})-[0-9]{2}-[0-9]{2}-[0-9]{2}$')::integer AS sequence
|
||||
FROM inspection_visits
|
||||
WHERE EXTRACT(
|
||||
YEAR FROM COALESCE(planned_start_at, created_at) AT TIME ZONE 'America/Argentina/Mendoza'
|
||||
)::integer = $1
|
||||
AND code ~ '^INSP-[0-9]{5}-[0-9]{2}-[0-9]{2}-[0-9]{2}$'
|
||||
UNION ALL
|
||||
SELECT RIGHT(code, 6)::integer AS sequence
|
||||
FROM inspection_visits
|
||||
WHERE code LIKE $2
|
||||
AND code ~ ('^INS-' || $1::text || '-[0-9]{6}$')
|
||||
) candidate
|
||||
`, [year, `INS-${year}-%`])) as Array<{ sequence: number }>;
|
||||
|
||||
const sequence = Number(sequenceRow?.sequence ?? 0) + 1;
|
||||
if (sequence > 999999) {
|
||||
if (sequence > 99999) {
|
||||
throw new Error(`Inspection visit sequence exhausted for ${year}`);
|
||||
}
|
||||
return `INS-${year}-${String(sequence).padStart(6, '0')}`;
|
||||
return `INSP-${String(sequence).padStart(5, '0')}-${datePart}`;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import { CurrentAuth } from '../auth/decorators/current-auth.decorator';
|
||||
import { RequirePermissions } from '../authorization/decorators/require-permissions.decorator';
|
||||
import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context';
|
||||
import { ChangeInspectionVisitStatusDto } from './dto/change-inspection-visit-status.dto';
|
||||
import { CloseInspectionVisitDto } from './dto/close-inspection-visit.dto';
|
||||
import { CreateInspectionVisitDto } from './dto/create-inspection-visit.dto';
|
||||
import { ExcludeInspectionVisitAssetDto } from './dto/exclude-inspection-visit-asset.dto';
|
||||
@@ -135,17 +134,6 @@ export class InspectionVisitsController {
|
||||
return this.visits.replaceTeam(id, dto, principal, request);
|
||||
}
|
||||
|
||||
@Put(':id/status')
|
||||
@RequirePermissions('inspections.manage')
|
||||
changeStatus(
|
||||
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
|
||||
@Body() dto: ChangeInspectionVisitStatusDto,
|
||||
@CurrentAuth() principal: AuthPrincipal,
|
||||
@Req() request: RequestWithContext,
|
||||
) {
|
||||
return this.visits.changeStatus(id, dto, principal, request);
|
||||
}
|
||||
|
||||
@Post(':id/start')
|
||||
@RequirePermissions('inspections.execute')
|
||||
start(
|
||||
|
||||
Reference in New Issue
Block a user