Assignment implementation for EV charger-specific Time-of-Use (TOU) pricing with:
- per-charger schedules
- hourly/minute-window pricing periods
- timezone-aware price lookup
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).
Layered architecture:
Router(Gin): HTTP transport, request parsing, status mapping.Service: business rules (timezone conversion, schedule/period selection).Repository: SQL access viago-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.
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_Angelesetc).
Why:
- Price selection depends on charger-local date/time, so timezone is part of domain state.
Purpose:
- Date-effective schedule envelope for a charger.
Important fields:
charger_idFK ->chargers.ideffective_from_dateeffective_to_date(nullable for open-ended active schedule)
Why:
- Supports future/previous schedule versions without deleting history.
Purpose:
- Intraday TOU windows under a schedule.
Important fields:
schedule_idFK ->pricing_schedules.idstart_minute,end_minute([start, end))price_per_kwh
Why:
- Minute boundaries are compact, deterministic, and easy to query.
For GET /api/v1/chargers/:charger_id/price?timestamp=...:
- Resolve charger by
external_id(pathcharger_id). - Load charger timezone.
- Convert input timestamp to charger local time.
- Extract local date (
YYYY-MM-DD) and minute-of-day. - Find active schedule where:
effective_from_date <= local_dateeffective_to_date IS NULL OR effective_to_date >= local_date- latest
effective_from_datewins.
- Find pricing period in that schedule where:
start_minute <= minute_of_dayend_minute > minute_of_day
- 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.
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
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
- Config utility:
internal/config/config.go- Loads env and
.env, then applies fallbacks.
- Migration utility:
cmd/migrateauto-inits migration table for non-init commands.
- Health utility:
/healthzexecutes DB ping via service/repository.
Checks service and DB connectivity.
Returns chargers list (including external_id, timezone, metadata).
Returns full charger-specific pricing view:
- charger metadata
- schedules
- periods for each schedule
Returns applicable price for that charger at a given time.
Defined in internal/pricing/dtos.go.
-
GetPriceRequestchargerId(path)timestamp(query parsed totime.Time)
-
GetPriceResponsechargerIdtimestamplocalTimetimezonepricePerKwhcurrencypricingPeriod { startTime, endTime }
-
Envelope:
PriceEnvelope { data: GetPriceResponse }
-
ChargerSpecificPricingResponsechargerIdlocationtimezoneschedules[]
-
ScheduleWithPricescheduleIdeffectiveFromDateeffectiveToDateperiods[]
-
PeriodPriceWindowperiodIdstartMinute,endMinutestartTime,endTimepricePerKwhcurrency
-
Envelope:
ChargerPricingEnvelope { data: ChargerSpecificPricingResponse }
- UI:
GET /swagger - Spec:
GET /swagger/openapi.yaml
Set environment:
export PORT=8080
export DB_ADDR=localhost:5432
export DB_USER=postgres
export DB_PASSWORD=postgres
export DB_NAME=evtoupricingRun migrations:
go run ./cmd/migrate init
go run ./cmd/migrate upRun server:
go run ./cmd/serverCurrent 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).