pricing

package module
v0.0.0-...-45679d4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2023 License: MIT Imports: 16 Imported by: 0

README

REST Pricing Server

Getting started

Run the application:

go run ./cmd/server/main.go

Query for brands:

# Default seeded brand
curl localhost:8080/api/v1/brands?name=EXAMPLE
{"id":1,"name":"EXAMPLE"}

# Non-existant brand
curl -I localhost:8080/api/v1/brands?name=NOTEXIST
HTTP/1.1 404 Not Found

Query for pricing, note time is in RFC3339:

curl -s 'localhost:8080/api/v1/prices?brand_id=1&product_id=35455&date=2020-06-14T10:00:00.00Z&string_id=test_1' | jq -r
{
  "brand_id": 1,
  "product_id": 35455,
  "price": "35.50",
  "curr": "EUR",
  "start_date": "2020-06-14 00:00:00 +0000 UTC",
  "end_date": "2020-12-31 23:59:59 +0000 UTC",
  "string_id": "test_1"
}

All pricing queries:

curl -s 'localhost:8080/api/v1/prices?brand_id=1&product_id=35455&date=2020-06-14T10:00:00.00Z&string_id=test_1' | jq -r

curl -s 'localhost:8080/api/v1/prices?brand_id=1&product_id=35455&date=2020-06-14T16:00:00.00Z&string_id=test_1' | jq -r

curl -s 'localhost:8080/api/v1/prices?brand_id=1&product_id=35455&date=2020-06-14T21:00:00.00Z&string_id=test_1' | jq -r

curl -s 'localhost:8080/api/v1/prices?brand_id=1&product_id=35455&date=2020-06-15T10:00:00.00Z&string_id=test_1' | jq -r

curl -s 'localhost:8080/api/v1/prices?brand_id=1&product_id=35455&date=2020-06-16T21:00:00.00Z&string_id=test_1' | jq -r

Use Postgres repository

Start a Postgres database with Docker:

docker run \
  --rm \
  --name postgres \
  --publish 5432:5432 \
  --env POSTGRES_PASSWORD=password \
  --detach \
  docker.io/postgres:15.3-alpine

Run the application:

go run ./cmd/server/main.go -enable-postgres=true

Tests

Run tests:

go test -race -v ./...

Run tests without testcontainers (Docker):

go test -short -race -v ./...

Run end to end test with provided date times:

go test -race -v ./... -run TestRun

=== RUN   TestRun
=== RUN   TestRun/Test_1
    server_test.go:142: test_1 - {1 35455 35.50 EUR 2020-06-14 00:00:00 +0000 UTC 2020-12-31 23:59:59 +0000 UTC test_1}
=== RUN   TestRun/Test_2
    server_test.go:142: test_2 - {1 35455 25.45 EUR 2020-06-14 15:00:00 +0000 UTC 2020-06-14 18:30:00 +0000 UTC test_2}
=== RUN   TestRun/Test_3
    server_test.go:142: test_3 - {1 35455 35.50 EUR 2020-06-14 00:00:00 +0000 UTC 2020-12-31 23:59:59 +0000 UTC test_3}
=== RUN   TestRun/Test_4
    server_test.go:142: test_4 - {1 35455 30.50 EUR 2020-06-15 00:00:00 +0000 UTC 2020-06-15 11:00:00 +0000 UTC test_4}
=== RUN   TestRun/Test_5
    server_test.go:142: test_5 - {1 35455 38.95 EUR 2020-06-15 16:00:00 +0000 UTC 2020-12-31 23:59:59 +0000 UTC test_5}
--- PASS: TestRun (0.00s)
    --- PASS: TestRun/Test_1 (0.00s)
    --- PASS: TestRun/Test_2 (0.00s)
    --- PASS: TestRun/Test_3 (0.00s)
    --- PASS: TestRun/Test_4 (0.00s)
    --- PASS: TestRun/Test_5 (0.00s)

Shortcuts

Many!

  • doc comments
  • not using https://github.com/Rhymond/go-money
  • table tests for all situations
  • all API's
  • context cancellations
  • no logger like zerolog, zap, logrus, etc
  • no Prometheus metrics
  • no OTEL
  • hardening
  • -version command based on git sha
  • OpenAPI yaml

Results

Same for in memory and postgres backend repositories.

$ curl -s 'localhost:8080/api/v1/prices?brand_id=1&product_id=35455&date=2020-06-14T10:00:00.00Z&string_id=test_1' | jq -r
{
  "brand_id": 1,
  "product_id": 35455,
  "price": "35.50",
  "curr": "EUR",
  "start_date": "2020-06-14 00:00:00 +0000 UTC",
  "end_date": "2020-12-31 23:59:59 +0000 UTC",
  "string_id": "test_1"
}

$ curl -s 'localhost:8080/api/v1/prices?brand_id=1&product_id=35455&date=2020-06-14T16:00:00.00Z&string_id=test_2' | jq -r
{
  "brand_id": 1,
  "product_id": 35455,
  "price": "25.45",
  "curr": "EUR",
  "start_date": "2020-06-14 15:00:00 +0000 UTC",
  "end_date": "2020-06-14 18:30:00 +0000 UTC",
  "string_id": "test_2"
}

$ curl -s 'localhost:8080/api/v1/prices?brand_id=1&product_id=35455&date=2020-06-14T21:00:00.00Z&string_id=test_3' | jq -r
{
  "brand_id": 1,
  "product_id": 35455,
  "price": "35.50",
  "curr": "EUR",
  "start_date": "2020-06-14 00:00:00 +0000 UTC",
  "end_date": "2020-12-31 23:59:59 +0000 UTC",
  "string_id": "test_3"
}

$ curl -s 'localhost:8080/api/v1/prices?brand_id=1&product_id=35455&date=2020-06-15T10:00:00.00Z&string_id=test_4' | jq -r
{
  "brand_id": 1,
  "product_id": 35455,
  "price": "30.50",
  "curr": "EUR",
  "start_date": "2020-06-15 00:00:00 +0000 UTC",
  "end_date": "2020-06-15 11:00:00 +0000 UTC",
  "string_id": "test_4"
}

$ curl -s 'localhost:8080/api/v1/prices?brand_id=1&product_id=35455&date=2020-06-16T21:00:00.00Z&string_id=test_5' | jq -r
{
  "brand_id": 1,
  "product_id": 35455,
  "price": "38.95",
  "curr": "EUR",
  "start_date": "2020-06-15 16:00:00 +0000 UTC",
  "end_date": "2020-12-31 23:59:59 +0000 UTC",
  "string_id": "test_5"
}

Documentation

Overview

package pricing implements implements storing and retrieving pricing data.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SeedExampleData

func SeedExampleData(ctx context.Context, repo Repository) error

Types

type AddBrandRequest

type AddBrandRequest struct {
	Name string `json:"name"`
}

type AddBrandResponse

type AddBrandResponse struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

type AddPriceRequest

type AddPriceRequest struct {
	BrandID   int       `json:"brand_id"`
	StartDate time.Time `json:"start_date"`
	EndDate   time.Time `json:"end_date"`
	ProductID int       `json:"product_id"`
	Priority  int       `json:"priority"`
	Price     int       `json:"price"`
	Curr      string    `json:"curr"`
}

type App

type App struct {
	// contains filtered or unexported fields
}

func NewApp

func NewApp(args []string) (*App, error)

func (*App) Run

func (app *App) Run(ctx context.Context) error

Run starts an HTTP server and gracefully shuts down when the provided context is marked done.

type Brand

type Brand struct {
	ID   int
	Name string
}

type FinalPrice

type FinalPrice struct {
	BrandID   int       // BRAND_ID: foreign key of the group chain (1 = EXAMPLE).
	StartDate time.Time // START_DATE: date range in which the indicated price applies.
	EndDate   time.Time // END_DATE: date range in which the indicated price applies.
	ProductID int       // PRODUCT_ID: Product code identifier.
	Price     int       // PRICE: final selling price. Lowest unit for currency, e.g: cents
	Curr      string    // CURR: currency iso.
}

type GetBrandRequest

type GetBrandRequest struct {
	Name string `json:"name"`
}

type GetBrandResponse

type GetBrandResponse struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

type GetPriceRequest

type GetPriceRequest struct {
	BrandID   int       `json:"brand_id"`
	ProductID int       `json:"product_id"`
	Date      time.Time `json:"date"`
	StringID  string    `json:"string_id"`
}

type GetPriceResponse

type GetPriceResponse struct {
	BrandID   int    `json:"brand_id"`
	ProductID int    `json:"product_id"`
	Price     string `json:"price"`
	Curr      string `json:"curr"`
	StartDate string `json:"start_date"`
	EndDate   string `json:"end_date"`
	StringID  string `json:"string_id"`
}

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler will expose our service via an "open host service"

func NewHandler

func NewHandler(svc *Service) (*Handler, error)

func (Handler) AddBrand

func (h Handler) AddBrand(w http.ResponseWriter, req *http.Request)

func (Handler) AddPrice

func (h Handler) AddPrice(w http.ResponseWriter, req *http.Request)

func (Handler) GetBrand

func (h Handler) GetBrand(w http.ResponseWriter, req *http.Request)

func (Handler) GetPrice

func (h Handler) GetPrice(w http.ResponseWriter, req *http.Request)

type InMemoryRepository

type InMemoryRepository struct {
	// contains filtered or unexported fields
}

func NewInMemoryRepository

func NewInMemoryRepository(ctx context.Context) (*InMemoryRepository, error)

NewInMemoryRepository returns a memory backed Repository for persisting pricing data.

func (*InMemoryRepository) AddBrand

func (imr *InMemoryRepository) AddBrand(ctx context.Context, name string) error

func (*InMemoryRepository) AddPrice

func (imr *InMemoryRepository) AddPrice(ctx context.Context, price Price) error

func (*InMemoryRepository) GetBrand

func (imr *InMemoryRepository) GetBrand(ctx context.Context, name string) (Brand, error)

func (*InMemoryRepository) GetPrice

func (imr *InMemoryRepository) GetPrice(ctx context.Context, brandID, productID int, date time.Time) (FinalPrice, error)

func (*InMemoryRepository) Shutdown

func (imr *InMemoryRepository) Shutdown(ctx context.Context) error

type MockRepository

type MockRepository struct {
}

func NewMockRepository

func NewMockRepository() *MockRepository

func (*MockRepository) AddBrand

func (mr *MockRepository) AddBrand(ctx context.Context, name string) error

func (*MockRepository) AddPrice

func (mr *MockRepository) AddPrice(ctx context.Context, price Price) error

func (*MockRepository) GetBrand

func (mr *MockRepository) GetBrand(ctx context.Context, name string) (Brand, error)

func (*MockRepository) GetPrice

func (mr *MockRepository) GetPrice(ctx context.Context, brandID, productID int, date time.Time) (FinalPrice, error)

func (*MockRepository) Shutdown

func (mr *MockRepository) Shutdown(ctx context.Context) error

type Postgres

type Postgres struct {
	// contains filtered or unexported fields
}

Postgres is an instance of the database handler and contains a connection pool for concurrent use by methods.

func NewPostgresRepository

func NewPostgresRepository(ctx context.Context, URL, poolSettings string) (*Postgres, error)

NewPostgresRepository returns a Postgres backed Repository for persisting pricing data. New also runs any migrations in the ./migrations directory and it does this over a single new connection before closing the connection and providing a Postgres connection pool for the application main use. It might be better to split this functionality and still avoid a race condition with connections. urlExample := "postgres://username:password@localhost:5432/database_name" poolSettingsExample := "?sslmode=verify-ca&pool_max_conns=10"

func (*Postgres) AddBrand

func (pg *Postgres) AddBrand(ctx context.Context, name string) error

func (*Postgres) AddPrice

func (pg *Postgres) AddPrice(ctx context.Context, price Price) error

func (*Postgres) GetBrand

func (pg *Postgres) GetBrand(ctx context.Context, name string) (Brand, error)

func (*Postgres) GetPrice

func (pg *Postgres) GetPrice(ctx context.Context, brandID, productID int, date time.Time) (FinalPrice, error)

func (*Postgres) Shutdown

func (pg *Postgres) Shutdown(ctx context.Context) error

Shutdown closes the connection pool to new acquires and waits gracefully for existing connections to close.

type Price

type Price struct {
	BrandID   int       // BRAND_ID: foreign key of the group chain (1 = EXAMPLE).
	StartDate time.Time // START_DATE: date range in which the indicated price applies.
	EndDate   time.Time // END_DATE: date range in which the indicated price applies.
	ProductID int       // PRODUCT_ID: Product code identifier.
	Priority  int       // PRIORITY: Price application disambiguator. If two prices coincide in a date range, the one with higher priority (higher numerical value) is applied.
	Price     int       // PRICE: final selling price. Lowest unit for currency, e.g: cents // could be money.Money
	Curr      string    // CURR: currency iso.
}

type Repository

type Repository interface {
	AddPrice(ctx context.Context, price Price) error
	GetPrice(ctx context.Context, brandID, productID int, date time.Time) (FinalPrice, error)
	AddBrand(ctx context.Context, name string) error
	GetBrand(ctx context.Context, name string) (Brand, error)
	Shutdown(ctx context.Context) error
}

Repository implements persisting and reading pricing data from a backend.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service contains a Repository and actions any business logic before/after interacting with the Repository.

func NewService

func NewService(repo Repository) *Service

NewService creates a new Service with the supplied repository ready for reading and writing Price data.

func (*Service) AddBrand

func (srv *Service) AddBrand(ctx context.Context, name string) error

func (*Service) AddPrice

func (srv *Service) AddPrice(ctx context.Context, price Price) error

AddPrice inserts a new Price into the backing storage repository.

func (*Service) GetBrand

func (srv *Service) GetBrand(ctx context.Context, name string) (Brand, error)

func (*Service) GetPrice

func (srv *Service) GetPrice(ctx context.Context, brandID, productID int, date time.Time) (FinalPrice, error)

GetPrice returns the final price to apply given the provided brand, product and date. Price is an integer in the currencies lowest common demoninator, For example, cents in USD, yen in JPY.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL