Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions packages/e-billing/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@

## Unreleased
### Added
- Supplier number (EN 16931 BT-29, ADR 0012): persist DTO `supplierNumber` onto `invoices.supplier_number` via `ParsedInvoiceMapper` / `InvoiceFactory`; MoSCoW `could` on invoice / credit-note / corrected-invoice field maps; ViewInvoice supplier (BG-4) group shows `supplier_number`; both Zugferd adapters expose `supplierNumber`. No SQL backfill — next `GenerateArtifactJob` / leave-edit fills the column from `bill_data`.
- Document-type MoSCoW / ViewInvoice profiles (ADR 0009): `FieldValidationProfile` selects `field_validation.credit_note_*` and `invoice_ui.credit_note_*_hidden` for BT-3 `381`, falling back to the `invoice_*` siblings when a host omits a `credit_note_*` key; every other document type keeps reading `invoice_*`. `EbillingDocument::profileDocumentType()` (from the linked invoice's `document_type`) is threaded through `fieldValidationsNeedHumanReview()`, `missingMustFields()`, `hasBlockingMustFieldFindings()`, `calculateValidationScore()`, `isFullyValidated()`, and the review-queue scope (now optional trailing `?string $documentType` params). Package default config clones the invoice maps onto `credit_note_fields` / `credit_note_line_fields` / contextual lists and hides `preceding_invoice_number` / `preceding_invoice_date` on invoices.
- Preceding invoice reference (BG-3, ADR 0009 addendum): DTO `Data\Invoice::$precedingInvoices` (bill_data key `preceding_invoices`, via `PrecedingInvoiceReferences`), mapped onto the persisted invoice by `ParsedInvoiceMapper` / `InvoiceFactory`, and onto `ZugferdInvoice::precedingInvoices` by both Zugferd adapters. `InvoiceFieldValidator` looks the referenced invoice up among stored invoices (separators ignored, e.g. `30641.25` matches `3064125`); not found or a differing date is a non-blocking warning (`reason: preceding_invoice_not_found` / `preceding_invoice_date_mismatch`); a match sets `matched_id` and ViewInvoice renders the field as a link to that invoice.
- Corrected invoice (384) profile (ADR 0009 addendum 2): `field_validation.document_type_profiles` maps type codes to profile key prefixes (default `381 => credit_note`, `384 => corrected_invoice`); new `corrected_invoice_*` MoSCoW maps and `invoice_ui.corrected_invoice_*_hidden`; the review-queue scope splits by every mapped type with its own priorities; the preceding-invoice lookup only searches unmapped types. `FieldValidationProfile::priorityMapsDifferByType()` is replaced by `documentTypesWithOwnPriorities()`.
- Document classification (ADR 0011): `ClassifyDocumentTypeAction` switches a document between `e-billing.document_classification.types` (381 / 384) before approval, negating all amounts when the signs differ, field changes are audited by moox/audit, and the act is logged as a `document_classified` activity (`ClassifyDocumentTypeAction::ACTIVITY_EVENT`) when moox/audit is installed. Translatable labels and hover hints via `DocumentClassificationLabels`. The dropdown itself lands with the review workspace (#47).
- Declared document type at manual upload (ADR 0011 addendum): `resources.{key}.manual_upload.document_types` (credit notes `['381', '384']`) makes the upload dialog show a required type choice without preselection and a collapsible "which type?" instruction (`e-billing::fields.document_classification.{code}.rule|examples`, `document_classification_help.*`, en + de; hosts override the examples). The choice is stored on `UploadedPdfSource.document_type` (migration `add_document_type_to_ebilling_uploaded_pdf_sources_table`), logged as a `document_classified` activity (origin `upload`) and applied after parsing by `StoreBillDataJob` through `DocumentClassification::applyDeclaredType()` (sign flip included). A parsed type outside the selectable set is kept and `document_type` becomes `needs_review` (`declared_document_type_mismatch`). Classification types count as one type in both duplicate checks. `allowed_document_type_codes` now includes `384`. `DocumentClassification` centralises signs, the duplicate family and the activity; `ClassifyDocumentTypeAction::classificationSigns()` is replaced by `DocumentClassification::signs()`.
- Credit-note resource lists 381 and 384 (`resources.credit_notes.document_types`), with `tabs.credit_notes` (per-type tabs) and a document-type badge column when a resource shows more than one type.
- `e-billing.credit_note_payment_terms` (default `null`): BT-20 text emitted for a 381 without due date and payment terms, so BR-CO-25 accepts a positive amount due.
- Credit note with a negative total blocks approval (ADR 0010): `CreditNoteSign::hasNegativeTotal()` (BT-3 `381` with negative BT-112) is a new blocking condition in `DocumentApprovalGuard`, `AutoApproveEvaluator` (`AutoApproveFailureReason::CreditNoteNegativeTotal`), and `DocumentDispatchGuard` (reason `credit_note_negative_total`).
- `e-billing.intake.scopes`: optional allowlist of mail-inbox Scope keys for `ProcessInboxAttachmentListener` (null/`[]` = all; non-listed PDFs marked Skipped, no `EbillingDocument`).
- Optional `e-billing.delivery.from_name` / `EBILLING_DELIVERY_FROM_NAME` (display name only; From address is host-owned).

### Fixed
- Buyer/seller address mapping: without a street, the next address line (e.g. a PO box "Postfach 16 20") becomes BT-50 instead of repeating the company name; the company only fills BT-50 when no address line exists. Shared as `Data\Address::toEn16931Address()` for `ParsedInvoiceMapper` and `InvoiceFactory` (removed the unused `InvoiceAddress` import).

### Changed
- BG-32 BT-160 attribute names and CAE `reason_text` fallbacks follow `e-billing.document_locale` / `EBILLING_DOCUMENT_LOCALE` (package default `en`; lang keys in `e-billing::emission`). Independent of Filament UI locale. Already-emitted artifacts are not regenerated. Hosts that need German (or other) labels set the env/config override.

Expand Down
14 changes: 10 additions & 4 deletions packages/e-billing/CONTEXT.md

Large diffs are not rendered by default.

16 changes: 15 additions & 1 deletion packages/e-billing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Published as `config/e-billing.php`.
| `default_customer_country` | Transitional fallback buyer country when the parser derives none (default `DE`); removed in a future master-data phase |
| `supplier` | Central supplier master data copied onto invoices as a snapshot at creation time |
| `corroboration` | Post-attribution master-data checks (never clears `customer_id`): `name_min_token_length`, `name_legal_form_stop_words`, `buyer_address_roles` (billing + postal), `delivery_address_roles` (delivery first, then postal/billing fallback) |
| `field_validation` | MoSCoW priority rules for invoice and line fields |
| `field_validation` | MoSCoW priority rules for invoice and line fields. `credit_note_*` siblings (fields, line fields, contextual-should lists) apply to BT-3 `381` documents; a host that omits a `credit_note_*` key falls back to the `invoice_*` key (ADR 0009) |
| `approval` | Dispatch approval gate: `required`, `auto_approve_enabled` |
| `notification` | Review announce strategy: `immediate` / `batched`, batch key, window minutes, optional `recorder` class |
| `escalation` | Overdue-approval scan: `day_counting`, `working_weekdays`, `exclude_dates`, ordered `levels` (`key` / `after` / `unit`); empty `levels` disables the feature |
Expand Down Expand Up @@ -220,6 +220,18 @@ Within awaiting-review statuses, both use the same field predicate (including va

Changing a field's configured priority changes its behaviour with no code change.

### Document-type profiles (credit notes, corrected invoices)

`FieldValidationProfile` selects which MoSCoW and ViewInvoice-denylist maps apply, keyed on the invoice's `document_type` (BT-3), ADR 0009. `field_validation.document_type_profiles` maps a type code to a key prefix — package default `381 => credit_note`, `384 => corrected_invoice`. A mapped type reads `field_validation.{prefix}_fields` / `{prefix}_line_fields` / `{prefix}_contextual_should` / `{prefix}_line_contextual_should` and `invoice_ui.{prefix}_fields_hidden` / `{prefix}_line_fields_hidden`; unmapped types read the `invoice_*` keys, and a host config that omits a `{prefix}_*` key falls back to the matching `invoice_*` key. The package default config spells out both profiles: the credit-note profile adds `preceding_invoice_number` / `preceding_invoice_date` as `could`, the corrected-invoice profile as `should`; invoices hide both fields.

**Document classification (ADR 0011):** `ClassifyDocumentTypeAction` lets a reviewer switch a document between the types in `e-billing.document_classification.types` (default `381 => positive`, `384 => negative`) before approval. It negates all document, line and allowance/charge amounts when the signs differ. The field changes are audited by moox/audit like any invoice update, and the act is logged as its own `document_classified` activity (from, to, amounts negated, origin) — never a value correction. `384 => negative` describes a correction issued as a credit (the delta); a host that issues full, positive restatements sets `positive`. Dropdown labels and "when to choose" hints: `DocumentClassificationLabels` (`e-billing::fields.document_classification.{code}.label|hint`). The credit-note list shows both types (`resources.credit_notes.document_types`) with per-type tabs and a type badge.

**Declared document type at upload:** `resources.{key}.manual_upload.document_types` (credit notes: `['381', '384']`) adds a required type choice without preselection to the upload dialog, with a collapsible instruction built from `document_classification.{code}.rule|examples` and `document_classification_help.*`. Codes must be in the resource's `document_types`, `document_classification.types` and `allowed_document_type_codes`. One code is applied without a choice; an empty list leaves the parser in charge. The choice is stored on the uploaded source, logged as `document_classified` (origin `upload`) and applied after parsing: a parsed 381/384 is replaced (with the sign flip), any other parsed type is kept and flagged for review. Credit notes and corrected invoices count as one type for duplicate detection. Hosts override the examples in `lang/vendor/e-billing/{locale}/fields.php`; example lists merge by index, so keep them the same length. Run the migration `add_document_type_to_ebilling_uploaded_pdf_sources_table`.

**Credit note payment terms:** `e-billing.credit_note_payment_terms` (default `null`) is emitted as BT-20 for a 381 without due date and payment terms, which BR-CO-25 otherwise rejects for a positive amount due.

**Preceding invoice reference (BG-3):** `preceding_invoice_number` / `preceding_invoice_date` map to `Invoice::$preceding_invoices[0]` (BT-25/BT-26). When present, `InvoiceFieldValidator` looks the referenced invoice up among stored invoices — number comparison ignores separators (`30641.25` matches `3064125`) — and never blocks: not found or a differing date is `status: parsed` with a `reason` (`preceding_invoice_not_found` / `preceding_invoice_date_mismatch`); a match sets `matched_id` and ViewInvoice renders `preceding_invoice_number` as a link to that invoice.

### Duplicate document-number rule

`InvoiceNumberDuplicateChecker` (used by `InvoiceFieldValidator`, and for identical-content discard in `GenerateArtifactJob` / `DiscardIdenticalContentDuplicateAction`) runs during field validation — before review clearance and the dispatch approval gate.
Expand Down Expand Up @@ -250,6 +262,8 @@ Distinct from `review_status` (field-review clearance) and `gateway_status` (KOS

Transitions write latest-only `approval_reason`, `approval_actor_id` (string; `'system'` for auto-approve), and `approval_acted_at` on the document. Approving a document that carries valid severity releases forwards those release reasons into `approval_reason` when no other reason is supplied. History is the `moox/audit` Activity trail on `EbillingDocument` (`approval_status`, `approval_reason` in the body; actor and time come from the Activity causer/timestamp, not extra attribute rows); the invoice detail Activity table aggregates the document via `aggregate_subjects`. Only `RecordApprovalTransitionAction` writes approval state for approve/reject/restore. Initialize and invalidate set `pending` and clear actor, time, and reason so a prior sign-off cannot dispatch. Rematch and manual attribution both invalidate prior approval. `DocumentApprovalTransitioned` is emitted for host listeners (the package does not send mail).

A credit note (BT-3 `381`) with a negative gross total (BT-112) blocks approval and dispatch (ADR 0010): `CreditNoteSign::hasNegativeTotal()` is checked by `DocumentApprovalGuard` (manual approve refused), `AutoApproveEvaluator` (`AutoApproveFailureReason::CreditNoteNegativeTotal`), and `DocumentDispatchGuard` (block reason `credit_note_negative_total`). Flipping the sign is the parser's job, not this package's.

`DocumentDispatchGuard` requires `approval_status = approved` and a non-empty actor id plus `approval_acted_at` when approval is required. Approved-but-missing actor or acted-at blocks with `approval_incomplete`. It never reads Activity. Auto-approve persists with no authenticated user so the Activity causer is the host `audit.system_causer` (when set), not a logged-in operator. Document actor id on the row stays `'system'`.

**Automatic approval** runs after gateway validation when every condition holds separately: gateway validated, no unresolved review findings, no blocking must-field, no duplicate flag (`approval_flags.duplicate`), no anomaly flag (`approval_flags.anomalies`). Failing any one leaves the document pending. Field validation syncs `approval_flags.duplicate` when `invoice_number` has reason `duplicate_invoice_number`; hosts may set `approval_flags.anomalies` on the document for anomaly flags (no dedicated writer API on the model). **Manual approve** requires pending status, a deliverable gateway artifact, no unresolved human-review findings, and no blocking must-field; duplicate and anomaly flags do not block a human sign-off after review is clear.
Expand Down
Loading
Loading