feat(history): add energy price history feature

This commit is contained in:
2026-08-02 11:29:58 +02:00
parent 1511de31a8
commit f70f3ab054
20 changed files with 262 additions and 52 deletions
+21 -2
View File
@@ -3,10 +3,12 @@
namespace App\Clients; namespace App\Clients;
use App\Contracts\PriceClient; use App\Contracts\PriceClient;
use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Http\Client\Factory; use Illuminate\Http\Client\Factory;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Override;
use Throwable; use Throwable;
class ZonneplanPriceClient implements PriceClient class ZonneplanPriceClient implements PriceClient
@@ -25,14 +27,31 @@ class ZonneplanPriceClient implements PriceClient
return collect($this->get('/energy-prices/gas/upcoming')); return collect($this->get('/energy-prices/gas/upcoming'));
} }
/** @return array<int, array<string, mixed>> */ #[Override]
private function get(string $path): array public function getElectricityPricesForDate(Carbon $date): Collection
{
return collect($this->get('/energy-prices/electricity/upcoming', ['date' => $date->format('Y-m-d')]));
}
#[Override]
public function getGasPricesForDate(Carbon $date): Collection
{
return collect($this->get('/energy-prices/gas/upcoming', ['date' => $date->format('Y-m-d')]));
}
/**
* @property array<string, string> $params
*
* @return array<int, array<string, mixed>>
*/
private function get(string $path, array $params = []): array
{ {
try { try {
$response = $this->http $response = $this->http
->baseUrl(config('api.baseurl')) ->baseUrl(config('api.baseurl'))
->withQueryParameters([ ->withQueryParameters([
'secret' => config('api.key'), 'secret' => config('api.key'),
...$params,
]) ])
->timeout(10) ->timeout(10)
->retry(2, 100) ->retry(2, 100)
+5
View File
@@ -2,6 +2,7 @@
namespace App\Contracts; namespace App\Contracts;
use Carbon\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
interface PriceClient interface PriceClient
@@ -9,4 +10,8 @@ interface PriceClient
public function getElectricityPrices(): Collection; public function getElectricityPrices(): Collection;
public function getGasPrices(): Collection; public function getGasPrices(): Collection;
public function getElectricityPricesForDate(Carbon $date): Collection;
public function getGasPricesForDate(Carbon $date): Collection;
} }
+5
View File
@@ -2,6 +2,7 @@
namespace App\Contracts; namespace App\Contracts;
use Carbon\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
interface PriceRepository interface PriceRepository
@@ -9,4 +10,8 @@ interface PriceRepository
public function retrieveElectricity(): Collection; public function retrieveElectricity(): Collection;
public function retrieveGas(): Collection; public function retrieveGas(): Collection;
public function retrieveElectricityForDate(Carbon $date): Collection;
public function retrieveGasForDate(Carbon $date): Collection;
} }
@@ -1,6 +1,6 @@
<?php <?php
namespace App\Features\Dashboard\Data; namespace App\Data;
use Carbon\Carbon; use Carbon\Carbon;
use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapInputName;
@@ -1,6 +1,6 @@
<?php <?php
namespace App\Features\Dashboard\Data; namespace App\Data;
use Carbon\Carbon; use Carbon\Carbon;
use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapInputName;
@@ -14,10 +14,10 @@ class GasData extends Data
public Carbon $startDate, public Carbon $startDate,
public Carbon $endDate, public Carbon $endDate,
public string $period, public string $period,
public int $marketPrice, public ?int $marketPrice,
public float $totalPriceTaxIncluded, public ?float $totalPriceTaxIncluded,
public int $priceInclHandlingVat, public ?int $priceInclHandlingVat,
public int $priceTaxWithVat, public ?int $priceTaxWithVat,
public Carbon $startDateDatetime, public Carbon $startDateDatetime,
) {} ) {}
} }
@@ -2,12 +2,13 @@
namespace App\Features\Dashboard\Controllers; namespace App\Features\Dashboard\Controllers;
use App\Features\Dashboard\Data\DashboardData;
use App\Features\Dashboard\Services\DashboardService; use App\Features\Dashboard\Services\DashboardService;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
class DashboardController extends Controller class DashboardController extends Controller
{ {
public function index(DashboardService $dashboardService) public function index(DashboardService $dashboardService): DashboardData
{ {
return $dashboardService->getDashboardData(); return $dashboardService->getDashboardData();
} }
@@ -2,6 +2,8 @@
namespace App\Features\Dashboard\Data; namespace App\Features\Dashboard\Data;
use App\Data\ElectricityData;
use App\Data\GasData;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
class DashboardData extends Data class DashboardData extends Data
@@ -0,0 +1,23 @@
<?php
namespace App\Features\History\Controllers;
use App\Features\History\Data\HistoryData;
use App\Features\History\Requests\HistoryRequest;
use App\Features\History\Services\HistoryService;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
class HistoryController extends Controller
{
public function index(HistoryRequest $request, HistoryService $historyService): HistoryData
{
$date = Carbon::now();
if ($request->date) {
$date = $request->date;
}
return $historyService->getHistoryData($date);
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Features\History\Data;
use App\Data\ElectricityData;
use App\Data\GasData;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Data;
class HistoryData extends Data
{
/**
* @property Collection<ElectricityData> $electricity
* @property Collection<GasData> $gas
*/
public function __construct(
public Collection $electricity,
public Collection $gas,
) {}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Features\History\Requests;
use Carbon\Carbon;
use Spatie\LaravelData\Data;
class HistoryRequest extends Data
{
public function __construct(
public ?Carbon $date
) {}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Features\History\Services;
use App\Contracts\PriceRepository;
use App\Data\ElectricityData;
use App\Data\GasData;
use App\Features\History\Data\HistoryData;
use Carbon\Carbon;
class HistoryService
{
public function __construct(
public PriceRepository $repository
) {}
public function getHistoryData(Carbon $date): HistoryData
{
$electricity = ElectricityData::collect($this->repository->retrieveElectricityForDate($date));
$gas = GasData::collect($this->repository->retrieveGasForDate($date));
return new HistoryData($electricity, $gas);
}
}
@@ -3,6 +3,7 @@
namespace App\Providers; namespace App\Providers;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Collection;
use Spatie\LaravelTypeScriptTransformer\LaravelData\Transformers\DataClassTransformer; use Spatie\LaravelTypeScriptTransformer\LaravelData\Transformers\DataClassTransformer;
use Spatie\LaravelTypeScriptTransformer\TypeScriptTransformerApplicationServiceProvider as BaseTypeScriptTransformerServiceProvider; use Spatie\LaravelTypeScriptTransformer\TypeScriptTransformerApplicationServiceProvider as BaseTypeScriptTransformerServiceProvider;
use Spatie\TypeScriptTransformer\Formatters\PrettierFormatter; use Spatie\TypeScriptTransformer\Formatters\PrettierFormatter;
@@ -22,6 +23,7 @@ class TypeScriptTransformerServiceProvider extends BaseTypeScriptTransformerServ
->transformDirectories(app_path()) ->transformDirectories(app_path())
->writer(new GlobalNamespaceWriter('generated.d.ts')) ->writer(new GlobalNamespaceWriter('generated.d.ts'))
->replaceType(Carbon::class, 'string') ->replaceType(Carbon::class, 'string')
->replaceType(Collection::class, 'array')
->formatter(PrettierFormatter::class); ->formatter(PrettierFormatter::class);
} }
} }
+23 -6
View File
@@ -4,8 +4,9 @@ namespace App\Repositories;
use App\Contracts\PriceClient; use App\Contracts\PriceClient;
use App\Contracts\PriceRepository as ContractsPriceRepository; use App\Contracts\PriceRepository as ContractsPriceRepository;
use App\Features\Dashboard\Data\ElectricityData; use App\Data\ElectricityData;
use App\Features\Dashboard\Data\GasData; use App\Data\GasData;
use Carbon\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Override; use Override;
@@ -19,16 +20,32 @@ class ZonneplanPriceRepository implements ContractsPriceRepository
#[Override] #[Override]
public function retrieveElectricity(): Collection public function retrieveElectricity(): Collection
{ {
$data = Cache::remember('electricity', 3600, fn () => $this->client->getElectricityPrices()->toArray()); $data = Cache::remember('electricity:upcomming', 3600, fn () => $this->client->getElectricityPrices()->toArray());
return collect(ElectricityData::collect($data)); return ElectricityData::collect($data, Collection::class);
} }
#[Override] #[Override]
public function retrieveGas(): Collection public function retrieveGas(): Collection
{ {
$data = Cache::remember('gas', 3600, fn () => $this->client->getGasPrices()->toArray()); $data = Cache::remember('gas:upcomming', 3600, fn () => $this->client->getGasPrices()->toArray());
return collect(GasData::collect($data)); return GasData::collect($data, Collection::class);
}
#[Override]
public function retrieveElectricityForDate(Carbon $date): Collection
{
$data = Cache::remember('electricity:'.$date->format('Y-m-d'), 3600, fn () => $this->client->getElectricityPricesForDate($date)->toArray());
return GasData::collect($data, Collection::class);
}
#[Override]
public function retrieveGasForDate(Carbon $date): Collection
{
$data = Cache::remember('gas:'.$date->format('Y-m-d'), 3600, fn () => $this->client->getGasPricesForDate($date)->toArray());
return GasData::collect($data, Collection::class);
} }
} }
+1 -1
View File
@@ -25,7 +25,7 @@ return [
* and will serialize dates using the first format from the array. * and will serialize dates using the first format from the array.
*/ */
'date_format' => [ 'date_format' => [
'Y-m-d\TH:i:s.u\Z', 'Y-m-d\TH:i:s.uP', 'Y-m-d H:i:s', 'U', 'Y-m-d\TH:i:s.u\Z', 'Y-m-d\TH:i:s.uP', 'Y-m-d H:i:s', 'U', 'Y-m-d',
], ],
/* /*
+23 -5
View File
@@ -1,5 +1,8 @@
import stylistic from '@stylistic/eslint-plugin'; import stylistic from '@stylistic/eslint-plugin';
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'; import {
defineConfigWithVueTs,
vueTsConfigs,
} from '@vue/eslint-config-typescript';
import prettier from 'eslint-config-prettier/flat'; import prettier from 'eslint-config-prettier/flat';
import importPlugin from 'eslint-plugin-import'; import importPlugin from 'eslint-plugin-import';
import vue from 'eslint-plugin-vue'; import vue from 'eslint-plugin-vue';
@@ -48,9 +51,16 @@ export default defineConfigWithVueTs(
}, },
], ],
'import/order': [ 'import/order': [
'error', 'off',
{ {
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
],
alphabetize: { alphabetize: {
order: 'asc', order: 'asc',
caseInsensitive: true, caseInsensitive: true,
@@ -68,7 +78,11 @@ export default defineConfigWithVueTs(
'@stylistic': stylistic, '@stylistic': stylistic,
}, },
rules: { rules: {
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: false }], '@stylistic/brace-style': [
'error',
'1tbs',
{ allowSingleLine: false },
],
'@stylistic/padding-line-between-statements': [ '@stylistic/padding-line-between-statements': [
'error', 'error',
...paddingAroundControl, ...paddingAroundControl,
@@ -96,7 +110,11 @@ export default defineConfigWithVueTs(
}, },
rules: { rules: {
curly: ['error', 'all'], curly: ['error', 'all'],
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: false }], '@stylistic/brace-style': [
'error',
'1tbs',
{ allowSingleLine: false },
],
}, },
}, },
); );
+20 -2
View File
@@ -1,5 +1,23 @@
<script setup lang="ts"></script> <script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import useHistoryViewModel from '../viewmodels/HistoryViewModel';
import moment from 'moment';
const model = useHistoryViewModel();
const date = ref<Date>();
onMounted(() => {
model.getHistoryData(moment().format('YYYY-MM-DD'));
});
watch(date, (value) => {
model.getHistoryData(moment(value).format('YYYY-MM-DD'));
});
</script>
<template> <template>
<div></div> <div>
<input type="date" v-model="date" />
</div>
</template> </template>
@@ -0,0 +1,26 @@
import { create } from 'axios';
import { ref } from 'vue';
import HistoryData = App.Features.History.Data.HistoryData;
export default function useHistoryViewModel() {
const isLoading = ref<boolean>(false);
const historyData = ref<HistoryData>();
function getHistoryData(date: string) {
isLoading.value = true;
const client = create();
client
.get<HistoryData>('/api/history', {
params: {
date,
},
})
.then((res) => {
historyData.value = res.data;
})
.finally(() => (isLoading.value = false));
}
return { isLoading, historyData, getHistoryData };
}
+43 -28
View File
@@ -1,38 +1,53 @@
declare namespace App { declare namespace App {
namespace Data {
export type ElectricityData = {
startDate: string;
endDate: string;
period: string;
marketPrice: number | null;
totalPriceTaxIncluded: number | null;
priceInclHandlingVat: number | null;
priceTaxWithVat: number | null;
pricingProfile: string | null;
carbonFootprintInGram: number | null;
sustainabilityScore: number | null;
startDateDatetime: string;
};
export type GasData = {
startDate: string;
endDate: string;
period: string;
marketPrice: number;
totalPriceTaxIncluded: number;
priceInclHandlingVat: number;
priceTaxWithVat: number;
startDateDatetime: string;
};
}
namespace Features { namespace Features {
namespace Dashboard { namespace Dashboard {
namespace Data { namespace Data {
export type DashboardData = { export type DashboardData = {
electricity: App.Features.Dashboard.Data.ElectricityData; electricity: App.Data.ElectricityData;
electricityCheapest: App.Features.Dashboard.Data.ElectricityData | null; electricityCheapest: App.Data.ElectricityData | null;
electricityDurable: App.Features.Dashboard.Data.ElectricityData | null; electricityDurable: App.Data.ElectricityData | null;
electricityPriciest: App.Features.Dashboard.Data.ElectricityData | null; electricityPriciest: App.Data.ElectricityData | null;
gas: App.Features.Dashboard.Data.GasData; gas: App.Data.GasData;
gasCheapest: App.Features.Dashboard.Data.GasData | null; gasCheapest: App.Data.GasData | null;
gasPriciest: App.Features.Dashboard.Data.GasData | null; gasPriciest: App.Data.GasData | null;
}; };
export type ElectricityData = { }
startDate: string; }
endDate: string; namespace History {
period: string; namespace Data {
marketPrice: number | null; export type HistoryData = {
totalPriceTaxIncluded: number | null; electricity: Array<any>;
priceInclHandlingVat: number | null; gas: Array<any>;
priceTaxWithVat: number | null;
pricingProfile: string | null;
carbonFootprintInGram: number | null;
sustainabilityScore: number | null;
startDateDatetime: string;
}; };
export type GasData = { }
startDate: string; namespace Requests {
endDate: string; export type HistoryRequest = {
period: string; date: string | null;
marketPrice: number;
totalPriceTaxIncluded: number;
priceInclHandlingVat: number;
priceTaxWithVat: number;
startDateDatetime: string;
}; };
} }
} }
@@ -1,3 +1,3 @@
{ {
"generated.d.ts": "f593095f6f419a96a4c39c2b93719f9a" "generated.d.ts": "7ca8a9c7f0ae5e5498bdee87bb2f005d"
} }
+2
View File
@@ -1,8 +1,10 @@
<?php <?php
use App\Features\Dashboard\Controllers\DashboardController; use App\Features\Dashboard\Controllers\DashboardController;
use App\Features\History\Controllers\HistoryController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::name('api.')->group(function () { Route::name('api.')->group(function () {
Route::get('/energy-prices', [DashboardController::class, 'index'])->name('dashboard.index'); Route::get('/energy-prices', [DashboardController::class, 'index'])->name('dashboard.index');
Route::get('/history', [HistoryController::class, 'index'])->name('history.index');
}); });