You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 13 Next »

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

 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.


DEV info when running on local machine bypassing the Swagger UI:

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:  

  • [FromRoute] string accountId
  • [FromQuery] DateTime dateFrom
  • [FromQuery] DateTime dateTo
  • [FromQuery] int? limit          (defaults to 100 in the repository)
  • CancellationToken ct

   Body:     none (GET)

   Returns: 

  • 200 OK                   -> IEnumerable<BrightInvoice>            
  • 400 BadRequest    -> DECLARED, never produced (catch commented out)            
  • 401 Unauthorized  -> produced by [Authorize] framework, not by code            
  • 404 NotFound      -> DECLARED, never produced (catch commented out)
  • 500                        -> any thrown exception (validation, SQL, mapping)

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

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

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

 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) → @PdfMimeType defaults to application/pdf

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

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

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;

  • 2→"2month"
  • 3→"3month"
  • 6→"6month"
  • 12→"12month"
  • anything else (incl. missing dates or 1-month) -> "1month".

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

  • No labels