================================================================================
BrightLocationsController - Summary & Response Structures
File: SonWinCommonAPI/Controllers/BrightLocationsController.cs
================================================================================
--------------------------------------------------------------------------------
WHAT THE CONTROLLER DOES
--------------------------------------------------------------------------------
BrightLocationsController exposes a customer's locations (installations /
"installationer") and the supply-type services attached to them from the
SonWin/Sonlinc system, shaped into the "Bright" location contract. It requires
authorization ([Authorize]) and, on any thrown exception, goes through
CustomBase.HandleError which logs the error and returns HTTP 500 with the
exception message in the body.
IMPORTANT SCOPE LIMITATION:
Only ELECTRICITY supply is currently returned. The service query hard-codes
FORSYNINGSART = 0 (El), so even if an installation has water, heat, gas, etc.,
those services are not included today. (SQL for the other supply types exists
but is commented out / future work.)
Call chain:
BrightLocationsController.GetLocations
-> BrightLocationService.GetLocationByCustomerAsync (validation + mapping)
- repository.GetLocationHeaderByCustomerAsync -> location headers
- repository.GetSonWinSupplyTypeDataAsync -> electricity services
- repository.GetSonWinSupplyRangeAsync -> supply start/end dates
(SQL lives in LocationSql; sources are Sonlinc.AINSD / AFORD / AUDEBFORS /
IndividualMPoints + SWMarket.* market tables for the dates.)
--------------------------------------------------------------------------------
METHODS
--------------------------------------------------------------------------------
1) GetLocations
Route: GET /bright/locations
Params: customerId (required), limit (optional) (+ CancellationToken)
Returns: 200 OK -> IEnumerable<BrightLocation>
400 BadRequest (declared; see note below)
401 Unauthorized
404 NotFound (declared; see note below)
What it does:
Returns the list of active locations for a customer, each with its
electricity service(s). It first validates the customer exists
(commonValidator.ValidateCustomer) and validates input
(customerId required; limit, if supplied, must be 1-1000, default 100).
It then loads location headers, loads the electricity services for those
installations, attaches each service's current supply start/end dates, and
maps everything to the BrightLocation response.
Validation that affects the result (all surface as HTTP 500 with a message):
- Missing customerId -> "Missing parameter 'CustomerId'."
- limit out of range (1-1000) -> "Parameter outside of allowed range..."
- No locations found -> "No locations found for customer {id}"
- A service with no start date -> "Location with missing supply start
date found for meteringpointid: {id}..."
(Every returned service MUST have a supply start date or the whole call
fails. The start/end dates come from the SWMarket supply-range query,
which only returns the CURRENTLY active supply: FromDate <= today and
ToDate is null or in the future.)
NOTE on status codes: although 400 and 404 are declared via
ProducesResponseType, the BadRequest branch is commented out in the
controller. In practice every error (bad input, not found, missing dates,
DB failure) currently comes back as HTTP 500 with the message in the body.
--------------------------------------------------------------------------------
RESPONSE STRUCTURE (BrightLocation)
--------------------------------------------------------------------------------
Returned as a JSON ARRAY (IEnumerable<BrightLocation>), one element per active
installation for the customer.
[
{
"Id": <value> // Data: SonWin installation number (AINSD.INSTNR), as string
"Address": <value> // Data: street + house number (AINSD.ADRESSE + AINSD.HUSNUMMER)
"ZipCode": <value> // Data: postal code (AINSD.POSTNUM)
"City": <value> // Data: city name (IPOST.NAVN, falling back to AINSD.STED; empty string if both null)
"Properties": null // Hardcoded null: BrightLocationProperties is never populated by the mapper
// (PropertyType, NumberOfResidents, Size, HouseAge, Cars[], Heating,
// HeatingSecondary, Gadgets[] are all unused today)
"PriceZone": <value> // Derived: from ZipCode -> "DK1" if zip >= 6000, "DK2" if zip < 6000,
// null if the zip code cannot be parsed
"Services": [ // Data: the electricity service(s) for this installation (empty array if none)
{
"Id": <value> // Data: metering point id / EAN (IndividualMPoints.MeteringPointId, aka AftageNr)
"Type": <value> // Derived: mapped from SonWin forsyningsart. For electricity this is
// always "consumption_trade". (Other types map to water/heating/
// cooling/tv/broadband/waste/gas_trade, but only El is queried today.)
"Status": "Active" // Hardcoded: BrightServiceStatus.Active.ToString().
// NOTE: this emits the enum NAME "Active" (capital A), NOT the
// display value "active". TODO in code: derive real status from SonWin.
"MeasurementResolution": <value> // Derived from forsyningsart: electricity => "hour"
// (broadband => "none"; other types default to "none" - TODO).
"MeasurementId": <value> // Data: metering point id (same value as Service.Id / AftageNr)
"DeliveryPointId": null // Hardcoded null: never set by the mapper
"CostResolution": null // Hardcoded null: never set by the mapper
"CostId": null // Hardcoded null: never set by the mapper
"Properties": null // Hardcoded null: BrightServiceProperties (ShowCost,
// RealtimeMeasurements) is never populated
"StartDate": <value> // Data: current supply start date (SWMarket MeteringPointRoleOccurrence.FromDate),
// as DateTimeOffset (UTC/zero offset). Required - null fails validation.
"EndDate": <value> // Data: current supply end date (SWMarket ...ToDate), as DateTimeOffset.
// OMITTED from the JSON entirely when null (JsonIgnore WhenWritingNull);
// null/absent means the supply has no scheduled end (still active).
}
],
"Longitude": null // Hardcoded null: never set by the mapper
"Latitude": null // Hardcoded null: never set by the mapper
}
]
Notes:
- Properties, Longitude and Latitude on the location are always null/absent.
- On a service, DeliveryPointId, CostResolution, CostId and Properties are
always null; Id and MeasurementId always carry the same metering point id.
- Service.Status is the only hardcoded non-null literal and is currently the
enum NAME "Active" rather than the lowercase display string "active".
- EndDate is the only field deliberately omitted (not null) when it has no
value, due to [JsonIgnore(WhenWritingNull)].
- A location can have an empty Services array, but if a service IS present it
must have a StartDate or the whole request fails validation.