================================================================================

 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):


     commonValidator.ValidateCustomer  (via AccountValidator.ValidateAccountGetOutput,

     invoked as "BrightInvoiceService.GetByCustomerAsync"):

       - 0 customer rows           -> "No customer found with Id '{id}'."

       - >1 customer rows          -> "Unique active customer could not be identified. Id: '{id}'."

       - row has blank CustomerId  -> "The customer returned did not have any CustomerId."

       - returned id != requested  -> "Returned customer '{row}' does not match requested '{id}'."

       - MitId deactivated         -> "The customer with Id '{id}' does not have an active MitId

                                        registration."  (skipped if Settings.IgnoreMitIdStatus)


     InvoicesValidator.ValidateGetInvoicesInput  (invoked as "GetInvoices"):

       - blank customerId          -> "Missing parameter 'AccountId'."

       - dateFrom out of SQL range -> "Missing or invalid parameter 'DateFrom'.

                  Provide a date between 1753-01-01 and 9999-12-31 (e.g. 2000-01-01)."

       - dateTo out of SQL range   -> "Missing or invalid parameter 'DateTo'.

                  Provide a date between 1753-01-01 and 9999-12-31 (e.g. 2030-12-31)."

       - dateFrom > dateTo         -> "Parameter outside of allowed range:

                  'DateFrom' must be earlier than or equal to 'DateTo'."

       - limit < 1                 -> "Parameter outside of allowed range:

                 'Limit' must be 1 or greater."

       (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.)


   NOTE on status codes:

     The two domain catches in the controller are commented out ("we make use of the internalerror500 for all errors inside the api"),

     so a bad date or an unknown customer returns 500 (not 400/404). The validation messages above are wrapped by ValidationHelper into:

     "The operation [{invokingMethodName}] could not be completed due to the following validation errors: \n <message(s)>" and that wrapped text becomes the 500 body.

--------------------------------------------------------------------------------

 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).


  ExcludeParked      = Settings.InvoiceExcludeParked      ?? BCHEC VISEJPARKEREDE

                       -> AND (UDSMARK IS NULL OR UDSMARK <= 25)        (hide UDSMARK > 25)

  CheckUdsmark       = Settings.InvoiceCheckUdsmark       ?? BCHEC CHECKUDSMARK

                       -> AND (UDSMARK IS NULL OR = 0 OR BETWEEN 2 AND 25)  (also drops UDSMARK = 1)

  ShowOnlyDelivered  = Settings.InvoiceShowOnlyDelivered  ?? BCHEC VISKUNUDSKREVNE

                       -> UDSMARK <= 25 + SNEX/DSEND delivery check (DELIVERYSTATE IN (5,9))

  ExcludeFutureDated = Settings.InvoiceExcludeFutureDated ?? BCHEC EJFREMTID

                       -> AND BILAGSDATO <= @Today

  ExcludeAfregnTypes = Settings.InvoiceExcludeAfregnTypes (non-empty) ?? BCHEC W11_W12EJAFRTYP

                       -> AND AFREGNTYPE NOT IN @ExcludeAfregnTypes

  OnlyRendered       = !Settings.InvoiceShowAlsoInvoicesWithoutPdf   (default true)

                       -> AND EXISTS (AKONDDOC -> BDOC, DOCTYPE=1, DATALENGTH(PAYLOAD)>0,

                                      MIMETYPE LIKE @PdfMimeType)


Always-applied (Base query) filters, independent of the toggles above:

  - FIRMANR = @CompanyId, KUNDENR = @CustomerId

  - TARIFART IN ('A-TOT','A-FAK')                  (header rows only; S-TOT excluded)

  - (SWIBASVIS IS NULL OR SWIBASVIS = 1)            (exclude SWIB-internal, non-visible)

  - (SAMLREGNINGNR IS NULL OR SAMLREGNINGNR <= 0)   (hide bills folded into a collective invoice)

  - BILAGSDATO BETWEEN @DateFrom AND @DateTo

  - ORDER BY BILAGSDATO DESC, REGNINGNR DESC, SWCOUNT DESC

    OFFSET 0 ROWS FETCH NEXT @Limit ROWS ONLY        (@Limit = limit ?? 100)


--------------------------------------------------------------------------------

 RESPONSE STRUCTURE (IEnumerable<BrightInvoice>)

--------------------------------------------------------------------------------

The endpoint returns a JSON array of invoice objects (no wrapper). Each object contains a nested `Info` array of supplementary key/value (or title/value) entries.

[
  {
    "Id":              "<InstNr>-<ForbnNr>-<UdebNr>-<Id>",  // Data: composite of AKOND.INSTNR + FORBNR + UDEBNR + REGNINGNR (REGNINGNR cast to varchar AS Id)
    "ServiceId":       "consumption_trade",                 // Data: AUDEBFORS.FORSYNINGSART (+ AKOND.AFREGNTYPE for KØL) via ForsyningsartMapper; null for unmapped/unknown/null supply types
    "DueDate":         "2026-01-31T00:00:00+00:00",         // Data: AKOND.FORFDATO (DateTimeOffset, UTC offset 0); null if NULL
    "InvoiceDate":     "2026-01-01T00:00:00+00:00",         // Data: AKOND.BILAGSDATO (DateTimeOffset, UTC offset 0); null if NULL
    "Period":          "1month",                            // Data: derived from AKOND.DATOFRA/DATOTIL month span (1/2/3/6/12month); defaults "1month" if either date null or span unmatched
    "StartDate":       "2026-01-01T00:00:00+00:00",         // Data: AKOND.DATOFRA (DateTimeOffset, UTC offset 0); null if NULL
    "EndDate":         "2026-01-31T00:00:00+00:00",         // Data: AKOND.DATOTIL (DateTimeOffset, UTC offset 0); null if NULL
    "RemainingAmount": 0.00,                                // Data: AKOND.KR (TotalAmount) - PBSI PaidAmount; null unless BOTH present
    "TotalAmount":     1953.12,                             // Data: AKOND.KR
    "InvoiceStatus":   "paid",                              // Data: derived (see MapInvoiceStatus); one of unpaid/paid/partly_paid/overdue/cancelled/credited only
    "InvoiceType":     "pdf",                               // Data: derived from HasPdf EXISTS check -> "pdf" or "missing" ("html" never produced)
    "Info": [
      { "Key": "invoiceNumber",   "Value": "<REGNINGNR>" },   // Data: AKOND.REGNINGNR (as Id) — NOTE: raw value, NOT the composite top-level Id
      { "Key": "invoiceDate",     "Value": "<offset>" },      // Data: AKOND.BILAGSDATO (DateTimeOffset) or null
      { "Key": "dueDate",         "Value": "<offset>" },      // Data: AKOND.FORFDATO (DateTimeOffset) or null
      { "Key": "totalAmount",     "Value": "1953.12" },       // Data: AKOND.KR (.ToString(), so a STRING here vs decimal at top level)
      { "Key": "period",          "Value": "1month" },        // Data: same derivation as top-level Period
      { "Key": "remainingAmount", "Value": "0.00" },          // Data: TotalAmount - PaidAmount (.ToString(); null unless both present)
      { "Key": "status",          "Value": "paid" },          // Data: same derivation as top-level InvoiceStatus
      { "Key": "description",     "Value": "<Tekst>" },       // Data: AKOND.TEKST
      { "Title": "Tekst",         "Value": "<Tekst>" }        // Data: AKOND.TEKST (DUPLICATE of description; uses Title (untranslated) instead of Key)
      // "ocr"  -> Not mapped (TODO: source field in SonWin not yet identified)
    ]
  }
]

Field origin detail (top-level BrightInvoice):
  Id              <- composite "{InstNr}-{ForbnNr}-{UdebNr}-{Id}" (Id = REGNINGNR)
  ServiceId       <- ForsyningsartMapper.MapToBrightServiceType(ForsyningsArt, AfregnType)
  DueDate         <- AKOND.FORFDATO
  InvoiceDate     <- AKOND.BILAGSDATO
  Period          <- MapPeriod(DATOFRA, DATOTIL)
  StartDate       <- AKOND.DATOFRA
  EndDate         <- AKOND.DATOTIL
  RemainingAmount <- AKOND.KR - PBSI PaidAmount (null unless both present)
  TotalAmount     <- AKOND.KR
  InvoiceStatus   <- MapInvoiceStatus(row).GetDisplayName()
  InvoiceType     <- MapInvoiceType(row)  ("pdf" if HasPdf else "missing")
  Info            <- list assembled in the mapper (see above)

InvoiceStatus derivation (MapInvoiceStatus), in order:
  1. KORTSTATUS == 99                    -> "cancelled"
  2. TotalAmount (KR) < 0                -> "credited"
  3. SettlementDate (UDLIGNDATO) set     -> "paid"  (regardless of PaidAmount)
  4. PaidAmount >= TotalAmount           -> "paid"
  5. DueDate < today (and not fully paid)-> "overdue"
  6. PaidAmount > 0                       -> "partly_paid"
  7. otherwise                            -> "unpaid"
  (collection / reminder / deferred_* / investigation / paid_out are never returned — TODO.)

PaidAmount source: OUTER APPLY over Sonlinc.AKOND where TARIFART='PBSI' for the same INSTNR+FORBNR+UDEBNR+REGNINGNR+FIRMANR; SUM(KR*ANTAL)*-1

(PBSI rows are negative), ISNULL(...,0). Not exposed as its own response field — only feeds RemainingAmount and the status derivation.

DbInvoice columns selected but NOT surfaced as their own response field:
  - AfregnType (AKOND.AFREGNTYPE)   used only by ServiceId (KØL -> cooling)
  - KortStatus (AKOND.KORTSTATUS)   used only by status (== 99 -> cancelled)
  - KortType   (AKOND.KORTTYPE)     selected, currently unused in mapping
  - SettlementDate (AKOND.UDLIGNDATO) used only by status
  - CollectiveBillNr (AKOND.SAMLREGNINGNR) selected; also used as a WHERE filter
  - HasPdf (EXISTS check)           used only by InvoiceType
  - PaidAmount (PBSI sum)           used by RemainingAmount + status