Skip to content

feat(access): a case's supervisors decide who changes another reviewer's decision - #3761

Merged
rubenvdlinde merged 48 commits into
developmentfrom
build/dq-l9-11
Oct 12, 2026
Merged

rubenvdlinde merged 48 commits into
developmentfrom
build/dq-l9-11

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

What

woo-request-scoped-access 5.2, built generic (decision 182) on the decision-object model (decision 197).

  • The document schema declares x-openregister-review {ownership: decider, supervisors: [dossiq-coordinators], hideOthers: false} in a new fragment, lib/Settings/register.d/89-document-review-ownership.json (document 1.2.0, register 0.20.32, digests regenerated). hideOthers matches the Woo case type's reviewerAccess.hideOthers, and a test keeps the two equal.
  • x-openregister-review joins SchemaSlugMap::SCHEMA_ANNOTATION_KEYS, so the block reaches an instance that imported the document schema before it existed. Without it, OpenRegister drops the key in silence.
  • New generic capability, REQ-CRA-006 in specs/case-reviewer-access: CaseDecisionAccess maps the object a decision is on to its case (the case itself, or every case that links it as a case document). CaseDecisionAccessListener answers OpenRegister's vetoable DecisionChangeRequestedEvent: on a case whose case type declares reviewer access, only that case type's supervising group or an administrator may change another reviewer's decision (case-supervisor-required). One refusing case refuses. When no guarded case is involved, OpenRegister's answer stays as it was. A read that throws refuses. The listener is registered in CaseAccessListenerRegistrar.
  • tests/Stubs/OpenRegister/Event/DecisionChangeRequestedEvent.php is a declaration-only stub of the event's getters (bootstrap include plus psalm stub; phpstan already scans that directory).

Round 9: case-type supervisors see hidden decisions (REQ-CRA-007)

  • CaseDecisionAccess::maySee() and the same CaseDecisionAccessListener now answer OpenRegister's DecisionVisibilityRequestedEvent (openregister#4592, REQ-DEC-003). A supervisor of every guarded case the object belongs to sees a decision hidden under hideOthers. Anyone else gets no answer, so the decision stays hidden. A read that throws also leaves it hidden. Registered in CaseAccessListenerRegistrar; declaration-only stub tests/Stubs/OpenRegister/Event/DecisionVisibilityRequestedEvent.php.
  • Test-first: the new CaseDecisionAccessTest and listener tests were red before the change (3 errors) and are green after it.
  • Verified, exit 0: tests/Unit/Service/Access, Listener, AppInfo 148 OK (2 skipped); Architecture + Listener 467 OK. phpcs, phpstan, psalm and phpmd 0 on the changed lib files.

Stack and dependencies

Verified

  • Touched PHPUnit, exit 0: tests/Unit/Settings 310 OK, Architecture 99 OK, AppInfo 85 (2 skipped), Listener 366 OK, Service/Access 53 OK, plus UnreadTriggerService and SchemaAnnotationReconciler 9 OK.
  • On the changed lib files: php -l, phpcs, phpstan, psalm and phpmd all clean.
  • I did not run check:strict, the npm legs or the hydra gates locally. A WSL restart took the clone and the gate package, so I recloned. CI is the full check (decision 149).

…rmed only when its term runs

WooRequestIntake::receive() takes opencatalogi's answer keys (portal-form and
opencatalogi origins, subjectRef optional for those two), writes the Woo case
directly (decision 179: no intake object in between), counts the term from
when the requester sent it, and reads the term back: armed only when a
statutory instance with an engine timer ends on the case deadline; otherwise
not-armed with an internal timeline entry, or unavailable with nothing
written. PortalContributionProvider::receiveWooRequest() has opencatalogi's
signature. Register 0.20.26 (wooRequest origin enum, phone and address).
…chable

formerReferences (list of {application, reference}) on the case schema, an
exact search declaration, register 0.20.27.
OpenCatalogiWooCase maps every status to its stage (received, in_progress,
awaiting_clarification, decided, withdrawn), carries start, requester,
channel, old reference, extension count and end date, on opencatalogi's own
request shape as fixture. First part of takes-over 4.2.
OneDateWritePathTest refused the private date parser in WooReceivedAnswers.
The receipt moment, the start date, the decided date and the due date now go
through CaseDateNormaliser, in the tenant's time zone, the one rule for what
a date is.
… declaration

Decision 182: the opencatalogi Woo request import becomes the generic
case-record-import capability (CaseRecordImport, RecordCaseMapping,
RecordImportStore, occ dossiq:case:import-records) and the generic term
carry-over (TermCarryOver). The Woo part is the recordImports declaration on
the Woo case type. caseType 1.17.0, register 0.20.28.
… kept

Row 10.8 of woo-request-takes-over-from-opencatalogi: a Woo request received
through the portal form was refused acknowledgement_no_address, because its
requester's address lives only in wooRequest.verzoekerEmail. The acknowledgement
declaration takes addressFields (generic); the Woo case type declares its path.
…y for any case type

CaseDocumentProvenance reads custodian, sourceSystem and the other
provenance fields of a case's documents, from the record or from this
case's join, and selects documents by them (REQ-DPV-001/002). The
collection report reads through it. The provenance fragment's text names
no procedure (decision 182); informatieobject 1.4.1, zaakinformatieobject
1.2.1, register 0.20.29.
A case type declares under reviewerAccess the records it keeps private to
the case, where its reviewers are named, the supervising group and
whether reviewers see each other's records. CaseReviewerAccess computes
who reads each record (the assignee, the reviewers unless hidden) and
who may change it (its author, a supervisor, an administrator);
ObjectReadGrants applies it through OpenRegister's per-object sharing
and owning group, read bit only, refusals counted. The assessment writer
refuses a change to another reviewer's assessment and reconciles after a
create; an administrator route reconciles every declaring case once.

The Woo case type declares it for its assessments, which become private
(decision 182: replaces the planned WooAssessmentAccess). Register
0.20.30, wooDocumentAssessment 1.2.0, Woo case type object 1.3.0.
…filinq

A case type names under documentDrafts.<purpose> the filinq template (by
slug and namespace), the format, the document type and the data the draft
renders. CaseDraftData builds that data from the declaration (fields by
dot path with value maps, related records as numbered rows, summaries
over a list field); CaseDocumentDrafts asks filinq through its published
DocumentGenerationRequestedEvent and files what filinq stored on the
case. No filinq is filinq-missing, filinq's error is the reason, nothing
is filed without a file.

The Woo case type declares its decision and inventory drafts (decision
182: replaces the planned WooDecisionDrafts); assembleDecision answers
both under draft and inventoryDraft. Register 0.20.31, Woo case type
object 1.4.0.
…re of nothing is no figure

Parity suite tests/Unit/Service/Term/StatutoryTermParityTest.php replays
opencatalogi's StatutoryTermTest and WooRequestServiceTest scenarios through
the extend endpoint, ask/receive and the quarterly report endpoint.

The report counted running terms in the share and answered 0.0 with nothing
decided. TermOutcome now classifies every term as met, missed, running or
suspended (a late decision is missed), and the share is met / (met + missed),
null when nothing is decided. The dashboard shows a dash for null, and the
quarterly widget reads the keys the report actually sends.
…a supervisor

REQ-CRA-005: CaseReassignGuardListener refuses an assignee change on a case
whose case type keeps records private (and that has some) unless a supervisor
or an administrator makes it, and reconciles the read grants after an accepted
change. Generic: any case type that declares reviewerAccess.

REQ-CRA-003 on the redaction half: saveRedactionProposal, which proposing and
reviewing both write through, refuses another reviewer's assessment with 403
owned-by-another-reviewer; the controller answers the rule instead of a 400.

REQ-WSA-004 backend: getOutstanding reads assessments unscoped, so a document
with an assessment the caller cannot read counts as assessed. The UI state
waits on a board (design backlog DqBeoordelingAfgeschermd).

The unscoped reads of CaseReviewerAccess move to CaseRecordLookup to keep the
class under the complexity ceiling.
…now the import's catches

casesBySource answered [] when the existing cases could not be read, and the
import then wrote a half-finished case a second time. It now answers null and
the run stops with the failure listed.

The case-record-import catch sites are classified in the catch-return-null
allowlist and CaseRecordImport's period-to-days conversion has its row in the
date-arithmetic audit (verdict neither); ServiceCatchReturnsNullTest and
EveryTermOnTheCalendarTest were red on these since #3632.
…rn-null allowlist

find() is a read miss counted as refused; service() is the degradation that
makes the reconciler answer unavailable. ServiceCatchReturnsNullTest was red
on these since #3660.
# Conflicts:
#	appinfo/routes.php
#	lib/Settings/dossiq_register.json
#	lib/Woo/WooRequestForm.php
#	lib/Woo/WooRequestIntake.php
#	tests/schemas/schema-version-digests.json
…now the import's catches

casesBySource answered [] when the existing cases could not be read, and the
import then wrote a half-finished case a second time. It now answers null and
the run stops with the failure listed.

The case-record-import catch sites are classified in the catch-return-null
allowlist and CaseRecordImport's period-to-days conversion has its row in the
date-arithmetic audit (verdict neither); ServiceCatchReturnsNullTest and
EveryTermOnTheCalendarTest were red on these since #3632.
# Conflicts:
#	appinfo/routes.php
#	tests/Unit/Architecture/catch-return-null.allowlist.json
# Conflicts:
#	lib/Service/DeadlineReportingService.php
#	lib/Service/Termijn/TermOutcome.php
#	tests/Unit/Service/DeadlineReportingServiceTest.php
#	tests/Unit/Service/Termijn/TermOutcomeTest.php
# Conflicts:
#	lib/AppInfo/Registrar/ObjectListenerRegistrar.php
…r's decision

The document schema declares x-openregister-review (decider ownership,
dossiq-coordinators, hideOthers false) and the key joins
SCHEMA_ANNOTATION_KEYS so it reaches existing instances. A generic
listener answers OpenRegister's DecisionChangeRequestedEvent through the
case the object belongs to (REQ-CRA-006, woo-request-scoped-access 5.2,
decision 197). Stacked on openregister#4583.
…hers

CaseDecisionAccess::maySee() answers OpenRegister's
DecisionVisibilityRequestedEvent (openregister#4592, REQ-DEC-003) through the
same listener, so a case-type supervisor outside the document schema's groups
sees hidden decisions on their cases. A failed read keeps the decision hidden
(REQ-CRA-007).

Assisted-by: Claude Code
@rubenvdlinde
rubenvdlinde merged commit 8024759 into development Oct 12, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant