@enyo-energy/energy-app-sdk 1.37.0 → 1.38.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 +103 -30
- package/dist/cjs/energy-app-package-definition.d.cts +19 -3
- package/dist/cjs/enyo-energy-app-sdk.cjs +7 -1
- package/dist/cjs/enyo-energy-app-sdk.d.cts +7 -1
- package/dist/cjs/implementations/subscriptions/in-memory-subscriptions.cjs +87 -21
- package/dist/cjs/implementations/subscriptions/in-memory-subscriptions.d.cts +59 -13
- package/dist/cjs/implementations/subscriptions/subscription-plan-validators.cjs +77 -8
- package/dist/cjs/implementations/subscriptions/subscription-plan-validators.d.cts +50 -9
- package/dist/cjs/packages/energy-app-appliance.d.cts +4 -0
- package/dist/cjs/packages/energy-app-subscriptions.d.cts +51 -26
- package/dist/cjs/packages/energy-app-vehicle.d.cts +12 -4
- package/dist/cjs/types/enyo-appliance.d.cts +10 -0
- package/dist/cjs/types/enyo-subscription.cjs +26 -3
- package/dist/cjs/types/enyo-subscription.d.cts +85 -2
- package/dist/cjs/types/enyo-vehicle.cjs +27 -1
- package/dist/cjs/types/enyo-vehicle.d.cts +65 -1
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/energy-app-package-definition.d.ts +19 -3
- package/dist/enyo-energy-app-sdk.d.ts +7 -1
- package/dist/enyo-energy-app-sdk.js +7 -1
- package/dist/implementations/subscriptions/in-memory-subscriptions.d.ts +59 -13
- package/dist/implementations/subscriptions/in-memory-subscriptions.js +89 -23
- package/dist/implementations/subscriptions/subscription-plan-validators.d.ts +50 -9
- package/dist/implementations/subscriptions/subscription-plan-validators.js +76 -9
- package/dist/packages/energy-app-appliance.d.ts +4 -0
- package/dist/packages/energy-app-subscriptions.d.ts +51 -26
- package/dist/packages/energy-app-vehicle.d.ts +12 -4
- package/dist/types/enyo-appliance.d.ts +10 -0
- package/dist/types/enyo-subscription.d.ts +85 -2
- package/dist/types/enyo-subscription.js +25 -2
- package/dist/types/enyo-vehicle.d.ts +65 -1
- package/dist/types/enyo-vehicle.js +26 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -244,7 +244,8 @@ energyApp.onShutdown(async () => {
|
|
|
244
244
|
- `running`: App is functioning normally
|
|
245
245
|
- `configuration-required`: App needs user configuration
|
|
246
246
|
- `internet-connection-required`: App needs internet connectivity
|
|
247
|
-
- `subscription-required`: App needs a subscription plan; the host shows the plans declared in the package definition
|
|
247
|
+
- `subscription-required`: App needs a subscription plan; the host shows the plans declared in the package definition.
|
|
248
|
+
Only for packages with `subscriptionMode: 'required'` — see [Subscription mode](#subscription-mode)
|
|
248
249
|
|
|
249
250
|
### Package Definition
|
|
250
251
|
|
|
@@ -886,8 +887,10 @@ vehicles and it does not go looking for them — the flow starts with the user:
|
|
|
886
887
|
```typescript
|
|
887
888
|
const vehicles = energyApp.useVehicle();
|
|
888
889
|
|
|
889
|
-
vehicles.onPairVehicle(async (vehicle, selection) => {
|
|
890
|
-
|
|
890
|
+
vehicles.onPairVehicle(async (vehicle, selection, context) => {
|
|
891
|
+
// The brand the user selected in the enyo app, from the package's `compatibility`
|
|
892
|
+
// vendors — lets an aggregator package pick the right vendor API.
|
|
893
|
+
const cars = await vendorApi.listVehicles(context?.vendorName);
|
|
891
894
|
|
|
892
895
|
// Several cars in the account and nothing to tell them apart —
|
|
893
896
|
// hand the choice back to the user instead of guessing
|
|
@@ -929,6 +932,9 @@ vehicles.onPairVehicle(async (vehicle, selection) => {
|
|
|
929
932
|
|
|
930
933
|
The handler is called a second time with `selection` after the user picks from
|
|
931
934
|
`candidates`. A package serving single-car accounts can ignore the parameter.
|
|
935
|
+
`context.vendorName` is the vendor the user selected and is passed on both calls; it is
|
|
936
|
+
also kept on the link (`EnyoLinkedVehicle.vendorName`) so the package can read it back
|
|
937
|
+
after a restart.
|
|
932
938
|
|
|
933
939
|
Once paired, the package has the `vehicleId` it was always missing and publishes
|
|
934
940
|
readings the normal way — `VehicleSocUpdateV1`, keyed by that id. Nothing about
|
|
@@ -947,9 +953,11 @@ vehicles.onUnpairVehicle(async (link, reason) => {
|
|
|
947
953
|
}
|
|
948
954
|
});
|
|
949
955
|
|
|
950
|
-
// The car vanished from the vendor account, or the
|
|
956
|
+
// The car vanished from the vendor account, or the vendor revoked access.
|
|
951
957
|
// Does NOT call onUnpairVehicle — the package already knows.
|
|
952
|
-
|
|
958
|
+
// The reason decides how a billed car is billed — see "What a removal means for billing".
|
|
959
|
+
await vehicles.unpairVehicle(vehicleId, EnyoVehiclePackageUnpairReasonEnum.VehicleNotFound);
|
|
960
|
+
await vehicles.unpairVehicle(otherVehicleId, EnyoVehiclePackageUnpairReasonEnum.AccessRevoked);
|
|
953
961
|
|
|
954
962
|
// Handlers only fire on change, so pick the links back up after a restart
|
|
955
963
|
for (const link of await vehicles.listLinkedVehicles()) {
|
|
@@ -2322,7 +2330,8 @@ subscriptions.listenForSubscriptionChanges(async ({ subscription, previousStatus
|
|
|
2322
2330
|
- **Per-unit plans:** report the units with `setUnits(planKey, units)` and check each one with
|
|
2323
2331
|
`isUnitActive(subscriptions, planKey, unit)` — see [Per-unit plans](#per-unit-plans).
|
|
2324
2332
|
- **Point the user to an offer** with a notification target `{ type: 'subscription-offer', planKey: 'premium' }`,
|
|
2325
|
-
or set `EnergyAppStateEnum.SubscriptionRequired` when the whole app needs a plan
|
|
2333
|
+
or set `EnergyAppStateEnum.SubscriptionRequired` when the whole app needs a plan (`subscriptionMode: 'required'`
|
|
2334
|
+
only).
|
|
2326
2335
|
|
|
2327
2336
|
For tests, `InMemorySubscriptions` implements the same interface; `upsertSubscription()` plays the part of enyo
|
|
2328
2337
|
and notifies the listeners.
|
|
@@ -2949,6 +2958,7 @@ import {
|
|
|
2949
2958
|
defineSubscriptionPlan,
|
|
2950
2959
|
EnergyAppPermissionTypeEnum,
|
|
2951
2960
|
EnyoCurrencyEnum,
|
|
2961
|
+
EnyoSubscriptionModeEnum,
|
|
2952
2962
|
EnyoSubscriptionPaymentIntervalEnum,
|
|
2953
2963
|
validatePackageSubscriptionPlans
|
|
2954
2964
|
} from '@enyo-energy/energy-app-sdk';
|
|
@@ -2956,6 +2966,7 @@ import {
|
|
|
2956
2966
|
const packageDef = defineEnergyAppPackage({
|
|
2957
2967
|
// ...
|
|
2958
2968
|
permissions: [EnergyAppPermissionTypeEnum.Subscriptions /* , ... */],
|
|
2969
|
+
subscriptionMode: EnyoSubscriptionModeEnum.Required, // see "Subscription mode" below
|
|
2959
2970
|
subscriptionPlans: [
|
|
2960
2971
|
defineSubscriptionPlan({
|
|
2961
2972
|
planKey: 'premium',
|
|
@@ -2985,7 +2996,7 @@ const packageDef = defineEnergyAppPackage({
|
|
|
2985
2996
|
|---|---|
|
|
2986
2997
|
| `planKey` | Kebab-case slug, unique in the package. Subscriptions refer to the plan by it — treat it as an API. |
|
|
2987
2998
|
| `paymentInterval` | `one-time`, `monthly` or `yearly`. One-time purchases never renew and stay `active` until refunded. |
|
|
2988
|
-
| `pricingModel` | `per-package` (default): one price for the package on the device. `per-unit`: the price is charged per unit
|
|
2999
|
+
| `pricingModel` | `per-package` (default): one price for the package on the device. `per-unit`: the price is charged per unit — see [Per-unit plans](#per-unit-plans). |
|
|
2989
3000
|
| `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. |
|
|
2990
3001
|
| `trialDays` | Optional free trial in days. Not allowed for `one-time` plans. |
|
|
2991
3002
|
| `storeEntry` | Title, short description and optional benefits; `de` and `en` are required. |
|
|
@@ -2994,51 +3005,113 @@ const packageDef = defineEnergyAppPackage({
|
|
|
2994
3005
|
What a plan unlocks is decided by the app: check `isPlanActive(subscriptions, planKey)` at runtime (see
|
|
2995
3006
|
[`useSubscriptions()`](#usesubscriptions-energyappsubscriptions)).
|
|
2996
3007
|
|
|
3008
|
+
### Subscription mode
|
|
3009
|
+
|
|
3010
|
+
As soon as a package declares `subscriptionPlans`, it must also declare `subscriptionMode`: does the app work
|
|
3011
|
+
without a plan at all? enyo uses it to decide what to offer — a free base option next to the plans, a downgrade to
|
|
3012
|
+
that base in the subscription management, and the wording of the cancellation and uninstall texts.
|
|
3013
|
+
|
|
3014
|
+
| `subscriptionMode` | Meaning | App state without an active plan |
|
|
3015
|
+
|---|---|---|
|
|
3016
|
+
| `required` | The app sells its whole function; without a plan it does nothing useful. enyo offers no free option, and cancelling warns that the app stops working. | Reports `SubscriptionRequired`, and `Running` again once a plan is active. |
|
|
3017
|
+
| `optional` | The app has a free base; its plans add features. enyo offers the free base and lets the customer downgrade to it. | Keeps running. **Never** reports `SubscriptionRequired` — gate only the paid features with `isPlanActive()` / `isUnitActive()`. |
|
|
3018
|
+
|
|
3019
|
+
```typescript
|
|
3020
|
+
defineEnergyAppPackage({
|
|
3021
|
+
// ...
|
|
3022
|
+
subscriptionMode: EnyoSubscriptionModeEnum.Optional,
|
|
3023
|
+
subscriptionPlans: [/* price optimisation on top of free basic control */]
|
|
3024
|
+
});
|
|
3025
|
+
```
|
|
3026
|
+
|
|
3027
|
+
In tests, `InMemorySubscriptions` takes the mode as its third constructor argument; `assertEnergyAppState(state)`
|
|
3028
|
+
throws when an `optional` package reports `SubscriptionRequired`. The same check is available as
|
|
3029
|
+
`assertEnergyAppStateMatchesSubscriptionMode(mode, state)`.
|
|
3030
|
+
|
|
2997
3031
|
### Per-unit plans
|
|
2998
3032
|
|
|
2999
|
-
For a `per-unit` plan the price is charged once per unit
|
|
3000
|
-
|
|
3001
|
-
unit has a `type` and a `referenceId` it is paid under:
|
|
3033
|
+
For a `per-unit` plan the price is charged once per unit — 3 units on a yearly plan for 10 € cost 30 € per year
|
|
3034
|
+
(net). Each unit has a `type` and a `referenceId` it is paid under:
|
|
3002
3035
|
|
|
3003
3036
|
| `type` | `referenceId` | Name shown to the customer |
|
|
3004
3037
|
|---|---|---|
|
|
3005
|
-
| `appliance` | the appliance id | taken from the appliance |
|
|
3006
|
-
| `vehicle` | the vehicle id | taken from the vehicle |
|
|
3038
|
+
| `appliance` | the enyo appliance id (`EnyoAppliance.id`) | taken from the appliance |
|
|
3039
|
+
| `vehicle` | the enyo vehicle id (`EnyoVehicle.id` = `EnyoLinkedVehicle.vehicleId`), **never** the vendor's `externalId` | taken from the vehicle |
|
|
3007
3040
|
| `custom` | an id chosen by the app | `name`, translated; `de` and `en` are required |
|
|
3008
3041
|
|
|
3042
|
+
Where a unit comes from depends on its type:
|
|
3043
|
+
|
|
3044
|
+
| Unit | How the app declares it | Who keeps the list |
|
|
3045
|
+
|---|---|---|
|
|
3046
|
+
| Appliance | `billing: { billed: true, planKey }` on the appliance it saves | The host — added on save, removed when the appliance is removed or the mark is cleared |
|
|
3047
|
+
| Vehicle | `billing: { billed: true, planKey }` in the pair result | The host — added on pairing, removed on unpairing |
|
|
3048
|
+
| Custom | `setUnits(planKey, units)` — on boot and whenever one is added or removed | The app |
|
|
3049
|
+
|
|
3050
|
+
Marking appliances and vehicles where they are created keeps the billed units in step with what is actually
|
|
3051
|
+
connected, gives every unit the right enyo id, and lets the host bill a removal by its reason (see below). An app
|
|
3052
|
+
that also creates appliances it doesn't bill, such as a meter next to the heat pump, simply leaves `billing` off
|
|
3053
|
+
them. `onUnpairVehicle` and `listenForApplianceRemoved` then only need to stop serving the unit. `billing.planKey`
|
|
3054
|
+
must name a `per-unit` plan of the package; check it with `validateSubscriptionBilling(billing, plans)`.
|
|
3055
|
+
|
|
3056
|
+
```typescript
|
|
3057
|
+
// A heat pump billed per unit; the meter the app also creates is not billed.
|
|
3058
|
+
await energyApp.useAppliances().save({
|
|
3059
|
+
...heatpump,
|
|
3060
|
+
billing: { billed: true, planKey: 'heatpump-optimisation' }
|
|
3061
|
+
});
|
|
3062
|
+
await energyApp.useAppliances().save(meter);
|
|
3063
|
+
|
|
3064
|
+
// A car billed per unit, marked in the pair result.
|
|
3065
|
+
vehicles.onPairVehicle(async (vehicle, selection) => ({
|
|
3066
|
+
paired: true,
|
|
3067
|
+
externalId: car.id,
|
|
3068
|
+
billing: { billed: true, planKey: 'smartcar-vehicle' }
|
|
3069
|
+
}));
|
|
3070
|
+
```
|
|
3071
|
+
|
|
3072
|
+
For anything else, report custom units yourself. `setUnits()` only takes custom units:
|
|
3073
|
+
|
|
3009
3074
|
```typescript
|
|
3010
3075
|
import { EnyoSubscriptionUnitTypeEnum, isUnitActive } from '@enyo-energy/energy-app-sdk';
|
|
3011
3076
|
|
|
3012
3077
|
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
3078
|
|
|
3021
|
-
// A custom unit carries its own translated name.
|
|
3022
3079
|
await subscriptions.setUnits('room-climate', [{
|
|
3023
3080
|
type: EnyoSubscriptionUnitTypeEnum.Custom,
|
|
3024
3081
|
referenceId: 'living-room',
|
|
3025
3082
|
name: [{ language: 'en', value: 'Living room' }, { language: 'de', value: 'Wohnzimmer' }]
|
|
3026
3083
|
}]);
|
|
3027
3084
|
|
|
3028
|
-
//
|
|
3085
|
+
// Whatever the type: only the units the customer paid for are active.
|
|
3029
3086
|
const all = await subscriptions.getSubscriptions();
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
}
|
|
3087
|
+
setClimateControl('living-room', isUnitActive(all, 'room-climate', { type: 'custom', referenceId: 'living-room' }));
|
|
3088
|
+
setOptimisation(heatpump, isUnitActive(all, 'heatpump-optimisation', { type: 'appliance', referenceId: heatpump.id }));
|
|
3033
3089
|
```
|
|
3034
3090
|
|
|
3091
|
+
`getUnits(planKey)` returns all units of a plan — the billed appliances and vehicles plus the custom units.
|
|
3092
|
+
|
|
3035
3093
|
- **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.
|
|
3094
|
+
for more units, or a unit is removed, the change listeners are called.
|
|
3037
3095
|
- **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
3096
|
- `validateSubscriptionUnits(units)` applies the host's rules up front: known type, non-empty reference, no unit
|
|
3040
3097
|
twice, `de` and `en` names on custom units.
|
|
3041
3098
|
|
|
3099
|
+
#### What a removal means for billing
|
|
3100
|
+
|
|
3101
|
+
The host decides, never the app's unit list:
|
|
3102
|
+
|
|
3103
|
+
| Unit leaves because… | Billing |
|
|
3104
|
+
|---|---|
|
|
3105
|
+
| The customer confirmed „Trennen" / „Entfernen" in the enyo app (vehicle unpaired by the user, appliance removed by the user) | The quantity drops at the next renewal; removing the last unit cancels the subscription at the end of the period. Nothing is refunded. |
|
|
3106
|
+
| The vehicle was deleted in the enyo app | Same as above. |
|
|
3107
|
+
| The app unlinked the car because it is gone from the vendor account — `unpairVehicle(id, EnyoVehiclePackageUnpairReasonEnum.VehicleNotFound)` | Same as above. |
|
|
3108
|
+
| Vendor sign-out | **Unchanged.** Usually followed by signing in again; a paid unit stays paid. |
|
|
3109
|
+
| The app unlinked the car because the vendor revoked access — `unpairVehicle(id, EnyoVehiclePackageUnpairReasonEnum.AccessRevoked)` | Unchanged — access is usually granted again. |
|
|
3110
|
+
| Unknown reason, `unpairVehicle()` without a reason, or the app's own report shrinks | Unchanged. |
|
|
3111
|
+
|
|
3112
|
+
A shorter unit list alone — an expired token, an app bug — never cancels anything a customer paid for. Every
|
|
3113
|
+
change takes effect at the next renewal or at the end of the paid period; there are no pro-rata refunds.
|
|
3114
|
+
|
|
3042
3115
|
**A price change needs a new release** and only applies to new purchases — running subscriptions keep the price
|
|
3043
3116
|
they were bought at.
|
|
3044
3117
|
|
|
@@ -3049,10 +3122,10 @@ const { ok, errors } = validatePackageSubscriptionPlans(packageDef);
|
|
|
3049
3122
|
// or: assertValidPackageSubscriptionPlans(packageDef) — throws PackageSubscriptionPlansValidationError
|
|
3050
3123
|
```
|
|
3051
3124
|
|
|
3052
|
-
It checks that plan keys are unique slugs, that the payment interval and
|
|
3053
|
-
|
|
3054
|
-
`one-time` plan, that `de` and
|
|
3055
|
-
|
|
3125
|
+
It checks that `subscriptionMode` is declared, that plan keys are unique slugs, that the payment interval and
|
|
3126
|
+
pricing model are known, that every plan has at least one positive price per currency with at most two decimals,
|
|
3127
|
+
that `trialDays` is a positive whole number and not set on a `one-time` plan, that `de` and `en` store entries
|
|
3128
|
+
exist, and that the package requests the `Subscriptions` permission. A `subscriptionMode` without plans is a warning.
|
|
3056
3129
|
|
|
3057
3130
|
## Printable Manual Setup Guide
|
|
3058
3131
|
|
|
@@ -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, EnyoSubscriptionPricingModel } from "./types/enyo-subscription.cjs";
|
|
5
|
+
import type { EnyoSubscriptionMode, 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",
|
|
@@ -336,8 +336,10 @@ export interface EnergyAppPackageSubscriptionPlan {
|
|
|
336
336
|
/**
|
|
337
337
|
* Whether the price is charged once for the package on the device
|
|
338
338
|
* (`per-package`, the default) or once per unit (`per-unit`). For
|
|
339
|
-
* `per-unit` the
|
|
340
|
-
*
|
|
339
|
+
* `per-unit` the units are the appliances and vehicles the app marks as
|
|
340
|
+
* billed under this plan ({@link EnyoAppliance.billing},
|
|
341
|
+
* {@link EnyoVehiclePairResult.billing}), plus the custom units it reports
|
|
342
|
+
* with {@link EnergyAppSubscriptions.setUnits}.
|
|
341
343
|
*/
|
|
342
344
|
pricingModel?: EnyoSubscriptionPricingModel;
|
|
343
345
|
/**
|
|
@@ -890,6 +892,20 @@ export interface EnergyAppPackageDefinition {
|
|
|
890
892
|
* Omit for packages that are free.
|
|
891
893
|
*/
|
|
892
894
|
subscriptionPlans?: EnergyAppPackageSubscriptionPlan[];
|
|
895
|
+
/**
|
|
896
|
+
* Whether the app needs one of its plans to work at all.
|
|
897
|
+
*
|
|
898
|
+
* - `'required'`: without an active plan the app does nothing useful and
|
|
899
|
+
* reports {@link EnergyAppStateEnum.SubscriptionRequired}. enyo offers no
|
|
900
|
+
* free option, and cancelling warns that the app stops working.
|
|
901
|
+
* - `'optional'`: the app has a free base; its plans add features. The app
|
|
902
|
+
* never reports `SubscriptionRequired`. enyo offers a free base option
|
|
903
|
+
* and lets the customer downgrade to it.
|
|
904
|
+
*
|
|
905
|
+
* Required as soon as {@link subscriptionPlans} is set; omit it for free
|
|
906
|
+
* packages.
|
|
907
|
+
*/
|
|
908
|
+
subscriptionMode?: EnyoSubscriptionMode;
|
|
893
909
|
}
|
|
894
910
|
/**
|
|
895
911
|
* Defines an Energy App package with automatic SDK version injection.
|
|
@@ -9,6 +9,12 @@ var EnergyAppStateEnum;
|
|
|
9
9
|
EnergyAppStateEnum["ConfigurationRequired"] = "configuration-required";
|
|
10
10
|
/** This state tells the user that an internet connection of the system is required which blocks the energy app from running*/
|
|
11
11
|
EnergyAppStateEnum["InternetConnectionRequired"] = "internet-connection-required";
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* This state tells the user that a subscription plan is required which blocks the energy app from running. The host shows the plans declared in the package definition.
|
|
14
|
+
*
|
|
15
|
+
* Only for packages with `subscriptionMode: 'required'`: such an app reports this state while it has no active
|
|
16
|
+
* plan, and returns to `Running` once one is active. An app with `subscriptionMode: 'optional'` must never report
|
|
17
|
+
* it — it keeps its free base running and gates only the paid features with `isPlanActive()` / `isUnitActive()`.
|
|
18
|
+
*/
|
|
13
19
|
EnergyAppStateEnum["SubscriptionRequired"] = "subscription-required";
|
|
14
20
|
})(EnergyAppStateEnum || (exports.EnergyAppStateEnum = EnergyAppStateEnum = {}));
|
|
@@ -60,7 +60,13 @@ export declare enum EnergyAppStateEnum {
|
|
|
60
60
|
ConfigurationRequired = "configuration-required",
|
|
61
61
|
/** This state tells the user that an internet connection of the system is required which blocks the energy app from running*/
|
|
62
62
|
InternetConnectionRequired = "internet-connection-required",
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* This state tells the user that a subscription plan is required which blocks the energy app from running. The host shows the plans declared in the package definition.
|
|
65
|
+
*
|
|
66
|
+
* Only for packages with `subscriptionMode: 'required'`: such an app reports this state while it has no active
|
|
67
|
+
* plan, and returns to `Running` once one is active. An app with `subscriptionMode: 'optional'` must never report
|
|
68
|
+
* it — it keeps its free base running and gates only the paid features with `isPlanActive()` / `isUnitActive()`.
|
|
69
|
+
*/
|
|
64
70
|
SubscriptionRequired = "subscription-required"
|
|
65
71
|
}
|
|
66
72
|
/**
|
|
@@ -11,7 +11,9 @@ const subscription_plan_validators_js_1 = require("./subscription-plan-validator
|
|
|
11
11
|
* {@link upsertSubscription} simulates a purchase, renewal, cancellation,
|
|
12
12
|
* failed payment or expiry and notifies the registered listeners exactly like
|
|
13
13
|
* the host would. Units reported with {@link setUnits} are kept per plan and
|
|
14
|
-
* can be read back with {@link getUnits}
|
|
14
|
+
* can be read back with {@link getUnits}, together with the billed appliance
|
|
15
|
+
* and vehicle units a test sets with {@link setBilledUnits}. {@link assertEnergyAppState}
|
|
16
|
+
* checks a reported app state against the package's subscription mode.
|
|
15
17
|
*
|
|
16
18
|
* @example
|
|
17
19
|
* ```typescript
|
|
@@ -34,52 +36,92 @@ const subscription_plan_validators_js_1 = require("./subscription-plan-validator
|
|
|
34
36
|
*/
|
|
35
37
|
class InMemorySubscriptions {
|
|
36
38
|
plans;
|
|
39
|
+
subscriptionMode;
|
|
37
40
|
subscriptions = new Map();
|
|
38
41
|
listeners = new Map();
|
|
39
|
-
|
|
42
|
+
customUnitsByPlan = new Map();
|
|
43
|
+
billedUnitsByPlan = new Map();
|
|
40
44
|
nextListenerId = 1;
|
|
41
45
|
/**
|
|
42
46
|
* @param initialSubscriptions - Subscriptions that exist before the app
|
|
43
47
|
* starts. They do not trigger listeners.
|
|
44
|
-
* @param plans - Optional plans of the package. When given,
|
|
45
|
-
*
|
|
46
|
-
* does. When omitted, every
|
|
48
|
+
* @param plans - Optional plans of the package. When given,
|
|
49
|
+
* {@link setUnits} and {@link setBilledUnits} reject plan keys that are
|
|
50
|
+
* not declared as `per-unit`, like the host does. When omitted, every
|
|
51
|
+
* plan key is accepted.
|
|
52
|
+
* @param subscriptionMode - Optional subscription mode of the package,
|
|
53
|
+
* checked by {@link assertEnergyAppState}.
|
|
47
54
|
*/
|
|
48
|
-
constructor(initialSubscriptions = [], plans) {
|
|
55
|
+
constructor(initialSubscriptions = [], plans, subscriptionMode) {
|
|
49
56
|
this.plans = plans;
|
|
57
|
+
this.subscriptionMode = subscriptionMode;
|
|
50
58
|
for (const subscription of initialSubscriptions) {
|
|
51
59
|
this.subscriptions.set(subscription.subscriptionId, structuredClone(subscription));
|
|
52
60
|
}
|
|
53
61
|
}
|
|
54
62
|
/**
|
|
55
|
-
* Stores the units reported for a `per-unit` plan, replacing
|
|
63
|
+
* Stores the custom units reported for a `per-unit` plan, replacing
|
|
64
|
+
* earlier ones.
|
|
56
65
|
*
|
|
57
66
|
* @param planKey - The key of the plan.
|
|
58
|
-
* @param units - Every unit the app currently manages for the plan.
|
|
67
|
+
* @param units - Every custom unit the app currently manages for the plan.
|
|
59
68
|
* @returns Promise that resolves once the units are stored
|
|
60
|
-
* @throws {Error} If
|
|
61
|
-
* `planKey` is not a `per-unit` plan among them.
|
|
69
|
+
* @throws {Error} If a unit is not a custom unit or invalid, or `plans`
|
|
70
|
+
* were given and `planKey` is not a `per-unit` plan among them.
|
|
62
71
|
*/
|
|
63
72
|
async setUnits(planKey, units) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
73
|
+
this.assertPerUnitPlan(planKey);
|
|
74
|
+
const notCustom = units.filter(unit => unit.type !== enyo_subscription_js_1.EnyoSubscriptionUnitTypeEnum.Custom);
|
|
75
|
+
if (notCustom.length > 0) {
|
|
76
|
+
throw new Error(`setUnits() only takes custom units — mark appliances and vehicles with \`billing\` instead (got ${notCustom.map(unit => unit.type).join(', ')}).`);
|
|
69
77
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
this.assertValidUnits(units);
|
|
79
|
+
this.customUnitsByPlan.set(planKey, units.map(unit => structuredClone(unit)));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Test helper: sets the appliance and vehicle units the host keeps for a
|
|
83
|
+
* plan from their billing marks, replacing earlier ones — as if the app
|
|
84
|
+
* had saved those appliances or paired those vehicles with
|
|
85
|
+
* `billing: {billed: true, planKey}`.
|
|
86
|
+
*
|
|
87
|
+
* @param planKey - The key of the plan.
|
|
88
|
+
* @param units - The billed appliance and vehicle units.
|
|
89
|
+
* @returns Promise that resolves once the units are stored
|
|
90
|
+
* @throws {Error} If a unit is a custom unit or invalid, or `plans` were
|
|
91
|
+
* given and `planKey` is not a `per-unit` plan among them.
|
|
92
|
+
*/
|
|
93
|
+
async setBilledUnits(planKey, units) {
|
|
94
|
+
this.assertPerUnitPlan(planKey);
|
|
95
|
+
if (units.some(unit => unit.type === enyo_subscription_js_1.EnyoSubscriptionUnitTypeEnum.Custom)) {
|
|
96
|
+
throw new Error('setBilledUnits() only takes appliance and vehicle units — report custom units with setUnits().');
|
|
97
|
+
}
|
|
98
|
+
this.assertValidUnits(units);
|
|
99
|
+
this.billedUnitsByPlan.set(planKey, units.map(unit => structuredClone(unit)));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Test helper: checks an app state against the subscription mode given to
|
|
103
|
+
* the constructor. Call it wherever the app under test reports its state.
|
|
104
|
+
*
|
|
105
|
+
* @param state - The state the app reports.
|
|
106
|
+
* @throws {Error} When the package's subscription mode is `optional` and
|
|
107
|
+
* the state is `subscription-required`.
|
|
108
|
+
*/
|
|
109
|
+
assertEnergyAppState(state) {
|
|
110
|
+
(0, subscription_plan_validators_js_1.assertEnergyAppStateMatchesSubscriptionMode)(this.subscriptionMode, state);
|
|
74
111
|
}
|
|
75
112
|
/**
|
|
76
|
-
* Returns copies of the units
|
|
113
|
+
* Returns copies of the current units of a plan: the billed appliances and
|
|
114
|
+
* vehicles set with {@link setBilledUnits}, followed by the custom units
|
|
115
|
+
* reported with {@link setUnits}.
|
|
77
116
|
*
|
|
78
117
|
* @param planKey - The key of the plan.
|
|
79
118
|
* @returns Promise that resolves to the units; an empty list when none were reported
|
|
80
119
|
*/
|
|
81
120
|
async getUnits(planKey) {
|
|
82
|
-
return
|
|
121
|
+
return [
|
|
122
|
+
...(this.billedUnitsByPlan.get(planKey) ?? []),
|
|
123
|
+
...(this.customUnitsByPlan.get(planKey) ?? []),
|
|
124
|
+
].map(unit => structuredClone(unit));
|
|
83
125
|
}
|
|
84
126
|
/**
|
|
85
127
|
* Returns copies of all stored subscriptions.
|
|
@@ -130,5 +172,29 @@ class InMemorySubscriptions {
|
|
|
130
172
|
await listener({ subscription: structuredClone(subscription), previousStatus });
|
|
131
173
|
}
|
|
132
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Throws when `plans` were given and `planKey` is not a `per-unit` plan
|
|
177
|
+
* among them.
|
|
178
|
+
*
|
|
179
|
+
* @param planKey - The key of the plan.
|
|
180
|
+
*/
|
|
181
|
+
assertPerUnitPlan(planKey) {
|
|
182
|
+
if (!this.plans)
|
|
183
|
+
return;
|
|
184
|
+
const plan = this.plans.find(candidate => candidate.planKey === planKey);
|
|
185
|
+
if (plan?.pricingModel !== enyo_subscription_js_1.EnyoSubscriptionPricingModelEnum.PerUnit) {
|
|
186
|
+
throw new Error(`Plan "${planKey}" is not a per-unit plan of this package.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Throws when the units break the host's rules.
|
|
191
|
+
*
|
|
192
|
+
* @param units - The units to check.
|
|
193
|
+
*/
|
|
194
|
+
assertValidUnits(units) {
|
|
195
|
+
const { ok, errors } = (0, subscription_plan_validators_js_1.validateSubscriptionUnits)(units);
|
|
196
|
+
if (!ok)
|
|
197
|
+
throw new Error(`Invalid subscription units:\n- ${errors.join('\n- ')}`);
|
|
198
|
+
}
|
|
133
199
|
}
|
|
134
200
|
exports.InMemorySubscriptions = InMemorySubscriptions;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EnergyAppPackageSubscriptionPlan } from '../../energy-app-package-definition.cjs';
|
|
2
|
+
import type { EnergyAppStateEnum } from '../../enyo-energy-app-sdk.cjs';
|
|
2
3
|
import type { EnergyAppSubscriptions } from '../../packages/energy-app-subscriptions.cjs';
|
|
3
|
-
import { EnyoSubscription, EnyoSubscriptionChangeListener, EnyoSubscriptionUnit } from '../../types/enyo-subscription.cjs';
|
|
4
|
+
import { EnyoSubscription, EnyoSubscriptionChangeListener, EnyoSubscriptionMode, EnyoSubscriptionCustomUnit, EnyoSubscriptionUnit } from '../../types/enyo-subscription.cjs';
|
|
4
5
|
/**
|
|
5
6
|
* In-memory implementation of {@link EnergyAppSubscriptions} for tests and
|
|
6
7
|
* local development.
|
|
@@ -9,7 +10,9 @@ import { EnyoSubscription, EnyoSubscriptionChangeListener, EnyoSubscriptionUnit
|
|
|
9
10
|
* {@link upsertSubscription} simulates a purchase, renewal, cancellation,
|
|
10
11
|
* failed payment or expiry and notifies the registered listeners exactly like
|
|
11
12
|
* the host would. Units reported with {@link setUnits} are kept per plan and
|
|
12
|
-
* can be read back with {@link getUnits}
|
|
13
|
+
* can be read back with {@link getUnits}, together with the billed appliance
|
|
14
|
+
* and vehicle units a test sets with {@link setBilledUnits}. {@link assertEnergyAppState}
|
|
15
|
+
* checks a reported app state against the package's subscription mode.
|
|
13
16
|
*
|
|
14
17
|
* @example
|
|
15
18
|
* ```typescript
|
|
@@ -32,30 +35,60 @@ import { EnyoSubscription, EnyoSubscriptionChangeListener, EnyoSubscriptionUnit
|
|
|
32
35
|
*/
|
|
33
36
|
export declare class InMemorySubscriptions implements EnergyAppSubscriptions {
|
|
34
37
|
private readonly plans?;
|
|
38
|
+
private readonly subscriptionMode?;
|
|
35
39
|
private readonly subscriptions;
|
|
36
40
|
private readonly listeners;
|
|
37
|
-
private readonly
|
|
41
|
+
private readonly customUnitsByPlan;
|
|
42
|
+
private readonly billedUnitsByPlan;
|
|
38
43
|
private nextListenerId;
|
|
39
44
|
/**
|
|
40
45
|
* @param initialSubscriptions - Subscriptions that exist before the app
|
|
41
46
|
* starts. They do not trigger listeners.
|
|
42
|
-
* @param plans - Optional plans of the package. When given,
|
|
43
|
-
*
|
|
44
|
-
* does. When omitted, every
|
|
47
|
+
* @param plans - Optional plans of the package. When given,
|
|
48
|
+
* {@link setUnits} and {@link setBilledUnits} reject plan keys that are
|
|
49
|
+
* not declared as `per-unit`, like the host does. When omitted, every
|
|
50
|
+
* plan key is accepted.
|
|
51
|
+
* @param subscriptionMode - Optional subscription mode of the package,
|
|
52
|
+
* checked by {@link assertEnergyAppState}.
|
|
45
53
|
*/
|
|
46
|
-
constructor(initialSubscriptions?: EnyoSubscription[], plans?: EnergyAppPackageSubscriptionPlan[] | undefined);
|
|
54
|
+
constructor(initialSubscriptions?: EnyoSubscription[], plans?: EnergyAppPackageSubscriptionPlan[] | undefined, subscriptionMode?: EnyoSubscriptionMode | undefined);
|
|
47
55
|
/**
|
|
48
|
-
* Stores the units reported for a `per-unit` plan, replacing
|
|
56
|
+
* Stores the custom units reported for a `per-unit` plan, replacing
|
|
57
|
+
* earlier ones.
|
|
49
58
|
*
|
|
50
59
|
* @param planKey - The key of the plan.
|
|
51
|
-
* @param units - Every unit the app currently manages for the plan.
|
|
60
|
+
* @param units - Every custom unit the app currently manages for the plan.
|
|
52
61
|
* @returns Promise that resolves once the units are stored
|
|
53
|
-
* @throws {Error} If
|
|
54
|
-
* `planKey` is not a `per-unit` plan among them.
|
|
62
|
+
* @throws {Error} If a unit is not a custom unit or invalid, or `plans`
|
|
63
|
+
* were given and `planKey` is not a `per-unit` plan among them.
|
|
55
64
|
*/
|
|
56
|
-
setUnits(planKey: string, units:
|
|
65
|
+
setUnits(planKey: string, units: EnyoSubscriptionCustomUnit[]): Promise<void>;
|
|
57
66
|
/**
|
|
58
|
-
*
|
|
67
|
+
* Test helper: sets the appliance and vehicle units the host keeps for a
|
|
68
|
+
* plan from their billing marks, replacing earlier ones — as if the app
|
|
69
|
+
* had saved those appliances or paired those vehicles with
|
|
70
|
+
* `billing: {billed: true, planKey}`.
|
|
71
|
+
*
|
|
72
|
+
* @param planKey - The key of the plan.
|
|
73
|
+
* @param units - The billed appliance and vehicle units.
|
|
74
|
+
* @returns Promise that resolves once the units are stored
|
|
75
|
+
* @throws {Error} If a unit is a custom unit or invalid, or `plans` were
|
|
76
|
+
* given and `planKey` is not a `per-unit` plan among them.
|
|
77
|
+
*/
|
|
78
|
+
setBilledUnits(planKey: string, units: EnyoSubscriptionUnit[]): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Test helper: checks an app state against the subscription mode given to
|
|
81
|
+
* the constructor. Call it wherever the app under test reports its state.
|
|
82
|
+
*
|
|
83
|
+
* @param state - The state the app reports.
|
|
84
|
+
* @throws {Error} When the package's subscription mode is `optional` and
|
|
85
|
+
* the state is `subscription-required`.
|
|
86
|
+
*/
|
|
87
|
+
assertEnergyAppState(state: EnergyAppStateEnum | `${EnergyAppStateEnum}`): void;
|
|
88
|
+
/**
|
|
89
|
+
* Returns copies of the current units of a plan: the billed appliances and
|
|
90
|
+
* vehicles set with {@link setBilledUnits}, followed by the custom units
|
|
91
|
+
* reported with {@link setUnits}.
|
|
59
92
|
*
|
|
60
93
|
* @param planKey - The key of the plan.
|
|
61
94
|
* @returns Promise that resolves to the units; an empty list when none were reported
|
|
@@ -94,4 +127,17 @@ export declare class InMemorySubscriptions implements EnergyAppSubscriptions {
|
|
|
94
127
|
* @returns Promise that resolves once every listener has completed
|
|
95
128
|
*/
|
|
96
129
|
upsertSubscription(subscription: EnyoSubscription): Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* Throws when `plans` were given and `planKey` is not a `per-unit` plan
|
|
132
|
+
* among them.
|
|
133
|
+
*
|
|
134
|
+
* @param planKey - The key of the plan.
|
|
135
|
+
*/
|
|
136
|
+
private assertPerUnitPlan;
|
|
137
|
+
/**
|
|
138
|
+
* Throws when the units break the host's rules.
|
|
139
|
+
*
|
|
140
|
+
* @param units - The units to check.
|
|
141
|
+
*/
|
|
142
|
+
private assertValidUnits;
|
|
97
143
|
}
|