Skip to content

codewithdpk/EV-Pricing-Server

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EV TOU Pricing Engine API

Assignment implementation for EV charger-specific Time-of-Use (TOU) pricing with:

  • per-charger schedules
  • hourly/minute-window pricing periods
  • timezone-aware price lookup

Problem Statement

The API must satisfy two core requirements:

  • Charger-specific pricing: each charger has independent schedules/rates.
  • Pricing for hours in a day: each price applies to a minute range inside a day (no weekday/weekend split for now).

Technical Architecture

Layered architecture:

  • Router (Gin): HTTP transport, request parsing, status mapping.
  • Service: business rules (timezone conversion, schedule/period selection).
  • Repository: SQL access via go-pg/pg.
  • PostgreSQL: source of truth for charger metadata and TOU pricing.

Why this structure:

  • Keeps business logic testable and independent of HTTP and SQL.
  • Lets data access evolve without touching transport/business layers.
  • Prevents controller-fat logic and duplication.

Data Model and Storage Design

1) chargers

Purpose:

  • Stores charger identity and timezone context.

Important fields:

  • id (UUID PK): internal relational key.
  • external_id (unique): API/business identifier used by clients.
  • timezone: IANA timezone (America/Los_Angeles etc).

Why:

  • Price selection depends on charger-local date/time, so timezone is part of domain state.

2) pricing_schedules

Purpose:

  • Date-effective schedule envelope for a charger.

Important fields:

  • charger_id FK -> chargers.id
  • effective_from_date
  • effective_to_date (nullable for open-ended active schedule)

Why:

  • Supports future/previous schedule versions without deleting history.

3) pricing_periods

Purpose:

  • Intraday TOU windows under a schedule.

Important fields:

  • schedule_id FK -> pricing_schedules.id
  • start_minute, end_minute ([start, end))
  • price_per_kwh

Why:

  • Minute boundaries are compact, deterministic, and easy to query.

Retrieval Algorithm (Critical Flow)

For GET /api/v1/chargers/:charger_id/price?timestamp=...:

  1. Resolve charger by external_id (path charger_id).
  2. Load charger timezone.
  3. Convert input timestamp to charger local time.
  4. Extract local date (YYYY-MM-DD) and minute-of-day.
  5. Find active schedule where:
    • effective_from_date <= local_date
    • effective_to_date IS NULL OR effective_to_date >= local_date
    • latest effective_from_date wins.
  6. Find pricing period in that schedule where:
    • start_minute <= minute_of_day
    • end_minute > minute_of_day
  7. Return normalized response contract (GetPriceResponse).

Why this is correct:

  • Schedule choice is date-scoped and charger-local.
  • Period choice is minute-scoped inside the chosen schedule.
  • Avoids timezone ambiguity by always normalizing to charger-local time before selection.

Indexing Strategy

Implemented indexes:

  • pricing_schedules (charger_id, effective_from_date, effective_to_date)
  • pricing_periods (schedule_id, start_minute, end_minute)

Why:

  • Schedule lookup uses charger + date bounds.
  • Period lookup uses schedule + minute window predicates.
  • Reduces latency for the two hottest query paths in price retrieval.

Migrations:

  • Base schema: internal/db/migrations/202603021520_create_charger_prices.go
  • Seed data: internal/db/migrations/202603021715_seed_initial_pricing_data.go
  • Index migration: internal/db/migrations/202603021900_add_pricing_indexes.go

Project Structure

cmd/
  server/main.go        # API bootstrap
  migrate/main.go       # migration runner

internal/
  api/router.go         # endpoints + HTTP error mapping
  config/config.go      # env + .env loading
  db/
    postgres.go         # go-pg connection
    migrations/         # schema/data/index migrations
  pricing/
    model.go            # DB models and query row models
    dtos.go             # request/response contracts
    repository.go       # repository interface
    pg_repository.go    # SQL implementation
    service.go          # domain logic (selection algorithm)

docs/
  openapi.yaml          # OpenAPI spec
  swagger.html          # Swagger UI

Utilities and Operational Concerns

  • Config utility:
    • internal/config/config.go
    • Loads env and .env, then applies fallbacks.
  • Migration utility:
    • cmd/migrate auto-inits migration table for non-init commands.
  • Health utility:
    • /healthz executes DB ping via service/repository.

API Endpoints

GET /healthz

Checks service and DB connectivity.

GET /api/v1/chargers

Returns chargers list (including external_id, timezone, metadata).

GET /api/v1/chargers/:charger_id/pricing

Returns full charger-specific pricing view:

  • charger metadata
  • schedules
  • periods for each schedule

GET /api/v1/chargers/:charger_id/price?timestamp=...

Returns applicable price for that charger at a given time.

Data Contracts

Defined in internal/pricing/dtos.go.

Price lookup

  • GetPriceRequest

    • chargerId (path)
    • timestamp (query parsed to time.Time)
  • GetPriceResponse

    • chargerId
    • timestamp
    • localTime
    • timezone
    • pricePerKwh
    • currency
    • pricingPeriod { startTime, endTime }
  • Envelope: PriceEnvelope { data: GetPriceResponse }

Charger-specific pricing

  • ChargerSpecificPricingResponse

    • chargerId
    • location
    • timezone
    • schedules[]
  • ScheduleWithPrice

    • scheduleId
    • effectiveFromDate
    • effectiveToDate
    • periods[]
  • PeriodPriceWindow

    • periodId
    • startMinute, endMinute
    • startTime, endTime
    • pricePerKwh
    • currency
  • Envelope: ChargerPricingEnvelope { data: ChargerSpecificPricingResponse }

Swagger

  • UI: GET /swagger
  • Spec: GET /swagger/openapi.yaml

Local Run

Set environment:

export PORT=8080
export DB_ADDR=localhost:5432
export DB_USER=postgres
export DB_PASSWORD=postgres
export DB_NAME=evtoupricing

Run migrations:

go run ./cmd/migrate init
go run ./cmd/migrate up

Run server:

go run ./cmd/server

Design Tradeoffs and Next Improvements

Current design is optimized for correctness and maintainability.

Potential future improvements:

  • Add constraints to enforce non-overlapping periods per schedule.
  • Add import/bulk update pipeline for external tariff feeds.
  • Add caching for hot chargers.
  • Add contract tests for edge times (00:00, 23:59, DST boundaries).

About

EV TOU Pricing API

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages