BE-PR-004: implement EssentialCode encoding rules
- Format: {proposal_code}:E{seq:05x} (e.g. PROJ01:P00001:E00001)
- Prefix 'E' for Essential, 5-digit zero-padded hex sequence
- Sequence scoped per Proposal, derived from max existing code
- No separate counter table needed (uses max-suffix approach)
- Supports batch_offset for bulk creation during Proposal Accept
- Includes validate_essential_code() helper
This commit is contained in:
123
app/services/essential_code.py
Normal file
123
app/services/essential_code.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""EssentialCode generation service.
|
||||
|
||||
Encoding rule: {proposal_code}:E{seq:05x}
|
||||
|
||||
Where:
|
||||
- ``proposal_code`` is the parent Proposal's code (e.g. ``PROJ01:P00001``)
|
||||
- ``E`` is the fixed Essential prefix
|
||||
- ``seq`` is a 5-digit zero-padded hex sequence scoped per Proposal
|
||||
|
||||
Sequence assignment:
|
||||
Uses the max existing ``essential_code`` suffix under the same Proposal
|
||||
to derive the next value. No separate counter table is needed because
|
||||
Essentials are always scoped to a single Proposal and created one at a
|
||||
time (or in a small batch during Proposal Accept).
|
||||
|
||||
Examples:
|
||||
PROJ01:P00001:E00001
|
||||
PROJ01:P00001:E00002
|
||||
HRBFRG:P00003:E0000a
|
||||
|
||||
See: NEXT_WAVE_DEV_DIRECTION.md §8.5 / §8.6
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import func as sa_func
|
||||
|
||||
from app.models.essential import Essential
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.proposal import Proposal
|
||||
|
||||
# Matches the trailing hex portion after ":E"
|
||||
_SUFFIX_RE = re.compile(r":E([0-9a-fA-F]+)$")
|
||||
|
||||
# Fixed prefix letter for Essential codes
|
||||
ESSENTIAL_PREFIX = "E"
|
||||
|
||||
# Width of the hex sequence portion
|
||||
SEQ_WIDTH = 5
|
||||
|
||||
|
||||
def _extract_seq(essential_code: str) -> int:
|
||||
"""Extract the numeric sequence from an EssentialCode string.
|
||||
|
||||
Returns 0 if the code doesn't match the expected pattern.
|
||||
"""
|
||||
m = _SUFFIX_RE.search(essential_code)
|
||||
if m:
|
||||
return int(m.group(1), 16)
|
||||
return 0
|
||||
|
||||
|
||||
def _max_seq_for_proposal(db: "Session", proposal_id: int) -> int:
|
||||
"""Return the highest existing sequence number for a given Proposal.
|
||||
|
||||
Returns 0 if no Essentials exist yet.
|
||||
"""
|
||||
essentials = (
|
||||
db.query(Essential.essential_code)
|
||||
.filter(Essential.proposal_id == proposal_id)
|
||||
.all()
|
||||
)
|
||||
if not essentials:
|
||||
return 0
|
||||
return max(_extract_seq(row[0]) for row in essentials)
|
||||
|
||||
|
||||
def generate_essential_code(
|
||||
db: "Session",
|
||||
proposal: "Proposal",
|
||||
*,
|
||||
batch_offset: int = 0,
|
||||
) -> str:
|
||||
"""Generate the next EssentialCode for *proposal*.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db:
|
||||
Active SQLAlchemy session (must be inside a transaction so the
|
||||
caller can flush/commit to avoid race conditions).
|
||||
proposal:
|
||||
The parent Proposal ORM instance. Its ``proposal_code``
|
||||
(hybrid property over ``propose_code``) is used as the prefix.
|
||||
batch_offset:
|
||||
When creating multiple Essentials in a single transaction (e.g.
|
||||
during Proposal Accept), pass an incrementing offset (0, 1, 2, …)
|
||||
so each call returns a unique code without needing intermediate
|
||||
flushes.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
A unique EssentialCode such as ``PROJ01:P00001:E00001``.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If the parent Proposal has no code assigned.
|
||||
"""
|
||||
proposal_code = proposal.proposal_code
|
||||
if not proposal_code:
|
||||
raise ValueError(
|
||||
f"Proposal id={proposal.id} has no proposal_code; "
|
||||
"cannot generate EssentialCode"
|
||||
)
|
||||
|
||||
current_max = _max_seq_for_proposal(db, proposal.id)
|
||||
next_seq = current_max + 1 + batch_offset
|
||||
suffix = format(next_seq, "x").upper().zfill(SEQ_WIDTH)
|
||||
return f"{proposal_code}:{ESSENTIAL_PREFIX}{suffix}"
|
||||
|
||||
|
||||
def validate_essential_code(code: str) -> bool:
|
||||
"""Check whether *code* conforms to the EssentialCode format.
|
||||
|
||||
Expected format: ``{any}:E{hex_digits}``
|
||||
"""
|
||||
return bool(_SUFFIX_RE.search(code))
|
||||
Reference in New Issue
Block a user