Skip to content
Draft
Show file tree
Hide file tree
Changes from 72 commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
48f9185
test(hire): reject identity runtime subclasses
seonghobae Aug 21, 2026
ef3f995
test(hire): reject validation-bypassing contract subclasses
seonghobae Aug 21, 2026
a78edad
fix(hire): protect governed identity and contract runtime types
seonghobae Aug 21, 2026
15c3ffc
test(people): reject mutation runtime type confusion
seonghobae Aug 21, 2026
6f9db10
test(people): reject idempotency evidence runtime confusion
seonghobae Aug 21, 2026
e98cd1c
fix(people): protect mutation identity and idempotency runtime types
seonghobae Aug 21, 2026
ad38eb4
test(people): reject forged hire authority runtime types
seonghobae Aug 22, 2026
63eb051
fix(people): require exact hire authority runtime types
seonghobae Aug 22, 2026
2762349
test(people): reject forged mutation authorization subtype
seonghobae Aug 22, 2026
76cb5b0
fix(people): require exact mutation authorization type
seonghobae Aug 22, 2026
e859d20
test(people): reject forged mutation command subtypes
seonghobae Aug 22, 2026
0196bf5
fix(people): require exact mutation command runtime types
seonghobae Aug 22, 2026
be156d8
test(people): reject forged mutation text evidence
seonghobae Aug 22, 2026
1967d7b
fix(people): require exact hire display-name text
seonghobae Aug 22, 2026
23fc4f4
fix(people): require exact governed mutation text
seonghobae Aug 22, 2026
c397053
fix(people): close remaining governed text gaps
seonghobae Aug 28, 2026
07e4230
chore(people): adopt protected develop without rewriting runtime hard…
seonghobae Sep 3, 2026
0c53d40
merge(people): adopt protected workflow consolidation
seonghobae Sep 4, 2026
399010c
merge(people): preserve #161 changelog delta after restack
seonghobae Sep 4, 2026
1968592
fix(ci): reseal People manifest after protected restack
seonghobae Sep 4, 2026
cde8df2
test(people): reject executable hire decision timestamps
seonghobae Sep 4, 2026
9888223
fix(people): exact-gate hire decision time providers
seonghobae Sep 4, 2026
61cba0f
fix(people): restore hire insert contract after timestamp repair
seonghobae Sep 4, 2026
42d988a
test(people): document executable timezone tripwire
seonghobae Sep 4, 2026
792dfe3
test(people): reject executable durable hire UUID evidence
seonghobae Sep 4, 2026
6443ee5
fix(people): exact-gate durable hire UUID evidence
seonghobae Sep 4, 2026
2ac5a11
test(people): reject executable hire idempotency digest text
seonghobae Sep 4, 2026
d81260f
fix(people): exact-gate durable hire idempotency digest text
seonghobae Sep 4, 2026
878e98b
test(people): reject executable hire provenance text
seonghobae Sep 4, 2026
c260b05
fix(people): exact-gate durable hire provenance text
seonghobae Sep 4, 2026
5514b74
test(people): reject executable durable hire row containers
seonghobae Sep 4, 2026
adab344
fix(people): exact-gate durable hire row containers
seonghobae Sep 4, 2026
cf76786
test(people): reject executable generic mutation row containers
seonghobae Sep 4, 2026
34c6e55
fix(people): exact-gate generic mutation row containers
seonghobae Sep 4, 2026
e7f84b2
test(people): reject executable generic mutation durable scalars
seonghobae Sep 4, 2026
36bdc32
fix(people): exact-gate generic mutation durable scalars
seonghobae Sep 4, 2026
0498eaa
test(people): reject executable generic replay digest
seonghobae Sep 4, 2026
55033e7
fix(people): exact-gate generic replay digest
seonghobae Sep 4, 2026
19f2129
test(people): reject executable persisted status text
seonghobae Sep 4, 2026
2c0b114
fix(people): exact-gate persisted status codes
seonghobae Sep 4, 2026
ad1c1db
test(people): reject executable persisted allocation Decimal
seonghobae Sep 4, 2026
3a63e92
fix(people): exact-gate persisted allocation Decimal
seonghobae Sep 4, 2026
11c30cd
test(people): reject executable Position parent UUID evidence
seonghobae Sep 4, 2026
46884c2
fix(people): validate Position parent UUIDs before equality
seonghobae Sep 4, 2026
3e7eb20
test(people): prove post-construction mutation escapes runtime integrity
seonghobae Sep 5, 2026
e278023
fix(people): revalidate mutation evidence at consumption boundaries
seonghobae Sep 5, 2026
c7e0319
test(people): prove rewritten command reaches PostgreSQL authority
seonghobae Sep 5, 2026
6826999
fix(people): revalidate commands at PostgreSQL mutation entry
seonghobae Sep 5, 2026
8c40a18
test(people): cover all PostgreSQL rewritten command entries
seonghobae Sep 5, 2026
1ab0bd0
test(people): prove confirmed-hire post-construction integrity gap
seonghobae Sep 5, 2026
0a39455
fix(people): revalidate confirmed-hire application evidence
seonghobae Sep 5, 2026
bf24c2e
fix(people): revalidate confirmed-hire durable entry command
seonghobae Sep 5, 2026
2064a52
test(people): expose post-validation command rewrite
seonghobae Sep 5, 2026
e4cab11
fix(people): detach validated mutation commands before callbacks
seonghobae Sep 5, 2026
0daa68a
test(people): bind mutation results to commanded identities
seonghobae Sep 5, 2026
e09e557
fix(people): bind mutation results to command identities
seonghobae Sep 5, 2026
40ef9b2
fix(people): bind confirmed hire result identities
seonghobae Sep 5, 2026
6142f7d
test(people): bind result checks to pre-port targets
seonghobae Sep 5, 2026
abd5492
fix(people): compare results to detached pre-port targets
seonghobae Sep 5, 2026
62a6b7a
fix(people): compare hire result to detached targets
seonghobae Sep 5, 2026
f741508
test(people): bind mutation commands before authorization callbacks
seonghobae Sep 5, 2026
e4d538c
fix(people): detach commands before authorization callbacks
seonghobae Sep 5, 2026
5a6354a
test(people): bind hire command before authorization callbacks
seonghobae Sep 5, 2026
9771be6
fix(people): detach hire command before authorization callbacks
seonghobae Sep 5, 2026
6fcb1c4
test(people): reject allocation text runtime subtype
seonghobae Sep 5, 2026
7779a85
fix(people): require exact allocation text
seonghobae Sep 5, 2026
8d38776
style(people): preserve source trailing newline
seonghobae Sep 5, 2026
abd5dc5
test(people): expose zero allocation contract split
seonghobae Sep 5, 2026
7a95aa9
fix(people): reject zero allocation before domain construction
seonghobae Sep 5, 2026
f659b65
fix(api): align assignment allocation contract with domain invariant
seonghobae Sep 5, 2026
da4b628
chore(manifest): reseal updated OpenAPI allocation contract
seonghobae Sep 5, 2026
8f98685
fix(manifest): restore unaffected outbox digest
seonghobae Sep 5, 2026
d7440d4
test(people): specify idempotent replay result evidence
seonghobae Sep 5, 2026
cc1cc53
fix(people): bind foreign replay identity to semantic digest
seonghobae Sep 5, 2026
a61617f
fix(people): return verified idempotency replay receipt
seonghobae Sep 5, 2026
3f3b23a
test(people): cover fixed projection shape guards
seonghobae Sep 5, 2026
f933fbc
test(people): use standard UUID attribute trap
seonghobae Sep 5, 2026
4e9e04f
test(people): use standard row-container trap errors
seonghobae Sep 5, 2026
7485cb2
test(people): use standard durable UUID trap
seonghobae Sep 5, 2026
59f6eae
test(people): use standard projection-container trap errors
seonghobae Sep 5, 2026
998e06f
test(people): use standard scalar tripwire protocols
seonghobae Sep 5, 2026
4be7f16
test(people): use numeric protocol errors for Decimal tripwires
seonghobae Sep 5, 2026
9de94ba
test(people): expose stale internal package pins
seonghobae Sep 6, 2026
358152d
fix(people): align HRIS kernel package dependency
seonghobae Sep 6, 2026
f488fef
test(people): reject impossible Python runtime metadata
seonghobae Sep 7, 2026
a605749
fix(people): align Python floor with owned dependencies
seonghobae Sep 7, 2026
d5c2937
test(people): reject executable exact UUID payloads
seonghobae Sep 7, 2026
9b40b8e
fix(people): validate UUID integer payload before comparison
seonghobae Sep 7, 2026
a0b3695
fix(people): gate durable hire UUID payload
seonghobae Sep 7, 2026
b863478
fix(people): gate durable mutation UUID payload
seonghobae Sep 7, 2026
c72519e
test(people): reject forged out-of-range UUID integers
seonghobae Sep 7, 2026
87a3224
fix(people): enforce UUID integer range
seonghobae Sep 7, 2026
1406657
fix(people): enforce hire UUID integer range
seonghobae Sep 7, 2026
598b64a
fix(people): enforce mutation UUID integer range
seonghobae Sep 7, 2026
f4dac8a
test(people): cover all People UUID boundaries
seonghobae Sep 7, 2026
3f24bfc
fix(people): harden hire UUID payload validation
seonghobae Sep 7, 2026
dab8487
fix(people): harden read UUID payload validation
seonghobae Sep 7, 2026
0bc1c53
chore(people): preserve dedicated read-owner boundary
seonghobae Sep 7, 2026
e1f9da8
test(people): keep UUID regression in mutation owner
seonghobae Sep 7, 2026
b5ca7e4
chore(people): restore read-owner bytes exactly
seonghobae Sep 7, 2026
9227ff2
test(people): prove PostgreSQL capability replacement RED
seonghobae Sep 7, 2026
b6bc808
fix(people): bind validated hire connection capability
seonghobae Sep 7, 2026
51f132b
fix(people): bind validated mutation connection capability
seonghobae Sep 7, 2026
2a2dd54
test(people): forbid descriptor capability indirection
seonghobae Sep 7, 2026
368951e
fix(people): remove descriptor capability path
seonghobae Sep 7, 2026
f566fd2
fix(people): remove descriptor capability path
seonghobae Sep 7, 2026
338305f
test(people): expose direct hire command drift during DB acquisition
seonghobae Sep 7, 2026
e01e825
fix(people): snapshot hire command before database acquisition
seonghobae Sep 7, 2026
86a346b
test(people): expose retained hire UUID alias during authorization
seonghobae Sep 7, 2026
b01ffad
fix(people): detach nested hire UUID aliases at command validation
seonghobae Sep 7, 2026
728c63d
test(people): expose generic nested UUID alias across authorization
seonghobae Sep 7, 2026
5a7be53
test(people): expose generic nested UUID alias at PostgreSQL boundary
seonghobae Sep 7, 2026
6c8be6d
fix(people): detach generic nested UUID aliases at validation
seonghobae Sep 7, 2026
0cd270f
test(people): expose retained result alias drift
seonghobae Sep 7, 2026
6f3a54b
fix(people): detach generic mutation results from port aliases
seonghobae Sep 7, 2026
41829e0
fix(people): detach confirmed-hire results from port aliases
seonghobae Sep 7, 2026
a9848fb
test(people): bind replay verification to pre-port semantics
seonghobae Sep 7, 2026
ee7b03d
fix(people): bind replay checks to pre-port command semantics
seonghobae Sep 7, 2026
d9cb26e
test(people): bind replay receipt to pre-port authorization evidence
seonghobae Sep 7, 2026
41a35a5
fix(people): freeze replay evidence before executable persistence
seonghobae Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ All notable changes to Orgmetra will be documented in this file.
- `employment_record_version.employment_concurrency_code` constrained to `exclusive` or `concurrent`.
- ADR 0005 for exclusive employment and staffable seats.
- `orgmetra_hris_kernel` 0.3.0 with identity-scoped bitemporal resolution, assignment-employment coverage, allocation-portfolio checks, and a Memorial Hospital RN correction case at 100% statement and branch coverage.
- `employment_record_version` and `position_record_version` so employment and position identity stay stable across retroactive corrections.
- `employment_record_version` and `position_record_version` so corrections no longer mint a new employment or position identifier.
- `assignment_record.employment_record_id` bound to the same person as the covering employment.
- `orgmetra_keyverse_adapter` that binds an opaque Keyverse subject to a person and rejects passwords, passkeys, and tokens.
- Design tokens for the repeating HR actions: approve, review, correct, request evidence, compare, export, and escalate.
Expand Down Expand Up @@ -54,6 +54,7 @@ All notable changes to Orgmetra will be documented in this file.
- Made assignment coverage status-aware: `active` and `leave` remain staffable while `terminated` and other non-eligible employment statuses fail closed.
- Made organization hierarchy reconstruction fail closed on a cycle at the requested tenant, effective day, and knowledge cutoff while ignoring future-recorded and foreign-tenant facts.
- Build the outbox due-work index concurrently during migration 0008, requiring that index step to run outside an explicit transaction block so established queues do not block writers while the index is built; pre-index hardening and post-index privileged role setup use separate explicit transactions.
- Active-PR People mutation commands now require exact built-in governance text and hire status values before digesting or persisting high-impact employment evidence.

### Security

Expand Down
10 changes: 5 additions & 5 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
},
{
"path": "CHANGELOG.md",
"sha256": "f2d2e0b488c0440533effa821808f2f17e37d92f8fb586174c2fdb594f760ca5",
"bytes": 17539,
"lines": 77
"sha256": "9ad6dad273c94c30741522ca87205ff24eb92c53becc8b53739d93acb28126f9",
"bytes": 17697,
"lines": 78
},
{
"path": "CLAUDE.md",
Expand Down Expand Up @@ -353,8 +353,8 @@
},
{
"path": "schemas/openapi.yaml",
"sha256": "09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f",
"bytes": 29503,
"sha256": "c37522504d1f6ac6410eaac833dddbf09aacc85572da38a1cf7539541833ea8e",
"bytes": 29511,
"lines": 1020
},
{
Expand Down
2 changes: 1 addition & 1 deletion schemas/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ components:
format: uuid
allocation_ratio:
type: string
pattern: '^(0\.[0-9]{4}|1\.0000)$'
pattern: '^(0\.(?!0000)[0-9]{4}|1\.0000)$'
effective_from:
type: string
format: date
Expand Down
27 changes: 20 additions & 7 deletions services/people-api/src/orgmetra_people_api/hire.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from __future__ import annotations

from dataclasses import dataclass
from dataclasses import dataclass, replace
from datetime import date
import re
from typing import Protocol, runtime_checkable
Expand All @@ -35,8 +35,8 @@ class HireDecisionIntegrityError(RuntimeError):


def _validate_operational_uuid(field_name: str, value: object) -> None:
"""Require a real UUID outside Orgmetra's reserved protocol sentinels."""
if not isinstance(value, UUID) or value.int in (0, _MAX_UUID_INT):
"""Require an exact UUID outside Orgmetra's reserved protocol sentinels."""
if type(value) is not UUID or value.int in (0, _MAX_UUID_INT):
raise ValueError(f"{field_name} must be an operational UUID.")


Expand Down Expand Up @@ -83,7 +83,7 @@ def __post_init__(self) -> None:
_validate_operational_uuid(field_name, getattr(self, field_name))
if type(self.effective_from) is not date:
raise ValueError("effective_from must be a business date.")
if not isinstance(self.display_name, str):
if type(self.display_name) is not str:
raise ValueError("display_name must be a string.")
try:
self.display_name.encode("utf-8")
Expand All @@ -95,7 +95,7 @@ def __post_init__(self) -> None:
raise ValueError("display_name must not contain control characters.")
validate_idempotency_key(self.idempotency_key)
if (
not isinstance(self.employment_status_code, str)
type(self.employment_status_code) is not str
or _STATUS_CODE_PATTERN.fullmatch(self.employment_status_code) is None
):
raise ValueError("employment_status_code must be a lower snake_case code.")
Expand Down Expand Up @@ -147,8 +147,14 @@ def accept_confirmed_hire(
``materialize_worker`` operation and ``candidate_worker_conversion`` field;
possession of an identity token or purpose string alone is insufficient.
"""
if not isinstance(command, HireAcceptanceCommand):
if type(command) is not HireAcceptanceCommand:
raise TypeError("command must be a HireAcceptanceCommand")
command = replace(command)
expected_person_record_id = UUID(int=command.person_record_id.int)
expected_employment_record_id = UUID(int=command.employment_record_id.int)
expected_conversion_record_id = UUID(
int=command.candidate_worker_conversion_record_id.int
)
if not isinstance(mutation_port, HireAcceptancePort):
raise TypeError("mutation_port must implement HireAcceptancePort")

Expand All @@ -164,6 +170,13 @@ def accept_confirmed_hire(
policy=policy,
)
result = mutation_port.accept_hire(command=command, authorization=authorization)
if not isinstance(result, HireAcceptanceResult):
if type(result) is not HireAcceptanceResult:
raise TypeError("mutation_port must return HireAcceptanceResult")
HireAcceptanceResult.__post_init__(result)
if (
result.person_record_id != expected_person_record_id
or result.employment_record_id != expected_employment_record_id
or result.candidate_worker_conversion_record_id != expected_conversion_record_id
):
raise HireDecisionIntegrityError("hire result identity does not match command")
return result
67 changes: 43 additions & 24 deletions services/people-api/src/orgmetra_people_api/mutations.py
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from __future__ import annotations

from dataclasses import dataclass
from dataclasses import dataclass, replace
from datetime import date
from decimal import Decimal
from hashlib import sha256
Expand Down Expand Up @@ -47,26 +47,26 @@ class PeopleMutationIntegrityError(RuntimeError):


def _validate_operational_uuid(field_name: str, value: object) -> None:
"""Require a real UUID outside Orgmetra's reserved protocol sentinels."""
if not isinstance(value, UUID) or value.int in (0, _MAX_UUID_INT):
"""Require an exact UUID outside Orgmetra's reserved protocol sentinels."""
if type(value) is not UUID or value.int in (0, _MAX_UUID_INT):
raise ValueError(f"{field_name} must be an operational UUID.")
Comment thread
seonghobae marked this conversation as resolved.
Outdated


def _validate_confirmation(value: object) -> None:
"""Require one namespaced human-confirmation reference."""
if not isinstance(value, str) or _REFERENCE_PATTERN.fullmatch(value) is None:
if type(value) is not str or _REFERENCE_PATTERN.fullmatch(value) is None:
raise ValueError("confirmation_reference must be a namespaced opaque reference.")


def _validate_evidence_version(value: object) -> None:
"""Require one whitespace-free evidence version token."""
if not isinstance(value, str) or _VERSION_PATTERN.fullmatch(value) is None:
if type(value) is not str or _VERSION_PATTERN.fullmatch(value) is None:
raise ValueError("evidence_version_code must be a whitespace-free version token.")


def validate_idempotency_key(value: object) -> str:
"""Require the same visible-ASCII Idempotency-Key contract as the HTTP boundary."""
if not isinstance(value, str) or not (_IDEMPOTENCY_MIN <= len(value) <= _IDEMPOTENCY_MAX):
if type(value) is not str or not (_IDEMPOTENCY_MIN <= len(value) <= _IDEMPOTENCY_MAX):
raise ValueError("idempotency_key must be 16 to 200 visible ASCII characters.")
if any(ord(character) < 0x21 or ord(character) > 0x7E for character in value):
raise ValueError("idempotency_key must be 16 to 200 visible ASCII characters.")
Expand All @@ -83,11 +83,11 @@ def command_route(
command: EmploymentMutationCommand | PositionMutationCommand | AssignmentMutationCommand,
) -> str:
"""Return the durable route that scopes one People mutation idempotency key."""
if isinstance(command, EmploymentMutationCommand):
if type(command) is EmploymentMutationCommand:
return "employment-records"
if isinstance(command, PositionMutationCommand):
if type(command) is PositionMutationCommand:
return "position-records"
if isinstance(command, AssignmentMutationCommand):
if type(command) is AssignmentMutationCommand:
return "assignment-records"
raise TypeError("command must be a governed People mutation command")

Expand All @@ -99,6 +99,7 @@ def idempotency_record_id(
idempotency_key: str,
) -> UUID:
"""Derive a stable operational identity for one tenant/route/key binding."""
_validate_operational_uuid("tenant_record_id", tenant_record_id)
return uuid5(
_IDEMPOTENCY_NAMESPACE,
f"{tenant_record_id}:{command_route_value}:{idempotency_key}",
Comment thread
seonghobae marked this conversation as resolved.
Expand All @@ -115,9 +116,10 @@ def mutation_command_digest(
Generated record identifiers are excluded so a retry that allocates fresh
UUIDs still matches the first committed command.
"""
if not isinstance(authorization, AuthorizationDecision):
if type(authorization) is not AuthorizationDecision:
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
raise TypeError("authorization must be an AuthorizationDecision")
if isinstance(command, EmploymentMutationCommand):
if type(command) is EmploymentMutationCommand:
EmploymentMutationCommand.__post_init__(command)
route = "employment-records"
semantic_command: dict[str, object] = {
"confirmation_reference": command.confirmation_reference,
Expand All @@ -127,7 +129,8 @@ def mutation_command_digest(
"evidence_version_code": command.evidence_version_code,
"person_record_id": str(command.person_record_id),
}
elif isinstance(command, PositionMutationCommand):
elif type(command) is PositionMutationCommand:
PositionMutationCommand.__post_init__(command)
route = "position-records"
semantic_command = {
"confirmation_reference": command.confirmation_reference,
Expand All @@ -137,7 +140,8 @@ def mutation_command_digest(
"organization_unit_id": str(command.organization_unit_id),
"position_status_code": command.position_status_code,
}
elif isinstance(command, AssignmentMutationCommand):
elif type(command) is AssignmentMutationCommand:
AssignmentMutationCommand.__post_init__(command)
route = "assignment-records"
semantic_command = {
"allocation_ratio": _canonical_allocation_ratio(command.allocation_ratio),
Expand Down Expand Up @@ -191,10 +195,10 @@ def __post_init__(self) -> None:
_validate_operational_uuid(field_name, getattr(self, field_name))
if type(self.effective_from) is not date:
raise ValueError("effective_from must be a business date.")
if not isinstance(self.employment_status_code, str) or self.employment_status_code not in _EMPLOYMENT_STATUSES:
if type(self.employment_status_code) is not str or self.employment_status_code not in _EMPLOYMENT_STATUSES:
raise ValueError("employment_status_code must be active, leave, or terminated.")
if (
not isinstance(self.employment_concurrency_code, str)
type(self.employment_concurrency_code) is not str
or self.employment_concurrency_code not in _CONCURRENCY_CODES
):
raise ValueError("employment_concurrency_code must be exclusive or concurrent.")
Expand Down Expand Up @@ -234,7 +238,7 @@ def __post_init__(self) -> None:
_validate_operational_uuid(field_name, getattr(self, field_name))
if type(self.effective_from) is not date:
raise ValueError("effective_from must be a business date.")
if not isinstance(self.position_status_code, str) or self.position_status_code not in _POSITION_STATUSES:
if type(self.position_status_code) is not str or self.position_status_code not in _POSITION_STATUSES:
raise ValueError("position_status_code must be a staffable or closed seat status.")
_validate_confirmation(self.confirmation_reference)
_validate_evidence_version(self.evidence_version_code)
Expand Down Expand Up @@ -272,7 +276,7 @@ def __post_init__(self) -> None:
_validate_operational_uuid(field_name, getattr(self, field_name))
if type(self.effective_from) is not date:
raise ValueError("effective_from must be a business date.")
if not isinstance(self.allocation_ratio, Decimal):
if type(self.allocation_ratio) is not Decimal:
raise ValueError("allocation_ratio must be a Decimal.")
if not self.allocation_ratio.is_finite():
raise ValueError("allocation_ratio must be finite.")
Expand Down Expand Up @@ -363,8 +367,10 @@ def create_employment_record(
mutation_port: PeopleMutationPort,
) -> EmploymentMutationResult:
"""Authorize the exact employment target before persisting worker employment truth."""
if not isinstance(command, EmploymentMutationCommand):
if type(command) is not EmploymentMutationCommand:
raise TypeError("command must be an EmploymentMutationCommand")
command = replace(command)
expected_employment_record_id = UUID(int=command.employment_record_id.int)
port = _require_port(mutation_port)
authorization = authorize_resource_fields(
principal=principal,
Expand All @@ -378,8 +384,11 @@ def create_employment_record(
policy=policy,
)
result = port.create_employment(command=command, authorization=authorization)
if not isinstance(result, EmploymentMutationResult):
if type(result) is not EmploymentMutationResult:
raise TypeError("mutation_port must return EmploymentMutationResult")
EmploymentMutationResult.__post_init__(result)
if result.employment_record_id != expected_employment_record_id:
raise PeopleMutationIntegrityError("employment result identity does not match command")
return result


Expand All @@ -392,8 +401,10 @@ def create_position_record(
mutation_port: PeopleMutationPort,
) -> PositionMutationResult:
"""Authorize the exact position target before persisting a staffable seat."""
if not isinstance(command, PositionMutationCommand):
if type(command) is not PositionMutationCommand:
raise TypeError("command must be a PositionMutationCommand")
command = replace(command)
expected_position_record_id = UUID(int=command.position_record_id.int)
port = _require_port(mutation_port)
authorization = authorize_resource_fields(
principal=principal,
Expand All @@ -407,8 +418,11 @@ def create_position_record(
policy=policy,
)
result = port.create_position(command=command, authorization=authorization)
if not isinstance(result, PositionMutationResult):
if type(result) is not PositionMutationResult:
raise TypeError("mutation_port must return PositionMutationResult")
PositionMutationResult.__post_init__(result)
if result.position_record_id != expected_position_record_id:
raise PeopleMutationIntegrityError("position result identity does not match command")
return result


Expand All @@ -421,8 +435,10 @@ def create_assignment_record(
mutation_port: PeopleMutationPort,
) -> AssignmentMutationResult:
"""Authorize the exact assignment target before persisting seat allocation."""
if not isinstance(command, AssignmentMutationCommand):
if type(command) is not AssignmentMutationCommand:
raise TypeError("command must be an AssignmentMutationCommand")
command = replace(command)
expected_assignment_record_id = UUID(int=command.assignment_record_id.int)
port = _require_port(mutation_port)
authorization = authorize_resource_fields(
principal=principal,
Expand All @@ -436,13 +452,16 @@ def create_assignment_record(
policy=policy,
)
result = port.create_assignment(command=command, authorization=authorization)
if not isinstance(result, AssignmentMutationResult):
if type(result) is not AssignmentMutationResult:
raise TypeError("mutation_port must return AssignmentMutationResult")
AssignmentMutationResult.__post_init__(result)
if result.assignment_record_id != expected_assignment_record_id:
raise PeopleMutationIntegrityError("assignment result identity does not match command")
return result


def parse_allocation_ratio(raw_value: object) -> Decimal:
"""Parse the OpenAPI allocation token into an exact four-decimal ratio."""
if not isinstance(raw_value, str) or re.fullmatch(r"^(0\.[0-9]{4}|1\.0000)$", raw_value) is None:
if type(raw_value) is not str or re.fullmatch(r"^(0\.(?!0000)[0-9]{4}|1\.0000)$", raw_value) is None:
raise ValueError("allocation_ratio must match 0.0001-1.0000 four-decimal form.")
return Decimal(raw_value)
Loading
Loading