You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When creating a new budget line in direct amount mode and deselecting the "Price includes VAT (19%)" checkbox, the form converts the entered gross-equivalent value to a net value (multiplies by 1.19) before sending it to the server. The server stores plannedAmount as that already-uplifted value AND stores includesVat = false. When consumers (budget overview, work item totals, invoice pipeline, etc.) read the row, the shared helper effectivePlannedAmount() applies another ×1.19 uplift because includesVat === false is the documented signal that the stored amount is net. The result is a budget line whose effective planned amount is uplifted twice (≈41.6% higher than intended).
The same flow triggered from the invoice itemization path (creating a budget line while itemizing an invoice) yields the same wrong stored plannedAmount on the budget line, even though the itemization amount itself is correctly applied to the invoice.
Behaviour must be consistent across all entry points. Deselecting "Price includes VAT" should never produce a double-uplifted planned amount.
Expected vs Actual Behaviour
Entry
User enters
"Includes VAT" toggle
Expected effective planned (gross)
Actual effective planned (gross)
Direct amount, new line
100.00
OFF (= net)
119.00
141.61 (100 × 1.19 × 1.19)
Direct amount, new line
100.00
ON (= gross)
100.00
100.00 (correct)
Unit pricing, new line
qty=1 × price=100
OFF (= net)
119.00
119.00 (correct)
Invoice itemization, budget line for 100.00
100.00
OFF (= net)
119.00
141.61 (same bug)
The unit-pricing mode is correct because it stores raw qty × unitPrice (net) without applying the multiplier in the hook, so effectivePlannedAmount() correctly applies the single ×1.19 when reading. The direct mode pre-multiplies before submitting, which is the source of the duplication.
Reproduction Steps
Open any work item or household item detail page (e.g. /work-items/<id> or /household-items/<id>).
Click + Add Line in the Budget section.
Keep Direct amount mode selected.
Enter 100.00 in Planned amount.
Uncheck "Price includes VAT (19%)".
Save the line.
Observe: the saved card shows €141.61 as the planned amount (instead of €119.00).
Inspect the row via the API: plannedAmount = 119.00, includesVat = false — both stored values are wrong relative to each other.
Repeat from the invoice itemization flow (invoice detail page → Itemize → add new budget line with VAT off): the budget line is created with the same double-uplift on the stored record.
Acceptance Criteria
Given a user opens the Add Budget Line form in any entry point (work item, household item, invoice itemization) When they enter a value in Direct amount mode with Includes VAT off Then the resulting persisted budget line has plannedAmount equal to the value the user typed (net) andincludesVat = false, andeffectivePlannedAmount() returns exactly value × 1.19 — no double uplift.
Given a user opens the Add Budget Line form in any entry point When they enter a value in Direct amount mode with Includes VAT on Then the resulting persisted budget line has plannedAmount equal to the value the user typed (gross) andincludesVat = true, andeffectivePlannedAmount() returns the typed value unchanged.
Given a user opens the Add Budget Line form in any entry point When they enter qty and unit price in Unit pricing mode with Includes VAT off Then the persisted budget line has plannedAmount = qty × unitPrice (net), includesVat = false, and effectivePlannedAmount() returns qty × unitPrice × 1.19 (this is already the correct behaviour — confirm parity is preserved).
Given an existing budget line with the double-uplift bug already persisted When the user edits it via either the work item, household item, or invoice itemization page Then the edit form correctly round-trips the stored plannedAmount and includesVat such that re-saving without changes does not further inflate the stored value.
Given the invoice itemization flow When a user adds a new budget line with Includes VAT off from the invoice detail page Then the stored budget line has the same plannedAmount and includesVat semantics as a line created directly on a work item — both flows produce equivalent records.
Given/When/Then UAT Scenarios
Scenario 1 — Direct amount, VAT off, work item entry
Given an authenticated user on a work item detail page
When they add a budget line with planned amount 100.00, direct mode, Includes VAT off
Then the budget card displays €119.00 as the effective planned amount (not €141.61)
And the stored row has plannedAmount = 100.00 and includesVat = false
Scenario 2 — Direct amount, VAT on, work item entry
Given an authenticated user on a work item detail page
When they add a budget line with planned amount 100.00, direct mode, Includes VAT on
Then the budget card displays €100.00 as the effective planned amount
And the stored row has plannedAmount = 100.00 and includesVat = true
Scenario 3 — Direct amount, VAT off, household item entry
Given an authenticated user on a household item detail page
When they add a budget line with planned amount 50.00, direct mode, Includes VAT off
Then the budget card displays €59.50 as the effective planned amount
And the work-item-equivalent budget overview totals reflect €59.50, not €70.80
Scenario 4 — Invoice itemization, VAT off
Given an invoice exists and a user opens its itemization
When they add a new budget line linked to a work item, direct mode, planned 200.00, Includes VAT off
Then the new budget line has plannedAmount = 200.00, includesVat = false
And the work item's Budget Cost Overview shows €238.00 planned (not €283.22)
Scenario 5 — Round-trip edit
Given a budget line created with the corrected behaviour (plannedAmount = 100, includesVat = false)
When the user opens the edit form and saves without changes
Then the stored values remain plannedAmount = 100, includesVat = false (no inflation)
Relevant Files / Areas
client/src/hooks/useBudgetSection.ts (lines 188–213) — the handleSaveBudgetLine function pre-multiplies plannedAmount by 1.19 in direct mode when includesVat = false. This is the bug. Compare with the unit-pricing branch (line 212) which correctly stores raw qty × unitPrice without multiplying.
client/src/components/budget/BudgetLineForm.tsx (lines 113–118, 196–211) — the form's "Includes VAT" checkbox lives in both direct and unit modes. The note text shown when VAT is off should not imply the form will pre-apply VAT.
shared/src/types/budget.ts (lines 95–108) — effectivePlannedAmount() documents the canonical semantics: plannedAmount is stored as net when includesVat === false, and consumers apply ×1.19 themselves.
server/src/services/budgetOverviewService.ts (lines 208–210) — server-side effective(l) mirror of the same semantics. Consumers across the system rely on this; the form is the single deviation.
server/src/services/shared/budgetServiceFactory.ts (lines 540–545) — server passes through includesVat and plannedAmount unchanged, so the fix is purely on the client form/hook.
server/src/services/invoiceBudgetLineService.ts — invoice-itemization budget-line creation path; needs to use the same fixed value semantics.
Test coverage to add: client/src/hooks/useBudgetSection.test.ts to assert direct-mode payload is not pre-multiplied; existing unit-pricing tests should continue to pass.
Notes
This is a recent regression: issue Add 'Includes VAT' checkbox to Direct amount mode in budget line editor #1371 (closed 2026-04-28) added the "Includes VAT" checkbox to Direct amount mode. The unit-pricing mode was already correctly handling the semantics; the direct-mode handler was added with the wrong arithmetic.
The fix should be on the client (remove the × multiplier line in useBudgetSection.ts direct-mode branch). The server already stores plannedAmount and includesVat faithfully, and downstream consumers (effectivePlannedAmount, budget overview, totals, payback engine) all apply the ×1.19 uplift exactly once based on the includesVat flag.
Migration consideration: budget lines already saved with the double-uplift will need either a data fix or user re-entry. The fix story should include a one-time corrective script that finds rows with includes_vat = 0 whose planned_amount shows the ×1.19 fingerprint, OR document that historical rows are user-corrected.
Problem
When creating a new budget line in direct amount mode and deselecting the "Price includes VAT (19%)" checkbox, the form converts the entered gross-equivalent value to a net value (multiplies by 1.19) before sending it to the server. The server stores
plannedAmountas that already-uplifted value AND storesincludesVat = false. When consumers (budget overview, work item totals, invoice pipeline, etc.) read the row, the shared helpereffectivePlannedAmount()applies another ×1.19 uplift becauseincludesVat === falseis the documented signal that the stored amount is net. The result is a budget line whose effective planned amount is uplifted twice (≈41.6% higher than intended).The same flow triggered from the invoice itemization path (creating a budget line while itemizing an invoice) yields the same wrong stored
plannedAmounton the budget line, even though the itemization amount itself is correctly applied to the invoice.Behaviour must be consistent across all entry points. Deselecting "Price includes VAT" should never produce a double-uplifted planned amount.
Expected vs Actual Behaviour
100.00119.00141.61(100 × 1.19 × 1.19)100.00100.00100.00(correct)qty=1 × price=100119.00119.00(correct)100.00100.00119.00141.61(same bug)The unit-pricing mode is correct because it stores raw
qty × unitPrice(net) without applying the multiplier in the hook, soeffectivePlannedAmount()correctly applies the single ×1.19 when reading. The direct mode pre-multiplies before submitting, which is the source of the duplication.Reproduction Steps
/work-items/<id>or/household-items/<id>).100.00in Planned amount.€141.61as the planned amount (instead of€119.00).plannedAmount = 119.00,includesVat = false— both stored values are wrong relative to each other.Repeat from the invoice itemization flow (invoice detail page → Itemize → add new budget line with VAT off): the budget line is created with the same double-uplift on the stored record.
Acceptance Criteria
Given a user opens the Add Budget Line form in any entry point (work item, household item, invoice itemization)
When they enter a value in Direct amount mode with Includes VAT off
Then the resulting persisted budget line has
plannedAmountequal to the value the user typed (net) andincludesVat = false, andeffectivePlannedAmount()returns exactly value × 1.19 — no double uplift.Given a user opens the Add Budget Line form in any entry point
When they enter a value in Direct amount mode with Includes VAT on
Then the resulting persisted budget line has
plannedAmountequal to the value the user typed (gross) andincludesVat = true, andeffectivePlannedAmount()returns the typed value unchanged.Given a user opens the Add Budget Line form in any entry point
When they enter qty and unit price in Unit pricing mode with Includes VAT off
Then the persisted budget line has
plannedAmount = qty × unitPrice(net),includesVat = false, andeffectivePlannedAmount()returnsqty × unitPrice × 1.19(this is already the correct behaviour — confirm parity is preserved).Given an existing budget line with the double-uplift bug already persisted
When the user edits it via either the work item, household item, or invoice itemization page
Then the edit form correctly round-trips the stored
plannedAmountandincludesVatsuch that re-saving without changes does not further inflate the stored value.Given the invoice itemization flow
When a user adds a new budget line with Includes VAT off from the invoice detail page
Then the stored budget line has the same
plannedAmountandincludesVatsemantics as a line created directly on a work item — both flows produce equivalent records.Given/When/Then UAT Scenarios
Scenario 1 — Direct amount, VAT off, work item entry
100.00, direct mode, Includes VAT off€119.00as the effective planned amount (not€141.61)plannedAmount = 100.00andincludesVat = falseScenario 2 — Direct amount, VAT on, work item entry
100.00, direct mode, Includes VAT on€100.00as the effective planned amountplannedAmount = 100.00andincludesVat = trueScenario 3 — Direct amount, VAT off, household item entry
50.00, direct mode, Includes VAT off€59.50as the effective planned amount€59.50, not€70.80Scenario 4 — Invoice itemization, VAT off
200.00, Includes VAT offplannedAmount = 200.00,includesVat = false€238.00planned (not€283.22)Scenario 5 — Round-trip edit
plannedAmount = 100,includesVat = false)plannedAmount = 100,includesVat = false(no inflation)Relevant Files / Areas
client/src/hooks/useBudgetSection.ts(lines 188–213) — thehandleSaveBudgetLinefunction pre-multipliesplannedAmountby 1.19 in direct mode whenincludesVat = false. This is the bug. Compare with the unit-pricing branch (line 212) which correctly stores rawqty × unitPricewithout multiplying.client/src/components/budget/BudgetLineForm.tsx(lines 113–118, 196–211) — the form's "Includes VAT" checkbox lives in bothdirectandunitmodes. The note text shown when VAT is off should not imply the form will pre-apply VAT.shared/src/types/budget.ts(lines 95–108) —effectivePlannedAmount()documents the canonical semantics:plannedAmountis stored as net whenincludesVat === false, and consumers apply ×1.19 themselves.server/src/services/budgetOverviewService.ts(lines 208–210) — server-sideeffective(l)mirror of the same semantics. Consumers across the system rely on this; the form is the single deviation.server/src/services/shared/budgetServiceFactory.ts(lines 540–545) — server passes throughincludesVatandplannedAmountunchanged, so the fix is purely on the client form/hook.server/src/services/invoiceBudgetLineService.ts— invoice-itemization budget-line creation path; needs to use the same fixed value semantics.client/src/hooks/useBudgetSection.test.tsto assert direct-mode payload is not pre-multiplied; existing unit-pricing tests should continue to pass.Notes
× multiplierline inuseBudgetSection.tsdirect-mode branch). The server already storesplannedAmountandincludesVatfaithfully, and downstream consumers (effectivePlannedAmount, budget overview, totals, payback engine) all apply the ×1.19 uplift exactly once based on theincludesVatflag.includes_vat = 0whoseplanned_amountshows the ×1.19 fingerprint, OR document that historical rows are user-corrected.