@zeniai/client-epic-state 5.2.59 → 5.2.60

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.
@@ -56,6 +56,98 @@ const isShippingAddressUnsaved = (state, id, savedShippingAddressId) => {
56
56
  pending.country === saved.country &&
57
57
  (pending.countryCode ?? '') === (saved.countryCode ?? ''));
58
58
  };
59
+ /**
60
+ * The add-ons / one-time charges a plan actually accepts.
61
+ *
62
+ * A catalog item is either `restricted` — it names the item ids it may be
63
+ * subscribed alongside in `applicableItems` — or unrestricted, in which case
64
+ * every item is fair game. Matching is on `itemID` (the catalog item the price
65
+ * row was flattened from), not the price code, because applicability is
66
+ * declared item-to-item.
67
+ *
68
+ * Fails open where the rule is unknown: no plan selected yet, a plan that is
69
+ * not `restricted`, or a `restricted` plan whose allow-list never came down all
70
+ * return the full list. The create/update endpoint rejects a disallowed item
71
+ * with a 422 regardless, so offering one too many costs a server error the user
72
+ * already gets today, where hiding one too few makes a legitimate add-on
73
+ * unreachable with nothing on screen to explain why.
74
+ *
75
+ * An allow-list that arrived *empty* is not unknown — a restricted item is
76
+ * saved with `applicable_items: []` when nothing was picked — so that narrows
77
+ * to nothing, as the server does.
78
+ *
79
+ * Lives here rather than in a screen so the create/edit form and the detail
80
+ * page's inline edit narrow their pickers by the same rule.
81
+ */
82
+ export const getInvoicingItemsAllowedForPlan = (products, plan) => {
83
+ const allowedItemIDs = plan?.applicableItems;
84
+ if (plan?.itemApplicability !== 'restricted' || allowedItemIDs == null) {
85
+ return products;
86
+ }
87
+ return products.filter((product) => product.itemID != null && allowedItemIDs.includes(product.itemID));
88
+ };
89
+ /**
90
+ * A price row can only be subscribed by its price code, so one without a code
91
+ * is not an option the form can offer.
92
+ */
93
+ const hasInvoicingPriceCode = (product) => product.priceCode != null && product.priceCode.trim() !== '';
94
+ /**
95
+ * `displayPrice` is the service-computed single-unit amount. Tiered, volume and
96
+ * stairstep price points store `price` as `0` because the money lives in their
97
+ * tiers, so `price` is only a fallback for responses predating `displayPrice`.
98
+ */
99
+ const invoicingUnitPrice = (product) => product.displayPrice ?? product.price;
100
+ const toInvoicingPlanOption = (plan) => ({
101
+ billingCycles: plan.billingCycles,
102
+ // The price point's own cadence: the backend derives billing period from the
103
+ // price point and rejects a subscription that claims a different one.
104
+ billingPeriod: plan.period,
105
+ billingPeriodUnit: plan.periodUnit,
106
+ label: plan.name ?? plan.id,
107
+ price: invoicingUnitPrice(plan),
108
+ productCode: plan.productCode,
109
+ trialPeriod: plan.trialPeriod,
110
+ trialPeriodUnit: plan.trialPeriodUnit,
111
+ value: plan.priceCode,
112
+ });
113
+ const toInvoicingCatalogItemOption = (product) => ({
114
+ // A one-time charge has no cadence, so its price is quoted flat rather than
115
+ // per period.
116
+ billingPeriodUnit: product.itemType === 'charge' ? undefined : product.periodUnit,
117
+ label: product.name ?? product.id,
118
+ price: invoicingUnitPrice(product),
119
+ productCode: product.productCode,
120
+ value: product.priceCode,
121
+ });
122
+ const toInvoicingCouponOption = (coupon) => ({
123
+ label: coupon.name ?? coupon.couponCode ?? coupon.id,
124
+ // Create/apply/remove coupon APIs resolve by coupon_code.
125
+ value: coupon.couponCode ?? coupon.id,
126
+ });
127
+ /**
128
+ * Company first, then the person's name, then the email — whichever the
129
+ * customer actually has. The id is the last resort so a record with no
130
+ * identifying field at all is still selectable rather than a blank row.
131
+ */
132
+ const invoicingCustomerDisplayName = (customer) => {
133
+ const fullName = [customer.firstName, customer.lastName]
134
+ .filter((part) => part != null && part.trim() !== '')
135
+ .join(' ')
136
+ .trim();
137
+ const company = customer.company?.trim() ?? '';
138
+ const email = customer.email?.trim() ?? '';
139
+ return company !== ''
140
+ ? company
141
+ : fullName !== ''
142
+ ? fullName
143
+ : email !== ''
144
+ ? email
145
+ : customer.id;
146
+ };
147
+ const toInvoicingCustomerOption = (customer) => ({
148
+ label: invoicingCustomerDisplayName(customer),
149
+ value: customer.id,
150
+ });
59
151
  export const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID) => {
60
152
  const { editDraftById, error, fetchState, newSubscriptionDraft, savedSubscriptionId, } = state.editInvoicingSubscriptionDetailViewState;
61
153
  const mode = invoicingSubscriptionID != null ? 'edit' : 'create';
@@ -77,8 +169,15 @@ export const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID)
77
169
  : undefined;
78
170
  const hasUnsavedShippingAddress = isShippingAddressUnsaved(state, invoicingSubscriptionID, subscription?.shippingAddressId);
79
171
  const catalogListView = getInvoicingCatalogListView(state);
80
- const planPrices = getInvoicingCatalogPrices(catalogListView.plans);
81
- const addonAndChargePrices = getInvoicingCatalogPrices(catalogListView.products);
172
+ const planPrices = getInvoicingCatalogPrices(catalogListView.plans).filter(hasInvoicingPriceCode);
173
+ // A restricted plan names the catalog items it may be sold with, so the
174
+ // pickers offer only those: subscription create rejects the rest with a 422,
175
+ // and an option that can only fail is not a choice. The applicability rides
176
+ // on the price rows already in the store, so this is a join, not a refetch.
177
+ const allowedCatalogPrices = getInvoicingItemsAllowedForPlan(getInvoicingCatalogPrices(catalogListView.products), planPrices.find((plan) => plan.priceCode === localData.planPriceCode)).filter(hasInvoicingPriceCode);
178
+ const catalogItemOptionsOfType = (itemType) => allowedCatalogPrices
179
+ .filter((product) => product.itemType === itemType)
180
+ .map(toInvoicingCatalogItemOption);
82
181
  // In create mode there is nothing to load, so the record state only applies
83
182
  // when editing an existing subscription.
84
183
  const recordState = invoicingSubscriptionID != null
@@ -100,12 +199,13 @@ export const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID)
100
199
  recordState,
101
200
  isFormLoading: isInvoicingFormDraftPending(editDraftById, invoicingSubscriptionID),
102
201
  fieldHelp: invoicingFieldHelpForGroup(state, 'subscription'),
103
- coupons: getInvoicingCouponListItems(state),
104
- customers: getInvoicingCustomerListView(state).customers,
202
+ addons: catalogItemOptionsOfType('addon'),
203
+ charges: catalogItemOptionsOfType('charge'),
204
+ coupons: getInvoicingCouponListItems(state).map(toInvoicingCouponOption),
205
+ customers: getInvoicingCustomerListView(state).customers.map(toInvoicingCustomerOption),
105
206
  localData,
106
207
  mode,
107
- plans: planPrices,
108
- products: addonAndChargePrices,
208
+ plans: planPrices.map(toInvoicingPlanOption),
109
209
  savedSubscriptionId,
110
210
  shippingAddressLabel,
111
211
  hasUnsavedShippingAddress,
@@ -2,8 +2,20 @@ import { createSelector } from '@reduxjs/toolkit';
2
2
  import { getInvoicingPlansByIds } from '../../../entity/invoicing/plan/planSelector';
3
3
  import { runningInvoicingActionId } from '../runningInvoicingActionId';
4
4
  import { INVOICING_PRODUCT_COLUMNS, } from './invoicingCatalogListViewState';
5
+ /**
6
+ * Flatten catalog items into the price rows the pickers render.
7
+ *
8
+ * The parent item's `applicableItems` / `itemApplicability` are carried onto
9
+ * every row it produces: applicability is a property of the catalog item, not
10
+ * of its price points, and a row that has lost it cannot be matched against the
11
+ * selected plan's allow-list (see `getInvoicingItemsAllowedForPlan`). Both are
12
+ * written after the spread so the item's own rule wins over any stale copy a
13
+ * price point payload may carry.
14
+ */
5
15
  export const getInvoicingCatalogPrices = (items) => items.flatMap((item) => (item.subFamilies ?? []).map((subFamily) => ({
6
16
  ...subFamily,
17
+ applicableItems: item.applicableItems,
18
+ itemApplicability: item.itemApplicability,
7
19
  itemID: item.id,
8
20
  itemType: item.type ?? item.itemType,
9
21
  name: subFamily.name,
package/lib/index.d.ts CHANGED
@@ -1132,7 +1132,7 @@ export type { SubmitInvoicingSubscriptionFormPayload, UpdateInvoicingSubscriptio
1132
1132
  export type { InvoicingSubscriptionFormLocalData } from './view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewState';
1133
1133
  export { INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE, entityToInvoicingSubscriptionFormLocalData, initialInvoicingSubscriptionFormLocalData, invoicingSubscriptionTodayIso, toInvoicingSubscriptionQuantity, } from './view/invoicing/editInvoicingSubscriptionDetailView/invoicingSubscriptionFormConfig';
1134
1134
  export { getEditInvoicingSubscriptionDetailViewState, getInvoicingSubscriptionFormView, } from './view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector';
1135
- export type { EditInvoicingSubscriptionDetailView, InvoicingSubscriptionFormView, } from './view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector';
1135
+ export type { EditInvoicingSubscriptionDetailView, InvoicingSubscriptionFormView, InvoicingSubscriptionOption, } from './view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector';
1136
1136
  export { resetEditInvoicingCatalogItemDetailView, submitInvoicingCatalogItemForm, updateInvoicingCatalogItemFormDraft, } from './view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer';
1137
1137
  export type { SubmitInvoicingCatalogItemFormPayload, UpdateInvoicingCatalogItemFormDraftPayload, } from './view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer';
1138
1138
  export type { InvoicingCatalogAccountingLookup } from './view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload';
@@ -1,8 +1,7 @@
1
+ import { Amount } from '../../../commonStateTypes/amount';
1
2
  import { FetchStateAndError, ID } from '../../../commonStateTypes/common';
2
3
  import { SelectorView } from '../../../commonStateTypes/viewAndReport/viewAndReport';
3
- import { InvoicingCoupon } from '../../../entity/invoicing/coupon/couponState';
4
4
  import { InvoicingFieldHelpByCode, InvoicingNamedOption, InvoicingSubscriptionLifecycleActionOption } from '../../../entity/invoicing/invoicingCommonPayload';
5
- import { InvoicingCustomer } from '../../../entity/invoicing/invoicingCustomer/invoicingCustomerState';
6
5
  import { InvoicingSubscription } from '../../../entity/invoicing/invoicingSubscription/invoicingSubscriptionState';
7
6
  import { InvoicingProduct } from '../../../entity/invoicing/product/productState';
8
7
  import { RootState } from '../../../rootStateTypes';
@@ -25,21 +24,25 @@ export interface EditInvoicingSubscriptionDetailView extends SelectorView {
25
24
  * The full data the subscription form screen needs in one selector: the draft
26
25
  * `localData` (seeded from the existing subscription in edit mode until the user
27
26
  * edits), the shipping-address label (from the shared Address screen), the
28
- * canonical billing-frequency value set, the raw option lists (customers, plans,
29
- * products, coupons), and the save state. The screen maps the raw lists/values
30
- * to its own option shapes, so this stays free of any web-components dependency.
27
+ * canonical billing-frequency value set, the picker option lists (customers,
28
+ * plans, add-ons, charges, coupons) and the save state. The options are plain
29
+ * `{label, value}` records, so this stays free of any web-components
30
+ * dependency while leaving the screen nothing to derive.
31
31
  */
32
32
  export interface InvoicingSubscriptionFormView extends SelectorView {
33
33
  /** Lifecycle action in flight, folded in from the record view. */
34
34
  actionState: FetchStateAndError;
35
- /** Cancel reasons from `/1.0/config`, so the dialog hardcodes nothing. */
36
- cancelReasonCodeOptions: InvoicingNamedOption[];
37
- coupons: InvoicingCoupon[];
38
- customers: InvoicingCustomer[];
39
35
  /**
40
- * Backend help copy for this screen's field group, keyed by field code.
41
- * Folded in here so the screen never reaches for the config view itself.
36
+ * Picker options, built here rather than by each screen so the create form
37
+ * and the detail page's inline edit cannot drift. `addons` and `charges` are
38
+ * already narrowed to what the selected plan allows.
42
39
  */
40
+ addons: InvoicingSubscriptionOption[];
41
+ /** Cancel reasons from `/1.0/config`, so the dialog hardcodes nothing. */
42
+ cancelReasonCodeOptions: InvoicingNamedOption[];
43
+ charges: InvoicingSubscriptionOption[];
44
+ coupons: InvoicingSubscriptionOption[];
45
+ customers: InvoicingSubscriptionOption[];
43
46
  fieldHelp: InvoicingFieldHelpByCode;
44
47
  /**
45
48
  * True while the subscription being edited is still on its way to the form.
@@ -63,8 +66,7 @@ export interface InvoicingSubscriptionFormView extends SelectorView {
63
66
  * real values.
64
67
  */
65
68
  pageFetchState: FetchStateAndError;
66
- plans: InvoicingProduct[];
67
- products: InvoicingProduct[];
69
+ plans: InvoicingSubscriptionOption[];
68
70
  recordState: FetchStateAndError;
69
71
  /** Config-owned edit/lifecycle rules, folded in so the screen reads one selector. */
70
72
  subscriptionCommercialEditAllowedFromStatuses: string[];
@@ -82,4 +84,53 @@ export interface InvoicingSubscriptionFormView extends SelectorView {
82
84
  subscription?: InvoicingSubscription;
83
85
  }
84
86
  export declare const getEditInvoicingSubscriptionDetailViewState: (state: RootState, invoicingSubscriptionID?: ID) => EditInvoicingSubscriptionDetailView;
87
+ /**
88
+ * The add-ons / one-time charges a plan actually accepts.
89
+ *
90
+ * A catalog item is either `restricted` — it names the item ids it may be
91
+ * subscribed alongside in `applicableItems` — or unrestricted, in which case
92
+ * every item is fair game. Matching is on `itemID` (the catalog item the price
93
+ * row was flattened from), not the price code, because applicability is
94
+ * declared item-to-item.
95
+ *
96
+ * Fails open where the rule is unknown: no plan selected yet, a plan that is
97
+ * not `restricted`, or a `restricted` plan whose allow-list never came down all
98
+ * return the full list. The create/update endpoint rejects a disallowed item
99
+ * with a 422 regardless, so offering one too many costs a server error the user
100
+ * already gets today, where hiding one too few makes a legitimate add-on
101
+ * unreachable with nothing on screen to explain why.
102
+ *
103
+ * An allow-list that arrived *empty* is not unknown — a restricted item is
104
+ * saved with `applicable_items: []` when nothing was picked — so that narrows
105
+ * to nothing, as the server does.
106
+ *
107
+ * Lives here rather than in a screen so the create/edit form and the detail
108
+ * page's inline edit narrow their pickers by the same rule.
109
+ */
110
+ export declare const getInvoicingItemsAllowedForPlan: (products: InvoicingProduct[], plan: InvoicingProduct | undefined) => InvoicingProduct[];
111
+ /**
112
+ * One entry in a subscription-form picker, ready for the screen to render.
113
+ *
114
+ * `label` and `value` are the whole contract for a coupon or a customer. A
115
+ * catalog item adds the pieces its subtitle is built from — `productCode`,
116
+ * `price`, `billingPeriodUnit` — rather than a finished string, because
117
+ * formatting money is the UI's job: CES holds {@link Amount} as data and has
118
+ * no currency formatter. `subtitle` is the escape hatch for a caller that
119
+ * already has the line it wants.
120
+ *
121
+ * A plan carries the extra fields the form prefills from it: its paid-cycle
122
+ * limit, its cadence, and its trial length.
123
+ */
124
+ export interface InvoicingSubscriptionOption {
125
+ label: string;
126
+ value: string;
127
+ billingCycles?: number;
128
+ billingPeriod?: number;
129
+ billingPeriodUnit?: string;
130
+ price?: Amount;
131
+ productCode?: string;
132
+ subtitle?: string;
133
+ trialPeriod?: number;
134
+ trialPeriodUnit?: string;
135
+ }
85
136
  export declare const getInvoicingSubscriptionFormView: (state: RootState, invoicingSubscriptionID?: ID) => InvoicingSubscriptionFormView;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getInvoicingSubscriptionFormView = exports.getEditInvoicingSubscriptionDetailViewState = void 0;
3
+ exports.getInvoicingSubscriptionFormView = exports.getInvoicingItemsAllowedForPlan = exports.getEditInvoicingSubscriptionDetailViewState = void 0;
4
4
  const reduceFetchState_1 = require("../../../commonStateTypes/reduceFetchState");
5
5
  const addressSelector_1 = require("../../../entity/address/addressSelector");
6
6
  const invoicingSubscriptionSelector_1 = require("../../../entity/invoicing/invoicingSubscription/invoicingSubscriptionSelector");
@@ -60,6 +60,99 @@ const isShippingAddressUnsaved = (state, id, savedShippingAddressId) => {
60
60
  pending.country === saved.country &&
61
61
  (pending.countryCode ?? '') === (saved.countryCode ?? ''));
62
62
  };
63
+ /**
64
+ * The add-ons / one-time charges a plan actually accepts.
65
+ *
66
+ * A catalog item is either `restricted` — it names the item ids it may be
67
+ * subscribed alongside in `applicableItems` — or unrestricted, in which case
68
+ * every item is fair game. Matching is on `itemID` (the catalog item the price
69
+ * row was flattened from), not the price code, because applicability is
70
+ * declared item-to-item.
71
+ *
72
+ * Fails open where the rule is unknown: no plan selected yet, a plan that is
73
+ * not `restricted`, or a `restricted` plan whose allow-list never came down all
74
+ * return the full list. The create/update endpoint rejects a disallowed item
75
+ * with a 422 regardless, so offering one too many costs a server error the user
76
+ * already gets today, where hiding one too few makes a legitimate add-on
77
+ * unreachable with nothing on screen to explain why.
78
+ *
79
+ * An allow-list that arrived *empty* is not unknown — a restricted item is
80
+ * saved with `applicable_items: []` when nothing was picked — so that narrows
81
+ * to nothing, as the server does.
82
+ *
83
+ * Lives here rather than in a screen so the create/edit form and the detail
84
+ * page's inline edit narrow their pickers by the same rule.
85
+ */
86
+ const getInvoicingItemsAllowedForPlan = (products, plan) => {
87
+ const allowedItemIDs = plan?.applicableItems;
88
+ if (plan?.itemApplicability !== 'restricted' || allowedItemIDs == null) {
89
+ return products;
90
+ }
91
+ return products.filter((product) => product.itemID != null && allowedItemIDs.includes(product.itemID));
92
+ };
93
+ exports.getInvoicingItemsAllowedForPlan = getInvoicingItemsAllowedForPlan;
94
+ /**
95
+ * A price row can only be subscribed by its price code, so one without a code
96
+ * is not an option the form can offer.
97
+ */
98
+ const hasInvoicingPriceCode = (product) => product.priceCode != null && product.priceCode.trim() !== '';
99
+ /**
100
+ * `displayPrice` is the service-computed single-unit amount. Tiered, volume and
101
+ * stairstep price points store `price` as `0` because the money lives in their
102
+ * tiers, so `price` is only a fallback for responses predating `displayPrice`.
103
+ */
104
+ const invoicingUnitPrice = (product) => product.displayPrice ?? product.price;
105
+ const toInvoicingPlanOption = (plan) => ({
106
+ billingCycles: plan.billingCycles,
107
+ // The price point's own cadence: the backend derives billing period from the
108
+ // price point and rejects a subscription that claims a different one.
109
+ billingPeriod: plan.period,
110
+ billingPeriodUnit: plan.periodUnit,
111
+ label: plan.name ?? plan.id,
112
+ price: invoicingUnitPrice(plan),
113
+ productCode: plan.productCode,
114
+ trialPeriod: plan.trialPeriod,
115
+ trialPeriodUnit: plan.trialPeriodUnit,
116
+ value: plan.priceCode,
117
+ });
118
+ const toInvoicingCatalogItemOption = (product) => ({
119
+ // A one-time charge has no cadence, so its price is quoted flat rather than
120
+ // per period.
121
+ billingPeriodUnit: product.itemType === 'charge' ? undefined : product.periodUnit,
122
+ label: product.name ?? product.id,
123
+ price: invoicingUnitPrice(product),
124
+ productCode: product.productCode,
125
+ value: product.priceCode,
126
+ });
127
+ const toInvoicingCouponOption = (coupon) => ({
128
+ label: coupon.name ?? coupon.couponCode ?? coupon.id,
129
+ // Create/apply/remove coupon APIs resolve by coupon_code.
130
+ value: coupon.couponCode ?? coupon.id,
131
+ });
132
+ /**
133
+ * Company first, then the person's name, then the email — whichever the
134
+ * customer actually has. The id is the last resort so a record with no
135
+ * identifying field at all is still selectable rather than a blank row.
136
+ */
137
+ const invoicingCustomerDisplayName = (customer) => {
138
+ const fullName = [customer.firstName, customer.lastName]
139
+ .filter((part) => part != null && part.trim() !== '')
140
+ .join(' ')
141
+ .trim();
142
+ const company = customer.company?.trim() ?? '';
143
+ const email = customer.email?.trim() ?? '';
144
+ return company !== ''
145
+ ? company
146
+ : fullName !== ''
147
+ ? fullName
148
+ : email !== ''
149
+ ? email
150
+ : customer.id;
151
+ };
152
+ const toInvoicingCustomerOption = (customer) => ({
153
+ label: invoicingCustomerDisplayName(customer),
154
+ value: customer.id,
155
+ });
63
156
  const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID) => {
64
157
  const { editDraftById, error, fetchState, newSubscriptionDraft, savedSubscriptionId, } = state.editInvoicingSubscriptionDetailViewState;
65
158
  const mode = invoicingSubscriptionID != null ? 'edit' : 'create';
@@ -81,8 +174,15 @@ const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID) => {
81
174
  : undefined;
82
175
  const hasUnsavedShippingAddress = isShippingAddressUnsaved(state, invoicingSubscriptionID, subscription?.shippingAddressId);
83
176
  const catalogListView = (0, invoicingCatalogListViewSelector_1.getInvoicingCatalogListView)(state);
84
- const planPrices = (0, invoicingCatalogListViewSelector_1.getInvoicingCatalogPrices)(catalogListView.plans);
85
- const addonAndChargePrices = (0, invoicingCatalogListViewSelector_1.getInvoicingCatalogPrices)(catalogListView.products);
177
+ const planPrices = (0, invoicingCatalogListViewSelector_1.getInvoicingCatalogPrices)(catalogListView.plans).filter(hasInvoicingPriceCode);
178
+ // A restricted plan names the catalog items it may be sold with, so the
179
+ // pickers offer only those: subscription create rejects the rest with a 422,
180
+ // and an option that can only fail is not a choice. The applicability rides
181
+ // on the price rows already in the store, so this is a join, not a refetch.
182
+ const allowedCatalogPrices = (0, exports.getInvoicingItemsAllowedForPlan)((0, invoicingCatalogListViewSelector_1.getInvoicingCatalogPrices)(catalogListView.products), planPrices.find((plan) => plan.priceCode === localData.planPriceCode)).filter(hasInvoicingPriceCode);
183
+ const catalogItemOptionsOfType = (itemType) => allowedCatalogPrices
184
+ .filter((product) => product.itemType === itemType)
185
+ .map(toInvoicingCatalogItemOption);
86
186
  // In create mode there is nothing to load, so the record state only applies
87
187
  // when editing an existing subscription.
88
188
  const recordState = invoicingSubscriptionID != null
@@ -104,12 +204,13 @@ const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID) => {
104
204
  recordState,
105
205
  isFormLoading: (0, invoicingFormDraft_1.isInvoicingFormDraftPending)(editDraftById, invoicingSubscriptionID),
106
206
  fieldHelp: (0, invoicingConfigViewSelector_1.invoicingFieldHelpForGroup)(state, 'subscription'),
107
- coupons: (0, couponViewSelector_1.getInvoicingCouponListItems)(state),
108
- customers: (0, invoicingCustomerListViewSelector_1.getInvoicingCustomerListView)(state).customers,
207
+ addons: catalogItemOptionsOfType('addon'),
208
+ charges: catalogItemOptionsOfType('charge'),
209
+ coupons: (0, couponViewSelector_1.getInvoicingCouponListItems)(state).map(toInvoicingCouponOption),
210
+ customers: (0, invoicingCustomerListViewSelector_1.getInvoicingCustomerListView)(state).customers.map(toInvoicingCustomerOption),
109
211
  localData,
110
212
  mode,
111
- plans: planPrices,
112
- products: addonAndChargePrices,
213
+ plans: planPrices.map(toInvoicingPlanOption),
113
214
  savedSubscriptionId,
114
215
  shippingAddressLabel,
115
216
  hasUnsavedShippingAddress,
@@ -27,6 +27,16 @@ export interface InvoicingCatalogListView extends SelectorView {
27
27
  */
28
28
  runningActionId?: ID;
29
29
  }
30
+ /**
31
+ * Flatten catalog items into the price rows the pickers render.
32
+ *
33
+ * The parent item's `applicableItems` / `itemApplicability` are carried onto
34
+ * every row it produces: applicability is a property of the catalog item, not
35
+ * of its price points, and a row that has lost it cannot be matched against the
36
+ * selected plan's allow-list (see `getInvoicingItemsAllowedForPlan`). Both are
37
+ * written after the spread so the item's own rule wins over any stale copy a
38
+ * price point payload may carry.
39
+ */
30
40
  export declare const getInvoicingCatalogPrices: (items: InvoicingPlan[]) => InvoicingProduct[];
31
41
  /**
32
42
  * Bundled read view for the single catalog page (plans + products tabs).
@@ -5,8 +5,20 @@ const toolkit_1 = require("@reduxjs/toolkit");
5
5
  const planSelector_1 = require("../../../entity/invoicing/plan/planSelector");
6
6
  const runningInvoicingActionId_1 = require("../runningInvoicingActionId");
7
7
  const invoicingCatalogListViewState_1 = require("./invoicingCatalogListViewState");
8
+ /**
9
+ * Flatten catalog items into the price rows the pickers render.
10
+ *
11
+ * The parent item's `applicableItems` / `itemApplicability` are carried onto
12
+ * every row it produces: applicability is a property of the catalog item, not
13
+ * of its price points, and a row that has lost it cannot be matched against the
14
+ * selected plan's allow-list (see `getInvoicingItemsAllowedForPlan`). Both are
15
+ * written after the spread so the item's own rule wins over any stale copy a
16
+ * price point payload may carry.
17
+ */
8
18
  const getInvoicingCatalogPrices = (items) => items.flatMap((item) => (item.subFamilies ?? []).map((subFamily) => ({
9
19
  ...subFamily,
20
+ applicableItems: item.applicableItems,
21
+ itemApplicability: item.itemApplicability,
10
22
  itemID: item.id,
11
23
  itemType: item.type ?? item.itemType,
12
24
  name: subFamily.name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.2.59",
3
+ "version": "5.2.60",
4
4
  "description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/esm/index.js",