@zeniai/client-epic-state 5.2.59 → 5.2.61
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/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +106 -6
- package/lib/esm/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewSelector.js +12 -0
- package/lib/index.d.ts +1 -1
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.d.ts +64 -13
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +108 -7
- package/lib/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewSelector.d.ts +10 -0
- package/lib/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewSelector.js +12 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
*
|
|
41
|
-
*
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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.
|
|
3
|
+
"version": "5.2.61",
|
|
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",
|