Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ ignis has no authentication of its own. The reverse proxy in front of it is the

Step 2 is optional. Call it when you need to inspect or report the inputs; `calculate` uses them either way.

If you have a construction year rather than a period code, send `year=` in place of `period=` at step 1 and ignis resolves it. Exactly one of the two is required. `GET /api/v1/periods/{country}` lists a country's bands as `{ period, year_from, year_to }`, oldest first, with `year_from` 0 and `year_to` 9999 marking the open-ended bands.

## Overriding archetype inputs

`calculate` accepts an optional JSON body. Every field is independent and optional: send only what you want to change, and every other input still comes from the archetype's TABULA defaults.
Expand Down
84 changes: 76 additions & 8 deletions docs/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@ paths:
summary: Match refurbishment variants for a building type and period
description: >
Returns the refurbishment levels for one building type and construction
period, ordered from existing state to most refurbished.
period, ordered from existing state to most refurbished. Give the period
either directly (period) or as a construction year (year), which ignis
resolves to the period whose band contains it for that country and type.
A year with no matching archetype for that type returns an empty data
list.
parameters:
- $ref: "#/components/parameters/CountryIso2"
- name: type
Expand All @@ -115,11 +119,22 @@ paths:
example: SFH
- name: period
in: query
required: true
description: TABULA construction-period code
required: false
description: >
TABULA construction-period code. Exactly one of period or year is
required.
schema:
type: string
example: "01"
- name: year
in: query
required: false
description: >
Construction year, resolved to a period. Exactly one of period or
year is required.
schema:
type: integer
example: 1975
responses:
"200":
description: Matched refurbishment levels
Expand All @@ -132,6 +147,29 @@ paths:
"403":
$ref: "#/components/responses/Forbidden"

/api/v1/periods/{country_iso2}:
get:
tags: [Variants]
operationId: listPeriods
summary: List a country's construction-year bands
description: >
The construction periods for a country, oldest first. year_from 0 is
open-ended (oldest band); year_to 9999 is open-ended (newest band).
period is the value the match endpoint's period parameter takes.
parameters:
- $ref: "#/components/parameters/CountryIso2"
responses:
"200":
description: Construction-year bands for the country
content:
application/json:
schema:
$ref: "#/components/schemas/PeriodsResponse"
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Forbidden"

/api/v1/data/{code}:
get:
tags: [Data]
Expand Down Expand Up @@ -238,11 +276,12 @@ components:
responses:
BadRequest:
description: >
Invalid input (unknown country, malformed code, or an invalid
CalculateRequest override — A_ref, h_room, n_Storey, or c_m not greater
than 0, HeatingDays, I_Sol_*, n_air_infiltration, or n_air_use negative,
a thermal-bridging ΔU negative, or a surface with an unknown type /
non-positive area / non-positive u_value)
Invalid input (unknown country, malformed code, a match request missing
type or giving neither or both of period and year or a non-integer year,
or an invalid CalculateRequest override — A_ref, h_room, n_Storey, or c_m
not greater than 0, HeatingDays, I_Sol_*, n_air_infiltration, or
n_air_use negative, a thermal-bridging ΔU negative, or a surface with an
unknown type / non-positive area / non-positive u_value)
content:
application/json:
schema:
Expand Down Expand Up @@ -321,6 +360,35 @@ components:
example: Existing state
required: [code, label]

PeriodsResponse:
type: object
properties:
country:
type: string
example: germany
data:
type: array
items:
$ref: "#/components/schemas/ConstructionPeriod"
required: [country, data]

ConstructionPeriod:
type: object
properties:
period:
type: string
description: Period code, as taken by the match endpoint's period parameter
example: "03"
year_from:
type: integer
description: First year of the band; 0 means open-ended
example: 1958
year_to:
type: integer
description: Last year of the band; 9999 means open-ended
example: 1968
required: [period, year_from, year_to]

DataResponse:
type: object
properties:
Expand Down
22 changes: 19 additions & 3 deletions internal/api/handler/calculation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import (

// mockRepo implements repository.TabulaReader for use in handler tests.
type mockRepo struct {
listVariants func(ctx context.Context, tableName string) ([]string, error)
matchVariants func(ctx context.Context, tableName, prefix string) ([]string, error)
getVariant func(ctx context.Context, tableName, code string) (*models.TabulaBuildingParameters, string, float64, error)
listVariants func(ctx context.Context, tableName string) ([]string, error)
matchVariants func(ctx context.Context, tableName, prefix string) ([]string, error)
resolvePeriodByYear func(ctx context.Context, tableName, typePrefix string, year int) (string, error)
listPeriods func(ctx context.Context, tableName string) ([]repository.ConstructionPeriod, error)
getVariant func(ctx context.Context, tableName, code string) (*models.TabulaBuildingParameters, string, float64, error)
}

func (m *mockRepo) ListVariants(ctx context.Context, tableName string) ([]string, error) {
Expand All @@ -33,6 +35,20 @@ func (m *mockRepo) MatchVariants(ctx context.Context, tableName, prefix string)
return nil, nil
}

func (m *mockRepo) ResolvePeriodByYear(ctx context.Context, tableName, typePrefix string, year int) (string, error) {
if m.resolvePeriodByYear != nil {
return m.resolvePeriodByYear(ctx, tableName, typePrefix, year)
}
return "", nil
}

func (m *mockRepo) ListPeriods(ctx context.Context, tableName string) ([]repository.ConstructionPeriod, error) {
if m.listPeriods != nil {
return m.listPeriods(ctx, tableName)
}
return nil, nil
}

func (m *mockRepo) GetVariant(ctx context.Context, tableName, code string) (*models.TabulaBuildingParameters, string, float64, error) {
return m.getVariant(ctx, tableName, code)
}
Expand Down
69 changes: 62 additions & 7 deletions internal/api/handler/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -60,8 +61,10 @@ func refurbishmentLabel(index int) string {
}

// MatchVariants returns all refurbishment variants for a building type and construction period.
// Query params: type (e.g. SFH), period (e.g. 01). Both are required.
// The response is ordered from existing state to most-refurbished.
// Query params: type (e.g. SFH) is required; exactly one of period (e.g. 01) or
// year (e.g. 1975) must be given. With year, ignis resolves the period whose band
// contains it for that country and type. The response is ordered from existing
// state to most-refurbished.
func (h *Handler) MatchVariants(c *gin.Context) {
isoCode := strings.ToUpper(strings.TrimSpace(c.Param("country_iso2")))
tableName, err := tableNameFromISO(isoCode)
Expand All @@ -72,18 +75,44 @@ func (h *Handler) MatchVariants(c *gin.Context) {

buildingType := strings.ToUpper(strings.TrimSpace(c.Query("type")))
period := strings.TrimSpace(c.Query("period"))
yearParam := strings.TrimSpace(c.Query("year"))

if buildingType == "" || period == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "query params 'type' and 'period' are required"})
if buildingType == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "query param 'type' is required"})
return
}
if (period == "") == (yearParam == "") {
c.JSON(http.StatusBadRequest, gin.H{"error": "exactly one of 'period' or 'year' is required"})
return
}

// TABULA codes follow CC.N.TYPE.PERIOD.VariantSuffix — N is the national dataset identifier.
prefix := fmt.Sprintf("%s.N.%s.%s", isoCode, buildingType, period)

ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
defer cancel()

// TABULA codes follow CC.N.TYPE.PERIOD.VariantSuffix — N is the national dataset identifier.
typePrefix := fmt.Sprintf("%s.N.%s", isoCode, buildingType)

if yearParam != "" {
year, convErr := strconv.Atoi(yearParam)
if convErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("query param 'year' must be an integer, got %q", yearParam)})
return
}
period, err = h.repo.ResolvePeriodByYear(ctx, tableName, typePrefix, year)
if err != nil {
utils.Error.Printf("failed to resolve period for %s year %d: %v", typePrefix, year, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to resolve construction period"})
return
}
if period == "" {
// No archetype of this type covers the year — an empty match list, not an error.
c.JSON(http.StatusOK, gin.H{"country": tableName, "prefix": typePrefix, "data": []any{}})
return
}
}

prefix := fmt.Sprintf("%s.%s", typePrefix, period)

codes, err := h.repo.MatchVariants(ctx, tableName, prefix)
if err != nil {
utils.Error.Printf("failed to match variants for %s: %v", prefix, err)
Expand All @@ -107,6 +136,32 @@ func (h *Handler) MatchVariants(c *gin.Context) {
})
}

// ListPeriods returns the country's construction-year bands, oldest first.
// year_from 0 means open-ended (oldest band); year_to 9999 means open-ended (newest band).
func (h *Handler) ListPeriods(c *gin.Context) {
isoCode := strings.ToUpper(strings.TrimSpace(c.Param("country_iso2")))
tableName, err := tableNameFromISO(isoCode)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
defer cancel()

periods, err := h.repo.ListPeriods(ctx, tableName)
if err != nil {
utils.Error.Printf("failed to load construction periods for %s: %v", tableName, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to query construction periods"})
return
}

c.JSON(http.StatusOK, gin.H{
"country": tableName,
"data": periods,
})
}

// GetVariantData retrieves TABULA data for a specific building variant.
func (h *Handler) GetVariantData(c *gin.Context) {
variantCode := strings.TrimSpace(c.Param("code"))
Expand Down
Loading
Loading