@enyo-energy/energy-app-sdk 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -1
- package/dist/cjs/energy-app-package-definition.cjs +1 -0
- package/dist/cjs/energy-app-package-definition.d.cts +1 -0
- package/dist/cjs/energy-app-permission.type.cjs +2 -0
- package/dist/cjs/energy-app-permission.type.d.cts +3 -1
- package/dist/cjs/energy-app.cjs +16 -0
- package/dist/cjs/energy-app.d.cts +15 -0
- package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
- package/dist/cjs/implementations/pricing/compose-electricity-prices.cjs +218 -0
- package/dist/cjs/implementations/pricing/compose-electricity-prices.d.cts +100 -0
- package/dist/cjs/implementations/pricing/price-component-validators.cjs +295 -0
- package/dist/cjs/implementations/pricing/price-component-validators.d.cts +90 -0
- package/dist/cjs/implementations/pricing/price-schedule-resolver.cjs +182 -0
- package/dist/cjs/implementations/pricing/price-schedule-resolver.d.cts +95 -0
- package/dist/cjs/index.cjs +7 -0
- package/dist/cjs/index.d.cts +7 -0
- package/dist/cjs/packages/energy-app-electricity-tariff.d.cts +34 -1
- package/dist/cjs/packages/energy-app-grid-fee.cjs +2 -0
- package/dist/cjs/packages/energy-app-grid-fee.d.cts +181 -0
- package/dist/cjs/types/enyo-electricity-tariff.cjs +33 -1
- package/dist/cjs/types/enyo-electricity-tariff.d.cts +161 -4
- package/dist/cjs/types/enyo-grid-fee.cjs +15 -0
- package/dist/cjs/types/enyo-grid-fee.d.cts +129 -0
- package/dist/cjs/types/enyo-price-composition.cjs +15 -0
- package/dist/cjs/types/enyo-price-composition.d.cts +126 -0
- package/dist/cjs/types/enyo-price-schedule.cjs +45 -0
- package/dist/cjs/types/enyo-price-schedule.d.cts +137 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/energy-app-package-definition.d.ts +1 -0
- package/dist/energy-app-package-definition.js +1 -0
- package/dist/energy-app-permission.type.d.ts +3 -1
- package/dist/energy-app-permission.type.js +2 -0
- package/dist/energy-app.d.ts +15 -0
- package/dist/energy-app.js +16 -0
- package/dist/enyo-energy-app-sdk.d.ts +3 -0
- package/dist/implementations/pricing/compose-electricity-prices.d.ts +100 -0
- package/dist/implementations/pricing/compose-electricity-prices.js +212 -0
- package/dist/implementations/pricing/price-component-validators.d.ts +90 -0
- package/dist/implementations/pricing/price-component-validators.js +285 -0
- package/dist/implementations/pricing/price-schedule-resolver.d.ts +95 -0
- package/dist/implementations/pricing/price-schedule-resolver.js +175 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/packages/energy-app-electricity-tariff.d.ts +34 -1
- package/dist/packages/energy-app-grid-fee.d.ts +181 -0
- package/dist/packages/energy-app-grid-fee.js +1 -0
- package/dist/types/enyo-electricity-tariff.d.ts +161 -4
- package/dist/types/enyo-electricity-tariff.js +32 -0
- package/dist/types/enyo-grid-fee.d.ts +129 -0
- package/dist/types/enyo-grid-fee.js +12 -0
- package/dist/types/enyo-price-composition.d.ts +126 -0
- package/dist/types/enyo-price-composition.js +12 -0
- package/dist/types/enyo-price-schedule.d.ts +137 -0
- package/dist/types/enyo-price-schedule.js +42 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { EnyoTariffBonus } from '../../types/enyo-electricity-tariff.js';
|
|
2
|
+
import { EnyoEnergyPriceEntry } from '../../types/enyo-energy-prices.js';
|
|
3
|
+
import { EnyoEpexSpotPriceEntry } from '../../types/enyo-epex-spot-price.js';
|
|
4
|
+
import { EnyoComposableEnergyPriceEntry, EnyoComposeElectricityPricesInput, EnyoComposedElectricityPriceEntry } from '../../types/enyo-price-composition.js';
|
|
5
|
+
import { EnyoPriceAppliesToEnum } from '../../types/enyo-price-schedule.js';
|
|
6
|
+
/**
|
|
7
|
+
* Adapts a tariff price series from `useElectricityPrices().getPrices()` into
|
|
8
|
+
* composer input.
|
|
9
|
+
*
|
|
10
|
+
* @param entries - Price entries as returned by the electricity prices API
|
|
11
|
+
* @param direction - Which price to take; defaults to consumption. Feed-in
|
|
12
|
+
* entries without a `feedInPricePerKwh` are skipped.
|
|
13
|
+
* @returns Composable energy price entries
|
|
14
|
+
*/
|
|
15
|
+
export declare function fromEnergyPriceEntries(entries: EnyoEnergyPriceEntry[], direction?: EnyoPriceAppliesToEnum): EnyoComposableEnergyPriceEntry[];
|
|
16
|
+
/**
|
|
17
|
+
* Adapts an EPEX SPOT day-ahead series into composer input.
|
|
18
|
+
*
|
|
19
|
+
* Spot prices are the raw exchange result and contain neither grid fees nor
|
|
20
|
+
* levies, taxes or supplier margin — so the tariff's `priceComposition` for such
|
|
21
|
+
* a series should leave every `includes*` flag `false`.
|
|
22
|
+
*
|
|
23
|
+
* @param entries - Spot price entries as returned by the EPEX SPOT price API
|
|
24
|
+
* @returns Composable energy price entries
|
|
25
|
+
*/
|
|
26
|
+
export declare function fromEpexSpotEntries(entries: EnyoEpexSpotPriceEntry[]): EnyoComposableEnergyPriceEntry[];
|
|
27
|
+
/**
|
|
28
|
+
* Resolves a tariff's bonuses over a time range and combines them per interval
|
|
29
|
+
* according to the SDK's stacking rule.
|
|
30
|
+
*
|
|
31
|
+
* **Stacking rule:** overlapping bonuses add up, unless at least one bonus
|
|
32
|
+
* covering the interval is {@link EnyoTariffBonus.exclusive} — then the
|
|
33
|
+
* exclusive bonus with the highest `priority` applies alone (ties are broken by
|
|
34
|
+
* array order). Bonuses whose `appliesTo` does not match the requested
|
|
35
|
+
* direction, and intervals outside a bonus's validity window, contribute
|
|
36
|
+
* nothing.
|
|
37
|
+
*
|
|
38
|
+
* @param bonuses - The tariff's bonuses
|
|
39
|
+
* @param options - Range and direction to resolve for
|
|
40
|
+
* @returns A map from interval start (ISO) to the total discount and the contributing bonus ids
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const resolved = resolveTariffBonuses(tariff.bonuses ?? [], {
|
|
45
|
+
* fromIso: '2026-09-02T00:00:00Z',
|
|
46
|
+
* untilIso: '2026-09-03T00:00:00Z',
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveTariffBonuses(bonuses: EnyoTariffBonus[], options: {
|
|
51
|
+
/** Start of the range in ISO format (inclusive). */
|
|
52
|
+
fromIso: string;
|
|
53
|
+
/** End of the range in ISO format (exclusive). */
|
|
54
|
+
untilIso: string;
|
|
55
|
+
/** Direction to resolve for; defaults to consumption. */
|
|
56
|
+
appliesTo?: EnyoPriceAppliesToEnum;
|
|
57
|
+
}): Map<string, {
|
|
58
|
+
bonusPerKwh: number;
|
|
59
|
+
appliedBonusIds: string[];
|
|
60
|
+
}>;
|
|
61
|
+
/**
|
|
62
|
+
* Composes the effective electricity price per 15-minute interval from an energy
|
|
63
|
+
* price series, a dynamic grid fee and a tariff's bonuses.
|
|
64
|
+
*
|
|
65
|
+
* This runs **in the energy app**, never in the host, because only the app knows
|
|
66
|
+
* what its provider's API already returns: some providers deliver all-in prices
|
|
67
|
+
* with the network charge baked in, others deliver the pure energy price. Pass
|
|
68
|
+
* the tariff's `priceComposition` as `energyPriceComposition` and the composer
|
|
69
|
+
* reports each component without adding one that is already included.
|
|
70
|
+
*
|
|
71
|
+
* Every component is reported separately — `gridFeePerKwh` with its
|
|
72
|
+
* {@link EnyoPriceComponentOriginEnum}, the total `bonusPerKwh` and the
|
|
73
|
+
* `appliedBonusIds` behind it — so a UI can explain the number and an optimizer
|
|
74
|
+
* can reason about the parts.
|
|
75
|
+
*
|
|
76
|
+
* Entries are produced for the intervals present in `energyPrices`; grid fee and
|
|
77
|
+
* bonus values are matched by `timestampIso`, and an interval without a matching
|
|
78
|
+
* fee or bonus simply contributes `0`.
|
|
79
|
+
*
|
|
80
|
+
* @param input - Energy prices, what they already include, the grid fee and the bonuses
|
|
81
|
+
* @returns Composed entries in the order of the input energy prices
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const tariff = await energyApp.useElectricityTariff().getDefaultTariff();
|
|
86
|
+
* const fees = await energyApp.useGridFee().getDynamicGridFees({fromIso, untilIso});
|
|
87
|
+
* const spot = await energyApp.useEpexSpotPrices().getPrices({fromIso, untilIso});
|
|
88
|
+
*
|
|
89
|
+
* const prices = composeElectricityPrices({
|
|
90
|
+
* energyPrices: fromEpexSpotEntries(spot.entries),
|
|
91
|
+
* energyPriceComposition: tariff?.priceComposition,
|
|
92
|
+
* gridFees: fees,
|
|
93
|
+
* bonuses: tariff?.bonuses,
|
|
94
|
+
* currency: 'EUR',
|
|
95
|
+
* });
|
|
96
|
+
*
|
|
97
|
+
* const cheapest = prices.reduce((a, b) => (b.effectivePricePerKwh < a.effectivePricePerKwh ? b : a));
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export declare function composeElectricityPrices(input: EnyoComposeElectricityPricesInput): EnyoComposedElectricityPriceEntry[];
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { EnyoPriceComponentOriginEnum, } from '../../types/enyo-price-composition.js';
|
|
2
|
+
import { EnyoPriceAppliesToEnum } from '../../types/enyo-price-schedule.js';
|
|
3
|
+
import { resolvePriceSchedule } from './price-schedule-resolver.js';
|
|
4
|
+
/**
|
|
5
|
+
* Adapts a tariff price series from `useElectricityPrices().getPrices()` into
|
|
6
|
+
* composer input.
|
|
7
|
+
*
|
|
8
|
+
* @param entries - Price entries as returned by the electricity prices API
|
|
9
|
+
* @param direction - Which price to take; defaults to consumption. Feed-in
|
|
10
|
+
* entries without a `feedInPricePerKwh` are skipped.
|
|
11
|
+
* @returns Composable energy price entries
|
|
12
|
+
*/
|
|
13
|
+
export function fromEnergyPriceEntries(entries, direction = EnyoPriceAppliesToEnum.Consumption) {
|
|
14
|
+
if (direction === EnyoPriceAppliesToEnum.FeedIn) {
|
|
15
|
+
return entries
|
|
16
|
+
.filter(entry => entry.feedInPricePerKwh !== undefined)
|
|
17
|
+
.map(entry => ({ timestampIso: entry.timestampIso, pricePerKwh: entry.feedInPricePerKwh }));
|
|
18
|
+
}
|
|
19
|
+
return entries.map(entry => ({
|
|
20
|
+
timestampIso: entry.timestampIso,
|
|
21
|
+
pricePerKwh: entry.consumptionPricePerKwh,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Adapts an EPEX SPOT day-ahead series into composer input.
|
|
26
|
+
*
|
|
27
|
+
* Spot prices are the raw exchange result and contain neither grid fees nor
|
|
28
|
+
* levies, taxes or supplier margin — so the tariff's `priceComposition` for such
|
|
29
|
+
* a series should leave every `includes*` flag `false`.
|
|
30
|
+
*
|
|
31
|
+
* @param entries - Spot price entries as returned by the EPEX SPOT price API
|
|
32
|
+
* @returns Composable energy price entries
|
|
33
|
+
*/
|
|
34
|
+
export function fromEpexSpotEntries(entries) {
|
|
35
|
+
return entries.map(entry => ({ timestampIso: entry.timestampIso, pricePerKwh: entry.pricePerKwh }));
|
|
36
|
+
}
|
|
37
|
+
function appliesToDirection(componentAppliesTo, direction) {
|
|
38
|
+
return componentAppliesTo === EnyoPriceAppliesToEnum.Both || componentAppliesTo === direction;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolves a tariff's bonuses over a time range and combines them per interval
|
|
42
|
+
* according to the SDK's stacking rule.
|
|
43
|
+
*
|
|
44
|
+
* **Stacking rule:** overlapping bonuses add up, unless at least one bonus
|
|
45
|
+
* covering the interval is {@link EnyoTariffBonus.exclusive} — then the
|
|
46
|
+
* exclusive bonus with the highest `priority` applies alone (ties are broken by
|
|
47
|
+
* array order). Bonuses whose `appliesTo` does not match the requested
|
|
48
|
+
* direction, and intervals outside a bonus's validity window, contribute
|
|
49
|
+
* nothing.
|
|
50
|
+
*
|
|
51
|
+
* @param bonuses - The tariff's bonuses
|
|
52
|
+
* @param options - Range and direction to resolve for
|
|
53
|
+
* @returns A map from interval start (ISO) to the total discount and the contributing bonus ids
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* const resolved = resolveTariffBonuses(tariff.bonuses ?? [], {
|
|
58
|
+
* fromIso: '2026-09-02T00:00:00Z',
|
|
59
|
+
* untilIso: '2026-09-03T00:00:00Z',
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function resolveTariffBonuses(bonuses, options) {
|
|
64
|
+
const direction = options.appliesTo ?? EnyoPriceAppliesToEnum.Consumption;
|
|
65
|
+
const resolved = bonuses
|
|
66
|
+
.filter(bonus => appliesToDirection(bonus.appliesTo, direction))
|
|
67
|
+
.map(bonus => {
|
|
68
|
+
const entries = resolvePriceSchedule(bonus.schedule, {
|
|
69
|
+
fromIso: options.fromIso,
|
|
70
|
+
untilIso: options.untilIso,
|
|
71
|
+
timezone: bonus.timezone,
|
|
72
|
+
validFromIso: bonus.validFromIso,
|
|
73
|
+
validUntilIso: bonus.validUntilIso,
|
|
74
|
+
});
|
|
75
|
+
const amountByTimestamp = new Map();
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.amountPerKwh > 0) {
|
|
78
|
+
amountByTimestamp.set(entry.timestampIso, entry.amountPerKwh);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { bonus, amountByTimestamp };
|
|
82
|
+
});
|
|
83
|
+
const timestamps = new Set();
|
|
84
|
+
for (const candidate of resolved) {
|
|
85
|
+
for (const timestampIso of candidate.amountByTimestamp.keys()) {
|
|
86
|
+
timestamps.add(timestampIso);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const combined = new Map();
|
|
90
|
+
for (const timestampIso of timestamps) {
|
|
91
|
+
const active = resolved.filter(candidate => candidate.amountByTimestamp.has(timestampIso));
|
|
92
|
+
const exclusive = active.filter(candidate => candidate.bonus.exclusive === true);
|
|
93
|
+
if (exclusive.length > 0) {
|
|
94
|
+
const winner = exclusive.reduce((best, candidate) => (candidate.bonus.priority ?? 0) > (best.bonus.priority ?? 0) ? candidate : best);
|
|
95
|
+
combined.set(timestampIso, {
|
|
96
|
+
bonusPerKwh: winner.amountByTimestamp.get(timestampIso),
|
|
97
|
+
appliedBonusIds: [winner.bonus.id],
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
combined.set(timestampIso, {
|
|
102
|
+
bonusPerKwh: active.reduce((sum, candidate) => sum + candidate.amountByTimestamp.get(timestampIso), 0),
|
|
103
|
+
appliedBonusIds: active.map(candidate => candidate.bonus.id),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return combined;
|
|
107
|
+
}
|
|
108
|
+
function gridFeeByTimestamp(series) {
|
|
109
|
+
const map = new Map();
|
|
110
|
+
for (const entry of series?.entries ?? []) {
|
|
111
|
+
map.set(entry.timestampIso, entry.feePerKwh);
|
|
112
|
+
}
|
|
113
|
+
return map;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Composes the effective electricity price per 15-minute interval from an energy
|
|
117
|
+
* price series, a dynamic grid fee and a tariff's bonuses.
|
|
118
|
+
*
|
|
119
|
+
* This runs **in the energy app**, never in the host, because only the app knows
|
|
120
|
+
* what its provider's API already returns: some providers deliver all-in prices
|
|
121
|
+
* with the network charge baked in, others deliver the pure energy price. Pass
|
|
122
|
+
* the tariff's `priceComposition` as `energyPriceComposition` and the composer
|
|
123
|
+
* reports each component without adding one that is already included.
|
|
124
|
+
*
|
|
125
|
+
* Every component is reported separately — `gridFeePerKwh` with its
|
|
126
|
+
* {@link EnyoPriceComponentOriginEnum}, the total `bonusPerKwh` and the
|
|
127
|
+
* `appliedBonusIds` behind it — so a UI can explain the number and an optimizer
|
|
128
|
+
* can reason about the parts.
|
|
129
|
+
*
|
|
130
|
+
* Entries are produced for the intervals present in `energyPrices`; grid fee and
|
|
131
|
+
* bonus values are matched by `timestampIso`, and an interval without a matching
|
|
132
|
+
* fee or bonus simply contributes `0`.
|
|
133
|
+
*
|
|
134
|
+
* @param input - Energy prices, what they already include, the grid fee and the bonuses
|
|
135
|
+
* @returns Composed entries in the order of the input energy prices
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* const tariff = await energyApp.useElectricityTariff().getDefaultTariff();
|
|
140
|
+
* const fees = await energyApp.useGridFee().getDynamicGridFees({fromIso, untilIso});
|
|
141
|
+
* const spot = await energyApp.useEpexSpotPrices().getPrices({fromIso, untilIso});
|
|
142
|
+
*
|
|
143
|
+
* const prices = composeElectricityPrices({
|
|
144
|
+
* energyPrices: fromEpexSpotEntries(spot.entries),
|
|
145
|
+
* energyPriceComposition: tariff?.priceComposition,
|
|
146
|
+
* gridFees: fees,
|
|
147
|
+
* bonuses: tariff?.bonuses,
|
|
148
|
+
* currency: 'EUR',
|
|
149
|
+
* });
|
|
150
|
+
*
|
|
151
|
+
* const cheapest = prices.reduce((a, b) => (b.effectivePricePerKwh < a.effectivePricePerKwh ? b : a));
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export function composeElectricityPrices(input) {
|
|
155
|
+
const direction = input.appliesTo ?? EnyoPriceAppliesToEnum.Consumption;
|
|
156
|
+
const gridFeeIncluded = input.energyPriceComposition?.includesGridFee === true;
|
|
157
|
+
const bonusesIncluded = input.energyPriceComposition?.includesBonuses === true;
|
|
158
|
+
const gridFeeSeriesApplies = input.gridFees != null && appliesToDirection(input.gridFees.appliesTo, direction);
|
|
159
|
+
const resolvedGridFees = gridFeeSeriesApplies ? gridFeeByTimestamp(input.gridFees) : new Map();
|
|
160
|
+
const constantGridFee = !gridFeeSeriesApplies && input.constantGridFeePerKwh !== undefined ? input.constantGridFeePerKwh : undefined;
|
|
161
|
+
const hasGridFeeSource = gridFeeSeriesApplies || constantGridFee !== undefined;
|
|
162
|
+
const range = {
|
|
163
|
+
fromIso: input.energyPrices[0]?.timestampIso,
|
|
164
|
+
untilIso: input.energyPrices[input.energyPrices.length - 1]?.timestampIso,
|
|
165
|
+
};
|
|
166
|
+
const resolvedBonuses = input.bonuses === undefined || input.bonuses.length === 0 || range.fromIso === undefined
|
|
167
|
+
? new Map()
|
|
168
|
+
: resolveTariffBonuses(input.bonuses, {
|
|
169
|
+
fromIso: range.fromIso,
|
|
170
|
+
// The last entry starts the final interval, so extend past it.
|
|
171
|
+
untilIso: new Date(Date.parse(range.untilIso) + 1).toISOString(),
|
|
172
|
+
appliesTo: direction,
|
|
173
|
+
});
|
|
174
|
+
return input.energyPrices.map(entry => {
|
|
175
|
+
const gridFeePerKwh = hasGridFeeSource
|
|
176
|
+
? constantGridFee ?? resolvedGridFees.get(entry.timestampIso) ?? 0
|
|
177
|
+
: 0;
|
|
178
|
+
const gridFeeOrigin = !hasGridFeeSource
|
|
179
|
+
? EnyoPriceComponentOriginEnum.NotApplicable
|
|
180
|
+
: gridFeeIncluded
|
|
181
|
+
? EnyoPriceComponentOriginEnum.Included
|
|
182
|
+
: EnyoPriceComponentOriginEnum.Added;
|
|
183
|
+
const bonus = resolvedBonuses.get(entry.timestampIso);
|
|
184
|
+
const bonusPerKwh = bonus?.bonusPerKwh ?? 0;
|
|
185
|
+
const bonusOrigin = bonus === undefined
|
|
186
|
+
? EnyoPriceComponentOriginEnum.NotApplicable
|
|
187
|
+
: bonusesIncluded
|
|
188
|
+
? EnyoPriceComponentOriginEnum.Included
|
|
189
|
+
: EnyoPriceComponentOriginEnum.Added;
|
|
190
|
+
let effectivePricePerKwh = entry.pricePerKwh;
|
|
191
|
+
if (gridFeeOrigin === EnyoPriceComponentOriginEnum.Added) {
|
|
192
|
+
effectivePricePerKwh += gridFeePerKwh;
|
|
193
|
+
}
|
|
194
|
+
if (bonusOrigin === EnyoPriceComponentOriginEnum.Added) {
|
|
195
|
+
effectivePricePerKwh -= bonusPerKwh;
|
|
196
|
+
}
|
|
197
|
+
if (input.minEffectivePricePerKwh !== undefined) {
|
|
198
|
+
effectivePricePerKwh = Math.max(effectivePricePerKwh, input.minEffectivePricePerKwh);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
timestampIso: entry.timestampIso,
|
|
202
|
+
energyPricePerKwh: entry.pricePerKwh,
|
|
203
|
+
gridFeePerKwh,
|
|
204
|
+
gridFeeOrigin,
|
|
205
|
+
bonusPerKwh,
|
|
206
|
+
bonusOrigin,
|
|
207
|
+
appliedBonusIds: bonus?.appliedBonusIds ?? [],
|
|
208
|
+
effectivePricePerKwh,
|
|
209
|
+
currency: input.currency,
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { EnyoElectricityTariff, EnyoTariffBonus, TariffGridFeeConfig, TariffPriceCompositionInfo } from '../../types/enyo-electricity-tariff.js';
|
|
2
|
+
import { EnyoDynamicGridFeeRegistration } from '../../types/enyo-grid-fee.js';
|
|
3
|
+
import { EnyoPriceSchedule } from '../../types/enyo-price-schedule.js';
|
|
4
|
+
/**
|
|
5
|
+
* Thrown when a grid fee, tariff bonus or price schedule violates one of the
|
|
6
|
+
* invariants enforced by this module. The message names the offending field or
|
|
7
|
+
* window index so callers can surface it directly to the user.
|
|
8
|
+
*/
|
|
9
|
+
export declare class PriceComponentValidationError extends Error {
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Validates a {@link EnyoPriceSchedule}. Throws on the first violation.
|
|
14
|
+
*
|
|
15
|
+
* Enforces finite, non-negative amounts, well-formed `HH:mm` times, day and
|
|
16
|
+
* month ranges, ordered absolute windows, and that no two windows which can
|
|
17
|
+
* occur on the same day overlap in time — an overlap would make the resolved
|
|
18
|
+
* amount depend on array order rather than on the schedule itself.
|
|
19
|
+
*
|
|
20
|
+
* Overlap detection for recurring windows is conservative: two windows are only
|
|
21
|
+
* compared when their day-of-week and month sets can intersect. A window that
|
|
22
|
+
* wraps past midnight is compared on both of its legs.
|
|
23
|
+
*
|
|
24
|
+
* @param schedule - The schedule to validate
|
|
25
|
+
* @param label - Prefix used in error messages, e.g. `'gridFee.schedule'`
|
|
26
|
+
* @throws {PriceComponentValidationError} On the first invariant violation
|
|
27
|
+
*/
|
|
28
|
+
export declare function validatePriceSchedule(schedule: EnyoPriceSchedule, label?: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Validates a dynamic grid fee registration. Throws on the first violation.
|
|
31
|
+
*
|
|
32
|
+
* @param registration - The grid fee to validate
|
|
33
|
+
* @throws {PriceComponentValidationError} On the first invariant violation
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* validateDynamicGridFee(registration);
|
|
38
|
+
* await energyApp.useGridFee().registerGridFee(registration);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function validateDynamicGridFee(registration: EnyoDynamicGridFeeRegistration): void;
|
|
42
|
+
/**
|
|
43
|
+
* Validates a single tariff bonus. Throws on the first violation.
|
|
44
|
+
*
|
|
45
|
+
* @param bonus - The bonus to validate
|
|
46
|
+
* @param label - Prefix used in error messages, e.g. `'bonuses[0]'`
|
|
47
|
+
* @throws {PriceComponentValidationError} On the first invariant violation
|
|
48
|
+
*/
|
|
49
|
+
export declare function validateTariffBonus(bonus: EnyoTariffBonus, label?: string): void;
|
|
50
|
+
/**
|
|
51
|
+
* Validates a tariff's bonus list: every bonus is individually valid and ids are
|
|
52
|
+
* unique within the tariff.
|
|
53
|
+
*
|
|
54
|
+
* @param bonuses - The bonuses to validate
|
|
55
|
+
* @throws {PriceComponentValidationError} On the first invariant violation
|
|
56
|
+
*/
|
|
57
|
+
export declare function validateTariffBonuses(bonuses: EnyoTariffBonus[]): void;
|
|
58
|
+
/**
|
|
59
|
+
* Validates how a tariff's pricing data declares its grid fee.
|
|
60
|
+
*
|
|
61
|
+
* Catches the configuration that silently double-charges the customer: declaring
|
|
62
|
+
* a dynamic grid fee while also declaring that the tariff's prices already
|
|
63
|
+
* include the grid fee. Anyone trusting the declaration would add the fee on top
|
|
64
|
+
* of prices that already contain it.
|
|
65
|
+
*
|
|
66
|
+
* @param config - The grid fee configuration of one tariff pricing shape
|
|
67
|
+
* @param priceComposition - The tariff's price composition declaration, if any
|
|
68
|
+
* @param label - Prefix used in error messages, e.g. `'dynamicTariffData'`
|
|
69
|
+
* @throws {PriceComponentValidationError} On the first invariant violation
|
|
70
|
+
*/
|
|
71
|
+
export declare function validateTariffGridFeeConfig(config: TariffGridFeeConfig, priceComposition: TariffPriceCompositionInfo | undefined, label?: string): void;
|
|
72
|
+
/**
|
|
73
|
+
* Validates the pricing-related parts of a complete electricity tariff: the grid
|
|
74
|
+
* fee link of whichever pricing shape is present, and the tariff's bonuses.
|
|
75
|
+
*
|
|
76
|
+
* Call it before {@link EnergyAppElectricityTariff.registerTariff} to turn what
|
|
77
|
+
* would otherwise be a silently mispriced tariff into an actionable error.
|
|
78
|
+
*
|
|
79
|
+
* @param tariff - The tariff to validate (with or without its `id`)
|
|
80
|
+
* @throws {PriceComponentValidationError} On the first invariant violation
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* validateElectricityTariffPricing(tariff);
|
|
85
|
+
* await energyApp.useElectricityTariff().registerTariff(tariff);
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare function validateElectricityTariffPricing(tariff: Omit<EnyoElectricityTariff, 'id'> & {
|
|
89
|
+
id?: string;
|
|
90
|
+
}): void;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { GridFeeModeEnum, } from '../../types/enyo-electricity-tariff.js';
|
|
2
|
+
import { EnyoPriceAppliesToEnum, EnyoPriceScheduleTypeEnum, } from '../../types/enyo-price-schedule.js';
|
|
3
|
+
import { parseTimeOfDay } from './price-schedule-resolver.js';
|
|
4
|
+
/**
|
|
5
|
+
* Thrown when a grid fee, tariff bonus or price schedule violates one of the
|
|
6
|
+
* invariants enforced by this module. The message names the offending field or
|
|
7
|
+
* window index so callers can surface it directly to the user.
|
|
8
|
+
*/
|
|
9
|
+
export class PriceComponentValidationError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'PriceComponentValidationError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function fail(message) {
|
|
16
|
+
throw new PriceComponentValidationError(message);
|
|
17
|
+
}
|
|
18
|
+
function requireNonEmpty(value, field) {
|
|
19
|
+
if (value === undefined || value.trim().length === 0) {
|
|
20
|
+
fail(`${field} must be a non-empty string`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function requireValidTimezone(timezone, field) {
|
|
24
|
+
requireNonEmpty(timezone, field);
|
|
25
|
+
try {
|
|
26
|
+
new Intl.DateTimeFormat('en-US', { timeZone: timezone });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
fail(`${field} must be a valid IANA time zone identifier, got '${timezone}'`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function requireNonNegativeAmount(amount, field) {
|
|
33
|
+
if (!Number.isFinite(amount)) {
|
|
34
|
+
fail(`${field} must be a finite number`);
|
|
35
|
+
}
|
|
36
|
+
if (amount < 0) {
|
|
37
|
+
fail(`${field} must not be negative — amounts are unsigned magnitudes; ` +
|
|
38
|
+
`grid fees are added to and bonuses subtracted from the energy price`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function requireIsoTimestamp(value, field) {
|
|
42
|
+
const parsed = Date.parse(value);
|
|
43
|
+
if (Number.isNaN(parsed)) {
|
|
44
|
+
fail(`${field} must be a valid ISO timestamp, got '${value}'`);
|
|
45
|
+
}
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
function requireValidityRange(validFromIso, validUntilIso, label) {
|
|
49
|
+
const from = validFromIso === undefined ? undefined : requireIsoTimestamp(validFromIso, `${label}.validFromIso`);
|
|
50
|
+
const until = validUntilIso === undefined ? undefined : requireIsoTimestamp(validUntilIso, `${label}.validUntilIso`);
|
|
51
|
+
if (from !== undefined && until !== undefined && until <= from) {
|
|
52
|
+
fail(`${label}.validUntilIso must be after ${label}.validFromIso`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function requireAppliesTo(appliesTo, field) {
|
|
56
|
+
if (!Object.values(EnyoPriceAppliesToEnum).includes(appliesTo)) {
|
|
57
|
+
fail(`${field} must be one of ${Object.values(EnyoPriceAppliesToEnum).join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function toMinuteIntervals(start, end) {
|
|
61
|
+
return start < end
|
|
62
|
+
? [{ start, end }]
|
|
63
|
+
: [{ start, end: 1440 }, { start: 0, end }];
|
|
64
|
+
}
|
|
65
|
+
function intervalsOverlap(a, b) {
|
|
66
|
+
return a.some(left => b.some(right => left.start < right.end && right.start < left.end));
|
|
67
|
+
}
|
|
68
|
+
function setsIntersect(a, b) {
|
|
69
|
+
if (a === undefined || b === undefined) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return a.some(value => b.includes(value));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Validates a {@link EnyoPriceSchedule}. Throws on the first violation.
|
|
76
|
+
*
|
|
77
|
+
* Enforces finite, non-negative amounts, well-formed `HH:mm` times, day and
|
|
78
|
+
* month ranges, ordered absolute windows, and that no two windows which can
|
|
79
|
+
* occur on the same day overlap in time — an overlap would make the resolved
|
|
80
|
+
* amount depend on array order rather than on the schedule itself.
|
|
81
|
+
*
|
|
82
|
+
* Overlap detection for recurring windows is conservative: two windows are only
|
|
83
|
+
* compared when their day-of-week and month sets can intersect. A window that
|
|
84
|
+
* wraps past midnight is compared on both of its legs.
|
|
85
|
+
*
|
|
86
|
+
* @param schedule - The schedule to validate
|
|
87
|
+
* @param label - Prefix used in error messages, e.g. `'gridFee.schedule'`
|
|
88
|
+
* @throws {PriceComponentValidationError} On the first invariant violation
|
|
89
|
+
*/
|
|
90
|
+
export function validatePriceSchedule(schedule, label = 'schedule') {
|
|
91
|
+
if (schedule === null || typeof schedule !== 'object') {
|
|
92
|
+
fail(`${label} must be an object`);
|
|
93
|
+
}
|
|
94
|
+
if (schedule.type === EnyoPriceScheduleTypeEnum.Constant) {
|
|
95
|
+
requireNonNegativeAmount(schedule.amountPerKwh, `${label}.amountPerKwh`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (schedule.type === EnyoPriceScheduleTypeEnum.Absolute) {
|
|
99
|
+
if (schedule.windows.length === 0) {
|
|
100
|
+
fail(`${label}.windows must contain at least one window`);
|
|
101
|
+
}
|
|
102
|
+
const ranges = schedule.windows.map((window, index) => {
|
|
103
|
+
const start = requireIsoTimestamp(window.startIso, `${label}.windows[${index}].startIso`);
|
|
104
|
+
const end = requireIsoTimestamp(window.endIso, `${label}.windows[${index}].endIso`);
|
|
105
|
+
if (end <= start) {
|
|
106
|
+
fail(`${label}.windows[${index}].endIso must be after startIso`);
|
|
107
|
+
}
|
|
108
|
+
requireNonNegativeAmount(window.amountPerKwh, `${label}.windows[${index}].amountPerKwh`);
|
|
109
|
+
return { start, end, index };
|
|
110
|
+
}).sort((a, b) => a.start - b.start);
|
|
111
|
+
for (let i = 1; i < ranges.length; i++) {
|
|
112
|
+
if (ranges[i].start < ranges[i - 1].end) {
|
|
113
|
+
fail(`${label}.windows[${ranges[i].index}] overlaps windows[${ranges[i - 1].index}]`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (schedule.type === EnyoPriceScheduleTypeEnum.Recurring) {
|
|
119
|
+
if (schedule.windows.length === 0) {
|
|
120
|
+
fail(`${label}.windows must contain at least one window`);
|
|
121
|
+
}
|
|
122
|
+
const parsed = schedule.windows.map((window, index) => {
|
|
123
|
+
const start = parseTimeOfDay(window.startTimeOfDay);
|
|
124
|
+
const end = parseTimeOfDay(window.endTimeOfDay);
|
|
125
|
+
if (start === null) {
|
|
126
|
+
fail(`${label}.windows[${index}].startTimeOfDay must be a 'HH:mm' time, got '${window.startTimeOfDay}'`);
|
|
127
|
+
}
|
|
128
|
+
if (end === null) {
|
|
129
|
+
fail(`${label}.windows[${index}].endTimeOfDay must be a 'HH:mm' time, got '${window.endTimeOfDay}'`);
|
|
130
|
+
}
|
|
131
|
+
if (start === end) {
|
|
132
|
+
fail(`${label}.windows[${index}] must not start and end at the same time of day`);
|
|
133
|
+
}
|
|
134
|
+
if (window.daysOfWeek !== undefined) {
|
|
135
|
+
if (window.daysOfWeek.length === 0) {
|
|
136
|
+
fail(`${label}.windows[${index}].daysOfWeek must not be empty — omit it for 'every day'`);
|
|
137
|
+
}
|
|
138
|
+
for (const day of window.daysOfWeek) {
|
|
139
|
+
if (!Number.isInteger(day) || day < 0 || day > 6) {
|
|
140
|
+
fail(`${label}.windows[${index}].daysOfWeek must contain integers 0 (Sunday) to 6 (Saturday), got ${day}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (window.months !== undefined) {
|
|
145
|
+
if (window.months.length === 0) {
|
|
146
|
+
fail(`${label}.windows[${index}].months must not be empty — omit it for 'every month'`);
|
|
147
|
+
}
|
|
148
|
+
for (const month of window.months) {
|
|
149
|
+
if (!Number.isInteger(month) || month < 1 || month > 12) {
|
|
150
|
+
fail(`${label}.windows[${index}].months must contain integers 1 to 12, got ${month}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
requireNonNegativeAmount(window.amountPerKwh, `${label}.windows[${index}].amountPerKwh`);
|
|
155
|
+
return { window, index, intervals: toMinuteIntervals(start, end) };
|
|
156
|
+
});
|
|
157
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
158
|
+
for (let j = i + 1; j < parsed.length; j++) {
|
|
159
|
+
const left = parsed[i];
|
|
160
|
+
const right = parsed[j];
|
|
161
|
+
if (!setsIntersect(left.window.daysOfWeek, right.window.daysOfWeek)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!setsIntersect(left.window.months, right.window.months)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (intervalsOverlap(left.intervals, right.intervals)) {
|
|
168
|
+
fail(`${label}.windows[${right.index}] overlaps windows[${left.index}]`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
fail(`${label}.type must be one of ${Object.values(EnyoPriceScheduleTypeEnum).join(', ')}`);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Validates a dynamic grid fee registration. Throws on the first violation.
|
|
178
|
+
*
|
|
179
|
+
* @param registration - The grid fee to validate
|
|
180
|
+
* @throws {PriceComponentValidationError} On the first invariant violation
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* validateDynamicGridFee(registration);
|
|
185
|
+
* await energyApp.useGridFee().registerGridFee(registration);
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
export function validateDynamicGridFee(registration) {
|
|
189
|
+
requireNonEmpty(registration.id, 'gridFee.id');
|
|
190
|
+
requireNonEmpty(registration.name, 'gridFee.name');
|
|
191
|
+
requireNonEmpty(registration.gridOperator, 'gridFee.gridOperator');
|
|
192
|
+
requireNonEmpty(registration.currency, 'gridFee.currency');
|
|
193
|
+
requireValidTimezone(registration.timezone, 'gridFee.timezone');
|
|
194
|
+
requireAppliesTo(registration.appliesTo, 'gridFee.appliesTo');
|
|
195
|
+
requireValidityRange(registration.validFromIso, registration.validUntilIso, 'gridFee');
|
|
196
|
+
validatePriceSchedule(registration.schedule, 'gridFee.schedule');
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Validates a single tariff bonus. Throws on the first violation.
|
|
200
|
+
*
|
|
201
|
+
* @param bonus - The bonus to validate
|
|
202
|
+
* @param label - Prefix used in error messages, e.g. `'bonuses[0]'`
|
|
203
|
+
* @throws {PriceComponentValidationError} On the first invariant violation
|
|
204
|
+
*/
|
|
205
|
+
export function validateTariffBonus(bonus, label = 'bonus') {
|
|
206
|
+
requireNonEmpty(bonus.id, `${label}.id`);
|
|
207
|
+
requireNonEmpty(bonus.name, `${label}.name`);
|
|
208
|
+
requireValidTimezone(bonus.timezone, `${label}.timezone`);
|
|
209
|
+
requireAppliesTo(bonus.appliesTo, `${label}.appliesTo`);
|
|
210
|
+
requireValidityRange(bonus.validFromIso, bonus.validUntilIso, label);
|
|
211
|
+
if (bonus.priority !== undefined && !Number.isFinite(bonus.priority)) {
|
|
212
|
+
fail(`${label}.priority must be a finite number`);
|
|
213
|
+
}
|
|
214
|
+
validatePriceSchedule(bonus.schedule, `${label}.schedule`);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Validates a tariff's bonus list: every bonus is individually valid and ids are
|
|
218
|
+
* unique within the tariff.
|
|
219
|
+
*
|
|
220
|
+
* @param bonuses - The bonuses to validate
|
|
221
|
+
* @throws {PriceComponentValidationError} On the first invariant violation
|
|
222
|
+
*/
|
|
223
|
+
export function validateTariffBonuses(bonuses) {
|
|
224
|
+
const seen = new Set();
|
|
225
|
+
bonuses.forEach((bonus, index) => {
|
|
226
|
+
validateTariffBonus(bonus, `bonuses[${index}]`);
|
|
227
|
+
if (seen.has(bonus.id)) {
|
|
228
|
+
fail(`bonuses[${index}].id '${bonus.id}' is used more than once in the same tariff`);
|
|
229
|
+
}
|
|
230
|
+
seen.add(bonus.id);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Validates how a tariff's pricing data declares its grid fee.
|
|
235
|
+
*
|
|
236
|
+
* Catches the configuration that silently double-charges the customer: declaring
|
|
237
|
+
* a dynamic grid fee while also declaring that the tariff's prices already
|
|
238
|
+
* include the grid fee. Anyone trusting the declaration would add the fee on top
|
|
239
|
+
* of prices that already contain it.
|
|
240
|
+
*
|
|
241
|
+
* @param config - The grid fee configuration of one tariff pricing shape
|
|
242
|
+
* @param priceComposition - The tariff's price composition declaration, if any
|
|
243
|
+
* @param label - Prefix used in error messages, e.g. `'dynamicTariffData'`
|
|
244
|
+
* @throws {PriceComponentValidationError} On the first invariant violation
|
|
245
|
+
*/
|
|
246
|
+
export function validateTariffGridFeeConfig(config, priceComposition, label = 'tariffData') {
|
|
247
|
+
const mode = config.gridFeeMode ?? GridFeeModeEnum.Static;
|
|
248
|
+
if (!Object.values(GridFeeModeEnum).includes(mode)) {
|
|
249
|
+
fail(`${label}.gridFeeMode must be one of ${Object.values(GridFeeModeEnum).join(', ')}`);
|
|
250
|
+
}
|
|
251
|
+
if (mode === GridFeeModeEnum.Dynamic && priceComposition?.includesGridFee === true) {
|
|
252
|
+
fail(`${label} declares a dynamic grid fee while priceComposition.includesGridFee is true — ` +
|
|
253
|
+
`the fee is already contained in the tariff's prices and must not be added again`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Validates the pricing-related parts of a complete electricity tariff: the grid
|
|
258
|
+
* fee link of whichever pricing shape is present, and the tariff's bonuses.
|
|
259
|
+
*
|
|
260
|
+
* Call it before {@link EnergyAppElectricityTariff.registerTariff} to turn what
|
|
261
|
+
* would otherwise be a silently mispriced tariff into an actionable error.
|
|
262
|
+
*
|
|
263
|
+
* @param tariff - The tariff to validate (with or without its `id`)
|
|
264
|
+
* @throws {PriceComponentValidationError} On the first invariant violation
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```typescript
|
|
268
|
+
* validateElectricityTariffPricing(tariff);
|
|
269
|
+
* await energyApp.useElectricityTariff().registerTariff(tariff);
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
export function validateElectricityTariffPricing(tariff) {
|
|
273
|
+
if (tariff.staticTariffData !== undefined) {
|
|
274
|
+
validateTariffGridFeeConfig(tariff.staticTariffData, tariff.priceComposition, 'staticTariffData');
|
|
275
|
+
}
|
|
276
|
+
if (tariff.timeVariableTariffData !== undefined) {
|
|
277
|
+
validateTariffGridFeeConfig(tariff.timeVariableTariffData, tariff.priceComposition, 'timeVariableTariffData');
|
|
278
|
+
}
|
|
279
|
+
if (tariff.dynamicTariffData !== undefined) {
|
|
280
|
+
validateTariffGridFeeConfig(tariff.dynamicTariffData, tariff.priceComposition, 'dynamicTariffData');
|
|
281
|
+
}
|
|
282
|
+
if (tariff.bonuses !== undefined) {
|
|
283
|
+
validateTariffBonuses(tariff.bonuses);
|
|
284
|
+
}
|
|
285
|
+
}
|