Skip to content

feat(documents): collected mail keeps its headers, a mail parties report, private review records - #3763

Merged
rubenvdlinde merged 66 commits into
developmentfrom
build/dq-l8-7
Oct 12, 2026
Merged

rubenvdlinde merged 66 commits into
developmentfrom
build/dq-l8-7

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Stacked on #3733 (which stacks on #3663 and #3660; merge train 2, #3742, is landing those). Review the last four commits; the rest is the stack.

What this builds

woo-review-reports 3.1: a collected mail keeps its header fields (REQ-WRR-003). Generic, per decision 182:

  • MailHeaderReader reads .eml headers through OpenRegister's parser (TextExtractionService::parseEmlStructured; no new dependency in dossiq) into bare, lower-cased addresses. An .msg, a parse failure, a missing parser or a mail without a sender is stored as {unreadable: true}.
  • CaseFileCollector writes collected bytes as a document on a case, with provenance, for any case type. It adds provenance.mail when the file is a mail, by extension or by the source's MIME type. The gather add now calls it. That keeps WooGatherAdd under phpmd's coupling limit.
  • provenance.mail is declared on informatieobject 1.4.2.

woo-review-reports 4.1: a mail parties report (REQ-WRR-004). Generic: MailPartiesReport counts a case's collected mail per sender, per recipient and per domain. Each count is per mail. Unreadable mail is counted separately, and a CSV comes with the report. The route is GET /api/cases/{id}/reports/mail-parties on a new MailPartiesReportController, with #[NoAdminRequired]. The parties switch and CaseAccessGuard are checked in the method body. The spec now uses this generic route. The parties half of the reports screen is not in this PR.

woo-review-triage 6.1 and 6.2, configuration. reviewBatch and triageRule now carry the private authorization block, with no hierarchy, and the woo-verzoek case type (1.3.2) lists them under reviewerAccess.records. Their creators already reconcile the case. documentReview, wooExclusion, wooSearchPlan and wooCollectionQuery stay public for now. Their writers do not reconcile yet, so making them private would hide each record from everyone except its author. The per-record grant table needs a generic readers option on CaseReviewerAccess, which L9 is restructuring at the moment. tasks.md lists what is still open.

Verified (targeted, slot rule / decision 149: CI is the full check)

  • PHPUnit tests/Unit/{Architecture,AppInfo,Woo,Service/Zaakdossier,Controller}: 1619 tests, exit 0. tests/Unit/{Settings,Service/Access,Review}: 415 tests OK. Settings again after the last commit: 309 OK.
  • The two 3.1 tests fail with the mail step removed (control run), and pass with it. Each uses a real .eml fixture and is validated against the real merged schema with RealSchemaValidator.
  • phpcs, phpmd, phpstan and psalm each report 0 findings on every touched lib file.
  • test:l10n, check:l10n-js and check:schema-l10n exit 0 (11 keys added in en and nl).
  • Not run: composer check:strict and the hydra gates (token budget). development is not merged in yet, because train 2 is still open.

Inherited: the debt-ratchet red on #3733 (+2 phpmd suppressions on WOODocumentAssessmentService) belongs to that PR. A train-2 agent is splitting that class, and this PR does not touch it.

… verdict

woo-review-triage section 1: wooDocumentReview, wooTriageRule and
wooReviewBatch schemas (87-woo-review.json, register 0.20.23); only an
in-scope document needs a verdict and an unmarked one is always outstanding;
POST relevance and GET summary on the Woo case; a repair step marks documents
assessed before this change in scope so they do not block their case.
(unit tests written, run pending a slot)
# Conflicts:
#	appinfo/info.xml
#	tests/schemas/schema-version-digests.json
…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.
…who gets a task for it

POST /api/cases/{id}/woo/batches takes a list of documents or a filter on
the rule that marked them, refuses a document already in another open
batch (409, naming the batch), writes the batch on each review and hands
the reviewer an engine task. GET lists the batches with assessed of total.

Also: WooThroughputReport gets its row in the date-arithmetic audit that
development's EveryTermOnTheCalendarTest now requires.
…d the viewer reports the pages seen

Review depth per document type (every page, or a sample drawn from a seed
recorded on the review). POST .../documents/{documentRef}/pages-seen
appends each displayed page with the reviewer and time; the first page
count a review hears sets its required pages from the recorded depth.
…seen every required page

A verdict on an in-scope document with unseen required pages is refused
in bulkAssess and, for direct OpenRegister writes, by WooPagesSeenGuard on
the object creating and updating events. assembleDecision() and publish()
answer 409 naming the documents and pages.
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.
…and a recall sample gets its own batch (wip)
…at any case type switches on

Decision 182: procedures are configuration, code is generic.
- Classes move to OCA\Dossiq\Review and are named by what they do:
  DocumentRelevance, ReviewSummary, ReviewBatches, ReviewDepth, PagesSeen,
  StoppingRules, ReviewConfiguration; DocumentReviewController,
  PagesSeenGuard, StoppingRuleGuard, CreateDocumentReviews,
  ReviewListenerRegistrar.
- Schemas documentReview, triageRule, reviewBatch, stoppingRule and
  recallSample (fragments 87-document-review, 88-review-recall); routes
  under /api/cases/{id}/review/...
- The case type declares documentReview {enabled, reviewDepth}; the review
  depth is read from the case type, and the Woo-verzoek case type switches
  the review on.
… 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.
…g rule and samples, the batches and what is open

Board DqWooReview (design-system #191), generic per decision 182: DocumentReviewPanel (registry
type document-review), RecallSampleDialog, ReviewBatchDialog, and the pagesSeenReporter a review
viewer calls as pages come into view. The summary answers enabled and unseen. e2e
document-review.spec.ts cites every document-review scenario (not run, decision 139).
# Conflicts:
#	appinfo/info.xml
#	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.
…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: SchemaSlugMap (both), WOODocumentAssessmentService (relevance and
reviewer access both injected; excluded documents leave the case before the
relevance read), its test (both; reviewer access passed by name), woo-verzoek
case type 1.3.1 over both, digests as the union (generator agrees), info.xml.
…cess reconciled after a batch

BatchSelection takes a batch's documents by the review's rule and by the
provenance fields of CaseDocumentProvenance (custodian, sourceSystem, source);
an unknown field is still refused. ReviewBatches::create() and
createSampleBatch() reconcile the case's reviewer access once the batch is
stored (a failed reconcile is logged and answered, the batch stands).
CaseDocumentProvenance::mailOf() reads provenance.mail for the triage rules.
…ce system, mail and text

TriageRules keeps a case's triageRule objects and applies the active ones in
order to every unmarked document; the first match marks it with its effect and
name, and a reviewer's marking is never touched. TriageConditions checks the
eight conditions at save and matches them; custodian and sourceSystem read the
provenance, senderDomain and the mail date provenance.mail, the text conditions
the text OpenRegister already extracted (DocumentText). A created rule
reconciles the case's reviewer access. TriageRuleController routes list,
create, change, delete and apply. The Review tab gets the rules section of
board DqWooReview and TriageRuleDialog; the batch dialog takes documents by
custodian, source system or rule.
… from the configuration

TriageFacts gathers what a rule reads about a document, so TriageRules stays
under the complexity limit. ReviewConfiguration::depthOfDocument() gives a
review its depth and required pages, and the review controller reaches the
pages seen through the reviews, so neither class needs a coupling suppression.
Prettier and eslint over the touched Vue files.
…ge turned into a type error

The test passed a PagesSeen as a seventh constructor argument the service never
declared; PHP ignored it until #3660 added reviewerAccess in that position.
…and message id

The gather add now writes through a generic CaseFileCollector, which adds
provenance.mail for a collected e-mail. MailHeaderReader reads the headers
through OpenRegister's EML parser into bare lower-cased addresses and marks
a mail whose headers cannot be read as unreadable (woo-review-reports 3.1,
REQ-WRR-003). provenance.mail is declared on informatieobject 1.4.2.
…ecipient and domain

MailPartiesReport counts a case's collected mail from provenance.mail, for
any case type, with unreadable mail counted apart and a CSV beside it.
GET /api/cases/{id}/reports/mail-parties needs the parties switch and read
access to the case (woo-review-reports 4.1, REQ-WRR-004; spec amended to
the generic route, decision 182).
… through the case type

documentReview, reviewBatch, triageRule, wooExclusion, wooSearchPlan and
wooCollectionQuery carry the private authorization block (no hierarchy),
and the woo-verzoek case type lists them under reviewerAccess.records so
CaseReviewerAccess grants the people on the case read on them
(woo-review-triage 6.1, 6.2 configuration; REQ-WRT-006). A generic test
holds every listed record schema private.
…ncile the case

documentReview and the three corpus records stay public until their
writers reconcile: a private record nobody granted reads as absent to
everyone but its author.
@rubenvdlinde
rubenvdlinde merged commit 1f14901 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