@enyo-energy/energy-app-sdk 1.44.0 → 1.45.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.
Files changed (40) hide show
  1. package/README.md +141 -1
  2. package/dist/cjs/energy-app-package-definition.d.cts +46 -1
  3. package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.cjs +79 -1
  4. package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.d.cts +4 -2
  5. package/dist/cjs/implementations/subscriptions/define-subscription-plan.cjs +48 -0
  6. package/dist/cjs/implementations/subscriptions/define-subscription-plan.d.cts +40 -0
  7. package/dist/cjs/implementations/subscriptions/subscription-offer-validators.cjs +301 -0
  8. package/dist/cjs/implementations/subscriptions/subscription-offer-validators.d.cts +54 -0
  9. package/dist/cjs/implementations/subscriptions/subscription-plan-validators.cjs +8 -0
  10. package/dist/cjs/implementations/subscriptions/subscription-plan-validators.d.cts +8 -3
  11. package/dist/cjs/index.cjs +2 -0
  12. package/dist/cjs/index.d.cts +2 -0
  13. package/dist/cjs/types/enyo-authentication.d.cts +11 -0
  14. package/dist/cjs/types/enyo-heatpump-appliance.cjs +33 -1
  15. package/dist/cjs/types/enyo-heatpump-appliance.d.cts +86 -0
  16. package/dist/cjs/types/enyo-settings.d.cts +22 -1
  17. package/dist/cjs/types/enyo-subscription-offer.cjs +113 -0
  18. package/dist/cjs/types/enyo-subscription-offer.d.cts +358 -0
  19. package/dist/cjs/version.cjs +1 -1
  20. package/dist/cjs/version.d.cts +1 -1
  21. package/dist/energy-app-package-definition.d.ts +46 -1
  22. package/dist/implementations/heatpump/heatpump-metadata-validators.d.ts +4 -2
  23. package/dist/implementations/heatpump/heatpump-metadata-validators.js +80 -2
  24. package/dist/implementations/subscriptions/define-subscription-plan.d.ts +40 -0
  25. package/dist/implementations/subscriptions/define-subscription-plan.js +45 -0
  26. package/dist/implementations/subscriptions/subscription-offer-validators.d.ts +54 -0
  27. package/dist/implementations/subscriptions/subscription-offer-validators.js +298 -0
  28. package/dist/implementations/subscriptions/subscription-plan-validators.d.ts +8 -3
  29. package/dist/implementations/subscriptions/subscription-plan-validators.js +8 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +2 -0
  32. package/dist/types/enyo-authentication.d.ts +11 -0
  33. package/dist/types/enyo-heatpump-appliance.d.ts +86 -0
  34. package/dist/types/enyo-heatpump-appliance.js +32 -0
  35. package/dist/types/enyo-settings.d.ts +22 -1
  36. package/dist/types/enyo-subscription-offer.d.ts +358 -0
  37. package/dist/types/enyo-subscription-offer.js +110 -0
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/package.json +1 -1
@@ -0,0 +1,301 @@
1
+ "use strict";
2
+ /**
3
+ * Client-side validator for how a package promotes its subscription plans:
4
+ * the feature catalog ({@link EnergyAppPackageDefinition.subscriptionFeatures}),
5
+ * the free tier ({@link EnergyAppPackageDefinition.subscriptionFreeTier}), the
6
+ * offer screen blocks ({@link EnergyAppPackageDefinition.subscriptionOfferScreen})
7
+ * and the plans' `includedFeatures` and `highlight`.
8
+ *
9
+ * `validatePackageSubscriptionPlans()` runs these checks as part of the plan
10
+ * validation, so most callers never call {@link validatePackageSubscriptionOffer}
11
+ * directly.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.validatePackageSubscriptionOffer = validatePackageSubscriptionOffer;
15
+ const enyo_subscription_js_1 = require("../../types/enyo-subscription.cjs");
16
+ const enyo_subscription_offer_js_1 = require("../../types/enyo-subscription-offer.cjs");
17
+ const define_subscription_plan_js_1 = require("./define-subscription-plan.cjs");
18
+ /** A feature key — the same kebab-case slug shape as a plan key. */
19
+ const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
20
+ /** Languages every text on the offer screen must be translated into. */
21
+ const REQUIRED_LANGUAGES = ['de', 'en'];
22
+ /**
23
+ * Checks that a list of localized entries has no duplicate language, covers
24
+ * every required language, and fills the required fields in every entry.
25
+ *
26
+ * @param entries - The localized entries, e.g. a feature's `storeEntry`.
27
+ * @param at - Location prefix for messages.
28
+ * @param requiredFields - Fields that must be non-empty in every entry.
29
+ * @param errors - Collector for blocking errors.
30
+ */
31
+ function validateTranslations(entries, at, requiredFields, errors) {
32
+ const languages = new Set();
33
+ for (const [index, entry] of (entries ?? []).entries()) {
34
+ const entryAt = `${at}[${index}] (${entry.language || '?'})`;
35
+ if (languages.has(entry.language)) {
36
+ errors.push(`${entryAt}: duplicate language "${entry.language}".`);
37
+ }
38
+ languages.add(entry.language);
39
+ for (const field of requiredFields) {
40
+ const value = entry[field];
41
+ if (typeof value !== 'string' || !value.trim()) {
42
+ errors.push(`${entryAt}: ${field} is required.`);
43
+ }
44
+ }
45
+ }
46
+ for (const language of REQUIRED_LANGUAGES) {
47
+ if (!languages.has(language)) {
48
+ errors.push(`${at}: entry for language "${language}" is required.`);
49
+ }
50
+ }
51
+ }
52
+ /**
53
+ * Checks the included features of a plan or of the free tier: every entry
54
+ * names a declared feature, no feature is listed twice, and every value is
55
+ * translated.
56
+ *
57
+ * @param included - The included features, or `undefined` when none are declared.
58
+ * @param featureKeys - The keys of the feature catalog.
59
+ * @param at - Location prefix for messages.
60
+ * @param errors - Collector for blocking errors.
61
+ */
62
+ function validateIncludedFeatures(included, featureKeys, at, errors) {
63
+ const seen = new Set();
64
+ for (const [index, feature] of (included ?? []).entries()) {
65
+ const key = (0, define_subscription_plan_js_1.getIncludedFeatureKey)(feature);
66
+ const featureAt = `${at}.includedFeatures[${index}] (${key || '?'})`;
67
+ if (!featureKeys.has(key)) {
68
+ errors.push(`${featureAt}: "${key}" is not a feature of subscriptionFeatures.`);
69
+ }
70
+ else if (seen.has(key)) {
71
+ errors.push(`${featureAt}: feature "${key}" is listed twice.`);
72
+ }
73
+ seen.add(key);
74
+ if (typeof feature !== 'string') {
75
+ validateTranslations(feature.value, `${featureAt}.value`, ['value'], errors);
76
+ }
77
+ }
78
+ }
79
+ /**
80
+ * Checks that every key of a block's key list is declared.
81
+ *
82
+ * @param keys - The keys the block refers to, or `undefined` for "all".
83
+ * @param known - The declared keys.
84
+ * @param kind - `'feature'` or `'plan'`, used in messages.
85
+ * @param at - Location prefix for messages.
86
+ * @param errors - Collector for blocking errors.
87
+ */
88
+ function validateBlockKeys(keys, known, kind, at, errors) {
89
+ if (keys === undefined)
90
+ return;
91
+ if (keys.length === 0) {
92
+ errors.push(`${at}: ${kind}Keys must not be empty — omit it to use every ${kind}.`);
93
+ }
94
+ const seen = new Set();
95
+ for (const key of keys) {
96
+ if (!known.has(key))
97
+ errors.push(`${at}: "${key}" is not a declared ${kind}.`);
98
+ else if (seen.has(key))
99
+ errors.push(`${at}: ${kind} "${key}" is listed twice.`);
100
+ seen.add(key);
101
+ }
102
+ }
103
+ /**
104
+ * Checks a block's optional heading and text: when set, they must be
105
+ * translated into every required language.
106
+ *
107
+ * @param storeEntry - The block's localized texts.
108
+ * @param at - Location prefix for messages.
109
+ * @param requiredFields - Fields that must be non-empty in every entry.
110
+ * @param errors - Collector for blocking errors.
111
+ */
112
+ function validateBlockTexts(storeEntry, at, requiredFields, errors) {
113
+ validateTranslations(storeEntry, `${at}.storeEntry`, requiredFields, errors);
114
+ for (const [index, entry] of (storeEntry ?? []).entries()) {
115
+ if (!entry.title?.trim() && !entry.text?.trim()) {
116
+ errors.push(`${at}.storeEntry[${index}] (${entry.language || '?'}): title or text is required.`);
117
+ }
118
+ }
119
+ }
120
+ /**
121
+ * Checks an optional icon against {@link EnyoSubscriptionOfferIconEnum}.
122
+ *
123
+ * @param icon - The icon, or `undefined`.
124
+ * @param at - Location prefix for messages.
125
+ * @param errors - Collector for blocking errors.
126
+ */
127
+ function validateIcon(icon, at, errors) {
128
+ if (icon !== undefined && !Object.values(enyo_subscription_offer_js_1.EnyoSubscriptionOfferIconEnum).includes(icon)) {
129
+ errors.push(`${at}: icon "${icon}" is not supported — use one of EnyoSubscriptionOfferIconEnum.`);
130
+ }
131
+ }
132
+ /**
133
+ * Validates how a package promotes its plans:
134
+ *
135
+ * - **Features**: keys are unique kebab-case slugs, icons are known, and every
136
+ * feature has a German and English title.
137
+ * - **Plans**: `includedFeatures` name declared features, at most once each,
138
+ * with translated values; `highlight` is known and set on at most one plan.
139
+ * - **Free tier**: only with `subscriptionMode: 'optional'`, translated, and
140
+ * its `includedFeatures` follow the plan rules.
141
+ * - **Offer screen**: at least one block, every block type is known, texts are
142
+ * translated (a `hero` needs a title, a `hint` a known kind and a text),
143
+ * referenced features and plans exist, a comparison has at least two
144
+ * columns and a free column only with a free tier, and there is at most one
145
+ * `plans` block.
146
+ *
147
+ * Warnings flag features no tier includes, comparison rows no column
148
+ * includes, and offer declarations without plans.
149
+ *
150
+ * @param definition - The package definition, or just the fields listed in
151
+ * {@link PackageSubscriptionOfferSource}.
152
+ * @returns The {@link PackageSubscriptionOfferValidationResult}.
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * const {ok, errors} = validatePackageSubscriptionOffer(packageDef);
157
+ * if (!ok) throw new Error(errors.join('\n'));
158
+ * ```
159
+ */
160
+ function validatePackageSubscriptionOffer(definition) {
161
+ const errors = [];
162
+ const warnings = [];
163
+ const plans = definition.subscriptionPlans ?? [];
164
+ const features = definition.subscriptionFeatures ?? [];
165
+ const freeTier = definition.subscriptionFreeTier;
166
+ const screen = definition.subscriptionOfferScreen;
167
+ if (!plans.length) {
168
+ if (definition.subscriptionFeatures || freeTier || screen) {
169
+ warnings.push('subscriptionFeatures, subscriptionFreeTier or subscriptionOfferScreen is set but no subscriptionPlans are declared — it has no effect.');
170
+ }
171
+ return { ok: true, errors, warnings };
172
+ }
173
+ const featureKeys = new Set();
174
+ for (const [index, feature] of features.entries()) {
175
+ const at = `subscriptionFeatures[${index}] (${feature.featureKey || '?'})`;
176
+ if (!feature.featureKey?.trim()) {
177
+ errors.push(`${at}: featureKey is required.`);
178
+ }
179
+ else if (!SLUG_RE.test(feature.featureKey)) {
180
+ errors.push(`${at}: featureKey must be a kebab-case slug, e.g. "price-optimisation".`);
181
+ }
182
+ else if (featureKeys.has(feature.featureKey)) {
183
+ errors.push(`${at}: duplicate featureKey "${feature.featureKey}" — feature keys must be unique within the package.`);
184
+ }
185
+ featureKeys.add(feature.featureKey);
186
+ validateIcon(feature.icon, at, errors);
187
+ validateTranslations(feature.storeEntry, `${at}.storeEntry`, ['title'], errors);
188
+ }
189
+ const includedSomewhere = new Set();
190
+ const highlighted = [];
191
+ for (const [index, plan] of plans.entries()) {
192
+ const at = `subscriptionPlans[${index}] (${plan.planKey || '?'})`;
193
+ validateIncludedFeatures(plan.includedFeatures, featureKeys, at, errors);
194
+ plan.includedFeatures?.forEach(feature => includedSomewhere.add((0, define_subscription_plan_js_1.getIncludedFeatureKey)(feature)));
195
+ if (plan.highlight !== undefined) {
196
+ if (!Object.values(enyo_subscription_offer_js_1.EnyoSubscriptionPlanHighlightEnum).includes(plan.highlight)) {
197
+ errors.push(`${at}: highlight "${plan.highlight}" is not supported.`);
198
+ }
199
+ highlighted.push(plan.planKey);
200
+ }
201
+ }
202
+ if (highlighted.length > 1) {
203
+ errors.push(`Only one plan may be highlighted, but ${highlighted.map(key => `"${key}"`).join(', ')} are.`);
204
+ }
205
+ if (freeTier) {
206
+ if (definition.subscriptionMode === enyo_subscription_js_1.EnyoSubscriptionModeEnum.Required) {
207
+ errors.push('subscriptionFreeTier is not allowed with subscriptionMode "required" — the app has no free base.');
208
+ }
209
+ validateTranslations(freeTier.storeEntry, 'subscriptionFreeTier.storeEntry', ['title'], errors);
210
+ validateIncludedFeatures(freeTier.includedFeatures, featureKeys, 'subscriptionFreeTier', errors);
211
+ freeTier.includedFeatures?.forEach(feature => includedSomewhere.add((0, define_subscription_plan_js_1.getIncludedFeatureKey)(feature)));
212
+ }
213
+ for (const feature of features) {
214
+ if (feature.featureKey && !includedSomewhere.has(feature.featureKey)) {
215
+ warnings.push(`Feature "${feature.featureKey}" is not included in any plan or the free tier.`);
216
+ }
217
+ }
218
+ if (screen) {
219
+ validateOfferScreenBlocks(screen.blocks, definition, featureKeys, errors, warnings);
220
+ }
221
+ return { ok: errors.length === 0, errors, warnings };
222
+ }
223
+ /**
224
+ * Checks the blocks of the offer screen.
225
+ *
226
+ * @param blocks - The declared blocks.
227
+ * @param definition - The package definition, for plans and the free tier.
228
+ * @param featureKeys - The keys of the feature catalog.
229
+ * @param errors - Collector for blocking errors.
230
+ * @param warnings - Collector for advisory problems.
231
+ */
232
+ function validateOfferScreenBlocks(blocks, definition, featureKeys, errors, warnings) {
233
+ if (!Array.isArray(blocks) || blocks.length === 0) {
234
+ errors.push('subscriptionOfferScreen.blocks must contain at least one block — omit subscriptionOfferScreen to show only the plans.');
235
+ return;
236
+ }
237
+ const plans = definition.subscriptionPlans ?? [];
238
+ const planKeys = new Set(plans.map(plan => plan.planKey));
239
+ let plansBlocks = 0;
240
+ for (const [index, block] of blocks.entries()) {
241
+ const at = `subscriptionOfferScreen.blocks[${index}] (${block?.type || '?'})`;
242
+ switch (block?.type) {
243
+ case enyo_subscription_offer_js_1.EnyoSubscriptionOfferBlockTypeEnum.Hero:
244
+ validateIcon(block.icon, at, errors);
245
+ validateBlockTexts(block.storeEntry, at, ['title'], errors);
246
+ break;
247
+ case enyo_subscription_offer_js_1.EnyoSubscriptionOfferBlockTypeEnum.Features:
248
+ if (featureKeys.size === 0) {
249
+ errors.push(`${at}: a features block needs subscriptionFeatures to list.`);
250
+ }
251
+ validateBlockKeys(block.featureKeys, featureKeys, 'feature', at, errors);
252
+ if (block.storeEntry)
253
+ validateBlockTexts(block.storeEntry, at, [], errors);
254
+ break;
255
+ case enyo_subscription_offer_js_1.EnyoSubscriptionOfferBlockTypeEnum.Comparison: {
256
+ if (featureKeys.size === 0) {
257
+ errors.push(`${at}: a comparison block needs subscriptionFeatures as its rows.`);
258
+ }
259
+ validateBlockKeys(block.featureKeys, featureKeys, 'feature', at, errors);
260
+ validateBlockKeys(block.planKeys, planKeys, 'plan', at, errors);
261
+ const freeColumn = block.includeFreeTier ?? definition.subscriptionFreeTier !== undefined;
262
+ if (freeColumn && !definition.subscriptionFreeTier) {
263
+ errors.push(`${at}: includeFreeTier needs subscriptionFreeTier to fill the free column.`);
264
+ }
265
+ const columnPlans = plans.filter(plan => block.planKeys?.includes(plan.planKey) ?? true);
266
+ if (columnPlans.length + (freeColumn ? 1 : 0) < 2) {
267
+ errors.push(`${at}: a comparison needs at least two columns — add the free tier or another plan.`);
268
+ }
269
+ const included = new Set([
270
+ ...(freeColumn ? definition.subscriptionFreeTier?.includedFeatures ?? [] : []),
271
+ ...columnPlans.flatMap(plan => plan.includedFeatures ?? []),
272
+ ].map(define_subscription_plan_js_1.getIncludedFeatureKey));
273
+ for (const key of block.featureKeys ?? featureKeys) {
274
+ if (featureKeys.has(key) && !included.has(key)) {
275
+ warnings.push(`${at}: feature "${key}" is not included in any compared column — its row stays empty.`);
276
+ }
277
+ }
278
+ if (block.storeEntry)
279
+ validateBlockTexts(block.storeEntry, at, [], errors);
280
+ break;
281
+ }
282
+ case enyo_subscription_offer_js_1.EnyoSubscriptionOfferBlockTypeEnum.Hint:
283
+ if (!Object.values(enyo_subscription_offer_js_1.EnyoSubscriptionOfferHintKindEnum).includes(block.kind)) {
284
+ errors.push(`${at}: kind "${block.kind}" is not supported.`);
285
+ }
286
+ validateIcon(block.icon, at, errors);
287
+ validateBlockTexts(block.storeEntry, at, ['text'], errors);
288
+ break;
289
+ case enyo_subscription_offer_js_1.EnyoSubscriptionOfferBlockTypeEnum.Plans:
290
+ plansBlocks++;
291
+ if (block.storeEntry)
292
+ validateBlockTexts(block.storeEntry, at, [], errors);
293
+ break;
294
+ default:
295
+ errors.push(`${at}: block type "${block?.type}" is not supported.`);
296
+ }
297
+ }
298
+ if (plansBlocks > 1) {
299
+ errors.push('subscriptionOfferScreen.blocks may contain at most one "plans" block.');
300
+ }
301
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Client-side validator for how a package promotes its subscription plans:
3
+ * the feature catalog ({@link EnergyAppPackageDefinition.subscriptionFeatures}),
4
+ * the free tier ({@link EnergyAppPackageDefinition.subscriptionFreeTier}), the
5
+ * offer screen blocks ({@link EnergyAppPackageDefinition.subscriptionOfferScreen})
6
+ * and the plans' `includedFeatures` and `highlight`.
7
+ *
8
+ * `validatePackageSubscriptionPlans()` runs these checks as part of the plan
9
+ * validation, so most callers never call {@link validatePackageSubscriptionOffer}
10
+ * directly.
11
+ */
12
+ import type { EnergyAppPackageDefinition } from '../../energy-app-package-definition.cjs';
13
+ /** The outcome of validating how a package promotes its plans. */
14
+ export interface PackageSubscriptionOfferValidationResult {
15
+ /** True when there are no blocking `errors` (warnings are still allowed). */
16
+ ok: boolean;
17
+ /** Blocking problems — these must be fixed before releasing. */
18
+ errors: string[];
19
+ /** Advisory problems — allowed, but usually worth reviewing. */
20
+ warnings: string[];
21
+ }
22
+ /**
23
+ * The parts of a package definition the offer validator reads.
24
+ */
25
+ export type PackageSubscriptionOfferSource = Pick<EnergyAppPackageDefinition, 'subscriptionPlans' | 'subscriptionMode' | 'subscriptionFeatures' | 'subscriptionFreeTier' | 'subscriptionOfferScreen'>;
26
+ /**
27
+ * Validates how a package promotes its plans:
28
+ *
29
+ * - **Features**: keys are unique kebab-case slugs, icons are known, and every
30
+ * feature has a German and English title.
31
+ * - **Plans**: `includedFeatures` name declared features, at most once each,
32
+ * with translated values; `highlight` is known and set on at most one plan.
33
+ * - **Free tier**: only with `subscriptionMode: 'optional'`, translated, and
34
+ * its `includedFeatures` follow the plan rules.
35
+ * - **Offer screen**: at least one block, every block type is known, texts are
36
+ * translated (a `hero` needs a title, a `hint` a known kind and a text),
37
+ * referenced features and plans exist, a comparison has at least two
38
+ * columns and a free column only with a free tier, and there is at most one
39
+ * `plans` block.
40
+ *
41
+ * Warnings flag features no tier includes, comparison rows no column
42
+ * includes, and offer declarations without plans.
43
+ *
44
+ * @param definition - The package definition, or just the fields listed in
45
+ * {@link PackageSubscriptionOfferSource}.
46
+ * @returns The {@link PackageSubscriptionOfferValidationResult}.
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * const {ok, errors} = validatePackageSubscriptionOffer(packageDef);
51
+ * if (!ok) throw new Error(errors.join('\n'));
52
+ * ```
53
+ */
54
+ export declare function validatePackageSubscriptionOffer(definition: PackageSubscriptionOfferSource): PackageSubscriptionOfferValidationResult;
@@ -22,6 +22,7 @@ const energy_app_permission_type_js_1 = require("../../energy-app-permission.typ
22
22
  const enyo_energy_app_sdk_js_1 = require("../../enyo-energy-app-sdk.cjs");
23
23
  const enyo_currency_js_1 = require("../../types/enyo-currency.cjs");
24
24
  const enyo_subscription_js_1 = require("../../types/enyo-subscription.cjs");
25
+ const subscription_offer_validators_js_1 = require("./subscription-offer-validators.cjs");
25
26
  /**
26
27
  * Thrown by {@link assertValidPackageSubscriptionPlans} when a package's
27
28
  * subscription plans fail validation. The message lists every blocking error
@@ -128,6 +129,10 @@ function validateStoreEntries(plan, at, errors) {
128
129
  *
129
130
  * A `subscriptionMode` without plans is a warning: it has no effect.
130
131
  *
132
+ * Also runs `validatePackageSubscriptionOffer()` on the feature catalog, the
133
+ * free tier, the offer screen and the plans' `includedFeatures` and
134
+ * `highlight`, and merges its errors and warnings into the result.
135
+ *
131
136
  * @param definition - The package definition, or just its `subscriptionPlans`,
132
137
  * `subscriptionMode` and `permissions`. A package without plans is valid.
133
138
  * @returns The {@link PackageSubscriptionPlansValidationResult}.
@@ -143,6 +148,8 @@ function validatePackageSubscriptionPlans(definition) {
143
148
  const warnings = [];
144
149
  const plans = definition.subscriptionPlans;
145
150
  const mode = definition.subscriptionMode;
151
+ const offer = (0, subscription_offer_validators_js_1.validatePackageSubscriptionOffer)(definition);
152
+ warnings.push(...offer.warnings);
146
153
  if (!plans?.length) {
147
154
  if (mode !== undefined) {
148
155
  warnings.push('subscriptionMode is set but no subscriptionPlans are declared — it has no effect.');
@@ -192,6 +199,7 @@ function validatePackageSubscriptionPlans(definition) {
192
199
  }
193
200
  validateStoreEntries(plan, at, errors);
194
201
  }
202
+ errors.push(...offer.errors);
195
203
  return { ok: errors.length === 0, errors, warnings };
196
204
  }
197
205
  /**
@@ -37,10 +37,11 @@ export interface PackageSubscriptionPlansValidationResult {
37
37
  }
38
38
  /**
39
39
  * The parts of a package definition the validator reads: the plans, the
40
- * subscription mode, and the permissions to check that the app can read its
41
- * subscriptions.
40
+ * subscription mode, the permissions to check that the app can read its
41
+ * subscriptions, and optionally how the plans are promoted (features, free
42
+ * tier and offer screen).
42
43
  */
43
- export type PackageSubscriptionPlansSource = Pick<EnergyAppPackageDefinition, 'subscriptionPlans' | 'permissions' | 'subscriptionMode'>;
44
+ export type PackageSubscriptionPlansSource = Pick<EnergyAppPackageDefinition, 'subscriptionPlans' | 'permissions' | 'subscriptionMode'> & Partial<Pick<EnergyAppPackageDefinition, 'subscriptionFeatures' | 'subscriptionFreeTier' | 'subscriptionOfferScreen'>>;
44
45
  /**
45
46
  * Structural validation of a package's subscription plans: plan keys are
46
47
  * unique kebab-case slugs, the payment interval is known, every plan has at
@@ -52,6 +53,10 @@ export type PackageSubscriptionPlansSource = Pick<EnergyAppPackageDefinition, 's
52
53
  *
53
54
  * A `subscriptionMode` without plans is a warning: it has no effect.
54
55
  *
56
+ * Also runs `validatePackageSubscriptionOffer()` on the feature catalog, the
57
+ * free tier, the offer screen and the plans' `includedFeatures` and
58
+ * `highlight`, and merges its errors and warnings into the result.
59
+ *
55
60
  * @param definition - The package definition, or just its `subscriptionPlans`,
56
61
  * `subscriptionMode` and `permissions`. A package without plans is valid.
57
62
  * @returns The {@link PackageSubscriptionPlansValidationResult}.
@@ -193,6 +193,7 @@ __exportStar(require("./packages/energy-app-cascade.cjs"), exports);
193
193
  __exportStar(require("./types/enyo-webhook.cjs"), exports);
194
194
  __exportStar(require("./packages/energy-app-webhook.cjs"), exports);
195
195
  __exportStar(require("./types/enyo-subscription.cjs"), exports);
196
+ __exportStar(require("./types/enyo-subscription-offer.cjs"), exports);
196
197
  __exportStar(require("./packages/energy-app-subscriptions.cjs"), exports);
197
198
  __exportStar(require("./types/enyo-room.cjs"), exports);
198
199
  __exportStar(require("./packages/energy-app-room.cjs"), exports);
@@ -200,6 +201,7 @@ __exportStar(require("./types/enyo-room-temperature.cjs"), exports);
200
201
  __exportStar(require("./packages/energy-app-room-temperatures.cjs"), exports);
201
202
  __exportStar(require("./implementations/subscriptions/define-subscription-plan.cjs"), exports);
202
203
  __exportStar(require("./implementations/subscriptions/subscription-plan-validators.cjs"), exports);
204
+ __exportStar(require("./implementations/subscriptions/subscription-offer-validators.cjs"), exports);
203
205
  __exportStar(require("./implementations/subscriptions/in-memory-subscriptions.cjs"), exports);
204
206
  __exportStar(require("./implementations/pricing/price-schedule-resolver.cjs"), exports);
205
207
  __exportStar(require("./implementations/pricing/price-component-validators.cjs"), exports);
@@ -177,6 +177,7 @@ export * from './packages/energy-app-cascade.cjs';
177
177
  export * from './types/enyo-webhook.cjs';
178
178
  export * from './packages/energy-app-webhook.cjs';
179
179
  export * from './types/enyo-subscription.cjs';
180
+ export * from './types/enyo-subscription-offer.cjs';
180
181
  export * from './packages/energy-app-subscriptions.cjs';
181
182
  export * from './types/enyo-room.cjs';
182
183
  export * from './packages/energy-app-room.cjs';
@@ -184,6 +185,7 @@ export * from './types/enyo-room-temperature.cjs';
184
185
  export * from './packages/energy-app-room-temperatures.cjs';
185
186
  export * from './implementations/subscriptions/define-subscription-plan.cjs';
186
187
  export * from './implementations/subscriptions/subscription-plan-validators.cjs';
188
+ export * from './implementations/subscriptions/subscription-offer-validators.cjs';
187
189
  export * from './implementations/subscriptions/in-memory-subscriptions.cjs';
188
190
  export * from './implementations/pricing/price-schedule-resolver.cjs';
189
191
  export * from './implementations/pricing/price-component-validators.cjs';
@@ -174,6 +174,17 @@ export interface EnyoAuthentication {
174
174
  * presented before or after the onboarding guide. When omitted, the host decides the default.
175
175
  */
176
176
  onboardingOrder?: EnyoAuthenticationOnboardingOrder;
177
+ /**
178
+ * Forces the host to show this authentication step even when the package is already in
179
+ * {@link EnyoAuthenticationStateEnum.Authenticated} state.
180
+ *
181
+ * By default the host skips the authentication step if the package reports it is already
182
+ * authenticated. Set this to `true` when the user must authenticate again regardless of the
183
+ * current state — for example to re-link an account, grant additional scopes, or authenticate
184
+ * a newly added appliance against the same vendor account. When omitted or `false`, the
185
+ * host keeps its default behaviour and may skip the step.
186
+ */
187
+ authenticationRequired?: boolean;
177
188
  }
178
189
  export interface EnyoApiKeyAuthenticationResponse {
179
190
  apiKey: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HEATPUMP_CURVE_OPTIMIZATION_DEFAULT_TARGET_C = exports.HEATPUMP_CURVE_OPTIMIZATION_MAX_LOWERING_K = exports.EnyoHeatpumpHeatingControlReferenceEnum = exports.EnyoHeatpumpRoomTemperatureSourceEnum = exports.EnyoHeatpumpProgramModeEnum = exports.EnyoHeatpumpTimeProgramLevelEnum = exports.EnyoHeatpumpApplianceHeatingCircuitTypeEnum = exports.EnyoHeatpumpApplianceStorageTypeEnum = exports.EnyoHeatpumpApplianceHeatingRodUsageEnum = exports.EnyoHeatpumpApplianceAdditionalDeviceEnum = exports.EnyoHeatpumpApplianceConnectionTypeEnum = exports.EnyoHeatpumpApplianceModeEnum = exports.EnyoHeatpumpApplianceAvailableFeaturesEnum = void 0;
3
+ exports.HEATPUMP_CURVE_OPTIMIZATION_DEFAULT_TARGET_C = exports.HEATPUMP_CURVE_OPTIMIZATION_MAX_LOWERING_K = exports.EnyoHeatpumpHeatingControlReferenceEnum = exports.EnyoHeatpumpRoomTemperatureSourceEnum = exports.EnyoHeatpumpProgramModeEnum = exports.EnyoHeatpumpTimeProgramLevelEnum = exports.EnyoHeatpumpApplianceHeatingCircuitTypeEnum = exports.EnyoHeatpumpApplianceStorageTypeEnum = exports.EnyoHeatpumpApplianceHeatingRodUsageEnum = exports.EnyoHeatpumpApplianceAdditionalDeviceEnum = exports.EnyoHeatpumpSgReadyStateEnum = exports.EnyoHeatpumpSgReadyFunctionEnum = exports.EnyoHeatpumpApplianceConnectionTypeEnum = exports.EnyoHeatpumpApplianceModeEnum = exports.EnyoHeatpumpApplianceAvailableFeaturesEnum = void 0;
4
4
  var EnyoHeatpumpApplianceAvailableFeaturesEnum;
5
5
  (function (EnyoHeatpumpApplianceAvailableFeaturesEnum) {
6
6
  /** If the heatpump is capable of domestic hot water*/
@@ -156,6 +156,38 @@ var EnyoHeatpumpApplianceConnectionTypeEnum;
156
156
  /** Connected via a vendor or local API (e.g. REST, Modbus, EEBus) */
157
157
  EnyoHeatpumpApplianceConnectionTypeEnum["Api"] = "api";
158
158
  })(EnyoHeatpumpApplianceConnectionTypeEnum || (exports.EnyoHeatpumpApplianceConnectionTypeEnum = EnyoHeatpumpApplianceConnectionTypeEnum = {}));
159
+ /**
160
+ * What an SG Ready relay of a heatpump installation acts on
161
+ * ({@link EnyoHeatpumpApplianceSgReadyRelay.function}).
162
+ *
163
+ * Installations often wire more than one SG Ready input — e.g. one for space
164
+ * heating and one for domestic hot water — so the energy manager can raise one
165
+ * without the other.
166
+ */
167
+ var EnyoHeatpumpSgReadyFunctionEnum;
168
+ (function (EnyoHeatpumpSgReadyFunctionEnum) {
169
+ /** Heats domestic hot water with the electric heating rod of the DHW tank. */
170
+ EnyoHeatpumpSgReadyFunctionEnum["DhwHeatingRod"] = "dhw-heating-rod";
171
+ /** Heats domestic hot water with the heatpump's compressor. */
172
+ EnyoHeatpumpSgReadyFunctionEnum["DhwHeatpump"] = "dhw-heatpump";
173
+ /** Space heating of a heating circuit. */
174
+ EnyoHeatpumpSgReadyFunctionEnum["Heating"] = "heating";
175
+ })(EnyoHeatpumpSgReadyFunctionEnum || (exports.EnyoHeatpumpSgReadyFunctionEnum = EnyoHeatpumpSgReadyFunctionEnum = {}));
176
+ /**
177
+ * The four operating states of an SG Ready input, numbered 1–4 by the SG Ready
178
+ * label of the Bundesverband Wärmepumpe.
179
+ */
180
+ var EnyoHeatpumpSgReadyStateEnum;
181
+ (function (EnyoHeatpumpSgReadyStateEnum) {
182
+ /** State 1 (contacts 1:0): operation blocked, e.g. by the grid operator. */
183
+ EnyoHeatpumpSgReadyStateEnum["Blocked"] = "blocked";
184
+ /** State 2 (contacts 0:0): normal operation; the heatpump follows its own control. */
185
+ EnyoHeatpumpSgReadyStateEnum["Normal"] = "normal";
186
+ /** State 3 (contacts 0:1): switch-on recommended; the heatpump raises its setpoints. */
187
+ EnyoHeatpumpSgReadyStateEnum["Recommended"] = "recommended";
188
+ /** State 4 (contacts 1:1): switch-on forced; the heatpump runs at its maximum setpoints. */
189
+ EnyoHeatpumpSgReadyStateEnum["Forced"] = "forced";
190
+ })(EnyoHeatpumpSgReadyStateEnum || (exports.EnyoHeatpumpSgReadyStateEnum = EnyoHeatpumpSgReadyStateEnum = {}));
159
191
  /**
160
192
  * Additional heating devices that can be attached to / combined with a
161
193
  * heatpump installation.
@@ -150,6 +150,82 @@ export declare enum EnyoHeatpumpApplianceConnectionTypeEnum {
150
150
  /** Connected via a vendor or local API (e.g. REST, Modbus, EEBus) */
151
151
  Api = "api"
152
152
  }
153
+ /**
154
+ * What an SG Ready relay of a heatpump installation acts on
155
+ * ({@link EnyoHeatpumpApplianceSgReadyRelay.function}).
156
+ *
157
+ * Installations often wire more than one SG Ready input — e.g. one for space
158
+ * heating and one for domestic hot water — so the energy manager can raise one
159
+ * without the other.
160
+ */
161
+ export declare enum EnyoHeatpumpSgReadyFunctionEnum {
162
+ /** Heats domestic hot water with the electric heating rod of the DHW tank. */
163
+ DhwHeatingRod = "dhw-heating-rod",
164
+ /** Heats domestic hot water with the heatpump's compressor. */
165
+ DhwHeatpump = "dhw-heatpump",
166
+ /** Space heating of a heating circuit. */
167
+ Heating = "heating"
168
+ }
169
+ /**
170
+ * The four operating states of an SG Ready input, numbered 1–4 by the SG Ready
171
+ * label of the Bundesverband Wärmepumpe.
172
+ */
173
+ export declare enum EnyoHeatpumpSgReadyStateEnum {
174
+ /** State 1 (contacts 1:0): operation blocked, e.g. by the grid operator. */
175
+ Blocked = "blocked",
176
+ /** State 2 (contacts 0:0): normal operation; the heatpump follows its own control. */
177
+ Normal = "normal",
178
+ /** State 3 (contacts 0:1): switch-on recommended; the heatpump raises its setpoints. */
179
+ Recommended = "recommended",
180
+ /** State 4 (contacts 1:1): switch-on forced; the heatpump runs at its maximum setpoints. */
181
+ Forced = "forced"
182
+ }
183
+ /**
184
+ * One SG Ready input of a heatpump installation — the two relay contacts that
185
+ * switch the four {@link EnyoHeatpumpSgReadyStateEnum | SG Ready states} — and
186
+ * what it acts on. Declared in {@link EnyoHeatpumpApplianceMetadata.sgReadyRelays}.
187
+ *
188
+ * An input wired with only one contact can switch only two states; declare
189
+ * them in {@link supportedStates}, e.g. `[Normal, Recommended]`.
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * sgReadyRelays: [
194
+ * {index: 0, function: EnyoHeatpumpSgReadyFunctionEnum.Heating, zoneIndex: 0, name: 'K1/K2'},
195
+ * {index: 1, function: EnyoHeatpumpSgReadyFunctionEnum.DhwHeatpump, zoneIndex: 0, name: 'K3/K4'},
196
+ * {
197
+ * index: 2,
198
+ * function: EnyoHeatpumpSgReadyFunctionEnum.DhwHeatingRod,
199
+ * supportedStates: [EnyoHeatpumpSgReadyStateEnum.Normal, EnyoHeatpumpSgReadyStateEnum.Forced]
200
+ * }
201
+ * ]
202
+ * ```
203
+ */
204
+ export interface EnyoHeatpumpApplianceSgReadyRelay {
205
+ /**
206
+ * Zero-based index of this relay, unique within the heatpump. Stable for as
207
+ * long as the wiring stays the same, so commands and readings can refer to
208
+ * the relay by it.
209
+ */
210
+ index: number;
211
+ /** What the relay acts on. */
212
+ function: EnyoHeatpumpSgReadyFunctionEnum;
213
+ /**
214
+ * The zone the relay acts on: the {@link EnyoHeatpumpApplianceHeatingCircuit.index}
215
+ * for {@link EnyoHeatpumpSgReadyFunctionEnum.Heating}, the
216
+ * {@link EnyoHeatpumpApplianceDomesticHotWater.index} for the DHW
217
+ * functions. Omit for the first (or only) zone.
218
+ */
219
+ zoneIndex?: number;
220
+ /** Optional label for installers and the UI, e.g. the terminal names `'K1/K2'`. */
221
+ name?: string;
222
+ /**
223
+ * The SG Ready states this relay can switch. Omit when all four are wired.
224
+ */
225
+ supportedStates?: EnyoHeatpumpSgReadyStateEnum[];
226
+ /** The state the relay is currently in, when known. Must be one of {@link supportedStates}. */
227
+ activeState?: EnyoHeatpumpSgReadyStateEnum;
228
+ }
153
229
  /**
154
230
  * Additional heating devices that can be attached to / combined with a
155
231
  * heatpump installation.
@@ -810,6 +886,16 @@ export interface EnyoHeatpumpApplianceMetadata {
810
886
  * states, while an API connection typically allows direct read/write.
811
887
  */
812
888
  connectionType?: EnyoHeatpumpApplianceConnectionTypeEnum;
889
+ /**
890
+ * The SG Ready relays of the installation, 0 to n, each with the function
891
+ * it acts on (space heating, DHW by heatpump, DHW by heating rod). Omit or
892
+ * leave empty when no SG Ready input is wired.
893
+ *
894
+ * Independent of {@link connectionType}: an installation read through an
895
+ * API can still have SG Ready relays wired for control. Validate with
896
+ * `validateHeatpumpAdvancedMetadata()`.
897
+ */
898
+ sgReadyRelays?: EnyoHeatpumpApplianceSgReadyRelay[];
813
899
  /**
814
900
  * Whether the energy manager is allowed to actively control (steer) this
815
901
  * heatpump. When `false`, the heatpump is treated as read-only/monitor-only
@@ -83,6 +83,25 @@ export interface EnyoPackageConfigurationSettingIntegerOptions {
83
83
  export interface EnyoPackageConfigurationSettingTextOptions {
84
84
  maxLength?: number;
85
85
  }
86
+ /**
87
+ * Configuration options for action-type settings.
88
+ *
89
+ * An action setting renders as a button instead of an input, for a one-off command (restart a
90
+ * device, start a self-test). Anything with an on/off state belongs in a `checkbox` instead.
91
+ * It holds no meaningful value of its own: every tap writes the current time as an ISO 8601
92
+ * string (e.g. `"2026-10-29T10:15:00.000Z"`) to the setting, so each tap reaches
93
+ * {@link EnyoSettingsChangeListener} as a change event with a new value — even a second tap
94
+ * of the same button. Packages react to the event, not to the stored value.
95
+ */
96
+ export interface EnyoPackageConfigurationSettingActionOptions {
97
+ /** Translated caption of the button (e.g. "Restart now") */
98
+ buttonCaption: EnyoPackageConfigurationTranslatedValue[];
99
+ /**
100
+ * Optional translated question the user has to confirm before the action is triggered.
101
+ * Omit it to trigger the action on the first tap.
102
+ */
103
+ confirmationText?: EnyoPackageConfigurationTranslatedValue[];
104
+ }
86
105
  /**
87
106
  * Represents a single configuration setting for an Energy App package.
88
107
  */
@@ -90,7 +109,7 @@ export interface EnyoPackageConfigurationSetting {
90
109
  /** internal name of the setting - must be unique */
91
110
  name: string;
92
111
  /** the type of the setting */
93
- type: 'text' | 'select' | 'float' | 'integer' | 'checkbox' | 'time' | 'priority';
112
+ type: 'text' | 'select' | 'float' | 'integer' | 'checkbox' | 'time' | 'priority' | 'action';
94
113
  /** if the setting is required */
95
114
  required: boolean;
96
115
  /** The displayed name of the field */
@@ -111,6 +130,8 @@ export interface EnyoPackageConfigurationSetting {
111
130
  timeOptions?: EnyoPackageConfigurationSettingTimeOptions;
112
131
  /** Optional configuration for priority-type settings, defining the available priority options with order indices */
113
132
  priorityOptions?: EnyoPackageConfigurationSettingPriorityOption[];
133
+ /** Configuration for action-type settings (a button). Required when `type` is `'action'`. */
134
+ actionOptions?: EnyoPackageConfigurationSettingActionOptions;
114
135
  }
115
136
  /**
116
137
  * Represents a change event when a setting value is modified.