@enyo-energy/energy-app-sdk 1.36.0 → 1.37.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 +61 -2
- package/dist/cjs/energy-app-package-definition.d.cts +10 -2
- package/dist/cjs/implementations/subscriptions/define-subscription-plan.cjs +39 -1
- package/dist/cjs/implementations/subscriptions/define-subscription-plan.d.cts +32 -2
- package/dist/cjs/implementations/subscriptions/in-memory-subscriptions.cjs +46 -6
- package/dist/cjs/implementations/subscriptions/in-memory-subscriptions.d.cts +28 -3
- package/dist/cjs/implementations/subscriptions/subscription-plan-validators.cjs +50 -0
- package/dist/cjs/implementations/subscriptions/subscription-plan-validators.d.cts +25 -0
- package/dist/cjs/packages/energy-app-subscriptions.d.cts +55 -3
- package/dist/cjs/packages/energy-app-webhook.d.cts +21 -2
- package/dist/cjs/types/enyo-subscription.cjs +30 -1
- package/dist/cjs/types/enyo-subscription.d.cts +107 -4
- package/dist/cjs/types/enyo-webhook.d.cts +27 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/energy-app-package-definition.d.ts +10 -2
- package/dist/implementations/subscriptions/define-subscription-plan.d.ts +32 -2
- package/dist/implementations/subscriptions/define-subscription-plan.js +37 -1
- package/dist/implementations/subscriptions/in-memory-subscriptions.d.ts +28 -3
- package/dist/implementations/subscriptions/in-memory-subscriptions.js +46 -6
- package/dist/implementations/subscriptions/subscription-plan-validators.d.ts +25 -0
- package/dist/implementations/subscriptions/subscription-plan-validators.js +50 -1
- package/dist/packages/energy-app-subscriptions.d.ts +55 -3
- package/dist/packages/energy-app-webhook.d.ts +21 -2
- package/dist/types/enyo-subscription.d.ts +107 -4
- package/dist/types/enyo-subscription.js +29 -0
- package/dist/types/enyo-webhook.d.ts +27 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2276,6 +2276,17 @@ const listenerId = webhook.listenForWebhook<VendorEvent>(async (event) => {
|
|
|
2276
2276
|
assertion, not validation — check the fields you rely on.
|
|
2277
2277
|
- **At-least-once delivery.** A call may be forwarded more than once and order is not guaranteed —
|
|
2278
2278
|
deduplicate on `webhookRequestId`.
|
|
2279
|
+
- **Hidden sources need a token.** Some webhook sources are only delivered to listeners that present their static
|
|
2280
|
+
access token. Pass it per listener; the listener then receives these sources in addition to the public ones:
|
|
2281
|
+
|
|
2282
|
+
```typescript
|
|
2283
|
+
webhook.listenForWebhook<VendorEvent>(handleEvent, {
|
|
2284
|
+
secretKeys: [{ source: 'vendor-cloud', token: vendorWebhookToken }]
|
|
2285
|
+
});
|
|
2286
|
+
```
|
|
2287
|
+
|
|
2288
|
+
The token only applies to that listener, and a source with a wrong token is not delivered. Keep the token in the
|
|
2289
|
+
secret manager, not in the source code.
|
|
2279
2290
|
|
|
2280
2291
|
Requires the `Webhook` permission.
|
|
2281
2292
|
|
|
@@ -2308,6 +2319,8 @@ subscriptions.listenForSubscriptionChanges(async ({ subscription, previousStatus
|
|
|
2308
2319
|
trial, grace or paid period) without a connection.
|
|
2309
2320
|
- **Fall back safely.** When a plan stops being active, release any control over appliances instead of leaving
|
|
2310
2321
|
them in the last commanded state.
|
|
2322
|
+
- **Per-unit plans:** report the units with `setUnits(planKey, units)` and check each one with
|
|
2323
|
+
`isUnitActive(subscriptions, planKey, unit)` — see [Per-unit plans](#per-unit-plans).
|
|
2311
2324
|
- **Point the user to an offer** with a notification target `{ type: 'subscription-offer', planKey: 'premium' }`,
|
|
2312
2325
|
or set `EnergyAppStateEnum.SubscriptionRequired` when the whole app needs a plan.
|
|
2313
2326
|
|
|
@@ -2972,7 +2985,8 @@ const packageDef = defineEnergyAppPackage({
|
|
|
2972
2985
|
|---|---|
|
|
2973
2986
|
| `planKey` | Kebab-case slug, unique in the package. Subscriptions refer to the plan by it — treat it as an API. |
|
|
2974
2987
|
| `paymentInterval` | `one-time`, `monthly` or `yearly`. One-time purchases never renew and stay `active` until refunded. |
|
|
2975
|
-
| `
|
|
2988
|
+
| `pricingModel` | `per-package` (default): one price for the package on the device. `per-unit`: the price is charged per unit the app reports — see [Per-unit plans](#per-unit-plans). |
|
|
2989
|
+
| `prices` | One net price per currency. `amountWithoutTax` is always the **total for one interval** — per month, per year, or the single one-time amount; per unit for `per-unit` plans — in the currency (not cent), at most two decimals; enyo adds VAT. |
|
|
2976
2990
|
| `trialDays` | Optional free trial in days. Not allowed for `one-time` plans. |
|
|
2977
2991
|
| `storeEntry` | Title, short description and optional benefits; `de` and `en` are required. |
|
|
2978
2992
|
| `internalComment` | Optional note, never shown to users. |
|
|
@@ -2980,6 +2994,51 @@ const packageDef = defineEnergyAppPackage({
|
|
|
2980
2994
|
What a plan unlocks is decided by the app: check `isPlanActive(subscriptions, planKey)` at runtime (see
|
|
2981
2995
|
[`useSubscriptions()`](#usesubscriptions-energyappsubscriptions)).
|
|
2982
2996
|
|
|
2997
|
+
### Per-unit plans
|
|
2998
|
+
|
|
2999
|
+
For a `per-unit` plan the price is charged once per unit, and the **quantity comes from the energy app**. The app
|
|
3000
|
+
reports every unit it currently manages with `setUnits()` — on boot and whenever a unit is added or removed. Each
|
|
3001
|
+
unit has a `type` and a `referenceId` it is paid under:
|
|
3002
|
+
|
|
3003
|
+
| `type` | `referenceId` | Name shown to the customer |
|
|
3004
|
+
|---|---|---|
|
|
3005
|
+
| `appliance` | the appliance id | taken from the appliance |
|
|
3006
|
+
| `vehicle` | the vehicle id | taken from the vehicle |
|
|
3007
|
+
| `custom` | an id chosen by the app | `name`, translated; `de` and `en` are required |
|
|
3008
|
+
|
|
3009
|
+
```typescript
|
|
3010
|
+
import { EnyoSubscriptionUnitTypeEnum, isUnitActive } from '@enyo-energy/energy-app-sdk';
|
|
3011
|
+
|
|
3012
|
+
const subscriptions = energyApp.useSubscriptions();
|
|
3013
|
+
const wallboxes = await energyApp.useAppliances().list();
|
|
3014
|
+
|
|
3015
|
+
// 3 wallboxes on a yearly plan for 10 € → 30 € per year (net).
|
|
3016
|
+
await subscriptions.setUnits('smart-charging', wallboxes.map(wallbox => ({
|
|
3017
|
+
type: EnyoSubscriptionUnitTypeEnum.Appliance,
|
|
3018
|
+
referenceId: wallbox.id
|
|
3019
|
+
})));
|
|
3020
|
+
|
|
3021
|
+
// A custom unit carries its own translated name.
|
|
3022
|
+
await subscriptions.setUnits('room-climate', [{
|
|
3023
|
+
type: EnyoSubscriptionUnitTypeEnum.Custom,
|
|
3024
|
+
referenceId: 'living-room',
|
|
3025
|
+
name: [{ language: 'en', value: 'Living room' }, { language: 'de', value: 'Wohnzimmer' }]
|
|
3026
|
+
}]);
|
|
3027
|
+
|
|
3028
|
+
// Only the units the customer paid for are active.
|
|
3029
|
+
const all = await subscriptions.getSubscriptions();
|
|
3030
|
+
for (const wallbox of wallboxes) {
|
|
3031
|
+
setSmartCharging(wallbox, isUnitActive(all, 'smart-charging', { type: 'appliance', referenceId: wallbox.id }));
|
|
3032
|
+
}
|
|
3033
|
+
```
|
|
3034
|
+
|
|
3035
|
+
- **Reporting a unit does not pay for it.** Paid units are listed in `subscription.units`; when the customer pays
|
|
3036
|
+
for more units, the change listeners are called.
|
|
3037
|
+
- **Keep `referenceId` stable.** `type` plus `referenceId` identify the unit; a new reference is a new, unpaid unit.
|
|
3038
|
+
- **enyo decides how a changed quantity is charged**, e.g. pro rata for the rest of the period.
|
|
3039
|
+
- `validateSubscriptionUnits(units)` applies the host's rules up front: known type, non-empty reference, no unit
|
|
3040
|
+
twice, `de` and `en` names on custom units.
|
|
3041
|
+
|
|
2983
3042
|
**A price change needs a new release** and only applies to new purchases — running subscriptions keep the price
|
|
2984
3043
|
they were bought at.
|
|
2985
3044
|
|
|
@@ -2990,7 +3049,7 @@ const { ok, errors } = validatePackageSubscriptionPlans(packageDef);
|
|
|
2990
3049
|
// or: assertValidPackageSubscriptionPlans(packageDef) — throws PackageSubscriptionPlansValidationError
|
|
2991
3050
|
```
|
|
2992
3051
|
|
|
2993
|
-
It checks that plan keys are unique slugs, that the payment interval
|
|
3052
|
+
It checks that plan keys are unique slugs, that the payment interval and pricing model are known, that every plan has at least one
|
|
2994
3053
|
positive price per currency with at most two decimals, that `trialDays` is a positive whole number and not set on a
|
|
2995
3054
|
`one-time` plan, that `de` and
|
|
2996
3055
|
`en` store entries exist, and that the package requests the `Subscriptions` permission.
|
|
@@ -2,7 +2,7 @@ import { EnergyAppPermissionType } from "./energy-app-permission.type.cjs";
|
|
|
2
2
|
import { EnergyAppModelFeatureEnum } from "./energy-app-model-feature.enum.cjs";
|
|
3
3
|
import type { EnyoFileTranslation } from "./types/enyo-file.cjs";
|
|
4
4
|
import type { EnyoOnboardingV2Guide } from "./types/enyo-onboarding-v2.cjs";
|
|
5
|
-
import type { EnyoSubscriptionPaymentInterval, EnyoSubscriptionPrice } from "./types/enyo-subscription.cjs";
|
|
5
|
+
import type { EnyoSubscriptionPaymentInterval, EnyoSubscriptionPrice, EnyoSubscriptionPricingModel } from "./types/enyo-subscription.cjs";
|
|
6
6
|
export type EnergyAppPackageLanguage = 'de' | 'en';
|
|
7
7
|
export declare enum EnergyAppPackageCategory {
|
|
8
8
|
Inverter = "inverter",
|
|
@@ -333,10 +333,18 @@ export interface EnergyAppPackageSubscriptionPlan {
|
|
|
333
333
|
planKey: string;
|
|
334
334
|
/** How often the plan is paid for: `one-time`, `monthly` or `yearly`. */
|
|
335
335
|
paymentInterval: EnyoSubscriptionPaymentInterval;
|
|
336
|
+
/**
|
|
337
|
+
* Whether the price is charged once for the package on the device
|
|
338
|
+
* (`per-package`, the default) or once per unit (`per-unit`). For
|
|
339
|
+
* `per-unit` the app reports the units — appliances, vehicles or custom
|
|
340
|
+
* units — at runtime with {@link EnergyAppSubscriptions.setUnits}.
|
|
341
|
+
*/
|
|
342
|
+
pricingModel?: EnyoSubscriptionPricingModel;
|
|
336
343
|
/**
|
|
337
344
|
* Net price, one entry per currency. At least one entry is required. The
|
|
338
345
|
* amount is always the total for one interval: per month for `monthly`,
|
|
339
|
-
* per year for `yearly`, and the single amount for `one-time`.
|
|
346
|
+
* per year for `yearly`, and the single amount for `one-time`. For a
|
|
347
|
+
* `per-unit` plan it is the amount per unit.
|
|
340
348
|
*/
|
|
341
349
|
prices: EnyoSubscriptionPrice[];
|
|
342
350
|
/**
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* where it is written — mirroring `definePublicFile()` and
|
|
8
8
|
* `defineFirmwareFile()`. `isSubscriptionActive()` and `isPlanActive()` hold
|
|
9
9
|
* the single rule for which subscription statuses grant access, so every app
|
|
10
|
-
* applies it the same way
|
|
10
|
+
* applies it the same way; `isUnitActive()` applies it to one unit of a
|
|
11
|
+
* `per-unit` plan.
|
|
11
12
|
*
|
|
12
13
|
* Pair with `validatePackageSubscriptionPlans()`
|
|
13
14
|
* (`./subscription-plan-validators.ts`) to fail fast before publishing.
|
|
@@ -17,6 +18,8 @@ exports.ACTIVE_SUBSCRIPTION_STATUSES = void 0;
|
|
|
17
18
|
exports.defineSubscriptionPlan = defineSubscriptionPlan;
|
|
18
19
|
exports.isSubscriptionActive = isSubscriptionActive;
|
|
19
20
|
exports.isPlanActive = isPlanActive;
|
|
21
|
+
exports.isSameSubscriptionUnit = isSameSubscriptionUnit;
|
|
22
|
+
exports.isUnitActive = isUnitActive;
|
|
20
23
|
const enyo_subscription_js_1 = require("../../types/enyo-subscription.cjs");
|
|
21
24
|
/**
|
|
22
25
|
* Subscription statuses in which the customer may use what a plan unlocks.
|
|
@@ -84,3 +87,38 @@ function isSubscriptionActive(subscription) {
|
|
|
84
87
|
function isPlanActive(subscriptions, planKey) {
|
|
85
88
|
return subscriptions.some(subscription => subscription.planKey === planKey && isSubscriptionActive(subscription));
|
|
86
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Tests whether two units are the same unit, i.e. have the same `type` and
|
|
92
|
+
* `referenceId`. The name of a custom unit is not compared.
|
|
93
|
+
*
|
|
94
|
+
* @param a - The first unit.
|
|
95
|
+
* @param b - The second unit.
|
|
96
|
+
* @returns True when both identify the same unit.
|
|
97
|
+
*/
|
|
98
|
+
function isSameSubscriptionUnit(a, b) {
|
|
99
|
+
return a.type === b.type && a.referenceId === b.referenceId;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Tests whether one unit of a `per-unit` plan is paid for, i.e. whether an
|
|
103
|
+
* active subscription for the plan lists the unit in
|
|
104
|
+
* {@link EnyoSubscription.units}.
|
|
105
|
+
*
|
|
106
|
+
* @param subscriptions - The subscriptions, as returned by
|
|
107
|
+
* {@link EnergyAppSubscriptions.getSubscriptions}.
|
|
108
|
+
* @param planKey - The {@link EnergyAppPackageSubscriptionPlan.planKey} to check.
|
|
109
|
+
* @param unit - The unit to check, identified by `type` and `referenceId`.
|
|
110
|
+
* @returns True when the unit is covered by an active subscription for the plan.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* const subscriptions = await energyApp.useSubscriptions().getSubscriptions();
|
|
115
|
+
* if (isUnitActive(subscriptions, 'smart-charging', {type: 'appliance', referenceId: wallbox.id})) {
|
|
116
|
+
* enableSmartCharging(wallbox);
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
function isUnitActive(subscriptions, planKey, unit) {
|
|
121
|
+
return subscriptions.some(subscription => subscription.planKey === planKey
|
|
122
|
+
&& isSubscriptionActive(subscription)
|
|
123
|
+
&& (subscription.units ?? []).some(paid => isSameSubscriptionUnit(paid, unit)));
|
|
124
|
+
}
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
* where it is written — mirroring `definePublicFile()` and
|
|
7
7
|
* `defineFirmwareFile()`. `isSubscriptionActive()` and `isPlanActive()` hold
|
|
8
8
|
* the single rule for which subscription statuses grant access, so every app
|
|
9
|
-
* applies it the same way
|
|
9
|
+
* applies it the same way; `isUnitActive()` applies it to one unit of a
|
|
10
|
+
* `per-unit` plan.
|
|
10
11
|
*
|
|
11
12
|
* Pair with `validatePackageSubscriptionPlans()`
|
|
12
13
|
* (`./subscription-plan-validators.ts`) to fail fast before publishing.
|
|
13
14
|
*/
|
|
14
15
|
import type { EnergyAppPackageSubscriptionPlan } from '../../energy-app-package-definition.cjs';
|
|
15
|
-
import { EnyoSubscription, EnyoSubscriptionStatus } from '../../types/enyo-subscription.cjs';
|
|
16
|
+
import { EnyoSubscription, EnyoSubscriptionStatus, EnyoSubscriptionUnit } from '../../types/enyo-subscription.cjs';
|
|
16
17
|
/**
|
|
17
18
|
* Subscription statuses in which the customer may use what a plan unlocks.
|
|
18
19
|
*
|
|
@@ -68,3 +69,32 @@ export declare function isSubscriptionActive(subscription: EnyoSubscription): bo
|
|
|
68
69
|
* ```
|
|
69
70
|
*/
|
|
70
71
|
export declare function isPlanActive(subscriptions: EnyoSubscription[], planKey: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Tests whether two units are the same unit, i.e. have the same `type` and
|
|
74
|
+
* `referenceId`. The name of a custom unit is not compared.
|
|
75
|
+
*
|
|
76
|
+
* @param a - The first unit.
|
|
77
|
+
* @param b - The second unit.
|
|
78
|
+
* @returns True when both identify the same unit.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isSameSubscriptionUnit(a: Pick<EnyoSubscriptionUnit, 'type' | 'referenceId'>, b: Pick<EnyoSubscriptionUnit, 'type' | 'referenceId'>): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Tests whether one unit of a `per-unit` plan is paid for, i.e. whether an
|
|
83
|
+
* active subscription for the plan lists the unit in
|
|
84
|
+
* {@link EnyoSubscription.units}.
|
|
85
|
+
*
|
|
86
|
+
* @param subscriptions - The subscriptions, as returned by
|
|
87
|
+
* {@link EnergyAppSubscriptions.getSubscriptions}.
|
|
88
|
+
* @param planKey - The {@link EnergyAppPackageSubscriptionPlan.planKey} to check.
|
|
89
|
+
* @param unit - The unit to check, identified by `type` and `referenceId`.
|
|
90
|
+
* @returns True when the unit is covered by an active subscription for the plan.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const subscriptions = await energyApp.useSubscriptions().getSubscriptions();
|
|
95
|
+
* if (isUnitActive(subscriptions, 'smart-charging', {type: 'appliance', referenceId: wallbox.id})) {
|
|
96
|
+
* enableSmartCharging(wallbox);
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export declare function isUnitActive(subscriptions: EnyoSubscription[], planKey: string, unit: Pick<EnyoSubscriptionUnit, 'type' | 'referenceId'>): boolean;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.InMemorySubscriptions = void 0;
|
|
4
|
+
const enyo_subscription_js_1 = require("../../types/enyo-subscription.cjs");
|
|
5
|
+
const subscription_plan_validators_js_1 = require("./subscription-plan-validators.cjs");
|
|
4
6
|
/**
|
|
5
7
|
* In-memory implementation of {@link EnergyAppSubscriptions} for tests and
|
|
6
8
|
* local development.
|
|
@@ -8,7 +10,8 @@ exports.InMemorySubscriptions = void 0;
|
|
|
8
10
|
* It holds the subscriptions in memory and lets a test play the part of enyo:
|
|
9
11
|
* {@link upsertSubscription} simulates a purchase, renewal, cancellation,
|
|
10
12
|
* failed payment or expiry and notifies the registered listeners exactly like
|
|
11
|
-
* the host would.
|
|
13
|
+
* the host would. Units reported with {@link setUnits} are kept per plan and
|
|
14
|
+
* can be read back with {@link getUnits}.
|
|
12
15
|
*
|
|
13
16
|
* @example
|
|
14
17
|
* ```typescript
|
|
@@ -20,6 +23,7 @@ exports.InMemorySubscriptions = void 0;
|
|
|
20
23
|
* planKey: 'premium',
|
|
21
24
|
* status: EnyoSubscriptionStatusEnum.Active,
|
|
22
25
|
* paymentInterval: EnyoSubscriptionPaymentIntervalEnum.Yearly,
|
|
26
|
+
* pricingModel: EnyoSubscriptionPricingModelEnum.PerPackage,
|
|
23
27
|
* price: {amountWithoutTax: 41.18, currency: EnyoCurrencyEnum.EUR},
|
|
24
28
|
* startedAtIso: '2026-01-01T00:00:00Z',
|
|
25
29
|
* currentPeriodStartIso: '2026-01-01T00:00:00Z',
|
|
@@ -29,25 +33,61 @@ exports.InMemorySubscriptions = void 0;
|
|
|
29
33
|
* ```
|
|
30
34
|
*/
|
|
31
35
|
class InMemorySubscriptions {
|
|
36
|
+
plans;
|
|
32
37
|
subscriptions = new Map();
|
|
33
38
|
listeners = new Map();
|
|
39
|
+
unitsByPlan = new Map();
|
|
34
40
|
nextListenerId = 1;
|
|
35
41
|
/**
|
|
36
42
|
* @param initialSubscriptions - Subscriptions that exist before the app
|
|
37
43
|
* starts. They do not trigger listeners.
|
|
44
|
+
* @param plans - Optional plans of the package. When given, {@link setUnits}
|
|
45
|
+
* rejects plan keys that are not declared as `per-unit`, like the host
|
|
46
|
+
* does. When omitted, every plan key is accepted.
|
|
38
47
|
*/
|
|
39
|
-
constructor(initialSubscriptions = []) {
|
|
48
|
+
constructor(initialSubscriptions = [], plans) {
|
|
49
|
+
this.plans = plans;
|
|
40
50
|
for (const subscription of initialSubscriptions) {
|
|
41
|
-
this.subscriptions.set(subscription.subscriptionId,
|
|
51
|
+
this.subscriptions.set(subscription.subscriptionId, structuredClone(subscription));
|
|
42
52
|
}
|
|
43
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Stores the units reported for a `per-unit` plan, replacing earlier ones.
|
|
56
|
+
*
|
|
57
|
+
* @param planKey - The key of the plan.
|
|
58
|
+
* @param units - Every unit the app currently manages for the plan.
|
|
59
|
+
* @returns Promise that resolves once the units are stored
|
|
60
|
+
* @throws {Error} If the units are invalid, or `plans` were given and
|
|
61
|
+
* `planKey` is not a `per-unit` plan among them.
|
|
62
|
+
*/
|
|
63
|
+
async setUnits(planKey, units) {
|
|
64
|
+
if (this.plans) {
|
|
65
|
+
const plan = this.plans.find(candidate => candidate.planKey === planKey);
|
|
66
|
+
if (plan?.pricingModel !== enyo_subscription_js_1.EnyoSubscriptionPricingModelEnum.PerUnit) {
|
|
67
|
+
throw new Error(`Plan "${planKey}" is not a per-unit plan of this package.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const { ok, errors } = (0, subscription_plan_validators_js_1.validateSubscriptionUnits)(units);
|
|
71
|
+
if (!ok)
|
|
72
|
+
throw new Error(`Invalid subscription units:\n- ${errors.join('\n- ')}`);
|
|
73
|
+
this.unitsByPlan.set(planKey, units.map(unit => structuredClone(unit)));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Returns copies of the units last reported for a plan.
|
|
77
|
+
*
|
|
78
|
+
* @param planKey - The key of the plan.
|
|
79
|
+
* @returns Promise that resolves to the units; an empty list when none were reported
|
|
80
|
+
*/
|
|
81
|
+
async getUnits(planKey) {
|
|
82
|
+
return (this.unitsByPlan.get(planKey) ?? []).map(unit => structuredClone(unit));
|
|
83
|
+
}
|
|
44
84
|
/**
|
|
45
85
|
* Returns copies of all stored subscriptions.
|
|
46
86
|
*
|
|
47
87
|
* @returns Promise that resolves to the subscriptions in insertion order
|
|
48
88
|
*/
|
|
49
89
|
async getSubscriptions() {
|
|
50
|
-
return [...this.subscriptions.values()].map(subscription => (
|
|
90
|
+
return [...this.subscriptions.values()].map(subscription => structuredClone(subscription));
|
|
51
91
|
}
|
|
52
92
|
/**
|
|
53
93
|
* Registers a listener for new or changed subscriptions.
|
|
@@ -85,9 +125,9 @@ class InMemorySubscriptions {
|
|
|
85
125
|
*/
|
|
86
126
|
async upsertSubscription(subscription) {
|
|
87
127
|
const previousStatus = this.subscriptions.get(subscription.subscriptionId)?.status;
|
|
88
|
-
this.subscriptions.set(subscription.subscriptionId,
|
|
128
|
+
this.subscriptions.set(subscription.subscriptionId, structuredClone(subscription));
|
|
89
129
|
for (const listener of [...this.listeners.values()]) {
|
|
90
|
-
await listener({ subscription:
|
|
130
|
+
await listener({ subscription: structuredClone(subscription), previousStatus });
|
|
91
131
|
}
|
|
92
132
|
}
|
|
93
133
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { EnergyAppPackageSubscriptionPlan } from '../../energy-app-package-definition.cjs';
|
|
1
2
|
import type { EnergyAppSubscriptions } from '../../packages/energy-app-subscriptions.cjs';
|
|
2
|
-
import
|
|
3
|
+
import { EnyoSubscription, EnyoSubscriptionChangeListener, EnyoSubscriptionUnit } from '../../types/enyo-subscription.cjs';
|
|
3
4
|
/**
|
|
4
5
|
* In-memory implementation of {@link EnergyAppSubscriptions} for tests and
|
|
5
6
|
* local development.
|
|
@@ -7,7 +8,8 @@ import type { EnyoSubscription, EnyoSubscriptionChangeListener } from '../../typ
|
|
|
7
8
|
* It holds the subscriptions in memory and lets a test play the part of enyo:
|
|
8
9
|
* {@link upsertSubscription} simulates a purchase, renewal, cancellation,
|
|
9
10
|
* failed payment or expiry and notifies the registered listeners exactly like
|
|
10
|
-
* the host would.
|
|
11
|
+
* the host would. Units reported with {@link setUnits} are kept per plan and
|
|
12
|
+
* can be read back with {@link getUnits}.
|
|
11
13
|
*
|
|
12
14
|
* @example
|
|
13
15
|
* ```typescript
|
|
@@ -19,6 +21,7 @@ import type { EnyoSubscription, EnyoSubscriptionChangeListener } from '../../typ
|
|
|
19
21
|
* planKey: 'premium',
|
|
20
22
|
* status: EnyoSubscriptionStatusEnum.Active,
|
|
21
23
|
* paymentInterval: EnyoSubscriptionPaymentIntervalEnum.Yearly,
|
|
24
|
+
* pricingModel: EnyoSubscriptionPricingModelEnum.PerPackage,
|
|
22
25
|
* price: {amountWithoutTax: 41.18, currency: EnyoCurrencyEnum.EUR},
|
|
23
26
|
* startedAtIso: '2026-01-01T00:00:00Z',
|
|
24
27
|
* currentPeriodStartIso: '2026-01-01T00:00:00Z',
|
|
@@ -28,14 +31,36 @@ import type { EnyoSubscription, EnyoSubscriptionChangeListener } from '../../typ
|
|
|
28
31
|
* ```
|
|
29
32
|
*/
|
|
30
33
|
export declare class InMemorySubscriptions implements EnergyAppSubscriptions {
|
|
34
|
+
private readonly plans?;
|
|
31
35
|
private readonly subscriptions;
|
|
32
36
|
private readonly listeners;
|
|
37
|
+
private readonly unitsByPlan;
|
|
33
38
|
private nextListenerId;
|
|
34
39
|
/**
|
|
35
40
|
* @param initialSubscriptions - Subscriptions that exist before the app
|
|
36
41
|
* starts. They do not trigger listeners.
|
|
42
|
+
* @param plans - Optional plans of the package. When given, {@link setUnits}
|
|
43
|
+
* rejects plan keys that are not declared as `per-unit`, like the host
|
|
44
|
+
* does. When omitted, every plan key is accepted.
|
|
37
45
|
*/
|
|
38
|
-
constructor(initialSubscriptions?: EnyoSubscription[]);
|
|
46
|
+
constructor(initialSubscriptions?: EnyoSubscription[], plans?: EnergyAppPackageSubscriptionPlan[] | undefined);
|
|
47
|
+
/**
|
|
48
|
+
* Stores the units reported for a `per-unit` plan, replacing earlier ones.
|
|
49
|
+
*
|
|
50
|
+
* @param planKey - The key of the plan.
|
|
51
|
+
* @param units - Every unit the app currently manages for the plan.
|
|
52
|
+
* @returns Promise that resolves once the units are stored
|
|
53
|
+
* @throws {Error} If the units are invalid, or `plans` were given and
|
|
54
|
+
* `planKey` is not a `per-unit` plan among them.
|
|
55
|
+
*/
|
|
56
|
+
setUnits(planKey: string, units: EnyoSubscriptionUnit[]): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Returns copies of the units last reported for a plan.
|
|
59
|
+
*
|
|
60
|
+
* @param planKey - The key of the plan.
|
|
61
|
+
* @returns Promise that resolves to the units; an empty list when none were reported
|
|
62
|
+
*/
|
|
63
|
+
getUnits(planKey: string): Promise<EnyoSubscriptionUnit[]>;
|
|
39
64
|
/**
|
|
40
65
|
* Returns copies of all stored subscriptions.
|
|
41
66
|
*
|
|
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.PackageSubscriptionPlansValidationError = void 0;
|
|
16
16
|
exports.validatePackageSubscriptionPlans = validatePackageSubscriptionPlans;
|
|
17
17
|
exports.assertValidPackageSubscriptionPlans = assertValidPackageSubscriptionPlans;
|
|
18
|
+
exports.validateSubscriptionUnits = validateSubscriptionUnits;
|
|
18
19
|
const energy_app_permission_type_js_1 = require("../../energy-app-permission.type.cjs");
|
|
19
20
|
const enyo_currency_js_1 = require("../../types/enyo-currency.cjs");
|
|
20
21
|
const enyo_subscription_js_1 = require("../../types/enyo-subscription.cjs");
|
|
@@ -159,6 +160,10 @@ function validatePackageSubscriptionPlans(definition) {
|
|
|
159
160
|
if (!Object.values(enyo_subscription_js_1.EnyoSubscriptionPaymentIntervalEnum).includes(plan.paymentInterval)) {
|
|
160
161
|
errors.push(`${at}: paymentInterval "${plan.paymentInterval}" is not supported.`);
|
|
161
162
|
}
|
|
163
|
+
if (plan.pricingModel !== undefined
|
|
164
|
+
&& !Object.values(enyo_subscription_js_1.EnyoSubscriptionPricingModelEnum).includes(plan.pricingModel)) {
|
|
165
|
+
errors.push(`${at}: pricingModel "${plan.pricingModel}" is not supported.`);
|
|
166
|
+
}
|
|
162
167
|
validatePrices(plan, at, errors);
|
|
163
168
|
if (plan.trialDays !== undefined) {
|
|
164
169
|
if (plan.paymentInterval === enyo_subscription_js_1.EnyoSubscriptionPaymentIntervalEnum.OneTime) {
|
|
@@ -189,3 +194,48 @@ function assertValidPackageSubscriptionPlans(definition) {
|
|
|
189
194
|
throw new PackageSubscriptionPlansValidationError(errors);
|
|
190
195
|
return definition.subscriptionPlans;
|
|
191
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Validates the units an app reports for a `per-unit` plan with
|
|
199
|
+
* {@link EnergyAppSubscriptions.setUnits}: every unit has a known type and a
|
|
200
|
+
* non-empty reference, no unit is reported twice (same `type` and
|
|
201
|
+
* `referenceId`), and every custom unit has a non-empty name in German and
|
|
202
|
+
* English. The host applies the same rules and rejects invalid units.
|
|
203
|
+
*
|
|
204
|
+
* @param units - The units to report.
|
|
205
|
+
* @returns The {@link SubscriptionUnitsValidationResult}.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* const {ok, errors} = validateSubscriptionUnits(units);
|
|
210
|
+
* if (!ok) logger.warn(errors.join('\n'));
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
function validateSubscriptionUnits(units) {
|
|
214
|
+
const errors = [];
|
|
215
|
+
const seen = new Set();
|
|
216
|
+
for (const [index, unit] of units.entries()) {
|
|
217
|
+
const at = `units[${index}] (${unit.type || '?'} ${unit.referenceId || '?'})`;
|
|
218
|
+
if (!Object.values(enyo_subscription_js_1.EnyoSubscriptionUnitTypeEnum).includes(unit.type)) {
|
|
219
|
+
errors.push(`${at}: type "${unit.type}" is not supported.`);
|
|
220
|
+
}
|
|
221
|
+
if (!unit.referenceId?.trim()) {
|
|
222
|
+
errors.push(`${at}: referenceId is required.`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const key = `${unit.type}\u0000${unit.referenceId}`;
|
|
226
|
+
if (seen.has(key)) {
|
|
227
|
+
errors.push(`${at}: duplicate unit — type and referenceId must be unique.`);
|
|
228
|
+
}
|
|
229
|
+
seen.add(key);
|
|
230
|
+
}
|
|
231
|
+
if (unit.type === enyo_subscription_js_1.EnyoSubscriptionUnitTypeEnum.Custom) {
|
|
232
|
+
const names = unit.name ?? [];
|
|
233
|
+
for (const language of REQUIRED_LANGUAGES) {
|
|
234
|
+
if (!names.some(name => name.language === language && name.value?.trim())) {
|
|
235
|
+
errors.push(`${at}: a custom unit needs a name in language "${language}".`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return { ok: errors.length === 0, errors };
|
|
241
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* {@link assertValidPackageSubscriptionPlans} to throw on failure.
|
|
12
12
|
*/
|
|
13
13
|
import type { EnergyAppPackageDefinition, EnergyAppPackageSubscriptionPlan } from '../../energy-app-package-definition.cjs';
|
|
14
|
+
import { EnyoSubscriptionUnit } from '../../types/enyo-subscription.cjs';
|
|
14
15
|
/**
|
|
15
16
|
* Thrown by {@link assertValidPackageSubscriptionPlans} when a package's
|
|
16
17
|
* subscription plans fail validation. The message lists every blocking error
|
|
@@ -69,3 +70,27 @@ export declare function validatePackageSubscriptionPlans(definition: PackageSubs
|
|
|
69
70
|
* @throws {PackageSubscriptionPlansValidationError} When validation produces any error.
|
|
70
71
|
*/
|
|
71
72
|
export declare function assertValidPackageSubscriptionPlans(definition: PackageSubscriptionPlansSource): EnergyAppPackageSubscriptionPlan[] | undefined;
|
|
73
|
+
/** The outcome of validating the units reported for a `per-unit` plan. */
|
|
74
|
+
export interface SubscriptionUnitsValidationResult {
|
|
75
|
+
/** True when there are no `errors`. */
|
|
76
|
+
ok: boolean;
|
|
77
|
+
/** Problems that make the host reject the units. */
|
|
78
|
+
errors: string[];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validates the units an app reports for a `per-unit` plan with
|
|
82
|
+
* {@link EnergyAppSubscriptions.setUnits}: every unit has a known type and a
|
|
83
|
+
* non-empty reference, no unit is reported twice (same `type` and
|
|
84
|
+
* `referenceId`), and every custom unit has a non-empty name in German and
|
|
85
|
+
* English. The host applies the same rules and rejects invalid units.
|
|
86
|
+
*
|
|
87
|
+
* @param units - The units to report.
|
|
88
|
+
* @returns The {@link SubscriptionUnitsValidationResult}.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const {ok, errors} = validateSubscriptionUnits(units);
|
|
93
|
+
* if (!ok) logger.warn(errors.join('\n'));
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export declare function validateSubscriptionUnits(units: EnyoSubscriptionUnit[]): SubscriptionUnitsValidationResult;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EnyoSubscription, EnyoSubscriptionChangeListener } from "../types/enyo-subscription.cjs";
|
|
1
|
+
import { EnyoSubscription, EnyoSubscriptionChangeListener, EnyoSubscriptionUnit } from "../types/enyo-subscription.cjs";
|
|
2
2
|
/**
|
|
3
3
|
* Interface for reading the subscriptions a customer bought for this energy
|
|
4
4
|
* app on this device.
|
|
@@ -16,6 +16,12 @@ import { EnyoSubscription, EnyoSubscriptionChangeListener } from "../types/enyo-
|
|
|
16
16
|
* or grace period ending, a cancelled subscription expiring — are reported even
|
|
17
17
|
* without a connection.
|
|
18
18
|
*
|
|
19
|
+
* A `per-unit` plan is charged once per unit — appliance, vehicle or custom
|
|
20
|
+
* unit — and the quantity comes from the app: it reports the units it
|
|
21
|
+
* currently manages with {@link setUnits}, and enyo offers and charges the
|
|
22
|
+
* plan for them. {@link EnyoSubscription.units} lists the units that are paid
|
|
23
|
+
* for; check a single unit with `isUnitActive()`.
|
|
24
|
+
*
|
|
19
25
|
* When a plan stops being active, the app must fall back to a safe behaviour.
|
|
20
26
|
* For example, release any control it holds over a heat pump, wallbox or
|
|
21
27
|
* battery instead of leaving the appliance in the last commanded state.
|
|
@@ -48,11 +54,57 @@ export interface EnergyAppSubscriptions {
|
|
|
48
54
|
* permission is not granted.
|
|
49
55
|
*/
|
|
50
56
|
getSubscriptions(): Promise<EnyoSubscription[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Reports the units the app currently manages for a `per-unit` plan,
|
|
59
|
+
* replacing the previously reported list. The number of units is the
|
|
60
|
+
* quantity enyo offers and charges the plan for.
|
|
61
|
+
*
|
|
62
|
+
* Call it on boot and whenever a unit is added or removed, with the full
|
|
63
|
+
* list each time. A unit is identified by its `type` plus `referenceId`,
|
|
64
|
+
* so keep the reference stable: a changed reference reads as a removed and
|
|
65
|
+
* a new, unpaid unit. How a changed quantity is charged — for example
|
|
66
|
+
* pro rata for the rest of the period — is decided by enyo.
|
|
67
|
+
*
|
|
68
|
+
* Reporting a unit does not pay for it. Once the customer has paid, the
|
|
69
|
+
* unit appears in {@link EnyoSubscription.units} and the change listeners
|
|
70
|
+
* are called.
|
|
71
|
+
*
|
|
72
|
+
* @param planKey - The key of a `per-unit` plan declared in the package definition.
|
|
73
|
+
* @param units - Every unit the app currently manages for the plan; an
|
|
74
|
+
* empty list when there is none.
|
|
75
|
+
* @returns Promise that resolves once the host has stored the units
|
|
76
|
+
* @throws {Error} If `planKey` is not a `per-unit` plan of this package, or
|
|
77
|
+
* a unit is invalid (empty reference, duplicate `type` + `referenceId`,
|
|
78
|
+
* custom unit without `de` and `en` name).
|
|
79
|
+
* @throws {EnergyAppPermissionNotGrantedError} If the `Subscriptions`
|
|
80
|
+
* permission is not granted.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const wallboxes = await energyApp.useAppliances().list();
|
|
85
|
+
* await subscriptions.setUnits('smart-charging', wallboxes.map(wallbox => ({
|
|
86
|
+
* type: EnyoSubscriptionUnitTypeEnum.Appliance,
|
|
87
|
+
* referenceId: wallbox.id
|
|
88
|
+
* })));
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
setUnits(planKey: string, units: EnyoSubscriptionUnit[]): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Returns the units last reported with {@link setUnits} for a `per-unit`
|
|
94
|
+
* plan. These are the units the app manages, whether paid for or not.
|
|
95
|
+
*
|
|
96
|
+
* @param planKey - The key of a `per-unit` plan declared in the package definition.
|
|
97
|
+
* @returns Promise that resolves to the reported units; an empty list when
|
|
98
|
+
* none were reported yet
|
|
99
|
+
* @throws {EnergyAppPermissionNotGrantedError} If the `Subscriptions`
|
|
100
|
+
* permission is not granted.
|
|
101
|
+
*/
|
|
102
|
+
getUnits(planKey: string): Promise<EnyoSubscriptionUnit[]>;
|
|
51
103
|
/**
|
|
52
104
|
* Registers a listener that is called whenever a subscription of this app
|
|
53
105
|
* on this device is created or changes — a purchase, a renewal, a
|
|
54
|
-
* cancellation, a failed payment,
|
|
55
|
-
* period.
|
|
106
|
+
* cancellation, a failed payment, units paid for or removed, or the end of
|
|
107
|
+
* a trial, grace or paid period.
|
|
56
108
|
*
|
|
57
109
|
* The listener is not called for the state that exists when it is
|
|
58
110
|
* registered. Read that with {@link getSubscriptions} on boot.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EnyoWebhookEvent } from "../types/enyo-webhook.cjs";
|
|
1
|
+
import { EnyoWebhookEvent, EnyoWebhookListenOptions } from "../types/enyo-webhook.cjs";
|
|
2
2
|
/**
|
|
3
3
|
* Interface for receiving webhooks from third-party services.
|
|
4
4
|
*
|
|
@@ -54,11 +54,30 @@ export interface EnergyAppWebhook {
|
|
|
54
54
|
* is at least once and the order of calls is not guaranteed; deduplicate
|
|
55
55
|
* with {@link EnyoWebhookEvent.webhookRequestId}.
|
|
56
56
|
*
|
|
57
|
+
* By default the listener receives calls from the public sources only.
|
|
58
|
+
* To also receive a hidden source, pass its access token in
|
|
59
|
+
* `options.secretKeys`. The tokens apply to this listener only; other
|
|
60
|
+
* listeners of the app do not receive the hidden source unless they pass
|
|
61
|
+
* the token themselves.
|
|
62
|
+
*
|
|
57
63
|
* @typeParam T - Shape of the JSON body the app expects.
|
|
58
64
|
* @param listener - Callback invoked for every forwarded webhook call. May be async.
|
|
65
|
+
* @param options - Optional settings, such as access tokens for hidden sources.
|
|
59
66
|
* @returns The listener ID, to be passed to {@link removeListener}
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* const token = await energyApp.useSecretManager().getSecret<string>('vendor-webhook-token', encryptionKey);
|
|
71
|
+
* webhook.listenForWebhook<VendorEvent>(async (event) => {
|
|
72
|
+
* if (event.source === 'vendor-cloud') {
|
|
73
|
+
* await handleVendorEvent(event.body);
|
|
74
|
+
* }
|
|
75
|
+
* }, {
|
|
76
|
+
* secretKeys: [{source: 'vendor-cloud', token}]
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
60
79
|
*/
|
|
61
|
-
listenForWebhook<T = unknown>(listener: (event: EnyoWebhookEvent<T>) => void | Promise<void
|
|
80
|
+
listenForWebhook<T = unknown>(listener: (event: EnyoWebhookEvent<T>) => void | Promise<void>, options?: EnyoWebhookListenOptions): string;
|
|
62
81
|
/**
|
|
63
82
|
* Removes a listener previously registered with {@link listenForWebhook}.
|
|
64
83
|
*
|