Skip to content

release: promote beta to main (cost basis filter, diary fix, auto-itemize refactor)#1791

Open
steilerDev wants to merge 6 commits into
mainfrom
beta
Open

release: promote beta to main (cost basis filter, diary fix, auto-itemize refactor)#1791
steilerDev wants to merge 6 commits into
mainfrom
beta

Conversation

@steilerDev

Copy link
Copy Markdown
Owner

Release Summary

This release ships a Cost Basis payment-status filter for the Cost Breakdown table, a diary UX fix, an auto-itemize workflow harmonization, and the corresponding E2E test corrections.

Changes

Features

Fixes

Refactoring

Change Inventory

Backend (server/, shared/)

  • shared/src/types/budgetBreakdown.ts — new actualCostPaid, actualCostPending fields on all breakdown entities
  • server/src/services/budgetBreakdownService.ts — deposit-aware paid/pending rollup via computeDepositAwareAggregates
  • server/src/services/budgetBreakdownService.paid.test.ts — 11 new integration tests

Frontend (client/)

  • client/src/components/CostBreakdownTable/CostBreakdownTable.tsxCostBasisSelect component, resolveBudgetLineCost/resolveAggregateCost helpers
  • client/src/components/CostBreakdownTable/CostBreakdownTable.module.css — new .controlBar, .costBasisField, .costBasisSelect styles
  • client/src/pages/BudgetOverviewPage/BudgetOverviewPage.tsx — URL state via ?paymentStatus=paid|outstanding
  • client/src/i18n/en/budget.json, client/src/i18n/de/budget.json — Cost Basis i18n keys
  • client/src/pages/DiaryPage/DiaryPage.tsx — default filter set to manual
  • Auto-itemize flow harmonization files

E2E Tests (e2e/)

  • e2e/tests/budget/budget-cost-basis.spec.ts — 16 new E2E scenarios for Cost Basis filter
  • e2e/pages/BudgetOverviewPage.ts — new costBasisSelect and costBasisLabel locators
  • e2e/tests/diary/diary-r2-uat.spec.ts — 3 scenarios updated for manual-default filter
  • Fixture updates across 4 existing budget E2E specs

Manual Validation Checklist

Cost Basis Filter (#1786)

  • Navigate to Budget Overview → Cost Breakdown table. Confirm a "Cost Basis" dropdown is visible next to the perspective toggle (min/avg/max).
  • Default is "All" and the table shows the existing cost view.
  • Select "Paid" — all cost columns show only paid/claimed amounts. Budget lines with no paid invoices show €0. URL updates to ?paymentStatus=paid.
  • Select "Outstanding" — invoiced lines show pending amounts; uninvoiced lines show projected costs. URL updates to ?paymentStatus=outstanding.
  • Reload the page with ?paymentStatus=paid in the URL — "Paid" is pre-selected.
  • Switch perspective toggle (min/avg/max) while "Paid" or "Outstanding" is active — both controls work independently.
  • Deselect a budget source while "Paid" is active — source filter and cost basis filter work together.

Diary Default Filter (#1782)

  • Navigate to the Construction Diary. Confirm the initial filter defaults to "Manual" (not "All"), hiding auto-generated events on load.
  • Switch filter to "All" — auto-generated events appear. Switch back to "Manual" — they hide.

Auto-itemize Harmonization (#1780)

  • Open an invoice in the auto-itemize flow for an existing invoice (one already linked to a work item). Confirm the flow works as before — items can be appended or replaced.
  • Open an invoice in the auto-itemize flow for a new invoice. Confirm the flow works as before.

Testing

  • DockerHub beta image: docker pull steilerDev/cornerstone:beta
  • PR-specific image: docker pull steilerDev/cornerstone:pr-1791

Supersedes #1789

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

steilerDev and others added 4 commits June 22, 2026 10:35
…ows (#1780)

* refactor(auto-itemize): harmonize existing-invoice and new-invoice flows

Extract shared logic duplicated between AutoItemizePage and
PaperlessInvoiceReviewPage into three reusable units:

- `materializeInlineDrafts` (autoItemizeDraftUtils.ts) — pure async helper
  that resolves inline budget-line drafts at save time. Financial fields
  (includesVat, quantity, unitPrice, totalAmount) now come from the LIVE
  line state rather than the draft snapshot, so the created budget line
  always matches the invoice line item as the user sees it at save time.

- `AutoItemizePdfPreview` component — shared PDF preview column (iframe +
  loading overlay + fallback) previously copy-pasted verbatim.

- `useAutoItemizeLines` hook — encapsulates all line-state management and
  picker integration (useBudgetLinePicker, all CRUD handlers, initializeStaticData
  effect, budgetSourceId re-defaulting). Both pages now call one hook instead
  of ~150 lines of duplicated boilerplate each.

Also aligns confidence derivation: both flows now derive confidence from the
Paperless documentType (Invoice/Quotation) before falling back to the ML score,
closing a behavioral divergence that previously only PaperlessInvoiceReviewPage
implemented.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(auto-itemize): absorb two-way VAT sync and fix mock types in tests

- Absorb beta's two-way VAT sync (from #1775/#1779) into useAutoItemizeLines:
  - onFieldChange: when field=includesVat, mirrors to inlineCreatedBudgetLineDraft
  - onInlineDraftChange: when updates.includesVat set, mirrors back to line
- Fix jest.fn() mock type annotations in autoItemizeDraftUtils.test.ts
  (TS2345: never inference with untyped mocks in Jest 30)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Frank Steiler <frank@steilerdev.de>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(diary): update E2E tests for default filter mode change from 'all' to 'manual'

- In diary-r2-uat.spec.ts: rename test and flip assertions so Manual chip
  is expected aria-pressed=true by default, All and Automatic are false
- In diary-list.spec.ts: add Scenario 12 (@smoke) asserting the Manual mode
  chip is the default when navigating to /diary with no params, and that the
  initial API request includes the daily_log type param

Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.6) <noreply@anthropic.com>

* fix(diary): default filter to 'manual' instead of 'all'

Change the diary page initial filter mode from 'all' to 'manual' so that
users land on a focused view of manually authored entries by default, rather
than seeing all auto-generated events mixed in.

- DiaryPage.tsx: default filterMode state reads 'manual' when no URL param
  is present
- DiaryPage.tsx: handleClearAll resets filterMode to 'manual' instead of 'all'
- DiaryPage.test.tsx: add 4 unit tests covering default mode, explicit URL
  param overrides (all, automatic), and Clear All reset behaviour

Fixes #1781

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <noreply@anthropic.com>

* test(diary): fix handleClearAll test to use search query so clear button renders

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>

* chore(diary): fix comment typo budget_update -> budget_breach in E2E spec

Co-Authored-By: Claude e2e-test-engineer (Sonnet) <noreply@anthropic.com>

---------

Co-authored-by: Frank Steiler <frank@steilerdev.de>
Co-authored-by: Claude e2e-test-engineer (Sonnet 4.6) <noreply@anthropic.com>
…1786)

Adds a "Cost Basis" dropdown filter (All / Paid / Outstanding) to the Cost Breakdown table, separate from the existing min/avg/max perspective toggle.

Fixes #1786
Diary default changed from 'all' to 'manual' in #1782. Three test scenarios
in diary-r2-uat.spec.ts were asserting the old default ('All' chip pressed)
or trying to click 'Manual' when it was already active (causing timeout).

Co-authored-by: Frank Steiler <frank@steilerdev.de>
Co-authored-by: Claude e2e-test-engineer (claude-sonnet-4-6) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.12.0-beta.2 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

…ter (#1792)

* fix(e2e): update diary-r2-uat Scenario 2 for manual-default diary filter

Scenario 2 ("Clicking Manual chip hides automatic type chips") was clicking
the Manual mode chip when Manual is already the default — a no-op that fires
no API request. waitForResponse would time out.

Apply the same pattern already used for Scenario 10: switch to "All" first,
wait for that API response, then click Manual and await the second response.

Co-Authored-By: Claude e2e-test-engineer (Sonnet) <noreply@anthropic.com>

* fix(e2e): update diary-automatic-events Scenario 2 for manual-default diary filter

The work_item_status type chip is hidden in manual mode (the new default
from #1782). Switch to "All" mode first before clicking it to make
automatic type chips visible, then reset request capture so only the
chip-click request is measured.

Co-Authored-By: Claude e2e-test-engineer (Sonnet) <noreply@anthropic.com>

* fix(e2e): fix queueCreateNewBudgetLine assertion to properly await picker close

The previous assertion checked pickerModal (filtered by step-1 h2) which was
already absent once the flow moved to step 2, causing it to pass immediately
without confirming the picker actually closed. This left a timing gap where the
test's next assertion (getCreatingNewBadge) ran before React had batched and
applied the setLines + closePicker state updates.

Changed to pickerStep2Modal().not.toBeVisible() which polls until the step-2
modal disappears — this happens in the same React render cycle that also sets
inlineCreatedBudgetLineDraft on the line card, ensuring the badge is visible
by the time the assertion resolves.

Fixes scenarios 16-18 in paperless-first-invoice.spec.ts (shard 5).

Co-Authored-By: Claude e2e-test-engineer (Sonnet) <noreply@anthropic.com>

* fix(e2e): fix deterministic E2E test failures in shards 5 and 12

**Shard 12: budget-cost-basis.spec.ts (mobile)**
Replace non-retrying getAttribute('class')+toMatch() assertions with
retrying toHaveClass(/Active/i). On mobile WebKit, React's state
update after selectOption() may not have committed when getAttribute
is called. toHaveClass retries until the DOM reflects the new class.

**Shard 5: paperless-first-invoice.spec.ts Scenario 18**
Fix the invalid-amount guard setup. materializeInlineDrafts reads
financial state from the LIVE LINE (not the inline draft form), so
clearing the inline form's unitPrice field had no effect — the save
proceeded and returned a Paperless config error instead of
inlineDraftInvalid.

Fix: manipulate the LIVE LINE's inputs BEFORE queuing the draft
(while the cardMetricGrid is still visible):
- Clear unitPrice → disables unit-pricing mode (hasUnitPricing=false)
- Set totalAmount = -1 → netBase = -1 < 0 → inlineDraftInvalid fires

Co-Authored-By: Claude e2e-test-engineer (Sonnet) <noreply@anthropic.com>

---------

Co-authored-by: Frank Steiler <frank@steilerdev.de>
Co-authored-by: Claude e2e-test-engineer (Sonnet) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.12.0-beta.3 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

…witch step (#1793)

In diary-r2-uat.spec.ts Scenarios 2 and 10 and diary-automatic-events.spec.ts
Scenario 2, the "switch to All first" step used waitForResponse() to wait for
the All-click's API response. This can race: the DiaryPage's useEffect fires
asynchronously (state update → re-render → aria-pressed changes → useEffect
→ API call → response), so in high-contention CI environments (fail-fast mode
with maxFailures:1) the response can arrive after the next waitForResponse()
listener is registered, causing the subsequent test step to be fulfilled by
the wrong response.

Replace with diaryPage.waitForLoaded(), which races between timeline.visible,
emptyState.visible, and errorBanner.visible. These elements are controlled by
isLoading which is set true at the start of loadEntries() and cleared in
finally. Returning waitForLoaded() guarantees the All-mode response cycle is
complete before the next waitForResponse() listener is registered.

Co-authored-by: Frank Steiler <frank@steilerdev.de>
Co-authored-by: Claude e2e-test-engineer (Sonnet) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.12.0-beta.4 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant