feat: implement set-based invoice payment calculations and enhance email logging configuration
Playwright Tests / test (push) Canceled after 0s
Playwright Tests / test (push) Canceled after 0s
This commit is contained in:
+74
@@ -0,0 +1,74 @@
|
||||
---
|
||||
status: Accepted
|
||||
date: 2026-09-16
|
||||
applyTo:
|
||||
- "Fuchs/Services/FuchsWidgetService.cs"
|
||||
- "Fuchs/Services/ProcessWebComService.cs"
|
||||
- "Fuchs_Database/dbo/Functions/fds__fn_unpaidInvoices.sql"
|
||||
- "Fuchs_Database/dbo/Functions/fds__fn_InvoicePaymentAmount*.sql"
|
||||
- "Fuchs_Database/dbo/Functions/fds_wgd_*.sql"
|
||||
- "Fuchs_Database/dbo/Functions/fds_wdg_*.sql"
|
||||
- "Fuchs_Database/dbo/Stored Procedures/fds__logEmail.sql"
|
||||
- "Fuchs_Database/Scripts/**"
|
||||
supersededBy: ""
|
||||
---
|
||||
|
||||
# 0026 — Dashboard widget SQL is set-based (no per-row scalar UDFs); `fds__emaillog.config` is a JSON contract
|
||||
|
||||
## Context
|
||||
Two dashboard widgets were failing in the DB log (`fds__debuglog`, 2026-09-16):
|
||||
|
||||
1. **`wdg_invoices_overdue_sum`** timed out after the widget command timeout of 90 s. Its SQL called
|
||||
`[dbo].[fds__fn_InvoicePaymentAmount](Id)` once per row of `fds__fn_unpaidInvoices()`. That scalar
|
||||
UDF declares a table variable, so SQL Server 2019 cannot inline it (`sys.sql_modules.is_inlineable = 0`)
|
||||
and re-executes the multi-statement TVF `fds__fn_bankingtransactions` — a full pass over
|
||||
~11 k banking rows plus a `STRING_SPLIT` cross-apply over the settings heap — for **every** invoice:
|
||||
≈0.5 s × 187 unpaid invoices ≈ 100 s measured. `wdg_unbez_akonto` had the same pattern (≈5 s,
|
||||
hidden only by `daycache = 1`). The same total computed set-based (one TVF pass, `GROUP BY InvId`,
|
||||
joined to the invoices) takes < 0.5 s and returns the identical figure.
|
||||
2. **`wgd_lastEmails`** raised *JSON text is not properly formatted*. `fds_wgd_lastEmails` parses
|
||||
`fds__emaillog.config` with `OPENJSON`/`JSON_QUERY`, but `ProcessWebComService` — the replacement
|
||||
for the legacy mailer — wrote `config = ''` on every path. One such row was enough to error the
|
||||
whole widget, and the audit trail had lost recipient/subject/attachment names for every email sent
|
||||
through the new service.
|
||||
|
||||
Widget definitions are **data** (`fis_widgets.sql`), not schema, so there is no compile-time check
|
||||
that a widget's SQL is cheap or that the columns it parses exist.
|
||||
|
||||
## Decision
|
||||
- **Widget SQL and the functions backing it are set-based.** Per-invoice payment totals come from the
|
||||
inline TVF `[dbo].[fds__fn_InvoicePaymentAmounts]()` (one banking pass, grouped by `InvId`) or from
|
||||
the `[PaymentAmount]`/`[OpenAmount]` columns `fds__fn_unpaidInvoices()` now exposes. The scalar
|
||||
`fds__fn_InvoicePaymentAmount(_full)` remains for single-invoice callers (invoice/reminder
|
||||
preparation, per-row `paymentstatus = 'p'` cells in the invoice lists) but must not be called
|
||||
per row from a widget or any other query that iterates a whole invoice set.
|
||||
- **`fds__emaillog.config` is a JSON contract**, written by `ProcessWebComService.BuildAuditConfig`
|
||||
in the legacy mailer's shape — `{subject, from, to[], cc[], bcc[], attachments[]}` with addresses
|
||||
as `"Name" <email>` and attachments as file names only (never the body, never attachment bytes) —
|
||||
on **every** audit path (success, API error, exception, invalid address, service disabled). SQL
|
||||
consumers of the column must still tolerate non-JSON text (`IIF(ISJSON(x)=1, x, N'{}')` before
|
||||
parsing), because `config` is `NOT NULL` and older rows/other writers may hold `''`.
|
||||
- **Widget-data migrations live in `Fuchs_Database/Scripts/`** as idempotent, dated T-SQL files
|
||||
(`<None Include>` in the sqlproj, not part of the schema build) and are run once per environment
|
||||
after the schema deploy they depend on.
|
||||
|
||||
## Consequences
|
||||
- `wdg_invoices_overdue_sum` 101.9 s → 0.45 s, `wdg_unbez_akonto` 4.7 s → 0.4 s, `wgd_lastEmails`
|
||||
error → 15 ms (dev, 2026-09-16). The widget command timeout stays at 90 s; it is a guard, not a
|
||||
budget.
|
||||
- Adding a scalar UDF call inside a widget's `SELECT`/`WHERE` over `fds__invoices` or the
|
||||
`fds__fn_unpaidInvoices()` set is a regression of this decision. Prefer exposing a set-based column
|
||||
on the TVF, as done for `PaymentAmount`.
|
||||
- `ProcessWebComServiceTests` assert the config contract on every audit path; changing the shape
|
||||
requires updating `fds_wgd_lastEmails` in the same change.
|
||||
- The `Scripts/2026-09-16_fis_widgets_set_based_payment_amounts.sql` migration must be run on
|
||||
Production after the schema deploy (it is a no-op once applied).
|
||||
|
||||
## Alternatives considered
|
||||
- **Raise the widget `CommandTimeout`** — masks an O(N) re-scan; the cost grows with every unpaid
|
||||
invoice and every banking upload, and it would still block a dashboard load for minutes.
|
||||
- **Convert `fds__fn_InvoicePaymentAmount` into an inlineable scalar UDF** — impossible while it
|
||||
needs a TVP (`fds__tt__idlist_vchar`) to call the banking TVF; rewriting the banking TVF as an
|
||||
inline TVF is a larger change to a function shared with the banking-assignment UI.
|
||||
- **Fix the bad `fds__emaillog` row and leave the widget function strict** — the next `''` row (any
|
||||
writer, any environment) would take the widget down again; tolerance belongs in the reader.
|
||||
Reference in New Issue
Block a user