================================================================================
BrightInvoicesController - Summary & Response Structures
File: SonWinCommonAPI/Controllers/BrightInvoicesController.cs
================================================================================
--------------------------------------------------------------------------------
WHAT THE CONTROLLER DOES
--------------------------------------------------------------------------------
Exposes a single read-only endpoint that returns a customer's invoices within a date range.
The domain is SonWin billing: invoice header rows live in Sonlinc.AKOND, enriched with supply type (AUDEBFORS), PBSI payment rows (also in AKOND), and a "has a rendered PDF" existence check (AKONDDOC -> BDOC).
The result is mapped into the Bright-facing BrightInvoice model.
Visibility/filtering is configuration-driven. Each filter toggle is resolved as "local appsettings config wins, otherwise defer to the company's BCHEC flag"
(Sonlinc.BCHED, cached in memory). The SQL is assembled from pure-SQL fragments so only the active clauses appear in the executed statement.
--------------------------------------------------------------------------------
METHODS
--------------------------------------------------------------------------------
1) GetInvoices
Route: GET accounts/{accountId}/invoices
Auth: [Authorize] (class-level); [ApiController]
Params:
Body: none (GET)
Returns:
What it does:
Validates the customer and the input range, resolves the company's invoice visibility filters, runs the list query, and maps each row to a BrightInvoice.
Returns the (possibly empty) list with 200.
Validation that affects the result (all failures THROW -> surface as HTTP 500):
Validation that affects the result (all surface as HTTP 500 with a message):
InvoicesValidator.ValidateGetInvoicesInput (invoked as "GetInvoices"):
(A missing/unparseable date query param binds to DateTime.MinValue, which is below SQL Server's datetime min; the range check catches this up front to avoid a cryptic SQL error.)
--------------------------------------------------------------------------------
FILTER RESOLUTION (InvoiceListFilters)
--------------------------------------------------------------------------------
Resolved in BrightInvoiceRepository.GetByCustomerAsync as "local appsettings config (Settings.Invoices:*) ?? company BCHEC flag".
The three UDSMARK visibility variants are independent and compose. Each active flag appends its own SQL clause; inactive flags add nothing (no runtime OR-gates).
Always-applied (Base query) filters, independent of the toggles above:
--------------------------------------------------------------------------------
RESPONSE STRUCTURE (IEnumerable<BrightInvoice>)
--------------------------------------------------------------------------------
The endpoint returns a JSON array of invoice objects. Each object contains a nested `Info` array of supplementary key/value (or title/value) entries.
[
{
"Id": <value>, // Composite: $"{InstNr}-{ForbnNr}-{UdebNr}-{Id}" = AKOND.INSTNR-FORBNR-UDEBNR-REGNINGNR
"ServiceId": <value>, // Data: AUDEBFORS.FORSYNINGSART (+AFREGNTYPE) via ForsyningsartMapper. El-only join => in practice "consumption_trade" or null (see limitations).
"InvoiceDate": <value>, // Data: AKOND.BILAGSDATO (DateTimeOffset, UTC offset 0) | null
"DueDate": <value>, // Data: AKOND.FORFDATO (DateTimeOffset, UTC offset 0) | null
"Period": <value>, // Derived from StartDate/EndDate month-span: "1month"/"2month"/"3month"/"6month"/"12month" (defaults to "1month" when dates missing or other span)
"StartDate": <value>, // Data: AKOND.DATOFRA (DateTimeOffset, UTC offset 0) | null
"EndDate": <value>, // Data: AKOND.DATOTIL (DateTimeOffset, UTC offset 0) | null
"RemainingAmount": <value>,// Data: running account saldo (SUM of AKOND.KR up to this line) - NOT a per-invoice remaining figure.
"TotalAmount": <value>, // Data: AKOND.KR (decimal) | null
"InvoiceStatus": <value>, // Derived (see status logic below): one of cancelled / credited / paid / overdue / unpaid non-empty BDOC.PAYLOAD exists, else "missing"
// Flat supplementary list; mostly DUPLICATES the structured fields above, as strings/dates.
"Info": [
{ "Key": "invoiceNumber", "Value": <Id> }, // Data: AKOND.REGNINGNR (string)
{ "Key": "invoiceDate", "Value": <InvoiceDate> },// Data: AKOND.BILAGSDATO (DateTimeOffset|null)
{ "Key": "dueDate", "Value": <DueDate> }, // Data: AKOND.FORFDATO (DateTimeOffset|null)
{ "Key": "totalAmount", "Value": <TotalAmount> },// Data: AKOND.KR .ToString() | null
{ "Key": "period", "Value": <Period> }, // Derived period string
{ "Key": "remainingAmount", "Value": <Saldo> }, // Data: running saldo .ToString() | null
{ "Key": "status", "Value": <InvoiceStatus> },// Derived status string
{ "Key": "description", "Value": <Tekst> }, // Data: AKOND.TEKST
{ "Title": "Tekst", "Value": <Tekst> } // Data: AKOND.TEKST (DUPLICATE of description,
// sent via Title (untranslated) not Key)
// NOTE: "ocr" key is NOT emitted (source field not yet identified - TODO)
]
}
] |
Field origin detail (top-level BrightInvoice):
INVOICE STATUS LOGIC (MapInvoiceStatus, first match wins):
1. KORTSTATUS = 99 -> "cancelled" (confirmed cancelled)
2. TotalAmount (KR) < 0 -> "credited" (credit note)
3. UDLIGNDATO set OR running saldo <= 0 -> "paid" (account square through this line; balance-forward)
4. DueDate (FORFDATO) in the past -> "overdue"
5. otherwise -> "unpaid"
('partly_paid' is intentionally never produced.)
PERIOD LOGIC (MapPeriod): month span = (EndDate - StartDate) in whole months;
(PBSI rows are negative), ISNULL(...,0). Not exposed as its own response field — only feeds RemainingAmount and the status derivation.