diff --git a/app/Features/Dashboard/Data/DashboardData.php b/app/Features/Dashboard/Data/DashboardData.php
index e49f53b..73cc70e 100644
--- a/app/Features/Dashboard/Data/DashboardData.php
+++ b/app/Features/Dashboard/Data/DashboardData.php
@@ -9,12 +9,10 @@ use Spatie\LaravelData\Data;
class DashboardData extends Data
{
public function __construct(
- public ElectricityData $electricity,
+ public ?ElectricityData $electricity,
public ?ElectricityData $electricityCheapest,
public ?ElectricityData $electricityDurable,
public ?ElectricityData $electricityPriciest,
- public GasData $gas,
- public ?GasData $gasCheapest,
- public ?GasData $gasPriciest,
+ public ?GasData $gas,
) {}
}
diff --git a/app/Features/Dashboard/Services/DashboardService.php b/app/Features/Dashboard/Services/DashboardService.php
index 633e8f0..5772b6e 100644
--- a/app/Features/Dashboard/Services/DashboardService.php
+++ b/app/Features/Dashboard/Services/DashboardService.php
@@ -14,19 +14,25 @@ class DashboardService
public function getDashboardData(): DashboardData
{
+ $electricity = $this->repository->retrieveElectricity();
+ $gas = $this->repository->retrieveGas();
+
$now = Carbon::now();
- $electricity = $this->repository->retrieveElectricity();
-
- $eNow = $electricity->whereBetween('startDate', [$now->copy()->subHour(), $now])->first();
+ $eNow = $electricity->first(
+ fn ($item) => $item->startDate instanceof Carbon
+ && $item->startDate->gte($now->copy()->subHour())
+ && $item->startDate->lte($now)
+ );
$eCheapest = $electricity->where('totalPriceTaxIncluded', $electricity->min('totalPriceTaxIncluded'))->first();
$eDurable = $electricity->where('sustainabilityScore', $electricity->max('sustainabilityScore'))->first();
$ePriciest = $electricity->where('totalPriceTaxIncluded', $electricity->max('totalPriceTaxIncluded'))->first();
- $gas = $this->repository->retrieveGas();
- $gNow = $gas->whereBetween('startDate', [$now->copy()->setHour(0), $now])->first();
- $gCheapest = $gas->where('totalPriceTaxIncluded', $gas->min('totalPriceTaxIncluded'))->first();
- $gPriciest = $gas->where('totalPriceTaxIncluded', $gas->max('totalPriceTaxIncluded'))->first();
+ $gNow = $gas->first(
+ fn ($item) => $item->startDate instanceof Carbon
+ && $item->startDate->gte($now->copy()->startOfDay())
+ && $item->startDate->lte($now)
+ );
return new DashboardData(
$eNow,
@@ -34,8 +40,6 @@ class DashboardService
$eDurable,
$ePriciest,
$gNow,
- $gCheapest,
- $gPriciest
);
}
}
diff --git a/app/Features/History/Services/HistoryService.php b/app/Features/History/Services/HistoryService.php
index 6dec047..6241c5a 100644
--- a/app/Features/History/Services/HistoryService.php
+++ b/app/Features/History/Services/HistoryService.php
@@ -3,8 +3,6 @@
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;
@@ -16,9 +14,12 @@ class HistoryService
public function getHistoryData(Carbon $date): HistoryData
{
- $electricity = ElectricityData::collect($this->repository->retrieveElectricityForDate($date));
- $gas = GasData::collect($this->repository->retrieveGasForDate($date));
+ $electricity = $this->repository->retrieveElectricityForDate($date);
+ $gas = $this->repository->retrieveGasForDate($date);
- return new HistoryData($electricity, $gas);
+ return new HistoryData(
+ electricity: $electricity,
+ gas: $gas
+ );
}
}
diff --git a/app/Repositories/ZonneplanPriceRepository.php b/app/Repositories/ZonneplanPriceRepository.php
index 98aff5f..ab45c8c 100644
--- a/app/Repositories/ZonneplanPriceRepository.php
+++ b/app/Repositories/ZonneplanPriceRepository.php
@@ -13,39 +13,74 @@ use Override;
class ZonneplanPriceRepository implements ContractsPriceRepository
{
+ private int $cacheTtl;
+ private string $cachePrefix;
+
public function __construct(
public PriceClient $client
- ) {}
+ ) {
+ $this->cacheTtl = config('cache_prices.ttl', 3600);
+ $this->cachePrefix = config('cache_prices.prefix', 'zonneplan:');
+ }
#[Override]
public function retrieveElectricity(): Collection
{
- $data = Cache::remember('electricity:upcomming', 3600, fn () => $this->client->getElectricityPrices()->toArray());
+ $raw = $this->remember(
+ 'electricity:upcoming',
+ fn () => $this->client->getElectricityPrices()->toArray()
+ );
- return ElectricityData::collect($data, Collection::class);
+ return ElectricityData::collect($raw, Collection::class);
}
#[Override]
public function retrieveGas(): Collection
{
- $data = Cache::remember('gas:upcomming', 3600, fn () => $this->client->getGasPrices()->toArray());
+ $raw = $this->remember(
+ 'gas:upcoming',
+ fn () => $this->client->getGasPrices()->toArray()
+ );
- return GasData::collect($data, Collection::class);
+ return GasData::collect($raw, 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());
+ $raw = $this->remember(
+ 'electricity:' . $date->format('Y-m-d'),
+ fn () => $this->client->getElectricityPricesForDate($date)->toArray()
+ );
- return GasData::collect($data, Collection::class);
+ return ElectricityData::collect($raw, 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());
+ $raw = $this->remember(
+ 'gas:' . $date->format('Y-m-d'),
+ fn () => $this->client->getGasPricesForDate($date)->toArray()
+ );
- return GasData::collect($data, Collection::class);
+ return GasData::collect($raw, Collection::class);
+ }
+
+ /**
+ * Cache via JSON encoding to ensure clean array structure on retrieval.
+ *
+ * @param callable(): array $callback
+ * @return array
+ */
+ private function remember(string $key, callable $callback): array
+ {
+ $cached = Cache::remember(
+ $this->cachePrefix . $key,
+ $this->cacheTtl,
+ fn () => json_encode($callback())
+ );
+
+ return json_decode($cached, true);
}
}
diff --git a/config/cache_prices.php b/config/cache_prices.php
new file mode 100644
index 0000000..4055d75
--- /dev/null
+++ b/config/cache_prices.php
@@ -0,0 +1,24 @@
+ env('CACHE_TTL', 3600),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Cache prefix
+ |--------------------------------------------------------------------------
+ |
+ | Prefix voor cache keys om conflicten te vermijden.
+ |
+ */
+ 'prefix' => 'zonneplan:',
+];
diff --git a/package-lock.json b/package-lock.json
index 4eb946a..75b048f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "html",
+ "name": "Zonneplan",
"lockfileVersion": 3,
"requires": true,
"packages": {
@@ -17,6 +17,7 @@
"i18next-vue": "^5.4.0",
"laravel-vite-plugin": "^3.1",
"moment": "^2.30.1",
+ "pinia": "^4.0.2",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1",
"typescript": "^5.2.2",
@@ -1907,6 +1908,36 @@
"he": "^1.2.0"
}
},
+ "node_modules/@vue/devtools-api": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz",
+ "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/devtools-kit": "^8.2.1"
+ }
+ },
+ "node_modules/@vue/devtools-kit": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz",
+ "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/devtools-shared": "^8.2.1",
+ "birpc": "^2.6.1",
+ "hookable": "^5.5.3",
+ "perfect-debounce": "^2.0.0"
+ }
+ },
+ "node_modules/@vue/devtools-shared": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz",
+ "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@vue/eslint-config-typescript": {
"version": "14.9.0",
"resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.9.0.tgz",
@@ -2308,6 +2339,16 @@
"node": "18 || 20 || >=22"
}
},
+ "node_modules/birpc": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
+ "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -3968,6 +4009,13 @@
"he": "bin/he"
}
},
+ "node_modules/hookable": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -5153,6 +5201,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/nostics": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz",
+ "integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==",
+ "license": "MIT"
+ },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -5395,6 +5449,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/perfect-debounce": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+ "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5413,6 +5474,31 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pinia": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-4.0.2.tgz",
+ "integrity": "sha512-yKVVA7bSj5oRZFp/Ab9wLlmyb5gPUYEiIm4ryiWTe/xe7PtkRdMVOp1X1ggvq0c6Uj7Q0Du1HnV2mtAwM0Ks1g==",
+ "license": "MIT",
+ "dependencies": {
+ "nostics": "^1.1.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "@vue/devtools-api": "^8.1.5",
+ "typescript": ">=5.6.0",
+ "vue": "^3.5.11"
+ },
+ "peerDependenciesMeta": {
+ "@vue/devtools-api": {
+ "optional": false
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
diff --git a/package.json b/package.json
index ab1c66b..f3eae73 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"i18next-vue": "^5.4.0",
"laravel-vite-plugin": "^3.1",
"moment": "^2.30.1",
+ "pinia": "^4.0.2",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1",
"typescript": "^5.2.2",
diff --git a/resources/js/app.ts b/resources/js/app.ts
index 4e27e1d..c41ca45 100644
--- a/resources/js/app.ts
+++ b/resources/js/app.ts
@@ -1,10 +1,12 @@
import { createInertiaApp } from '@inertiajs/vue3';
import I18NextVue from 'i18next-vue';
+import { createPinia } from 'pinia';
import type { DefineComponent } from 'vue';
import i18next from './lang/init';
import Layout from './layouts/Layout.vue';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
+const pinia = createPinia();
createInertiaApp({
title: (title) => (title ? `${title} - ${appName}` : appName),
@@ -33,6 +35,7 @@ createInertiaApp({
return pages[`./pages/${name}.vue`];
},
withApp(app, options) {
+ app.use(pinia);
app.use(I18NextVue, {
i18next,
...options,
diff --git a/resources/js/features/dashboard/components/Price.vue b/resources/js/features/dashboard/components/Price.vue
index 3bb69ca..85f87a0 100644
--- a/resources/js/features/dashboard/components/Price.vue
+++ b/resources/js/features/dashboard/components/Price.vue
@@ -13,7 +13,7 @@ defineProps<{
{{ label }}
- €{{ price?.toPrecision(2).toString().replace('.', ',') }}
+ €{{ price?.toFixed(2).replace('.', ',') }}
{{ moment.isMoment(time) ? time.format('DD-MM-YYYY HH:mm') : time }}
diff --git a/resources/js/features/dashboard/pages/Index.vue b/resources/js/features/dashboard/pages/Index.vue
index 8e9c640..5460b93 100644
--- a/resources/js/features/dashboard/pages/Index.vue
+++ b/resources/js/features/dashboard/pages/Index.vue
@@ -3,14 +3,14 @@ import { t } from 'i18next';
import moment from 'moment';
import { onMounted } from 'vue';
import Price from '../components/Price.vue';
-import useDashboardViewModel from '../viewmodels/DashboardViewModel';
+import { useDashboardStore } from '@/stores/dashboard';
import Error from '@/components/Error.vue';
import Loading from '@/components/Loading.vue';
-const model = useDashboardViewModel();
+const store = useDashboardStore();
onMounted(() => {
- model.getDashboardData();
+ store.fetchDashboardData();
});
@@ -18,124 +18,94 @@ onMounted(() => {
{{ t('dashboard.title') }}
-
-
-
- {{ t('dashboard.electricity') }}
-
-
-
-
-
-
+
+
+
+
+ {{ t('dashboard.electricity') }}
+
+
-
-
-
- {{ t('dashboard.gas') }}
-
-
-
-
-
+
+
+ {{ t('dashboard.gas') }}
+
+
+
+
+ {{ t('dashboard.no_data') }}
-
-
+
diff --git a/resources/js/features/dashboard/viewmodels/DashboardViewModel.ts b/resources/js/features/dashboard/viewmodels/DashboardViewModel.ts
deleted file mode 100644
index ee85494..0000000
--- a/resources/js/features/dashboard/viewmodels/DashboardViewModel.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { create } from 'axios';
-import { ref } from 'vue';
-import DashboardData = App.Features.Dashboard.Data.DashboardData;
-
-export default function useDashboardViewModel() {
- const isLoading = ref
(false);
- const error = ref();
- const dashboardData = ref();
-
- function getDashboardData() {
- isLoading.value = true;
-
- const client = create();
- client
- .get('/api/energy-prices')
- .then((res) => {
- dashboardData.value = res.data;
- })
- .catch((err) => {
- error.value = err.response.data.message;
- })
- .finally(() => (isLoading.value = false));
- }
-
- return { isLoading, dashboardData, error, getDashboardData };
-}
diff --git a/resources/js/features/history/components/Chart.vue b/resources/js/features/history/components/Chart.vue
index c09101f..89d32c6 100644
--- a/resources/js/features/history/components/Chart.vue
+++ b/resources/js/features/history/components/Chart.vue
@@ -29,6 +29,7 @@ export interface PricePoint {
const props = defineProps<{
data: PricePoint[];
+ color?: string;
}>();
const chartData = computed(() => ({
@@ -36,8 +37,10 @@ const chartData = computed(() => ({
datasets: [
{
data: props.data.map((d) => d.value),
- borderColor: '#4ba66a',
- backgroundColor: 'rgba(75, 166, 106, 0.1)',
+ borderColor: props.color || '#4ba66a',
+ backgroundColor: props.color
+ ? `${props.color}1a`
+ : 'rgba(75, 166, 106, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 2,
diff --git a/resources/js/features/history/pages/Index.vue b/resources/js/features/history/pages/Index.vue
index 93b7103..b49ee10 100644
--- a/resources/js/features/history/pages/Index.vue
+++ b/resources/js/features/history/pages/Index.vue
@@ -1,37 +1,108 @@
-
-
-
-
+
+ {{ t('history.title') }}
+
+
+
+
+
+
+
+
+
+ {{ t('history.electricity_prices') }}
+
+
+
+ {{ t('history.no_electricity_data') }}
+
+
+
+
+ {{ t('history.gas_prices') }}
+
+
+
+ {{ t('history.no_gas_data') }}
+
+
+
+
+ {{ t('history.no_data') }}
+
+
diff --git a/resources/js/features/history/viewmodels/HistoryViewModel.ts b/resources/js/features/history/viewmodels/HistoryViewModel.ts
deleted file mode 100644
index 314012e..0000000
--- a/resources/js/features/history/viewmodels/HistoryViewModel.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { create } from 'axios';
-import { ref } from 'vue';
-import HistoryData = App.Features.History.Data.HistoryData;
-
-export default function useHistoryViewModel() {
- const isLoading = ref(false);
- const error = ref();
- const historyData = ref();
-
- function getHistoryData(date: string) {
- isLoading.value = true;
-
- const client = create();
- client
- .get('/api/history', {
- params: {
- date,
- },
- })
- .then((res) => {
- historyData.value = res.data;
- })
- .catch((err) => {
- error.value = err.response.data.message;
- })
- .finally(() => (isLoading.value = false));
- }
-
- return { isLoading, historyData, error, getHistoryData };
-}
diff --git a/resources/js/generated/generated.d.ts b/resources/js/generated/generated.d.ts
index 325387a..d8a3ded 100644
--- a/resources/js/generated/generated.d.ts
+++ b/resources/js/generated/generated.d.ts
@@ -28,13 +28,11 @@ declare namespace App {
namespace Dashboard {
namespace Data {
export type DashboardData = {
- electricity: App.Data.ElectricityData;
+ electricity: App.Data.ElectricityData | null;
electricityCheapest: App.Data.ElectricityData | null;
electricityDurable: App.Data.ElectricityData | null;
electricityPriciest: App.Data.ElectricityData | null;
- gas: App.Data.GasData;
- gasCheapest: App.Data.GasData | null;
- gasPriciest: App.Data.GasData | null;
+ gas: App.Data.GasData | null;
};
}
}
diff --git a/resources/js/generated/typescript-transformer-manifest.json b/resources/js/generated/typescript-transformer-manifest.json
index 1fed8d0..6fda39a 100644
--- a/resources/js/generated/typescript-transformer-manifest.json
+++ b/resources/js/generated/typescript-transformer-manifest.json
@@ -1,3 +1,3 @@
{
- "generated.d.ts": "596cfad66b8ff3f8b6b98271630919ae"
+ "generated.d.ts": "50b70ce1a75284fa50634037e0073fb4"
}
\ No newline at end of file
diff --git a/resources/js/lang/translations/nl/dashboard.json b/resources/js/lang/translations/nl/dashboard.json
index 1b3e380..370ff10 100644
--- a/resources/js/lang/translations/nl/dashboard.json
+++ b/resources/js/lang/translations/nl/dashboard.json
@@ -8,5 +8,6 @@
"priciest_electricity_price": "Duurste elektriciteitsprijs",
"current_gas_price": "Actuele gasprijs",
"cheapest_gas_price": "Goedkoopste gasprijs",
- "priciest_gas_price": "Duurste gasprijs"
+ "priciest_gas_price": "Duurste gasprijs",
+ "no_data": "Geen prijsgegevens beschikbaar"
}
diff --git a/resources/js/lang/translations/nl/history.json b/resources/js/lang/translations/nl/history.json
index 29bf569..e04bc9c 100644
--- a/resources/js/lang/translations/nl/history.json
+++ b/resources/js/lang/translations/nl/history.json
@@ -1,3 +1,9 @@
{
- "title": "Prijshistorie"
+ "title": "Prijshistorie",
+ "select_date": "Selecteer een datum",
+ "electricity_prices": "Elektriciteitsprijzen",
+ "gas_prices": "Gasprijzen",
+ "no_data": "Geen prijsgegevens beschikbaar voor deze datum",
+ "no_electricity_data": "Geen elektriciteitsprijsgegevens beschikbaar",
+ "no_gas_data": "Geen gasprijsgegevens beschikbaar"
}
diff --git a/resources/js/lib/http.ts b/resources/js/lib/http.ts
new file mode 100644
index 0000000..bb70ce9
--- /dev/null
+++ b/resources/js/lib/http.ts
@@ -0,0 +1,24 @@
+import axios from 'axios';
+
+const http = axios.create({
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+});
+
+http.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ const message =
+ error.response?.data?.message ||
+ error.message ||
+ 'Er is een onverwachte fout opgetreden';
+
+ console.error('[HTTP Error]', message);
+
+ return Promise.reject(error);
+ },
+);
+
+export default http;
diff --git a/resources/js/lib/utils.ts b/resources/js/lib/utils.ts
deleted file mode 100644
index 66bf229..0000000
--- a/resources/js/lib/utils.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { clsx } from 'clsx';
-import type { ClassValue } from 'clsx';
-import { twMerge } from 'tailwind-merge';
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
-}
diff --git a/resources/js/stores/dashboard.ts b/resources/js/stores/dashboard.ts
new file mode 100644
index 0000000..487d49b
--- /dev/null
+++ b/resources/js/stores/dashboard.ts
@@ -0,0 +1,34 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import http from '@/lib/http';
+import DashboardData = App.Features.Dashboard.Data.DashboardData;
+
+export const useDashboardStore = defineStore('dashboard', () => {
+ const isLoading = ref(false);
+ const error = ref();
+ const dashboardData = ref();
+
+ async function fetchDashboardData() {
+ isLoading.value = true;
+ error.value = undefined;
+
+ try {
+ const { data } = await http.get('/api/energy-prices');
+ dashboardData.value = data;
+ } catch (err: unknown) {
+ const axiosError = err as { response?: { data?: { message?: string } } };
+ error.value =
+ axiosError.response?.data?.message ||
+ 'Kon dashboard data niet ophalen';
+ } finally {
+ isLoading.value = false;
+ }
+ }
+
+ return {
+ isLoading,
+ error,
+ dashboardData,
+ fetchDashboardData,
+ };
+});
diff --git a/resources/js/stores/history.ts b/resources/js/stores/history.ts
new file mode 100644
index 0000000..0fcf008
--- /dev/null
+++ b/resources/js/stores/history.ts
@@ -0,0 +1,41 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import http from '@/lib/http';
+import HistoryData = App.Features.History.Data.HistoryData;
+
+export const useHistoryStore = defineStore('history', () => {
+ const isLoading = ref(false);
+ const error = ref();
+ const historyData = ref();
+ const selectedDate = ref(new Date().toISOString().split('T')[0]);
+
+ async function fetchHistoryData(date: string) {
+ isLoading.value = true;
+ error.value = undefined;
+ selectedDate.value = date;
+
+ try {
+ const { data } = await http.get('/api/history', {
+ params: { date },
+ });
+ historyData.value = data;
+ } catch (err: unknown) {
+ const axiosError = err as {
+ response?: { data?: { message?: string } };
+ };
+ error.value =
+ axiosError.response?.data?.message ||
+ 'Kon prijshistorie niet ophalen';
+ } finally {
+ isLoading.value = false;
+ }
+ }
+
+ return {
+ isLoading,
+ error,
+ historyData,
+ selectedDate,
+ fetchHistoryData,
+ };
+});
diff --git a/resources/js/types/index.ts b/resources/js/types/index.ts
deleted file mode 100644
index 269586e..0000000
--- a/resources/js/types/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './auth';
diff --git a/tests/Feature/Api/DashboardApiTest.php b/tests/Feature/Api/DashboardApiTest.php
new file mode 100644
index 0000000..10e11cf
--- /dev/null
+++ b/tests/Feature/Api/DashboardApiTest.php
@@ -0,0 +1,156 @@
+ 60]);
+ config(['cache_prices.prefix' => 'test:']);
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ \Illuminate\Support\Facades\Cache::flush();
+ parent::tearDown();
+ }
+
+ public function test_dashboard_endpoint_returns_electricity_and_gas_data(): void
+ {
+ $now = Carbon::now();
+
+ $electricityResponse = collect([
+ $this->createElectricityArray($now->copy()->subMinutes(30), 15.0, 80),
+ $this->createElectricityArray($now->copy()->addHour(), 12.0, 90),
+ $this->createElectricityArray($now->copy()->addHours(2), 20.0, 60),
+ $this->createElectricityArray($now->copy()->addHours(3), 10.0, 95),
+ ]);
+
+ $gasResponse = collect([
+ $this->createGasArray($now->copy()->startOfDay(), 5.0),
+ $this->createGasArray($now->copy()->addDay(), 4.0),
+ $this->createGasArray($now->copy()->addDays(2), 6.0),
+ ]);
+
+ $client = Mockery::mock(PriceClient::class);
+ $client->shouldReceive('getElectricityPrices')->once()->andReturn($electricityResponse);
+ $client->shouldReceive('getGasPrices')->once()->andReturn($gasResponse);
+
+ $this->app->bind(PriceClient::class, fn () => $client);
+
+ $response = $this->getJson('/api/energy-prices');
+
+ $response->assertOk()
+ ->assertJsonStructure([
+ 'electricity' => [
+ 'startDate',
+ 'endDate',
+ 'period',
+ 'totalPriceTaxIncluded',
+ ],
+ 'electricityCheapest',
+ 'electricityDurable',
+ 'electricityPriciest',
+ 'gas' => [
+ 'startDate',
+ 'endDate',
+ 'period',
+ 'totalPriceTaxIncluded',
+ ],
+ 'gasCheapest',
+ 'gasPriciest',
+ ]);
+ }
+
+ public function test_dashboard_endpoint_returns_correct_prices(): void
+ {
+ $now = Carbon::now();
+
+ $electricityResponse = collect([
+ $this->createElectricityArray($now->copy()->subMinutes(30), 15.0, 80),
+ $this->createElectricityArray($now->copy()->addHour(), 12.0, 90),
+ $this->createElectricityArray($now->copy()->addHours(2), 20.0, 60),
+ ]);
+
+ $gasResponse = collect([
+ $this->createGasArray($now->copy()->startOfDay(), 5.0),
+ $this->createGasArray($now->copy()->addDay(), 4.0),
+ ]);
+
+ $client = Mockery::mock(PriceClient::class);
+ $client->shouldReceive('getElectricityPrices')->once()->andReturn($electricityResponse);
+ $client->shouldReceive('getGasPrices')->once()->andReturn($gasResponse);
+
+ $this->app->bind(PriceClient::class, fn () => $client);
+
+ $response = $this->getJson('/api/energy-prices');
+
+ $response->assertOk();
+
+ $data = $response->json();
+ $this->assertEquals(15.0, $data['electricity']['totalPriceTaxIncluded']);
+ $this->assertEquals(12.0, $data['electricityCheapest']['totalPriceTaxIncluded']);
+ $this->assertEquals(20.0, $data['electricityPriciest']['totalPriceTaxIncluded']);
+ $this->assertEquals(5.0, $data['gas']['totalPriceTaxIncluded']);
+ $this->assertEquals(4.0, $data['gasCheapest']['totalPriceTaxIncluded']);
+ }
+
+ public function test_dashboard_endpoint_handles_empty_data(): void
+ {
+ $client = Mockery::mock(PriceClient::class);
+ $client->shouldReceive('getElectricityPrices')->once()->andReturn(collect());
+ $client->shouldReceive('getGasPrices')->once()->andReturn(collect());
+
+ $this->app->bind(PriceClient::class, fn () => $client);
+
+ $response = $this->getJson('/api/energy-prices');
+
+ $response->assertOk();
+
+ $data = $response->json();
+ $this->assertNull($data['electricity'] ?? null);
+ $this->assertNull($data['gas'] ?? null);
+ }
+
+ private function createElectricityArray(Carbon $startDate, float $price, int $sustainabilityScore): array
+ {
+ return [
+ 'start_date' => $startDate->format('Y-m-d H:i:s'),
+ 'end_date' => $startDate->copy()->addHour()->format('Y-m-d H:i:s'),
+ 'period' => 'elec_hour',
+ 'market_price' => (int) ($price * 100),
+ 'total_price_tax_included' => $price,
+ 'price_incl_handling_vat' => (int) ($price * 100),
+ 'price_tax_with_vat' => (int) ($price * 20),
+ 'pricing_profile' => 'dynamic',
+ 'carbon_footprint_in_gram' => 500,
+ 'sustainability_score' => $sustainabilityScore,
+ 'start_date_datetime' => $startDate->format('Y-m-d H:i:s'),
+ ];
+ }
+
+ private function createGasArray(Carbon $startDate, float $price): array
+ {
+ return [
+ 'start_date' => $startDate->format('Y-m-d H:i:s'),
+ 'end_date' => $startDate->copy()->addDay()->format('Y-m-d H:i:s'),
+ 'period' => 'gas_day',
+ 'market_price' => (int) ($price * 100),
+ 'total_price_tax_included' => $price,
+ 'price_incl_handling_vat' => (int) ($price * 100),
+ 'price_tax_with_vat' => (int) ($price * 20),
+ 'start_date_datetime' => $startDate->format('Y-m-d H:i:s'),
+ ];
+ }
+}
diff --git a/tests/Feature/Api/HistoryApiTest.php b/tests/Feature/Api/HistoryApiTest.php
new file mode 100644
index 0000000..2e839ad
--- /dev/null
+++ b/tests/Feature/Api/HistoryApiTest.php
@@ -0,0 +1,179 @@
+ 60]);
+ config(['cache_prices.prefix' => 'test:']);
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ \Illuminate\Support\Facades\Cache::flush();
+ parent::tearDown();
+ }
+
+ public function test_history_endpoint_returns_electricity_and_gas_data(): void
+ {
+ $date = Carbon::today();
+
+ $electricityResponse = collect([
+ $this->createElectricityArray($date->copy()->setHour(0), 15.0),
+ $this->createElectricityArray($date->copy()->setHour(1), 16.0),
+ ]);
+
+ $gasResponse = collect([
+ $this->createGasArray($date, 5.0),
+ ]);
+
+ $client = Mockery::mock(PriceClient::class);
+ $client->shouldReceive('getElectricityPricesForDate')
+ ->once()
+ ->andReturn($electricityResponse);
+ $client->shouldReceive('getGasPricesForDate')
+ ->once()
+ ->andReturn($gasResponse);
+
+ $this->app->bind(PriceClient::class, fn () => $client);
+
+ $response = $this->getJson('/api/history?date=' . $date->format('Y-m-d'));
+
+ $response->assertOk()
+ ->assertJsonStructure([
+ 'electricity' => [
+ '*' => [
+ 'startDate',
+ 'endDate',
+ 'period',
+ 'totalPriceTaxIncluded',
+ ],
+ ],
+ 'gas' => [
+ '*' => [
+ 'startDate',
+ 'endDate',
+ 'period',
+ 'totalPriceTaxIncluded',
+ ],
+ ],
+ ]);
+ }
+
+ public function test_history_endpoint_returns_correct_data(): void
+ {
+ $date = Carbon::today();
+
+ $electricityResponse = collect([
+ $this->createElectricityArray($date->copy()->setHour(0), 15.0),
+ $this->createElectricityArray($date->copy()->setHour(1), 16.0),
+ ]);
+
+ $gasResponse = collect([
+ $this->createGasArray($date, 5.0),
+ ]);
+
+ $client = Mockery::mock(PriceClient::class);
+ $client->shouldReceive('getElectricityPricesForDate')
+ ->once()
+ ->andReturn($electricityResponse);
+ $client->shouldReceive('getGasPricesForDate')
+ ->once()
+ ->andReturn($gasResponse);
+
+ $this->app->bind(PriceClient::class, fn () => $client);
+
+ $response = $this->getJson('/api/history?date=' . $date->format('Y-m-d'));
+
+ $response->assertOk();
+
+ $data = $response->json();
+ $this->assertCount(2, $data['electricity']);
+ $this->assertCount(1, $data['gas']);
+ $this->assertEquals(15.0, $data['electricity'][0]['totalPriceTaxIncluded']);
+ $this->assertEquals(5.0, $data['gas'][0]['totalPriceTaxIncluded']);
+ }
+
+ public function test_history_endpoint_defaults_to_today(): void
+ {
+ $client = Mockery::mock(PriceClient::class);
+ $client->shouldReceive('getElectricityPricesForDate')
+ ->once()
+ ->andReturn(collect());
+ $client->shouldReceive('getGasPricesForDate')
+ ->once()
+ ->andReturn(collect());
+
+ $this->app->bind(PriceClient::class, fn () => $client);
+
+ $response = $this->getJson('/api/history');
+
+ $response->assertOk();
+ }
+
+ public function test_history_endpoint_handles_empty_data(): void
+ {
+ $date = Carbon::today();
+
+ $client = Mockery::mock(PriceClient::class);
+ $client->shouldReceive('getElectricityPricesForDate')
+ ->once()
+ ->andReturn(collect());
+ $client->shouldReceive('getGasPricesForDate')
+ ->once()
+ ->andReturn(collect());
+
+ $this->app->bind(PriceClient::class, fn () => $client);
+
+ $response = $this->getJson('/api/history?date=' . $date->format('Y-m-d'));
+
+ $response->assertOk();
+
+ $data = $response->json();
+ $this->assertEmpty($data['electricity']);
+ $this->assertEmpty($data['gas']);
+ }
+
+ private function createElectricityArray(Carbon $startDate, float $price): array
+ {
+ return [
+ 'start_date' => $startDate->format('Y-m-d H:i:s'),
+ 'end_date' => $startDate->copy()->addHour()->format('Y-m-d H:i:s'),
+ 'period' => 'elec_hour',
+ 'market_price' => (int) ($price * 100),
+ 'total_price_tax_included' => $price,
+ 'price_incl_handling_vat' => (int) ($price * 100),
+ 'price_tax_with_vat' => (int) ($price * 20),
+ 'pricing_profile' => 'dynamic',
+ 'carbon_footprint_in_gram' => 500,
+ 'sustainability_score' => 80,
+ 'start_date_datetime' => $startDate->format('Y-m-d H:i:s'),
+ ];
+ }
+
+ private function createGasArray(Carbon $startDate, float $price): array
+ {
+ return [
+ 'start_date' => $startDate->format('Y-m-d H:i:s'),
+ 'end_date' => $startDate->copy()->addDay()->format('Y-m-d H:i:s'),
+ 'period' => 'gas_day',
+ 'market_price' => (int) ($price * 100),
+ 'total_price_tax_included' => $price,
+ 'price_incl_handling_vat' => (int) ($price * 100),
+ 'price_tax_with_vat' => (int) ($price * 20),
+ 'start_date_datetime' => $startDate->format('Y-m-d H:i:s'),
+ ];
+ }
+}
diff --git a/tests/Unit/Clients/ZonneplanPriceClientTest.php b/tests/Unit/Clients/ZonneplanPriceClientTest.php
new file mode 100644
index 0000000..c661fcd
--- /dev/null
+++ b/tests/Unit/Clients/ZonneplanPriceClientTest.php
@@ -0,0 +1,143 @@
+shouldReceive('throw')->once();
+ $response->shouldReceive('json')
+ ->once()
+ ->with('data', [])
+ ->andReturn([
+ ['start_date' => Carbon::now()->format('Y-m-d H:i:s'), 'total_price_tax_included' => 15.0],
+ ]);
+ $response->shouldReceive('status')->once()->andReturn(200);
+
+ $http = Mockery::mock(Factory::class);
+ $http->shouldReceive('baseUrl')->once()->andReturnSelf();
+ $http->shouldReceive('withQueryParameters')->once()->andReturnSelf();
+ $http->shouldReceive('timeout')->once()->andReturnSelf();
+ $http->shouldReceive('retry')->once()->andReturnSelf();
+ $http->shouldReceive('get')->once()->andReturn($response);
+
+ $client = new ZonneplanPriceClient($http);
+ $result = $client->getElectricityPrices();
+
+ $this->assertCount(1, $result);
+ }
+
+ public function test_get_gas_prices_returns_collection(): void
+ {
+ $response = Mockery::mock();
+ $response->shouldReceive('throw')->once();
+ $response->shouldReceive('json')
+ ->once()
+ ->with('data', [])
+ ->andReturn([
+ ['start_date' => Carbon::now()->format('Y-m-d H:i:s'), 'total_price_tax_included' => 5.0],
+ ]);
+ $response->shouldReceive('status')->once()->andReturn(200);
+
+ $http = Mockery::mock(Factory::class);
+ $http->shouldReceive('baseUrl')->once()->andReturnSelf();
+ $http->shouldReceive('withQueryParameters')->once()->andReturnSelf();
+ $http->shouldReceive('timeout')->once()->andReturnSelf();
+ $http->shouldReceive('retry')->once()->andReturnSelf();
+ $http->shouldReceive('get')->once()->andReturn($response);
+
+ $client = new ZonneplanPriceClient($http);
+ $result = $client->getGasPrices();
+
+ $this->assertCount(1, $result);
+ }
+
+ public function test_get_electricity_prices_for_date_returns_collection(): void
+ {
+ $date = Carbon::today();
+ $response = Mockery::mock();
+ $response->shouldReceive('throw')->once();
+ $response->shouldReceive('json')
+ ->once()
+ ->with('data', [])
+ ->andReturn([
+ ['start_date' => $date->format('Y-m-d H:i:s'), 'total_price_tax_included' => 15.0],
+ ]);
+ $response->shouldReceive('status')->once()->andReturn(200);
+
+ $http = Mockery::mock(Factory::class);
+ $http->shouldReceive('baseUrl')->once()->andReturnSelf();
+ $http->shouldReceive('withQueryParameters')->once()->andReturnSelf();
+ $http->shouldReceive('timeout')->once()->andReturnSelf();
+ $http->shouldReceive('retry')->once()->andReturnSelf();
+ $http->shouldReceive('get')->once()->andReturn($response);
+
+ $client = new ZonneplanPriceClient($http);
+ $result = $client->getElectricityPricesForDate($date);
+
+ $this->assertCount(1, $result);
+ }
+
+ public function test_get_gas_prices_for_date_returns_collection(): void
+ {
+ $date = Carbon::today();
+ $response = Mockery::mock();
+ $response->shouldReceive('throw')->once();
+ $response->shouldReceive('json')
+ ->once()
+ ->with('data', [])
+ ->andReturn([
+ ['start_date' => $date->format('Y-m-d H:i:s'), 'total_price_tax_included' => 5.0],
+ ]);
+ $response->shouldReceive('status')->once()->andReturn(200);
+
+ $http = Mockery::mock(Factory::class);
+ $http->shouldReceive('baseUrl')->once()->andReturnSelf();
+ $http->shouldReceive('withQueryParameters')->once()->andReturnSelf();
+ $http->shouldReceive('timeout')->once()->andReturnSelf();
+ $http->shouldReceive('retry')->once()->andReturnSelf();
+ $http->shouldReceive('get')->once()->andReturn($response);
+
+ $client = new ZonneplanPriceClient($http);
+ $result = $client->getGasPricesForDate($date);
+
+ $this->assertCount(1, $result);
+ }
+
+ public function test_client_throws_exception_on_api_failure(): void
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('Kon energieprijzen niet ophalen');
+
+ $response = Mockery::mock();
+ $response->shouldReceive('throw')->once()->andThrow(
+ new \Illuminate\Http\Client\ConnectionException('API Error')
+ );
+
+ $http = Mockery::mock(Factory::class);
+ $http->shouldReceive('baseUrl')->once()->andReturnSelf();
+ $http->shouldReceive('withQueryParameters')->once()->andReturnSelf();
+ $http->shouldReceive('timeout')->once()->andReturnSelf();
+ $http->shouldReceive('retry')->once()->andReturnSelf();
+ $http->shouldReceive('get')->once()->andReturn($response);
+
+ $client = new ZonneplanPriceClient($http);
+ $client->getElectricityPrices();
+ }
+}
diff --git a/tests/Unit/Repositories/ZonneplanPriceRepositoryTest.php b/tests/Unit/Repositories/ZonneplanPriceRepositoryTest.php
new file mode 100644
index 0000000..2f7e31f
--- /dev/null
+++ b/tests/Unit/Repositories/ZonneplanPriceRepositoryTest.php
@@ -0,0 +1,189 @@
+ 60]);
+ config(['cache_prices.prefix' => 'test:']);
+ }
+
+ protected function tearDown(): void
+ {
+ Mockery::close();
+ Cache::flush();
+ parent::tearDown();
+ }
+
+ public function test_retrieve_electricity_returns_collection_of_electricity_data(): void
+ {
+ $now = Carbon::now();
+ $electricityResponse = collect([
+ [
+ 'start_date' => $now->format('Y-m-d H:i:s'),
+ 'end_date' => $now->copy()->addHour()->format('Y-m-d H:i:s'),
+ 'period' => 'elec_hour',
+ 'market_price' => 1500,
+ 'total_price_tax_included' => 15.50,
+ 'price_incl_handling_vat' => 1550,
+ 'price_tax_with_vat' => 300,
+ 'pricing_profile' => 'dynamic',
+ 'carbon_footprint_in_gram' => 500,
+ 'sustainability_score' => 80,
+ 'start_date_datetime' => $now->format('Y-m-d H:i:s'),
+ ],
+ ]);
+
+ $client = Mockery::mock(PriceClient::class);
+ $client->shouldReceive('getElectricityPrices')
+ ->once()
+ ->andReturn($electricityResponse);
+
+ $repository = new ZonneplanPriceRepository($client);
+ $result = $repository->retrieveElectricity();
+
+ $this->assertInstanceOf(Collection::class, $result);
+ $this->assertCount(1, $result);
+ $this->assertInstanceOf(ElectricityData::class, $result->first());
+ }
+
+ public function test_retrieve_gas_returns_collection_of_gas_data(): void
+ {
+ $now = Carbon::now();
+ $gasResponse = collect([
+ [
+ 'start_date' => $now->format('Y-m-d H:i:s'),
+ 'end_date' => $now->copy()->addDay()->format('Y-m-d H:i:s'),
+ 'period' => 'gas_day',
+ 'market_price' => 500,
+ 'total_price_tax_included' => 5.50,
+ 'price_incl_handling_vat' => 550,
+ 'price_tax_with_vat' => 100,
+ 'start_date_datetime' => $now->format('Y-m-d H:i:s'),
+ ],
+ ]);
+
+ $client = Mockery::mock(PriceClient::class);
+ $client->shouldReceive('getGasPrices')
+ ->once()
+ ->andReturn($gasResponse);
+
+ $repository = new ZonneplanPriceRepository($client);
+ $result = $repository->retrieveGas();
+
+ $this->assertInstanceOf(Collection::class, $result);
+ $this->assertCount(1, $result);
+ $this->assertInstanceOf(GasData::class, $result->first());
+ }
+
+ public function test_retrieve_electricity_for_date_returns_collection(): void
+ {
+ $date = Carbon::today();
+ $electricityResponse = collect([
+ [
+ 'start_date' => $date->copy()->setHour(0)->format('Y-m-d H:i:s'),
+ 'end_date' => $date->copy()->setHour(1)->format('Y-m-d H:i:s'),
+ 'period' => 'elec_hour',
+ 'market_price' => 1500,
+ 'total_price_tax_included' => 15.50,
+ 'price_incl_handling_vat' => 1550,
+ 'price_tax_with_vat' => 300,
+ 'pricing_profile' => 'dynamic',
+ 'carbon_footprint_in_gram' => 500,
+ 'sustainability_score' => 80,
+ 'start_date_datetime' => $date->copy()->setHour(0)->format('Y-m-d H:i:s'),
+ ],
+ ]);
+
+ $client = Mockery::mock(PriceClient::class);
+ $client->shouldReceive('getElectricityPricesForDate')
+ ->once()
+ ->with($date)
+ ->andReturn($electricityResponse);
+
+ $repository = new ZonneplanPriceRepository($client);
+ $result = $repository->retrieveElectricityForDate($date);
+
+ $this->assertInstanceOf(Collection::class, $result);
+ $this->assertCount(1, $result);
+ $this->assertInstanceOf(ElectricityData::class, $result->first());
+ }
+
+ public function test_retrieve_gas_for_date_returns_collection(): void
+ {
+ $date = Carbon::today();
+ $gasResponse = collect([
+ [
+ 'start_date' => $date->format('Y-m-d H:i:s'),
+ 'end_date' => $date->copy()->addDay()->format('Y-m-d H:i:s'),
+ 'period' => 'gas_day',
+ 'market_price' => 500,
+ 'total_price_tax_included' => 5.50,
+ 'price_incl_handling_vat' => 550,
+ 'price_tax_with_vat' => 100,
+ 'start_date_datetime' => $date->format('Y-m-d H:i:s'),
+ ],
+ ]);
+
+ $client = Mockery::mock(PriceClient::class);
+ $client->shouldReceive('getGasPricesForDate')
+ ->once()
+ ->with($date)
+ ->andReturn($gasResponse);
+
+ $repository = new ZonneplanPriceRepository($client);
+ $result = $repository->retrieveGasForDate($date);
+
+ $this->assertInstanceOf(Collection::class, $result);
+ $this->assertCount(1, $result);
+ $this->assertInstanceOf(GasData::class, $result->first());
+ }
+
+ public function test_retrieve_electricity_caches_result(): void
+ {
+ $now = Carbon::now();
+ $electricityResponse = collect([
+ [
+ 'start_date' => $now->format('Y-m-d H:i:s'),
+ 'end_date' => $now->copy()->addHour()->format('Y-m-d H:i:s'),
+ 'period' => 'elec_hour',
+ 'market_price' => 1500,
+ 'total_price_tax_included' => 15.50,
+ 'price_incl_handling_vat' => 1550,
+ 'price_tax_with_vat' => 300,
+ 'pricing_profile' => 'dynamic',
+ 'carbon_footprint_in_gram' => 500,
+ 'sustainability_score' => 80,
+ 'start_date_datetime' => $now->format('Y-m-d H:i:s'),
+ ],
+ ]);
+
+ $client = Mockery::mock(PriceClient::class);
+ $client->shouldReceive('getElectricityPrices')
+ ->once()
+ ->andReturn($electricityResponse);
+
+ $repository = new ZonneplanPriceRepository($client);
+
+ $result1 = $repository->retrieveElectricity();
+ $result2 = $repository->retrieveElectricity();
+
+ $this->assertEquals(
+ $result1->first()->totalPriceTaxIncluded,
+ $result2->first()->totalPriceTaxIncluded
+ );
+ }
+}
diff --git a/tests/Unit/Services/DashboardServiceTest.php b/tests/Unit/Services/DashboardServiceTest.php
new file mode 100644
index 0000000..2fb90f1
--- /dev/null
+++ b/tests/Unit/Services/DashboardServiceTest.php
@@ -0,0 +1,129 @@
+createElectricityData($now->copy()->subMinutes(30), 15.0, 80),
+ $this->createElectricityData($now->copy()->addHour(), 12.0, 90),
+ $this->createElectricityData($now->copy()->addHours(2), 20.0, 60),
+ $this->createElectricityData($now->copy()->addHours(3), 10.0, 95),
+ ]);
+
+ $gasData = collect([
+ $this->createGasData($now->copy()->startOfDay(), 5.0),
+ $this->createGasData($now->copy()->addDay(), 4.0),
+ $this->createGasData($now->copy()->addDays(2), 6.0),
+ ]);
+
+ $repository = Mockery::mock(PriceRepository::class);
+ $repository->shouldReceive('retrieveElectricity')->once()->andReturn($electricityData);
+ $repository->shouldReceive('retrieveGas')->once()->andReturn($gasData);
+
+ $service = new DashboardService($repository);
+ $result = $service->getDashboardData();
+
+ $this->assertEquals(15.0, $result->electricity->totalPriceTaxIncluded);
+ $this->assertEquals(10.0, $result->electricityCheapest->totalPriceTaxIncluded);
+ $this->assertEquals(95, $result->electricityDurable->sustainabilityScore);
+ $this->assertEquals(20.0, $result->electricityPriciest->totalPriceTaxIncluded);
+
+ $this->assertEquals(5.0, $result->gas->totalPriceTaxIncluded);
+ $this->assertEquals(4.0, $result->gasCheapest->totalPriceTaxIncluded);
+ $this->assertEquals(6.0, $result->gasPriciest->totalPriceTaxIncluded);
+ }
+
+ public function test_get_dashboard_data_returns_null_for_missing_current_price(): void
+ {
+ $now = Carbon::now();
+
+ $electricityData = collect([
+ $this->createElectricityData($now->copy()->addHours(5), 15.0, 80),
+ ]);
+
+ $gasData = collect([
+ $this->createGasData($now->copy()->addDays(5), 5.0),
+ ]);
+
+ $repository = Mockery::mock(PriceRepository::class);
+ $repository->shouldReceive('retrieveElectricity')->once()->andReturn($electricityData);
+ $repository->shouldReceive('retrieveGas')->once()->andReturn($gasData);
+
+ $service = new DashboardService($repository);
+ $result = $service->getDashboardData();
+
+ $this->assertNull($result->electricity);
+ $this->assertNull($result->gas);
+ $this->assertNotNull($result->electricityCheapest);
+ $this->assertNotNull($result->gasCheapest);
+ }
+
+ public function test_get_dashboard_data_with_empty_collections(): void
+ {
+ $repository = Mockery::mock(PriceRepository::class);
+ $repository->shouldReceive('retrieveElectricity')->once()->andReturn(collect());
+ $repository->shouldReceive('retrieveGas')->once()->andReturn(collect());
+
+ $service = new DashboardService($repository);
+ $result = $service->getDashboardData();
+
+ $this->assertNull($result->electricity);
+ $this->assertNull($result->electricityCheapest);
+ $this->assertNull($result->electricityDurable);
+ $this->assertNull($result->electricityPriciest);
+ $this->assertNull($result->gas);
+ $this->assertNull($result->gasCheapest);
+ $this->assertNull($result->gasPriciest);
+ }
+
+ private function createElectricityData(Carbon $startDate, float $price, int $sustainabilityScore): ElectricityData
+ {
+ return new ElectricityData(
+ startDate: $startDate,
+ endDate: $startDate->copy()->addHour(),
+ period: 'elec_hour',
+ marketPrice: (int) ($price * 100),
+ totalPriceTaxIncluded: $price,
+ priceInclHandlingVat: (int) ($price * 100),
+ priceTaxWithVat: (int) ($price * 20),
+ pricingProfile: 'dynamic',
+ carbonFootprintInGram: 500,
+ sustainabilityScore: $sustainabilityScore,
+ startDateDatetime: $startDate,
+ );
+ }
+
+ private function createGasData(Carbon $startDate, float $price): GasData
+ {
+ return new GasData(
+ startDate: $startDate,
+ endDate: $startDate->copy()->addDay(),
+ period: 'gas_day',
+ marketPrice: (int) ($price * 100),
+ totalPriceTaxIncluded: $price,
+ priceInclHandlingVat: (int) ($price * 100),
+ priceTaxWithVat: (int) ($price * 20),
+ startDateDatetime: $startDate,
+ );
+ }
+}
diff --git a/tests/Unit/Services/HistoryServiceTest.php b/tests/Unit/Services/HistoryServiceTest.php
new file mode 100644
index 0000000..07ef551
--- /dev/null
+++ b/tests/Unit/Services/HistoryServiceTest.php
@@ -0,0 +1,103 @@
+createElectricityData($date->copy()->setHour(0), 15.0),
+ $this->createElectricityData($date->copy()->setHour(1), 16.0),
+ ]);
+
+ $gasData = collect([
+ $this->createGasData($date, 5.0),
+ ]);
+
+ $repository = Mockery::mock(PriceRepository::class);
+ $repository->shouldReceive('retrieveElectricityForDate')
+ ->once()
+ ->with($date)
+ ->andReturn($electricityData);
+ $repository->shouldReceive('retrieveGasForDate')
+ ->once()
+ ->with($date)
+ ->andReturn($gasData);
+
+ $service = new HistoryService($repository);
+ $result = $service->getHistoryData($date);
+
+ $this->assertCount(2, $result->electricity);
+ $this->assertCount(1, $result->gas);
+ $this->assertEquals(15.0, $result->electricity->first()->totalPriceTaxIncluded);
+ $this->assertEquals(5.0, $result->gas->first()->totalPriceTaxIncluded);
+ }
+
+ public function test_get_history_data_with_empty_collections(): void
+ {
+ $date = Carbon::today();
+
+ $repository = Mockery::mock(PriceRepository::class);
+ $repository->shouldReceive('retrieveElectricityForDate')
+ ->once()
+ ->andReturn(collect());
+ $repository->shouldReceive('retrieveGasForDate')
+ ->once()
+ ->andReturn(collect());
+
+ $service = new HistoryService($repository);
+ $result = $service->getHistoryData($date);
+
+ $this->assertCount(0, $result->electricity);
+ $this->assertCount(0, $result->gas);
+ }
+
+ private function createElectricityData(Carbon $startDate, float $price): ElectricityData
+ {
+ return new ElectricityData(
+ startDate: $startDate,
+ endDate: $startDate->copy()->addHour(),
+ period: 'elec_hour',
+ marketPrice: (int) ($price * 100),
+ totalPriceTaxIncluded: $price,
+ priceInclHandlingVat: (int) ($price * 100),
+ priceTaxWithVat: (int) ($price * 20),
+ pricingProfile: 'dynamic',
+ carbonFootprintInGram: 500,
+ sustainabilityScore: 80,
+ startDateDatetime: $startDate,
+ );
+ }
+
+ private function createGasData(Carbon $startDate, float $price): GasData
+ {
+ return new GasData(
+ startDate: $startDate,
+ endDate: $startDate->copy()->addDay(),
+ period: 'gas_day',
+ marketPrice: (int) ($price * 100),
+ totalPriceTaxIncluded: $price,
+ priceInclHandlingVat: (int) ($price * 100),
+ priceTaxWithVat: (int) ($price * 20),
+ startDateDatetime: $startDate,
+ );
+ }
+}