"""Profile readiness gate for filing-grade modelo work.
Loads the active :class:`domain.user_profile.values.UserProfileRecord`, builds a
:class:`application.user_profile.commands.ProfilePreflightReport`, projects
local-work applicability through :class:`domain.deadlines.models.TaxpayerProfile`,
and raises :class:`application.modelo.action_errors.ModeloProfileReadinessError` before
filing-grade work proceeds when required profile facts are missing. The
revision-specific preflight branch may receive a :class:`ModeloRevision` that
has already been resolved by an operator-facing readiness surface. The same gate
also refuses Modelo 130 and Modelo 303 target periods whose date span ends
before the profile's ``censo.activity_start_date``; those pre-activity periods
have no filing obligation and must not produce stale work, calculation,
verification, filing, or export state.
See Also:
:func:`require_profile_ready_for_work_unit`:
Replays the same readiness checks for an existing
:class:`~WorkUnit`.
:class:`application.user_profile.commands.ProfilePreflightReport`:
User-profile preflight result consumed by this application gate.
"""
from __future__ import annotations
from collections.abc import Mapping
from datetime import date
from typing import TYPE_CHECKING
from ...core.errors.severity import BaseSeverity
from ...core.modelo import Modelo
from ...core.parsing.dates import parse_iso8601_date
from ...core.period import Period
from ...domain.calculations.registry.applicability import (
ApplicabilityVerdict,
derive_modelo_applicability,
)
from ...domain.calculations.registry.authority import PinnedAuthorityOperation
from ...domain.calculations.registry.errors import RegistrySnapshotError
from ...domain.calculations.registry.ids import RevisionId
from ...domain.calculations.registry.irpf_income_categories import irpf_income_category_actividad_economica_token
from ...domain.calculations.registry.profile_grounding import (
ProfileKeyGrounding,
build_profile_grounding_index,
)
from ...domain.calculations.registry.schema import ModeloRevision
from ...domain.contribuyente.entity_type import entity_type_natural_person_token
from ...domain.modelos.work_unit import WorkUnit
from ...domain.user_profile.errors import ProfileNotFoundError
from ...domain.user_profile.values import ProfileSetupState, UserProfileRecord
from ..user_profile.commands import ProfilePreflightReport, ProfilePreflightRequirement, ProfileValidationIssue
from ..user_profile.completeness import missing_required_field_paths
from ..user_profile.preflight import (
ProfilePreflightService,
build_profile_preflight_requirement,
format_profile_preflight_requirement,
)
from ..user_profile.profile_record_repository import ProfileRecordRepository, take_invocation_profile_record
from ..user_profile.projections import projection_for_taxpayer, record_to_path_values
from ..user_profile.validation import MODELO_WORK_PROFILE_BASELINE_MISSING_CODE, ProfileValidationService
from .action_errors import ModeloProfileReadinessError
from .work_profile import ModeloWorkProfile
if TYPE_CHECKING:
from ...domain.calculations.registry.authority_artifact import ProfileDecodeContext
_PROFILE_ACTIVITY_START_PATH = "censo.activity_start_date"
_PRE_ACTIVITY_LIFECYCLE_MODELOS = frozenset({Modelo("130").value, Modelo("303").value})
_FILING_BASELINE_PROFILE_PATHS = ("identity.tax_id",)
_PROFILE_ACTIVITY_DESCRIPTION_PATH = "activities.description"
#: Shared with :mod:`.work_create_policy`, which runs the same applicability
#: block ahead of work-unit provisioning; kept public so that cross-module use
#: is a declared contract rather than a private-symbol reach.
BLOCKING_APPLICABILITY_VERDICTS = frozenset(
{
ApplicabilityVerdict.NOT_APPLICABLE,
ApplicabilityVerdict.ATTRIBUTION_PASS_THROUGH,
},
)
def _requirement_for_profile_path(
path: str,
*,
selector: str | None = None,
grounding_index: Mapping[str, ProfileKeyGrounding] | None = None,
profile_decode_context: ProfileDecodeContext,
) -> ProfilePreflightRequirement:
"""Build one requirement row for a raw (possibly row-indexed) profile path.
Thin call-site wrapper over the shared
:func:`application.user_profile.preflight.build_profile_preflight_requirement`,
resolving the live schema singleton - the one requirement-row builder
this package and :class:`application.user_profile.preflight.ProfilePreflightService`
both route through.
"""
return build_profile_preflight_requirement(
path,
schema=profile_decode_context.schema,
selector=selector,
grounding_index=grounding_index,
)
def _dedupe_requirements(
requirements: tuple[ProfilePreflightRequirement, ...] | list[ProfilePreflightRequirement],
) -> tuple[ProfilePreflightRequirement, ...]:
seen: set[tuple[str, str]] = set()
deduped: list[ProfilePreflightRequirement] = []
for requirement in requirements:
key = (requirement.section_key, requirement.field_key)
if key in seen:
continue
seen.add(key)
deduped.append(requirement)
return tuple(deduped)
def _modelo_work_baseline_paths(record: UserProfileRecord, *, modelo: str | None) -> tuple[str, ...]:
"""Return the universal and target-aware profile baseline paths.
``activities.description`` is a real censo fact for economic-activity
work, but it is not a universal filing precondition. A resident
pensioner/landlord can legitimately have Modelo 100 as their only
applicable return and must not invent an activity merely to create it.
"""
values = record_to_path_values(record)
income_categories = frozenset(
token.strip() for token in values.get("taxpayer_type.irpf_income_categories", "").split(",") if token.strip()
)
declares_economic_activity = irpf_income_category_actividad_economica_token().value in income_categories
if modelo is None:
if (
values.get("taxpayer_type.entity_type") != entity_type_natural_person_token().value
or not income_categories
or declares_economic_activity
):
return (*_FILING_BASELINE_PROFILE_PATHS, _PROFILE_ACTIVITY_DESCRIPTION_PATH)
return _FILING_BASELINE_PROFILE_PATHS
if modelo.strip() != Modelo("100").value or not income_categories or declares_economic_activity:
return (*_FILING_BASELINE_PROFILE_PATHS, _PROFILE_ACTIVITY_DESCRIPTION_PATH)
return _FILING_BASELINE_PROFILE_PATHS
[docs]
def modelo_work_profile_baseline_missing_paths(
record: UserProfileRecord,
*,
modelo: str | None = None,
) -> tuple[str, ...]:
"""Return profile facts required before filing-grade work for a target.
Without a target this returns the profile-status baseline: every
non-natural entity, an economically active natural person, and an
incompletely declared natural person need an activity description. A
natural person with declared non-business income needs only a tax id.
Targeted Modelo 100 checks use the same distinction; all other targets
retain the existing censo-activity baseline.
Args:
record: Active :class:`domain.user_profile.values.UserProfileRecord`
projected into schema-path values.
modelo: Optional target modelo used to resolve conditional baseline
requirements.
"""
values = record_to_path_values(record)
return tuple(
path for path in _modelo_work_baseline_paths(record, modelo=modelo) if not values.get(path, "").strip()
)
[docs]
def modelo_work_profile_baseline_validation_issues(record: UserProfileRecord) -> tuple[ProfileValidationIssue, ...]:
"""Return profile-status issues for universal modelo-work baseline fields.
Args:
record: Active :class:`domain.user_profile.values.UserProfileRecord`
checked against the filing-grade baseline.
Returns:
Tuple of :class:`application.user_profile.commands.ProfileValidationIssue`
instances for missing filing-grade baseline facts.
"""
return tuple(
ProfileValidationIssue(
severity=BaseSeverity.ERROR,
code=MODELO_WORK_PROFILE_BASELINE_MISSING_CODE,
path=path,
message=f"modelo work profile baseline field {path} is missing",
)
for path in modelo_work_profile_baseline_missing_paths(record)
)
def _validation_missing_requirements(
record: UserProfileRecord,
*,
grounding_index: Mapping[str, ProfileKeyGrounding] | None = None,
profile_decode_context: ProfileDecodeContext,
) -> tuple[ProfilePreflightRequirement, ...]:
validation = ProfileValidationService(schema=profile_decode_context.schema).validate_record(record)
requirements: list[ProfilePreflightRequirement] = []
for issue in validation.issues:
if issue.severity.value != "error":
continue
path = issue.path or issue.code
requirements.append(
_requirement_for_profile_path(
path,
selector=issue.path or f"profile.validation.{issue.code}",
grounding_index=grounding_index,
profile_decode_context=profile_decode_context,
),
)
return tuple(requirements)
[docs]
def modelo_work_profile_preflight_report(
*,
record: UserProfileRecord,
modelo: str,
revision_id: RevisionId,
filing_year: int,
period: Period,
revision: ModeloRevision | None = None,
resolve_revision_when_missing: bool = True,
profile_decode_context: ProfileDecodeContext,
operation: PinnedAuthorityOperation,
) -> ProfilePreflightReport:
"""Return the profile-field report enforced by the modelo work creation gate.
This combines the filing-grade baseline that every modelo work unit needs
with the modelo/revision-specific profile selectors. Public readiness
surfaces consume this function so they cannot claim profile readiness before
:func:`application.modelo.work_lifecycle.create_work_unit` would reject the same active
profile.
Args:
record: Active :class:`domain.user_profile.values.UserProfileRecord`.
modelo: Modelo code being checked.
revision_id: Registry revision identifier for the target modelo work.
filing_year: Filing year for the target period.
period: Target :class:`core.period.Period` used for registry revision
resolution and profile selector evaluation.
revision: Optional :class:`ModeloRevision` supplied when the caller has
already resolved the target revision.
resolve_revision_when_missing: Whether to resolve the registry
revision when ``revision`` is not supplied.
profile_decode_context: Decode context from the caller-held authority
operation used to interpret the encrypted profile.
operation: Caller-held pinned authority operation used to select the
target revision and profile schema.
Returns:
:class:`application.user_profile.commands.ProfilePreflightReport` combining
baseline, validation, and modelo/revision-specific missing requirements.
"""
if revision is None and resolve_revision_when_missing:
report = _report_for_target(
record=record,
modelo=modelo,
revision_id=revision_id,
filing_year=filing_year,
period=period,
profile_decode_context=profile_decode_context,
operation=operation,
)
else:
report = ProfilePreflightService(schema=profile_decode_context.schema).report(
record=record,
modelo=modelo,
revision_id=revision_id,
period=period,
revision=revision,
)
grounding_index = build_profile_grounding_index(operation)
baseline = tuple(
_requirement_for_profile_path(
path,
grounding_index=grounding_index,
profile_decode_context=profile_decode_context,
)
for path in modelo_work_profile_baseline_missing_paths(record, modelo=modelo)
)
missing = _dedupe_requirements(
(
*baseline,
*_validation_missing_requirements(
record,
grounding_index=grounding_index,
profile_decode_context=profile_decode_context,
),
*report.missing,
),
)
return report.model_copy(update={"missing": missing, "ready": not missing})
def _report_for_target(
*,
record: UserProfileRecord,
modelo: str,
revision_id: RevisionId,
filing_year: int,
period: Period,
profile_decode_context: ProfileDecodeContext,
operation: PinnedAuthorityOperation,
) -> ProfilePreflightReport:
try:
revision = operation.revision_for_context(
modelo,
filing_year=filing_year,
period=period.registry_token,
revision_id=revision_id,
)
except (FileNotFoundError, RegistrySnapshotError):
revision = None
return ProfilePreflightService(schema=profile_decode_context.schema).report(
record=record,
modelo=modelo,
revision_id=revision_id,
period=period,
revision=revision,
)
[docs]
def profile_activity_start_date(record: UserProfileRecord) -> date | None:
"""Read the effective ``censo.activity_start_date`` through the canonical projection.
Which of several live facts at one path is *effective* is owned by
:func:`application.user_profile.projections.record_to_path_values`, which orders them by
``valid_from`` so the chronologically last window wins. Scanning declaration
order answers a different question: a record whose later window was declared
first resolves to the earlier date, and because this value decides whether a
target period is refused as pre-activity, the disagreement fails open — a
period before the effective activity start is admitted for work.
Core types:
:class:`~cadrumo.domain.user_profile.values.UserProfileRecord`.
"""
rendered = record_to_path_values(record).get(_PROFILE_ACTIVITY_START_PATH)
if rendered is None:
return None
return parse_iso8601_date(rendered)
def _require_not_pre_activity_period(
*,
record: UserProfileRecord,
bucket_id: str,
modelo: str,
filing_year: int,
period: Period,
) -> None:
refusal = pre_activity_period_refusal(
record=record,
bucket_id=bucket_id,
modelo=modelo,
filing_year=filing_year,
period=period,
)
if refusal is None:
return
message, context = refusal
raise ModeloProfileReadinessError(
message,
context=context,
)
def _require_modelo_applicable_for_local_work(
*,
record: UserProfileRecord,
bucket_id: str,
modelo: str,
operation: PinnedAuthorityOperation,
) -> None:
refusal = modelo_applicability_refusal(
record=record,
bucket_id=bucket_id,
modelo=modelo,
operation=operation,
)
if refusal is None:
return
message, context = refusal
raise ModeloProfileReadinessError(
message,
context=context,
)
[docs]
def modelo_applicability_refusal(
*,
record: UserProfileRecord,
bucket_id: str,
modelo: str,
operation: PinnedAuthorityOperation,
) -> tuple[str, dict[str, str]] | None:
"""Return the local-work applicability refusal for a target, if any.
Args:
record: Active :class:`domain.user_profile.values.UserProfileRecord`
projected into taxpayer facts for the modelo applicability check.
bucket_id: Active profile bucket identifier included in the refusal
context.
modelo: Modelo code being checked.
operation: Caller-held pinned operation for applicability facts.
"""
modelo_code = modelo.strip()
profile = projection_for_taxpayer(record, schema=operation.profile_schema())
applicability = derive_modelo_applicability(profile, modelo_code, operation=operation)
if applicability.verdict not in BLOCKING_APPLICABILITY_VERDICTS:
return None
return (
f"Modelo {modelo_code} is not applicable to the active profile: {applicability.reason}",
{
"bucket_id": bucket_id,
"modelo": modelo_code,
"applicability_verdict": applicability.verdict.value,
"legal_refs": ", ".join(applicability.legal_refs),
},
)
[docs]
def pre_activity_period_refusal(
*,
record: UserProfileRecord,
bucket_id: str,
modelo: str,
filing_year: int,
period: Period,
) -> tuple[str, dict[str, str | int]] | None:
"""Return the pre-activity lifecycle refusal for a target, if any.
Args:
record: Active :class:`domain.user_profile.values.UserProfileRecord`
carrying the profile facts used to resolve
``censo.activity_start_date``.
bucket_id: Active profile bucket identifier included in the refusal
context.
modelo: Modelo code being checked.
filing_year: Filing year for the target period.
period: Target :class:`core.period.Period` whose date span is compared
against the profile activity-start date.
"""
modelo_code = modelo.strip()
if modelo_code not in _PRE_ACTIVITY_LIFECYCLE_MODELOS or not period.has_date_span():
return None
activity_start_date = profile_activity_start_date(record)
if activity_start_date is None:
return None
period_end_date = period.end_date
if period_end_date >= activity_start_date:
return None
return (
f"Modelo {modelo_code} {filing_year} {period.registry_token} is before the profile "
f"activity-start date {activity_start_date.isoformat()}; the filing period ends on "
f"{period_end_date.isoformat()}, so no Modelo {modelo_code} work unit, calculation, or verification "
"may proceed for this pre-activity period.",
{
"bucket_id": bucket_id,
"modelo": modelo_code,
"filing_year": filing_year,
"period": period.registry_token,
"activity_start_date": activity_start_date.isoformat(),
"period_end_date": period_end_date.isoformat(),
},
)
def _require_profile_filing_ready(
*,
record: UserProfileRecord,
bucket_id: str,
modelo: str,
filing_year: int,
period: Period,
grounding_index: Mapping[str, ProfileKeyGrounding] | None = None,
profile_decode_context: ProfileDecodeContext,
) -> None:
"""Refuse when a baseline or validation-required profile fact is missing.
``grounding_index`` is optional and, when omitted, this stays the
registry-free check :func:`require_existing_profile_baseline_ready_for_modelo_work`
relies on. :func:`require_profile_ready_for_modelo_work` passes the
memoised grounding index so its refusal carries real legal grounding
instead of a bare label.
"""
missing: list[ProfilePreflightRequirement] = [
_requirement_for_profile_path(
path,
grounding_index=grounding_index,
profile_decode_context=profile_decode_context,
)
for path in modelo_work_profile_baseline_missing_paths(record, modelo=modelo)
]
seen = {(item.section_key, item.field_key) for item in missing}
for requirement in _validation_missing_requirements(
record,
grounding_index=grounding_index,
profile_decode_context=profile_decode_context,
):
key = (requirement.section_key, requirement.field_key)
if key not in seen:
seen.add(key)
missing.append(requirement)
if not missing:
return
raise ModeloProfileReadinessError(
translated_message="application.modelo.errors.profile_readiness_missing",
context={
"modelo": modelo,
"filing_year": filing_year,
"period": period.registry_token,
"missing": ", ".join(format_profile_preflight_requirement(requirement) for requirement in missing),
},
)
def _render_missing_requirement(requirement: ProfilePreflightRequirement) -> str:
"""Render one missing requirement as its label plus the articles demanding it.
A label alone tells an operator WHICH field is missing; the refusal's job is
also to say on whose authority. Where the registry grounds the field, the
refs follow the label in parentheses; where it grounds nothing, the label
stands alone rather than trailing an empty bracket.
"""
if not requirement.legal_refs:
return requirement.label
return f"{requirement.label} ({', '.join(requirement.legal_refs)})"
[docs]
def load_modelo_work_profile(
*,
bucket_id: str,
profile_decode_context: ProfileDecodeContext,
) -> ModeloWorkProfile | None:
"""Load the bucket's profile for modelo work, or ``None`` when no session serves it."""
try:
repository = ProfileRecordRepository.for_current_session(
bucket_id,
profile_decode_context=profile_decode_context,
)
return ModeloWorkProfile(
record=take_invocation_profile_record(bucket_id) or repository.load(bucket_id),
profile_decode_context=repository.session.profile_decode_context,
)
except ProfileNotFoundError:
return None
def _load_profile_for_modelo_work(
*,
bucket_id: str,
profile_decode_context: ProfileDecodeContext,
profile: ModeloWorkProfile | None,
) -> ModeloWorkProfile:
loaded = profile or load_modelo_work_profile(bucket_id=bucket_id, profile_decode_context=profile_decode_context)
if loaded is None:
raise ModeloProfileReadinessError(
translated_message="application.modelo.errors.profile_readiness_profile_missing",
context={"bucket_id": bucket_id},
)
return loaded
def _require_profile_setup_complete(
*,
record: UserProfileRecord,
bucket_id: str,
modelo: str,
profile_decode_context: ProfileDecodeContext,
grounding_index: Mapping[str, ProfileKeyGrounding] | None = None,
) -> None:
"""Refuse setup profiles before any filing-grade readiness checks run."""
if record.setup_state is not ProfileSetupState.INCOMPLETE:
return
schema = profile_decode_context.schema
missing_paths = missing_required_field_paths(schema, record_to_path_values(record))
if missing_paths:
grounding = grounding_index or {}
missing_labels = ", ".join(
_render_missing_requirement(
build_profile_preflight_requirement(path, schema=schema, grounding_index=grounding),
)
for path in missing_paths
)
raise ModeloProfileReadinessError(
translated_message="application.modelo.errors.profile_readiness_setup_incomplete_missing",
context={"bucket_id": bucket_id, "modelo": modelo, "missing": missing_labels},
)
raise ModeloProfileReadinessError(
translated_message="application.modelo.errors.profile_readiness_setup_incomplete",
context={"bucket_id": bucket_id, "modelo": modelo},
)
def _raise_if_profile_preflight_missing(
report: ProfilePreflightReport,
*,
modelo: str,
filing_year: int,
period: Period,
) -> None:
if report.ready:
return
raise ModeloProfileReadinessError(
translated_message="application.modelo.errors.profile_readiness_missing",
context={
"modelo": modelo,
"filing_year": filing_year,
"period": period.registry_token,
"missing": ", ".join(format_profile_preflight_requirement(requirement) for requirement in report.missing),
},
)
[docs]
def require_profile_ready_for_modelo_work(
*,
bucket_id: str,
modelo: str,
revision_id: RevisionId,
filing_year: int,
period: Period,
enforce_applicability: bool = True,
profile_decode_context: ProfileDecodeContext,
operation: PinnedAuthorityOperation,
profile: ModeloWorkProfile | None = None,
) -> ModeloWorkProfile:
"""Refuse filing-grade modelo work when the active profile is not eligible.
Loads the bucket's :class:`domain.user_profile.values.UserProfileRecord`,
evaluates modelo-specific profile requirements through
:class:`application.user_profile.commands.ProfilePreflightReport`, and then
applies the pre-activity period check for lifecycle modelos whose obligation
starts at ``censo.activity_start_date``. Unlike
:func:`require_existing_profile_baseline_ready_for_modelo_work`, this gate
passes the live registry authority through both the baseline/validation
refusal and the full preflight report, so a raised
:class:`ModeloProfileReadinessError` carries real ``legal_refs`` for every
missing field the registry grounds - the memoised
``build_profile_grounding_index`` keeps the added per-call cost bounded on
this hot path. ``profile`` is the record the calling command already
loaded for this target; when omitted the gate loads it. Returns the record
the gate checked, so a caller that let the gate load it passes that same
record on instead of decrypting it again.
"""
loaded = _load_profile_for_modelo_work(
bucket_id=bucket_id,
profile_decode_context=profile_decode_context,
profile=profile,
)
record, resolved_profile_decode_context = loaded.record, loaded.profile_decode_context
_require_profile_setup_complete(
record=record,
bucket_id=bucket_id,
modelo=modelo,
profile_decode_context=resolved_profile_decode_context,
grounding_index=build_profile_grounding_index(operation),
)
grounding_index: Mapping[str, ProfileKeyGrounding] = build_profile_grounding_index(operation)
applicability_first = enforce_applicability and modelo.strip() in _PRE_ACTIVITY_LIFECYCLE_MODELOS
if applicability_first:
_require_modelo_applicable_for_local_work(
record=record,
bucket_id=bucket_id,
modelo=modelo,
operation=operation,
)
_require_profile_filing_ready(
record=record,
bucket_id=bucket_id,
modelo=modelo,
filing_year=filing_year,
period=period,
grounding_index=grounding_index,
profile_decode_context=resolved_profile_decode_context,
)
if enforce_applicability and not applicability_first:
_require_modelo_applicable_for_local_work(
record=record,
bucket_id=bucket_id,
modelo=modelo,
operation=operation,
)
report = modelo_work_profile_preflight_report(
record=record,
modelo=modelo,
revision_id=revision_id,
filing_year=filing_year,
period=period,
profile_decode_context=resolved_profile_decode_context,
operation=operation,
)
_raise_if_profile_preflight_missing(report, modelo=modelo, filing_year=filing_year, period=period)
_require_not_pre_activity_period(
record=record,
bucket_id=bucket_id,
modelo=modelo,
filing_year=filing_year,
period=period,
)
return loaded
[docs]
def require_existing_profile_baseline_ready_for_modelo_work(
*,
bucket_id: str,
modelo: str,
filing_year: int,
period: Period,
enforce_applicability: bool = True,
profile_decode_context: ProfileDecodeContext,
operation: PinnedAuthorityOperation,
profile: ModeloWorkProfile | None = None,
) -> None:
"""Refuse plainly incomplete existing profiles before registry work.
This early gate is used by :func:`application.modelo.work_lifecycle.create_work_unit`
before the registry revision and period are validated. It catches missing
baseline profile facts, local-work applicability refusals, and pre-activity
lifecycle periods without requiring a resolvable :class:`ModeloRevision`.
Missing profiles still pass through so the later full readiness gate can
raise the canonical missing-profile error. ``profile`` is the record the
calling command already loaded; when omitted the gate loads it.
"""
loaded = profile or load_modelo_work_profile(bucket_id=bucket_id, profile_decode_context=profile_decode_context)
if loaded is None:
return
record = loaded.record
applicability_first = enforce_applicability and modelo.strip() in _PRE_ACTIVITY_LIFECYCLE_MODELOS
if applicability_first:
_require_modelo_applicable_for_local_work(
record=record,
bucket_id=bucket_id,
modelo=modelo,
operation=operation,
)
_require_profile_filing_ready(
record=record,
bucket_id=bucket_id,
modelo=modelo,
filing_year=filing_year,
period=period,
profile_decode_context=loaded.profile_decode_context,
)
if enforce_applicability and not applicability_first:
_require_modelo_applicable_for_local_work(
record=record,
bucket_id=bucket_id,
modelo=modelo,
operation=operation,
)
_require_not_pre_activity_period(
record=record,
bucket_id=bucket_id,
modelo=modelo,
filing_year=filing_year,
period=period,
)
[docs]
def require_profile_ready_for_work_unit(
work_unit: WorkUnit,
*,
enforce_applicability: bool = True,
profile_decode_context: ProfileDecodeContext,
operation: PinnedAuthorityOperation,
profile: ModeloWorkProfile | None = None,
) -> ModeloWorkProfile:
"""Run the profile readiness gate for an existing work unit.
Calculation, verification, filing, and export services call this wrapper so
a previously created :class:`~WorkUnit` is rechecked
against the current :class:`domain.user_profile.values.UserProfileRecord`
before any filing-grade mutation proceeds. Returns the checked record.
"""
return require_profile_ready_for_modelo_work(
bucket_id=work_unit.bucket_id,
modelo=str(work_unit.modelo),
revision_id=work_unit.revision_id,
filing_year=work_unit.filing_year,
period=work_unit.period,
enforce_applicability=enforce_applicability,
profile_decode_context=profile_decode_context,
operation=operation,
profile=profile,
)
__all__ = [
"BLOCKING_APPLICABILITY_VERDICTS",
"load_modelo_work_profile",
"modelo_applicability_refusal",
"modelo_work_profile_baseline_missing_paths",
"modelo_work_profile_baseline_validation_issues",
"modelo_work_profile_preflight_report",
"pre_activity_period_refusal",
"profile_activity_start_date",
"require_existing_profile_baseline_ready_for_modelo_work",
"require_profile_ready_for_modelo_work",
"require_profile_ready_for_work_unit",
]