feat: add repair parts tracking
This commit is contained in:
1 parent
6e550c1c13
commit
eab9646960
23 files changed
+2386
-4
No files matched your search
@@ -132,7 +132,7 @@ health_json="$(cat "${health_file}")"
|
||||
|
||||
echo "Checking migrations and required schema..."
|
||||
current_revision="$("${compose[@]}" exec -T backend alembic current 2>/dev/null)"
|
||||
[[ "${current_revision}" == *"0013_control_board_replacement"* ]]
|
||||
[[ "${current_revision}" == *"0014_repair_parts_tracking"* ]]
|
||||
|
||||
missing_tables="$(
|
||||
"${compose[@]}" exec -T postgres sh -c \
|
||||
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
project_name="${MINEROPS_REPAIR_PARTS_MIGRATION_PROJECT:-minerops-repair-parts-migration-$$}"
|
||||
compose=(docker compose --project-name "${project_name}" --env-file .env.dev -f compose.dev.yaml)
|
||||
|
||||
cleanup() {
|
||||
status=$?
|
||||
if (( status != 0 )); then
|
||||
"${compose[@]}" logs --no-color --tail 120 postgres || true
|
||||
fi
|
||||
"${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true
|
||||
exit "${status}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
run_alembic() { "${compose[@]}" run --rm --no-deps backend alembic "$@"; }
|
||||
psql_db() {
|
||||
"${compose[@]}" exec -T postgres sh -ec \
|
||||
'exec psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" "$@"' sh "$@"
|
||||
}
|
||||
|
||||
[[ -f .env.dev ]] || { echo "Missing .env.dev" >&2; exit 1; }
|
||||
"${compose[@]}" up --detach postgres
|
||||
for attempt in $(seq 1 60); do
|
||||
"${compose[@]}" exec -T postgres sh -ec \
|
||||
'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' >/dev/null 2>&1 && break
|
||||
(( attempt < 60 )) || { echo "PostgreSQL did not become ready" >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
"${compose[@]}" build backend
|
||||
|
||||
echo "Upgrading from Task 004 parent..."
|
||||
run_alembic upgrade 0013_control_board_replacement
|
||||
[[ "$(run_alembic current 2>/dev/null)" == *"0013_control_board_replacement"* ]]
|
||||
run_alembic upgrade 0014_repair_parts_tracking
|
||||
[[ "$(run_alembic current 2>/dev/null)" == *"0014_repair_parts_tracking"* ]]
|
||||
|
||||
psql_db <<'SQL'
|
||||
DO $$
|
||||
DECLARE component_roles text[];
|
||||
DECLARE create_roles text[];
|
||||
DECLARE manage_roles text[];
|
||||
BEGIN
|
||||
IF to_regclass('public.components') IS NULL OR to_regclass('public.repair_parts') IS NULL THEN
|
||||
RAISE EXCEPTION 'Task 005 tables are missing';
|
||||
END IF;
|
||||
SELECT array_agg(r.name ORDER BY r.name) INTO component_roles
|
||||
FROM role_permissions rp JOIN roles r ON r.id = rp.role_id
|
||||
JOIN permissions p ON p.id = rp.permission_id WHERE p.key = 'components.read';
|
||||
SELECT array_agg(r.name ORDER BY r.name) INTO create_roles
|
||||
FROM role_permissions rp JOIN roles r ON r.id = rp.role_id
|
||||
JOIN permissions p ON p.id = rp.permission_id WHERE p.key = 'components.create';
|
||||
SELECT array_agg(r.name ORDER BY r.name) INTO manage_roles
|
||||
FROM role_permissions rp JOIN roles r ON r.id = rp.role_id
|
||||
JOIN permissions p ON p.id = rp.permission_id WHERE p.key = 'repairs.parts.manage';
|
||||
IF component_roles <> ARRAY['Admin', 'Manager', 'Read Only', 'Technician'] THEN
|
||||
RAISE EXCEPTION 'Unexpected components.read roles: %', component_roles;
|
||||
END IF;
|
||||
IF create_roles <> ARRAY['Admin', 'Manager', 'Technician']
|
||||
OR manage_roles <> ARRAY['Admin', 'Manager', 'Technician'] THEN
|
||||
RAISE EXCEPTION 'Unexpected Task 005 write roles: %, %', create_roles, manage_roles;
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'ck_repair_parts_entry_xor'
|
||||
) OR NOT EXISTS (
|
||||
SELECT 1 FROM pg_trigger WHERE tgname = 'trg_validate_repair_part_history'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Task 005 constraints or triggers are missing';
|
||||
END IF;
|
||||
END $$;
|
||||
SQL
|
||||
|
||||
echo "Testing safe downgrade and re-upgrade..."
|
||||
run_alembic downgrade 0013_control_board_replacement
|
||||
[[ "$(run_alembic current 2>/dev/null)" == *"0013_control_board_replacement"* ]]
|
||||
absent="$(psql_db -Atc "SELECT to_regclass('public.components') IS NULL AND to_regclass('public.repair_parts') IS NULL")"
|
||||
[[ "${absent}" == "t" ]]
|
||||
run_alembic upgrade 0014_repair_parts_tracking
|
||||
|
||||
echo "Testing registry, XOR, snapshots, voids, and immutability..."
|
||||
psql_db <<'SQL'
|
||||
INSERT INTO users (id, username, display_name, password_hash, role_id)
|
||||
SELECT '00000000-0000-0000-0000-000000001501', 'repair-parts-migration',
|
||||
'Repair Parts Migration', 'unused', id FROM roles WHERE name = 'Admin';
|
||||
INSERT INTO miners (
|
||||
id, asset_tag, lifecycle_status, status, first_seen_at, last_seen_at
|
||||
) VALUES (
|
||||
'00000000-0000-0000-0000-000000001502', 'PARTS-MINER',
|
||||
'in_repair', 'unknown', NULL, NULL
|
||||
);
|
||||
INSERT INTO repairs (id, miner_id, status, reason, opened_by)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000001503',
|
||||
'00000000-0000-0000-0000-000000001502', 'OPEN', 'OTHER',
|
||||
'00000000-0000-0000-0000-000000001501'
|
||||
);
|
||||
INSERT INTO repair_events (id, repair_id, sequence, event_type, created_by, payload)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000001504',
|
||||
'00000000-0000-0000-0000-000000001503', 1, 'REPAIR_STARTED',
|
||||
'00000000-0000-0000-0000-000000001501',
|
||||
'{"reason":"OTHER","from_lifecycle":"active","to_lifecycle":"in_repair"}'
|
||||
);
|
||||
INSERT INTO components (
|
||||
id, component_type, asset_id, manufacturer, manufacturer_serial, created_by
|
||||
) VALUES (
|
||||
'00000000-0000-4000-8000-000000001505', 'hashboard', 'HB-005',
|
||||
'MicroBT', 'SER-005', '00000000-0000-0000-0000-000000001501'
|
||||
);
|
||||
INSERT INTO repair_events (id, repair_id, sequence, event_type, created_by, payload)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000001506',
|
||||
'00000000-0000-0000-0000-000000001503', 2, 'REPAIR_PART_RECORDED',
|
||||
'00000000-0000-0000-0000-000000001501',
|
||||
'{"repair_part_id":"00000000-0000-4000-8000-000000001507","entry_kind":"tracked_component","component_id":"00000000-0000-4000-8000-000000001505","component_type":"hashboard","component_asset_id":"HB-005","component_manufacturer":"MicroBT","component_model":null,"component_manufacturer_serial":"SER-005","notes":null}'
|
||||
);
|
||||
INSERT INTO repair_parts (
|
||||
id, repair_id, component_id, component_type_snapshot,
|
||||
component_asset_id_snapshot, component_manufacturer_snapshot,
|
||||
component_model_snapshot, component_manufacturer_serial_snapshot,
|
||||
recorded_by, recorded_at, recorded_event_id
|
||||
) SELECT
|
||||
'00000000-0000-4000-8000-000000001507',
|
||||
'00000000-0000-0000-0000-000000001503',
|
||||
'00000000-0000-4000-8000-000000001505', 'hashboard', 'HB-005',
|
||||
'MicroBT', NULL, 'SER-005', '00000000-0000-0000-0000-000000001501',
|
||||
created_at, id FROM repair_events
|
||||
WHERE id = '00000000-0000-0000-0000-000000001506';
|
||||
|
||||
UPDATE components SET manufacturer = 'Updated Manufacturer'
|
||||
WHERE id = '00000000-0000-4000-8000-000000001505';
|
||||
DO $$
|
||||
BEGIN
|
||||
IF (SELECT component_manufacturer_snapshot FROM repair_parts
|
||||
WHERE id = '00000000-0000-4000-8000-000000001507') <> 'MicroBT' THEN
|
||||
RAISE EXCEPTION 'Tracked snapshot changed with live metadata';
|
||||
END IF;
|
||||
BEGIN
|
||||
INSERT INTO repair_parts (
|
||||
repair_id, consumable_description, quantity, unit, component_id,
|
||||
recorded_by, recorded_event_id
|
||||
) VALUES (
|
||||
'00000000-0000-0000-0000-000000001503', 'invalid', 1, 'each',
|
||||
'00000000-0000-4000-8000-000000001505',
|
||||
'00000000-0000-0000-0000-000000001501',
|
||||
'00000000-0000-0000-0000-000000001506'
|
||||
);
|
||||
RAISE EXCEPTION 'XOR violation was accepted';
|
||||
EXCEPTION WHEN check_violation OR raise_exception THEN NULL;
|
||||
END;
|
||||
END $$;
|
||||
|
||||
INSERT INTO repair_events (id, repair_id, sequence, event_type, created_by, payload)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000001508',
|
||||
'00000000-0000-0000-0000-000000001503', 3, 'REPAIR_PART_VOIDED',
|
||||
'00000000-0000-0000-0000-000000001501',
|
||||
'{"repair_part_id":"00000000-0000-4000-8000-000000001507","reason":"Wrong repair"}'
|
||||
);
|
||||
UPDATE repair_parts SET
|
||||
voided_by = '00000000-0000-0000-0000-000000001501',
|
||||
voided_at = (SELECT created_at FROM repair_events WHERE id = '00000000-0000-0000-0000-000000001508'),
|
||||
void_reason = 'Wrong repair',
|
||||
void_event_id = '00000000-0000-0000-0000-000000001508'
|
||||
WHERE id = '00000000-0000-4000-8000-000000001507';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
BEGIN
|
||||
UPDATE repair_parts SET void_reason = 'Changed'
|
||||
WHERE id = '00000000-0000-4000-8000-000000001507';
|
||||
RAISE EXCEPTION 'Second void mutation was accepted';
|
||||
EXCEPTION WHEN raise_exception THEN NULL;
|
||||
END;
|
||||
BEGIN
|
||||
DELETE FROM repair_parts WHERE id = '00000000-0000-4000-8000-000000001507';
|
||||
RAISE EXCEPTION 'Repair-part deletion was accepted';
|
||||
EXCEPTION WHEN raise_exception THEN NULL;
|
||||
END;
|
||||
BEGIN
|
||||
DELETE FROM repair_events WHERE id = '00000000-0000-0000-0000-000000001508';
|
||||
RAISE EXCEPTION 'Repair-event deletion was accepted';
|
||||
EXCEPTION WHEN raise_exception THEN NULL;
|
||||
END;
|
||||
END $$;
|
||||
SQL
|
||||
|
||||
if downgrade_output="$(run_alembic downgrade 0013_control_board_replacement 2>&1)"; then
|
||||
echo "Task 005 downgrade unexpectedly destroyed history" >&2
|
||||
exit 1
|
||||
fi
|
||||
[[ "${downgrade_output}" == *"Cannot downgrade Repair Parts Tracking"* ]]
|
||||
[[ "$(run_alembic current 2>/dev/null)" == *"0014_repair_parts_tracking"* ]]
|
||||
|
||||
echo "Testing empty database through head..."
|
||||
"${compose[@]}" exec -T postgres sh -ec \
|
||||
'createdb -U "$POSTGRES_USER" minerops_task005_empty'
|
||||
"${compose[@]}" run --rm --no-deps -e POSTGRES_DB=minerops_task005_empty \
|
||||
backend alembic upgrade head
|
||||
empty_revision="$(
|
||||
"${compose[@]}" exec -T postgres sh -ec \
|
||||
'psql -U "$POSTGRES_USER" -d minerops_task005_empty -Atc "SELECT version_num FROM alembic_version"'
|
||||
)"
|
||||
[[ "${empty_revision}" == "0014_repair_parts_tracking" ]]
|
||||
|
||||
echo "Repair Parts Tracking migration tests passed."
|
||||
Reference in new issue
Block a user