Archive copy must succeed before a packet counts as filed (Gitea issue 57)
ci / frontend (pull_request) Successful in 1m0s
ci / server (pull_request) Successful in 13m45s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Successful in 5m49s

FILE checks the archive folder and copies the packet inside the filing
transaction. A failed check or copy rolls the filing back, so the packet
stays in its previous stage; the person filing gets 503 archive_failed,
and in its own transaction the server writes an archive.filing_blocked
audit row and raises the critical archive_failed condition to Admin for
that packet (who tried, folder, error). The next successful filing clears
it. The archive-copy job now only catches up filings made while the
archive was off. PLAN question 53, CHANGELOG, FEED and SECURITY updated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-11-03 15:52:09 -07:00
co-authored by Claude Opus 5.5
parent 5f8165c833
commit 6adaf7bf25
11 changed files with 254 additions and 98 deletions
+33 -24
View File
@@ -18,9 +18,9 @@ from psycopg.types.json import Jsonb
from pydantic import BaseModel, Field
from .. import audit, events, policy
from ..domain import archive, packets, search, shop
from ..domain import assets as asset_store
from ..domain import ncr as ncrs
from ..domain import packets, search, shop
from ..domain import qc as qc_dom
from ..domain.pipeline import (
Expect,
@@ -543,6 +543,10 @@ TRANSITION_FIELDS: dict[policy.Action, tuple[str, ...]] = {
A.FILE: ("note",),
A.ADMIN_REOPEN: ("reason",),
}
MSG_ARCHIVE = (
"The packet could not be copied to the archive: {reason}. Nothing was filed, and Admin has been told."
" File it again once the archive works."
)
DEPT_ACTIONS = (A.RELEASE_TO_SHOP, A.ROUTE_FROM_REVIEW, A.QC_ACCEPT_AND_ROUTE)
@@ -591,29 +595,34 @@ def transition(
else:
del extra["stamp"]
content = {"action": str(action), "packet_id": packet_id, "expect": body.expect.model_dump(), **extra}
with packet_write(
d,
p,
packet_id,
action,
request=request,
extra=extra,
it_reason=body.it_reason,
sign=body.sign,
expect=body.expect,
sign_content=content,
policy_principal=pp,
) as w:
if action is A.ASSIGN:
ok = conn.execute(
"SELECT 1 FROM stations WHERE id = %s AND dept_code = %s AND active",
(extra["station_id"], w.state.dept),
).fetchone()
if ok is None:
raise ApiError(422, "invalid_station", "Choose a station in this department.")
active_user(conn, extra["welder_id"], "welder")
shop.welder_check(conn, w.state.dept, str(extra["welder_id"]), int(extra["station_id"]))
apply_transition(w)
try:
with packet_write(
d,
p,
packet_id,
action,
request=request,
extra=extra,
it_reason=body.it_reason,
sign=body.sign,
expect=body.expect,
sign_content=content,
policy_principal=pp,
) as w:
if action is A.ASSIGN:
ok = conn.execute(
"SELECT 1 FROM stations WHERE id = %s AND dept_code = %s AND active",
(extra["station_id"], w.state.dept),
).fetchone()
if ok is None:
raise ApiError(422, "invalid_station", "Choose a station in this department.")
active_user(conn, extra["welder_id"], "welder")
shop.welder_check(conn, w.state.dept, str(extra["welder_id"]), int(extra["station_id"]))
apply_transition(w)
except archive.FilingCopyFailed as e:
# The filing rolled back (Gitea issue 57); record it and tell Admin in a transaction of its own.
archive.record_filing_failure(conn, p, packet_id, e)
raise ApiError(503, "archive_failed", MSG_ARCHIVE.format(reason=e.message.rstrip("."))) from None
return _read(d, pp, packet_id)