Files
pesco-ncr/backend/app/services/numbering.py
ang3l12 dea316b113 Initial commit: PESCO NCR system
Complete Non-Conformance Report system replacing the PowerApps/SharePoint
prototype: FastAPI + SQLAlchemy 2 (async) + Alembic + MySQL 8 backend,
React 18 + Vite + TypeScript + MUI frontend, Entra ID auth (MSAL / JWKS,
group-gated), Microsoft Graph delegated Mail.Send notifications (OBO),
six-stage workflow state machine with server-side enforcement, atomic
NCR-YYYY-NNNN numbering, attachments with camera capture, immutable
field-level audit trail, admin reopen, reports + CSV export, WeasyPrint
PDF traveler, Power BI reporting views + read-only DB user, documented
VISUAL ERP job-lookup stub, pytest suite (26 tests), docker-compose
deployment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 11:41:22 -06:00

57 lines
2.0 KiB
Python

"""Atomic NCR number allocation.
Format: NCR-YYYY-NNNN (zero-padded, per-calendar-year sequence).
Strategy: UPDATE-first on the per-year row in ncr_sequences. The UPDATE takes
a row lock (InnoDB) / reserved write lock (SQLite) that is held until the
enclosing transaction commits, so two concurrent submissions serialize and can
never read the same sequence value. If the year row doesn't exist yet
(first NCR of a new year), it is inserted inside a SAVEPOINT; a losing racer
gets an IntegrityError, rolls back only the savepoint, and proceeds to the
UPDATE which now finds the winner's row.
"""
from datetime import datetime
from sqlalchemy import select, update
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import NcrSequence
from app.models.base import utcnow
async def allocate_ncr_number(
db: AsyncSession, now: datetime | None = None
) -> tuple[str, int, int]:
"""Allocate the next NCR number inside the caller's transaction.
Returns (ncr_number, year, seq). Must be called within the same
transaction that inserts the NCR so the sequence row lock is held
until commit.
"""
year = (now or utcnow()).year
result = await db.execute(
update(NcrSequence)
.where(NcrSequence.year == year)
.values(last_seq=NcrSequence.last_seq + 1)
)
if result.rowcount == 0:
# First NCR of this calendar year — create the sequence row.
try:
async with db.begin_nested():
db.add(NcrSequence(year=year, last_seq=0))
await db.flush()
except IntegrityError:
pass # another request created it first; fall through to UPDATE
await db.execute(
update(NcrSequence)
.where(NcrSequence.year == year)
.values(last_seq=NcrSequence.last_seq + 1)
)
seq = (
await db.execute(select(NcrSequence.last_seq).where(NcrSequence.year == year))
).scalar_one()
return f"NCR-{year}-{seq:04d}", year, seq