Skip to content

Commit

Permalink
Merge pull request #26 from tairosonloa/feat/endpoint-to-get-prices
Browse files Browse the repository at this point in the history
Feat/endpoint to get prices
  • Loading branch information
tairosonloa committed Oct 2, 2023
2 parents 8bbe3ca + 2c007bd commit a65ba3d
Show file tree
Hide file tree
Showing 14 changed files with 287 additions and 18 deletions.
1 change: 1 addition & 0 deletions .github/workflows/k3s.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,5 @@ jobs:
./kubernetes/configmap.yml
./kubernetes/deployment.yml
./kubernetes/service.yml
./kubernetes/cronjob.yml
images: ghcr.io/${{ github.repository }}:${{ github.sha }}
1 change: 1 addition & 0 deletions internal/domain/prices.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func (id PricesID) String() string {
type PricesRepository interface {
// Save persists the given prices.
Save(ctx context.Context, prices []Prices) error

// Query returns the prices for the given date and zoneID.
//
// If zoneID is nil, it returns the prices for all zones.
Expand Down
2 changes: 2 additions & 0 deletions internal/domain/zone.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ func (id ZoneID) String() string {
type ZonesRepository interface {
// GetAll returns all the prices zones.
GetAll(ctx context.Context) ([]Zone, error)

// GetByID returns the prices zone with the given ID.
GetByID(ctx context.Context, id ZoneID) (Zone, error)

// GetByExternalID returns the prices zone with the given external ID.
GetByExternalID(ctx context.Context, externalID string) (Zone, error)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

[Test_GetPricesV1_Success - 1]
{"prices":[{"date":"2023-10-02","zone_id":"ABC","values":[{"datetime":"2023-10-02T00:00:00+02:00","value":0.1}]}]}
---

[Test_GetPricesV1_Empty - 1]
{"prices":[]}
---

[Test_GetPricesV1_Error - 1]
{"errorCode":"INTERNAL_SERVER_ERROR","message":"mock error","statusCode":500}
---
8 changes: 4 additions & 4 deletions internal/platform/http/handlers/prices/create_prices.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import (
"pvpc-backend/internal/services"
)

type response struct {
type createPricesResponse struct {
IDs []string `json:"IDs"`
}

// CreatePricesV1 returns a gin.HandlerFunc to fetch and store PVPC prices.
func CreatePricesV1(pricesService services.PricesService) gin.HandlerFunc {
// CreatePricesHandlerV1 returns a gin.HandlerFunc to fetch and store PVPC prices.
func CreatePricesHandlerV1(pricesService services.PricesService) gin.HandlerFunc {
return func(ctx *gin.Context) {
ids, err := pricesService.FetchAndStorePricesFromREE(ctx)
if err != nil {
Expand All @@ -23,7 +23,7 @@ func CreatePricesV1(pricesService services.PricesService) gin.HandlerFunc {
return
}

response := response{
response := createPricesResponse{
IDs: make([]string, len(ids)),
}

Expand Down
110 changes: 110 additions & 0 deletions internal/platform/http/handlers/prices/get_prices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package prices

import (
"context"
"net/http"
"net/url"
"time"

"github.com/gin-gonic/gin"

"pvpc-backend/internal/domain"
"pvpc-backend/internal/platform/http/responses"
"pvpc-backend/internal/services"
"pvpc-backend/pkg/logger"
)

type getPricesResponse struct {
Prices []pricesResponse `json:"prices"`
}

type pricesResponse struct {
Date string `json:"date"`
ZoneID string `json:"zone_id"`
Values []hourlyPriceResponse `json:"values"`
}

type hourlyPriceResponse struct {
Datetime string `json:"datetime"`
Value float64 `json:"value"`
}

// GetPricesHandlerV1 returns a gin.HandlerFunc to retrieve prices from storage.
func GetPricesHandlerV1(pricesService services.PricesService) gin.HandlerFunc {
return func(ctx *gin.Context) {

zoneID, date := parseGetPricesParams(ctx, ctx.Request.URL.Query())

prices, err := pricesService.GetPrices(ctx, zoneID, date)
if err != nil {
statusCode, response := responses.NewAPIErrorResponse(err)
ctx.JSON(statusCode, response)
return
}

response := getPricesResponse{
Prices: make([]pricesResponse, len(prices)),
}

for i, price := range prices {
response.Prices[i] = pricesResponse{
Date: price.Date().Format("2006-01-02"),
ZoneID: price.Zone().ID().String(),
Values: make([]hourlyPriceResponse, len(price.Values())),
}

for j, value := range price.Values() {
response.Prices[i].Values[j] = hourlyPriceResponse{
Datetime: value.Datetime().Format(time.RFC3339),
Value: value.Value(),
}
}
}

if len(response.Prices) == 0 {
ctx.JSON(http.StatusNotFound, response)
} else {
ctx.JSON(http.StatusOK, response)
}
}
}

func parseGetPricesParams(ctx context.Context, params url.Values) (*domain.ZoneID, *time.Time) {
var zoneID *domain.ZoneID
var date *time.Time

for key, value := range params {
switch key {
case "zone_id":
zoneID = parseZoneIDParamValue(ctx, value)
case "date":
date = parseDateParamValue(ctx, value)
}
}

return zoneID, date
}

func parseZoneIDParamValue(ctx context.Context, zoneID []string) *domain.ZoneID {
if len(zoneID) == 0 {
return nil
}
parsedZoneID, err := domain.NewZoneID(zoneID[0])
if err != nil {
logger.DebugContext(ctx, "Invalid zoneID", "zoneID", zoneID[0], "err", err)
return nil
}
return &parsedZoneID
}

func parseDateParamValue(ctx context.Context, date []string) *time.Time {
if len(date) == 0 {
return nil
}
parsedDate, err := time.Parse("2006-01-02", date[0])
if err != nil {
logger.DebugContext(ctx, "Invalid date", "date", date[0], "err", err)
return nil
}
return &parsedDate
}
135 changes: 135 additions & 0 deletions internal/platform/http/handlers/prices/get_prices_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package prices

import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"

"github.com/gin-gonic/gin"
"github.com/gkampitakis/go-snaps/snaps"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"pvpc-backend/internal/domain"
"pvpc-backend/internal/mocks"
"pvpc-backend/internal/services"
"pvpc-backend/pkg/logger"
)

func Test_GetPricesV1_Success(t *testing.T) {
logger.SetTestLogger(os.Stderr)
gin.SetMode(gin.TestMode)
repositoryMock := new(mocks.PricesRepository)
pricesService := services.NewPricesService(nil, nil, repositoryMock, nil)

r := gin.New()
r.GET("/v1/prices", GetPricesHandlerV1(pricesService))

prices, err := domain.NewPrices(domain.PricesDto{
ID: "ABC-2023-10-02",
Date: "2023-10-02T00:00:00+02:00",
Zone: domain.ZoneDto{
ID: "ABC",
ExternalID: "1234",
Name: "zone1",
},
Values: []domain.HourlyPriceDto{
{
Datetime: "2023-10-02T00:00:00+02:00",
Value: 0.1,
},
},
})
require.NoError(t, err)

repositoryMock.On(
"Query",
mock.Anything,
(*domain.ZoneID)(nil),
(*time.Time)(nil),
).Return([]domain.Prices{prices}, nil)

req, err := http.NewRequest(http.MethodGet, "/v1/prices", nil)
require.NoError(t, err)

rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)

res := rec.Result()
defer res.Body.Close()

repositoryMock.AssertExpectations(t)
require.Equal(t, http.StatusOK, res.StatusCode)
snaps.MatchSnapshot(t, rec.Body.String())

}

func Test_GetPricesV1_Empty(t *testing.T) {
logger.SetTestLogger(os.Stderr)
gin.SetMode(gin.TestMode)
repositoryMock := new(mocks.PricesRepository)
pricesService := services.NewPricesService(nil, nil, repositoryMock, nil)

zoneIDRaw := "ZON"
zoneID, err := domain.NewZoneID(zoneIDRaw)
require.NoError(t, err)

dateRaw := "2023-10-01"
date, err := time.Parse("2006-01-02", dateRaw)
require.NoError(t, err)

r := gin.New()
r.GET("/v1/prices", GetPricesHandlerV1(pricesService))

repositoryMock.On(
"Query",
mock.Anything,
&zoneID,
&date,
).Return([]domain.Prices{}, nil)

req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/v1/prices?date=%s&zone_id=%s", dateRaw, zoneIDRaw), nil)
require.NoError(t, err)

rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)

res := rec.Result()
defer res.Body.Close()

require.Equal(t, http.StatusNotFound, res.StatusCode)
snaps.MatchSnapshot(t, rec.Body.String())
}

func Test_GetPricesV1_Error(t *testing.T) {
logger.SetTestLogger(os.Stderr)
gin.SetMode(gin.TestMode)
repositoryMock := new(mocks.PricesRepository)
pricesService := services.NewPricesService(nil, nil, repositoryMock, nil)

r := gin.New()
r.GET("/v1/prices", GetPricesHandlerV1(pricesService))

repositoryMock.On(
"Query",
mock.Anything,
mock.Anything,
mock.Anything,
).Return(nil, errors.New("mock error"))

req, err := http.NewRequest(http.MethodGet, "/v1/prices", nil)
require.NoError(t, err)

rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)

res := rec.Result()
defer res.Body.Close()

require.Equal(t, http.StatusInternalServerError, res.StatusCode)
snaps.MatchSnapshot(t, rec.Body.String())
}
12 changes: 6 additions & 6 deletions internal/platform/http/handlers/zones/list_zones_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ func Test_ListZonesHandlerV1_Success(t *testing.T) {
logger.SetTestLogger(os.Stderr)
gin.SetMode(gin.TestMode)
repositoryMock := new(mocks.ZonesRepository)
listingService := services.NewZonesService(repositoryMock)
zonesService := services.NewZonesService(repositoryMock)

r := gin.New()
r.GET("/v1/zones", ListZonesHandlerV1(listingService))
r.GET("/v1/zones", ListZonesHandlerV1(zonesService))

zone1, err := domain.NewZone(domain.ZoneDto{
ID: "ABC",
Expand Down Expand Up @@ -65,10 +65,10 @@ func Test_ListZonesHandlerV1_Empty(t *testing.T) {
logger.SetTestLogger(os.Stderr)
gin.SetMode(gin.TestMode)
repositoryMock := new(mocks.ZonesRepository)
listingService := services.NewZonesService(repositoryMock)
zonesService := services.NewZonesService(repositoryMock)

r := gin.New()
r.GET("/v1/zones", ListZonesHandlerV1(listingService))
r.GET("/v1/zones", ListZonesHandlerV1(zonesService))

repositoryMock.On(
"GetAll",
Expand All @@ -92,10 +92,10 @@ func Test_ListZonesHandlerV1_Error(t *testing.T) {
logger.SetTestLogger(os.Stderr)
gin.SetMode(gin.TestMode)
repositoryMock := new(mocks.ZonesRepository)
listingService := services.NewZonesService(repositoryMock)
zonesService := services.NewZonesService(repositoryMock)

r := gin.New()
r.GET("/v1/zones", ListZonesHandlerV1(listingService))
r.GET("/v1/zones", ListZonesHandlerV1(zonesService))

repositoryMock.On(
"GetAll",
Expand Down
3 changes: 2 additions & 1 deletion internal/platform/http/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ func (s *HttpServer) registerRoutes() {
s.engine.GET("/v1/health", health.HealthCheckHandlerV1(s.storage.db, s.storage.dbTimeout))

// Prices
s.engine.POST("/v1/prices", prices.CreatePricesV1(s.services.pricesService))
s.engine.GET("/v1/prices", prices.GetPricesHandlerV1(s.services.pricesService))
s.engine.POST("/v1/prices", prices.CreatePricesHandlerV1(s.services.pricesService))

// Zones
s.engine.GET("/v1/zones", zones.ListZonesHandlerV1(s.services.zonesService))
Expand Down
7 changes: 4 additions & 3 deletions internal/platform/storage/postgresql/prices_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func (r *PricesRepository) Save(ctx context.Context, prices []domain.Prices) err

// Query implements the domain.PricesRepository interface.
func (r *PricesRepository) Query(ctx context.Context, zoneID *domain.ZoneID, date *time.Time) ([]domain.Prices, error) {
logger.DebugContext(ctx, "Getting all Zones from database")
logger.DebugContext(ctx, "Querying prices from database", "zoneID", fmt.Sprintf("%v", zoneID), "date", date)
pricesSQL := sqlbuilder.NewStruct(new(pricesSchema))

query := sqlbuilder.NewSelectBuilder().Select("prices.id", "prices.date", "prices.zone_id", "prices.values", "zones.external_id", "zones.name").
Expand All @@ -116,17 +116,18 @@ func (r *PricesRepository) Query(ctx context.Context, zoneID *domain.ZoneID, dat
From(pricesTableName).Join(zonesTableName, "prices.zone_id = zones.id").
OrderBy("prices.zone_id", "prices.date").Desc()
} else {
query = query.Where((fmt.Sprintf("zone_id = %s", zoneID.String()))).OrderBy("date").Desc().Limit(1)
query = query.Where((fmt.Sprintf("zone_id = '%s'", zoneID.String()))).OrderBy("date").Desc().Limit(1)
}
} else {
if zoneID == nil {
query = query.Where(query.Equal("date", date.Format("2006-01-02")))
} else {
query = query.Where(query.And(query.Equal("date", date.Format("2006-01-02"))), fmt.Sprintf("zone_id = %s", zoneID.String()))
query = query.Where(query.And(query.Equal("date", date.Format("2006-01-02"))), fmt.Sprintf("zone_id = '%s'", zoneID.String()))
}
}

querySQL, args := sqlbuilder.WithFlavor(query, sqlbuilder.PostgreSQL).Build()
logger.DebugContext(ctx, "Querying prices from database", "query", querySQL, "args", args)

ctxTimeout, cancel := context.WithTimeout(ctx, r.dbTimeout)
defer cancel()
Expand Down
Loading

0 comments on commit a65ba3d

Please sign in to comment.