@zeniai/client-epic-state 5.2.41-beta5MM → 5.2.41-beta7MM
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/entity/billPay/billTransaction/billTransactionReducer.d.ts +5 -1
- package/lib/entity/billPay/billTransaction/billTransactionReducer.js +17 -2
- package/lib/entity/invoicing/coupon/couponPayload.d.ts +8 -0
- package/lib/entity/invoicing/coupon/couponPayload.js +8 -0
- package/lib/entity/invoicing/coupon/couponState.d.ts +6 -0
- package/lib/entity/invoicing/dunningCase/dunningCaseTypes.d.ts +1 -1
- package/lib/entity/invoicing/plan/planSelector.d.ts +20 -0
- package/lib/entity/invoicing/plan/planSelector.js +42 -0
- package/lib/entity/invoicing/product/productPayload.d.ts +8 -1
- package/lib/entity/invoicing/product/productPayload.js +4 -1
- package/lib/entity/invoicing/product/productState.d.ts +16 -0
- package/lib/entity/jeSchedules/jeSchedulesReducer.d.ts +4 -1
- package/lib/entity/jeSchedules/jeSchedulesReducer.js +79 -5
- package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
- package/lib/entity/snackbar/snackbarTypes.js +1 -0
- package/lib/entity/task/taskState.d.ts +1 -1
- package/lib/epic.d.ts +4 -1
- package/lib/epic.js +10 -4
- package/lib/esm/entity/billPay/billTransaction/billTransactionReducer.js +16 -1
- package/lib/esm/entity/invoicing/coupon/couponPayload.js +8 -0
- package/lib/esm/entity/invoicing/plan/planSelector.js +39 -0
- package/lib/esm/entity/invoicing/product/productPayload.js +4 -1
- package/lib/esm/entity/jeSchedules/jeSchedulesReducer.js +78 -4
- package/lib/esm/entity/snackbar/snackbarTypes.js +1 -0
- package/lib/esm/epic.js +10 -4
- package/lib/esm/index.js +8 -7
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +3 -2
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/fetchFailedJePostingsEpic.js +39 -0
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.js +14 -1
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulesEpic.js +10 -13
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/initializeJeScheduleLocalDataEpic.js +38 -17
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/retryJeSchedulesEpic.js +17 -1
- package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +15 -4
- package/lib/esm/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +6 -0
- package/lib/esm/view/expenseAutomationView/reducers/jeSchedulesViewReducer.js +74 -1
- package/lib/esm/view/expenseAutomationView/reducers/reconciliationViewReducer.js +80 -53
- package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +10 -0
- package/lib/esm/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +11 -2
- package/lib/esm/view/expenseAutomationView/selectors/jeSchedulesViewSelector.js +49 -14
- package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +28 -11
- package/lib/esm/view/invoicing/createInvoice/createInvoiceSelector.js +28 -1
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +67 -35
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.js +22 -11
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +23 -4
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/invoicingCatalogItemFormHelper.js +1 -7
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +19 -5
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/seedInvoicingCatalogItemFormDraftEpic.js +30 -7
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewPayload.js +16 -1
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewReducer.js +22 -11
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +69 -6
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.js +22 -0
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/fetchInvoicingCouponDetailPageEpic.js +22 -4
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +18 -4
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/seedInvoicingCouponFormDraftEpic.js +29 -6
- package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer.js +28 -12
- package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.js +37 -2
- package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/initializeInvoicingCustomerAddressEpic.js +43 -17
- package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/saveInvoicingCustomerEpic.js +1 -1
- package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/seedInvoicingCustomerFormDraftEpic.js +29 -6
- package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.js +30 -14
- package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +143 -8
- package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionFormPageEpic.js +4 -1
- package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/initializeInvoicingSubscriptionAddressEpic.js +44 -18
- package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +1 -1
- package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/seedInvoicingSubscriptionFormDraftEpic.js +29 -8
- package/lib/esm/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewSelector.js +12 -0
- package/lib/esm/view/invoicing/invoicingDunningCaseListView/invoicingDunningCaseListViewState.js +6 -6
- package/lib/esm/view/invoicing/invoicingFormDraft.js +78 -0
- package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +57 -9
- package/lib/esm/view/recommendation/recommendationPayload.js +3 -1
- package/lib/esm/view/recommendation/recommendationReducer.js +27 -7
- package/lib/esm/view/recommendation/recommendationSelector.js +1 -0
- package/lib/esm/view/recommendation/submitRecommendationFeedbackEpic.js +20 -8
- package/lib/esm/view/spendManagement/billPay/billDetailView/billDetailViewReducer.js +87 -1
- package/lib/esm/view/spendManagement/billPay/billDetailView/billDetailViewSelector.js +1 -0
- package/lib/esm/view/spendManagement/billPay/billDetailView/billDetailViewState.js +4 -0
- package/lib/esm/view/spendManagement/billPay/billDetailView/epics/fetchBillRecommendationsEpic.js +97 -0
- package/lib/esm/view/spendManagement/billPay/billDetailView/epics/fetchRecommendationsForBill.js +85 -0
- package/lib/esm/view/spendManagement/billPay/editBillView/editBillViewReducer.js +18 -1
- package/lib/esm/view/spendManagement/billPay/editBillView/editBillViewSelector.js +3 -0
- package/lib/esm/view/spendManagement/billPay/editBillView/epics/applyBillRecommendationsToLocalStoreEpic.js +95 -0
- package/lib/index.d.ts +9 -8
- package/lib/index.js +54 -48
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +3 -2
- package/lib/view/expenseAutomationView/epics/jeSchedule/fetchFailedJePostingsEpic.d.ts +20 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/fetchFailedJePostingsEpic.js +43 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.d.ts +2 -2
- package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.js +13 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulesEpic.js +8 -11
- package/lib/view/expenseAutomationView/epics/jeSchedule/initializeJeScheduleLocalDataEpic.js +38 -17
- package/lib/view/expenseAutomationView/epics/jeSchedule/retryJeSchedulesEpic.d.ts +2 -2
- package/lib/view/expenseAutomationView/epics/jeSchedule/retryJeSchedulesEpic.js +17 -1
- package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +15 -4
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
- package/lib/view/expenseAutomationView/payload/jeSchedulesPayload.d.ts +18 -0
- package/lib/view/expenseAutomationView/payload/transactionCategorizationPayload.d.ts +18 -7
- package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +6 -0
- package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.d.ts +10 -3
- package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.js +76 -3
- package/lib/view/expenseAutomationView/reducers/reconciliationViewReducer.js +80 -53
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +10 -0
- package/lib/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.d.ts +3 -0
- package/lib/view/expenseAutomationView/selectorTypes/jeSchedulesSelectorTypes.d.ts +3 -0
- package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +8 -4
- package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.d.ts +1 -0
- package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +12 -2
- package/lib/view/expenseAutomationView/selectors/jeSchedulesViewSelector.js +49 -14
- package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +27 -10
- package/lib/view/expenseAutomationView/types/jeSchedulesViewState.d.ts +16 -0
- package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +8 -5
- package/lib/view/invoicing/createInvoice/createInvoiceSelector.d.ts +7 -2
- package/lib/view/invoicing/createInvoice/createInvoiceSelector.js +28 -1
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.d.ts +46 -5
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +69 -36
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.d.ts +3 -1
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.js +22 -11
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +17 -0
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +22 -3
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewState.d.ts +2 -1
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/invoicingCatalogItemFormHelper.d.ts +2 -2
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/invoicingCatalogItemFormHelper.js +1 -7
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +17 -3
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/seedInvoicingCatalogItemFormDraftEpic.d.ts +10 -6
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/seedInvoicingCatalogItemFormDraftEpic.js +29 -6
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewPayload.d.ts +5 -1
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewPayload.js +16 -1
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewReducer.d.ts +3 -1
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewReducer.js +22 -11
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.d.ts +46 -0
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +68 -5
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.d.ts +21 -1
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.js +24 -1
- package/lib/view/invoicing/editInvoicingCouponDetailView/fetchInvoicingCouponDetailPageEpic.d.ts +9 -5
- package/lib/view/invoicing/editInvoicingCouponDetailView/fetchInvoicingCouponDetailPageEpic.js +22 -4
- package/lib/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +18 -4
- package/lib/view/invoicing/editInvoicingCouponDetailView/seedInvoicingCouponFormDraftEpic.d.ts +10 -6
- package/lib/view/invoicing/editInvoicingCouponDetailView/seedInvoicingCouponFormDraftEpic.js +28 -5
- package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer.d.ts +5 -1
- package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer.js +29 -13
- package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.d.ts +16 -7
- package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.js +37 -2
- package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewState.d.ts +8 -1
- package/lib/view/invoicing/editInvoicingCustomerDetailView/initializeInvoicingCustomerAddressEpic.d.ts +15 -10
- package/lib/view/invoicing/editInvoicingCustomerDetailView/initializeInvoicingCustomerAddressEpic.js +42 -16
- package/lib/view/invoicing/editInvoicingCustomerDetailView/saveInvoicingCustomerEpic.js +1 -1
- package/lib/view/invoicing/editInvoicingCustomerDetailView/seedInvoicingCustomerFormDraftEpic.d.ts +10 -6
- package/lib/view/invoicing/editInvoicingCustomerDetailView/seedInvoicingCustomerFormDraftEpic.js +28 -5
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.d.ts +5 -1
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.js +31 -15
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.d.ts +79 -12
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +145 -9
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewState.d.ts +8 -1
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionFormPageEpic.js +4 -1
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/initializeInvoicingSubscriptionAddressEpic.d.ts +15 -11
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/initializeInvoicingSubscriptionAddressEpic.js +42 -16
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +1 -1
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/seedInvoicingSubscriptionFormDraftEpic.d.ts +10 -6
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/seedInvoicingSubscriptionFormDraftEpic.js +28 -7
- package/lib/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewSelector.d.ts +10 -0
- package/lib/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewSelector.js +12 -0
- package/lib/view/invoicing/invoicingDunningCaseListView/invoicingDunningCaseListViewReducer.d.ts +1 -1
- package/lib/view/invoicing/invoicingDunningCaseListView/invoicingDunningCaseListViewState.d.ts +2 -2
- package/lib/view/invoicing/invoicingDunningCaseListView/invoicingDunningCaseListViewState.js +6 -6
- package/lib/view/invoicing/invoicingFormDraft.d.ts +63 -0
- package/lib/view/invoicing/invoicingFormDraft.js +86 -0
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +49 -0
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +59 -10
- package/lib/view/invoicing/invoicingSubscriptionListView/invoicingSubscriptionListViewReducer.d.ts +1 -1
- package/lib/view/invoicing/invoicingSubscriptionListView/invoicingSubscriptionListViewState.d.ts +1 -1
- package/lib/view/recommendation/recommendationPayload.d.ts +1 -0
- package/lib/view/recommendation/recommendationPayload.js +5 -0
- package/lib/view/recommendation/recommendationReducer.d.ts +7 -1
- package/lib/view/recommendation/recommendationReducer.js +28 -8
- package/lib/view/recommendation/recommendationSelector.d.ts +2 -0
- package/lib/view/recommendation/recommendationSelector.js +3 -1
- package/lib/view/recommendation/recommendationState.d.ts +2 -1
- package/lib/view/recommendation/submitRecommendationFeedbackEpic.d.ts +2 -2
- package/lib/view/recommendation/submitRecommendationFeedbackEpic.js +18 -6
- package/lib/view/spendManagement/billPay/billDetailView/billDetailViewPayload.d.ts +19 -0
- package/lib/view/spendManagement/billPay/billDetailView/billDetailViewReducer.d.ts +10 -1
- package/lib/view/spendManagement/billPay/billDetailView/billDetailViewReducer.js +88 -2
- package/lib/view/spendManagement/billPay/billDetailView/billDetailViewSelector.d.ts +1 -0
- package/lib/view/spendManagement/billPay/billDetailView/billDetailViewSelector.js +1 -0
- package/lib/view/spendManagement/billPay/billDetailView/billDetailViewState.d.ts +7 -0
- package/lib/view/spendManagement/billPay/billDetailView/billDetailViewState.js +4 -0
- package/lib/view/spendManagement/billPay/billDetailView/epics/fetchBillRecommendationsEpic.d.ts +37 -0
- package/lib/view/spendManagement/billPay/billDetailView/epics/fetchBillRecommendationsEpic.js +104 -0
- package/lib/view/spendManagement/billPay/billDetailView/epics/fetchRecommendationsForBill.d.ts +24 -0
- package/lib/view/spendManagement/billPay/billDetailView/epics/fetchRecommendationsForBill.js +92 -0
- package/lib/view/spendManagement/billPay/editBillView/editBillViewReducer.d.ts +4 -1
- package/lib/view/spendManagement/billPay/editBillView/editBillViewReducer.js +19 -2
- package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.d.ts +1 -0
- package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.js +3 -0
- package/lib/view/spendManagement/billPay/editBillView/epics/applyBillRecommendationsToLocalStoreEpic.d.ts +18 -0
- package/lib/view/spendManagement/billPay/editBillView/epics/applyBillRecommendationsToLocalStoreEpic.js +102 -0
- package/package.json +2 -2
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { reduceAllFetchState } from '../../../commonStateTypes/reduceFetchState';
|
|
2
|
+
import { getAddressByAddressId } from '../../../entity/address/addressSelector';
|
|
2
3
|
import { getInvoicingSubscriptionById } from '../../../entity/invoicing/invoicingSubscription/invoicingSubscriptionSelector';
|
|
3
4
|
import { getFormattedAddress } from '../../addressView/addressViewSelector';
|
|
4
5
|
import { getInvoicingCouponListItems } from '../couponView/couponViewSelector';
|
|
5
6
|
import { getInvoicingCatalogListView, getInvoicingCatalogPrices, } from '../invoicingCatalogListView/invoicingCatalogListViewSelector';
|
|
6
7
|
import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
|
|
7
8
|
import { getInvoicingCustomerListView } from '../invoicingCustomerListView/invoicingCustomerListViewSelector';
|
|
9
|
+
import { getInvoicingFormDraftLocalData, isInvoicingFormDraftPending, } from '../invoicingFormDraft';
|
|
8
10
|
import { initialInvoicingSubscriptionFormLocalData } from './invoicingSubscriptionFormConfig';
|
|
9
11
|
import { INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE } from './invoicingSubscriptionFormConfig';
|
|
10
12
|
export const getEditInvoicingSubscriptionDetailViewState = (state, invoicingSubscriptionID) => {
|
|
@@ -23,6 +25,129 @@ export const getEditInvoicingSubscriptionDetailViewState = (state, invoicingSubs
|
|
|
23
25
|
version: 1.0,
|
|
24
26
|
};
|
|
25
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* Whether the shipping address entered on the Address screen is a change the
|
|
30
|
+
* subscription has not saved. Presence alone cannot say so: the detail page seeds
|
|
31
|
+
* the address store with the saved address on load so the Address screen opens
|
|
32
|
+
* prefilled. Only a pending address that differs from the saved one — or one
|
|
33
|
+
* where nothing is saved yet — is an unsaved change. Compared on what the user
|
|
34
|
+
* enters; a saved address's coordinates and timestamps are left out.
|
|
35
|
+
*/
|
|
36
|
+
const isShippingAddressUnsaved = (state, id, savedShippingAddressId) => {
|
|
37
|
+
// The slot is keyed by address type alone; only a draft this subscription owns counts.
|
|
38
|
+
if (id == null ||
|
|
39
|
+
state.editInvoicingSubscriptionDetailViewState.shippingAddressOwnerId !== id) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const pending = state.addressViewState.newAddressState?.[INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE]?.addressToCreate;
|
|
43
|
+
if (pending == null) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const saved = savedShippingAddressId != null
|
|
47
|
+
? getAddressByAddressId(state.addressState, savedShippingAddressId)
|
|
48
|
+
: undefined;
|
|
49
|
+
if (saved == null) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return !(pending.street.join('\u0000') === saved.street.join('\u0000') &&
|
|
53
|
+
pending.city === saved.city &&
|
|
54
|
+
pending.state === saved.state &&
|
|
55
|
+
pending.postalCode === saved.postalCode &&
|
|
56
|
+
pending.country === saved.country &&
|
|
57
|
+
(pending.countryCode ?? '') === (saved.countryCode ?? ''));
|
|
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
|
+
});
|
|
26
151
|
export const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID) => {
|
|
27
152
|
const { editDraftById, error, fetchState, newSubscriptionDraft, savedSubscriptionId, } = state.editInvoicingSubscriptionDetailViewState;
|
|
28
153
|
const mode = invoicingSubscriptionID != null ? 'edit' : 'create';
|
|
@@ -33,8 +158,7 @@ export const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID)
|
|
|
33
158
|
// `seedInvoicingSubscriptionFormDraftEpic` when the detail load lands. Until
|
|
34
159
|
// then the page is showing its skeleton.
|
|
35
160
|
const localData = invoicingSubscriptionID != null
|
|
36
|
-
? (editDraftById
|
|
37
|
-
initialInvoicingSubscriptionFormLocalData)
|
|
161
|
+
? getInvoicingFormDraftLocalData(editDraftById, invoicingSubscriptionID, initialInvoicingSubscriptionFormLocalData)
|
|
38
162
|
: newSubscriptionDraft;
|
|
39
163
|
// Shipping address is captured via the shared Address screen and seeded there
|
|
40
164
|
// (looked up in the shared address store by `shippingAddressId`) on detail
|
|
@@ -43,9 +167,17 @@ export const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID)
|
|
|
43
167
|
const shippingAddressLabel = newAddress?.addressToCreate != null
|
|
44
168
|
? getFormattedAddress(newAddress.addressToCreate)
|
|
45
169
|
: undefined;
|
|
170
|
+
const hasUnsavedShippingAddress = isShippingAddressUnsaved(state, invoicingSubscriptionID, subscription?.shippingAddressId);
|
|
46
171
|
const catalogListView = getInvoicingCatalogListView(state);
|
|
47
|
-
const planPrices = getInvoicingCatalogPrices(catalogListView.plans);
|
|
48
|
-
|
|
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);
|
|
49
181
|
// In create mode there is nothing to load, so the record state only applies
|
|
50
182
|
// when editing an existing subscription.
|
|
51
183
|
const recordState = invoicingSubscriptionID != null
|
|
@@ -65,15 +197,18 @@ export const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID)
|
|
|
65
197
|
subscriptionLifecycleActionOptions: configState.subscriptionLifecycleActionOptions,
|
|
66
198
|
subscriptionMetadataEditAllowedFromStatuses: configState.subscriptionMetadataEditAllowedFromStatuses,
|
|
67
199
|
recordState,
|
|
200
|
+
isFormLoading: isInvoicingFormDraftPending(editDraftById, invoicingSubscriptionID),
|
|
68
201
|
fieldHelp: invoicingFieldHelpForGroup(state, 'subscription'),
|
|
69
|
-
|
|
70
|
-
|
|
202
|
+
addons: catalogItemOptionsOfType('addon'),
|
|
203
|
+
charges: catalogItemOptionsOfType('charge'),
|
|
204
|
+
coupons: getInvoicingCouponListItems(state).map(toInvoicingCouponOption),
|
|
205
|
+
customers: getInvoicingCustomerListView(state).customers.map(toInvoicingCustomerOption),
|
|
71
206
|
localData,
|
|
72
207
|
mode,
|
|
73
|
-
plans: planPrices,
|
|
74
|
-
products: addonAndChargePrices,
|
|
208
|
+
plans: planPrices.map(toInvoicingPlanOption),
|
|
75
209
|
savedSubscriptionId,
|
|
76
210
|
shippingAddressLabel,
|
|
211
|
+
hasUnsavedShippingAddress,
|
|
77
212
|
subscription,
|
|
78
213
|
fetchState,
|
|
79
214
|
error,
|
|
@@ -16,8 +16,11 @@ export const fetchInvoicingSubscriptionFormPageEpic = (actions$) => actions$.pip
|
|
|
16
16
|
const { subscriptionId } = action.payload;
|
|
17
17
|
const pageActions = [
|
|
18
18
|
fetchInvoicingCustomerList({ filters: {}, keepExistingListItems: false }),
|
|
19
|
+
// Plans only: the form's plan picker is the one consumer, and an
|
|
20
|
+
// unfiltered list would replace the store's plans with every catalog
|
|
21
|
+
// item.
|
|
19
22
|
fetchInvoicingCatalogPlanList({
|
|
20
|
-
filters: {},
|
|
23
|
+
filters: { type: 'plan' },
|
|
21
24
|
keepExistingListItems: false,
|
|
22
25
|
}),
|
|
23
26
|
fetchInvoicingCouponList({ filters: {}, keepExistingListItems: false }),
|
|
@@ -1,36 +1,62 @@
|
|
|
1
|
-
import { EMPTY,
|
|
1
|
+
import { EMPTY, from } from 'rxjs';
|
|
2
2
|
import { filter, mergeMap } from 'rxjs/operators';
|
|
3
3
|
import { getAddressByAddressId } from '../../../entity/address/addressSelector';
|
|
4
4
|
import { getInvoicingSubscriptionById } from '../../../entity/invoicing/invoicingSubscription/invoicingSubscriptionSelector';
|
|
5
|
-
import { newAddressInLocalStore } from '../../addressView/addressViewReducer';
|
|
6
|
-
import { updateInvoicingSubscriptionDetail } from './editInvoicingSubscriptionDetailViewReducer';
|
|
7
|
-
import { initializeInvoicingSubscriptionAddress } from './editInvoicingSubscriptionDetailViewReducer';
|
|
5
|
+
import { newAddressInLocalStore, removeNewAddressDataInLocalStore, } from '../../addressView/addressViewReducer';
|
|
6
|
+
import { claimInvoicingSubscriptionShippingAddress, fetchInvoicingSubscriptionDetail, fetchInvoicingSubscriptionDetailPage, initializeInvoicingSubscriptionAddress, updateInvoicingSubscriptionDetail, } from './editInvoicingSubscriptionDetailViewReducer';
|
|
8
7
|
import { INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE } from './invoicingSubscriptionFormConfig';
|
|
9
8
|
/**
|
|
10
|
-
* Seed the shared Address screen with
|
|
11
|
-
*
|
|
12
|
-
* the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
9
|
+
* Seed the shared Address screen with the subscription's saved shipping address, and
|
|
10
|
+
* record that the slot is this subscription's. Runs when a detail request is issued
|
|
11
|
+
* (every path that loads a record issues one — the detail page when not cached,
|
|
12
|
+
* the create/edit form always), when the page opens, when the detail load
|
|
13
|
+
* lands, and on an explicit initialize trigger.
|
|
14
|
+
*
|
|
15
|
+
* The address store slot is keyed by address type alone, so it can still hold
|
|
16
|
+
* another subscription's draft. Only a draft this subscription owns is left alone —
|
|
17
|
+
* that is what lets an address the user just entered survive a trip to the
|
|
18
|
+
* Address screen. Anything else is replaced by this subscription's saved address,
|
|
19
|
+
* or cleared when it has none. Opening the page counts as a trigger because a
|
|
20
|
+
* record already loaded this session issues no detail request at all.
|
|
17
21
|
*/
|
|
18
22
|
export const initializeInvoicingSubscriptionAddressEpic = (actions$, state$) => actions$.pipe(filter((action) => initializeInvoicingSubscriptionAddress.match(action) ||
|
|
19
|
-
updateInvoicingSubscriptionDetail.match(action)
|
|
23
|
+
updateInvoicingSubscriptionDetail.match(action) ||
|
|
24
|
+
fetchInvoicingSubscriptionDetailPage.match(action) ||
|
|
25
|
+
fetchInvoicingSubscriptionDetail.match(action)), mergeMap((action) => {
|
|
20
26
|
const { id } = action.payload;
|
|
27
|
+
if (id == null) {
|
|
28
|
+
return EMPTY;
|
|
29
|
+
}
|
|
21
30
|
const rootState = state$.value;
|
|
22
|
-
const
|
|
23
|
-
|
|
31
|
+
const pending = rootState.addressViewState.newAddressState?.[INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE]?.addressToCreate;
|
|
32
|
+
const ownerId = rootState.editInvoicingSubscriptionDetailViewState
|
|
33
|
+
.shippingAddressOwnerId;
|
|
34
|
+
// A detail response speaks for the record it loaded, which may no longer
|
|
35
|
+
// be the one on screen: its request keeps running when the user moves on
|
|
36
|
+
// to a cached record, which issues no request of its own to cancel it.
|
|
37
|
+
// Only the page-open and explicit triggers define the current record.
|
|
38
|
+
if (updateInvoicingSubscriptionDetail.match(action) &&
|
|
39
|
+
ownerId != null &&
|
|
40
|
+
ownerId !== id) {
|
|
41
|
+
return EMPTY;
|
|
42
|
+
}
|
|
43
|
+
if (pending != null && ownerId === id) {
|
|
24
44
|
return EMPTY;
|
|
25
45
|
}
|
|
26
46
|
const subscription = getInvoicingSubscriptionById(rootState.invoicingSubscriptionState, id);
|
|
27
47
|
const address = subscription?.shippingAddressId != null
|
|
28
48
|
? getAddressByAddressId(rootState.addressState, subscription.shippingAddressId)
|
|
29
49
|
: undefined;
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
const actions = [];
|
|
51
|
+
if (address != null) {
|
|
52
|
+
actions.push(newAddressInLocalStore({
|
|
32
53
|
addressType: INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE,
|
|
33
54
|
addressToCreate: address,
|
|
34
|
-
}))
|
|
35
|
-
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
else if (pending != null) {
|
|
58
|
+
actions.push(removeNewAddressDataInLocalStore(INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE));
|
|
59
|
+
}
|
|
60
|
+
actions.push(claimInvoicingSubscriptionShippingAddress({ id }));
|
|
61
|
+
return from(actions);
|
|
36
62
|
}));
|
package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js
CHANGED
|
@@ -17,7 +17,7 @@ export const saveInvoicingSubscriptionEpic = (actions$, state$, zeniAPI) => acti
|
|
|
17
17
|
const rootState = state$.value;
|
|
18
18
|
const viewState = rootState.editInvoicingSubscriptionDetailViewState;
|
|
19
19
|
const localData = (id != null
|
|
20
|
-
? viewState.editDraftById[id]
|
|
20
|
+
? viewState.editDraftById[id]?.localData
|
|
21
21
|
: viewState.newSubscriptionDraft) ?? viewState.newSubscriptionDraft;
|
|
22
22
|
// Shipping address captured via the shared Address screen is sent as an
|
|
23
23
|
// object in the payload (same pattern as the customer save flow).
|
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
import { EMPTY, of } from 'rxjs';
|
|
2
2
|
import { filter, mergeMap, withLatestFrom } from 'rxjs/operators';
|
|
3
|
-
import { seedInvoicingSubscriptionFormDraft, updateInvoicingSubscriptionDetail, } from './editInvoicingSubscriptionDetailViewReducer';
|
|
3
|
+
import { fetchInvoicingSubscriptionFormPage, seedInvoicingSubscriptionFormDraft, updateInvoicingSubscriptionDetail, } from './editInvoicingSubscriptionDetailViewReducer';
|
|
4
4
|
import { getInvoicingSubscriptionById } from './invoicingSubscriptionDetailHelper';
|
|
5
5
|
import { entityToInvoicingSubscriptionFormLocalData } from './invoicingSubscriptionFormConfig';
|
|
6
6
|
/**
|
|
7
|
-
* Seeds the edit draft from the record the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Seeds the edit draft from the record, so the form has its values before it
|
|
8
|
+
* mounts. The reducer ignores a seed once the draft holds the record, which is
|
|
9
|
+
* what stops a background refetch from overwriting edits in progress.
|
|
10
|
+
*
|
|
11
|
+
* Opening the page counts as a trigger, not just the detail response: the page
|
|
12
|
+
* fetch is cache-guarded, so re-opening a form for a record already loaded this
|
|
13
|
+
* session issues no request at all — and a draft cleared in between (by a save
|
|
14
|
+
* or a cancel) would have nothing left to seed it.
|
|
11
15
|
*/
|
|
12
|
-
export const seedInvoicingSubscriptionFormDraftEpic = (actions$, state$) => actions$.pipe(filter(updateInvoicingSubscriptionDetail.match
|
|
13
|
-
|
|
14
|
-
const
|
|
16
|
+
export const seedInvoicingSubscriptionFormDraftEpic = (actions$, state$) => actions$.pipe(filter((action) => updateInvoicingSubscriptionDetail.match(action) ||
|
|
17
|
+
fetchInvoicingSubscriptionFormPage.match(action)), withLatestFrom(state$), mergeMap(([action, state]) => {
|
|
18
|
+
const id = updateInvoicingSubscriptionDetail.match(action)
|
|
19
|
+
? action.payload.id
|
|
20
|
+
: action.payload.subscriptionId;
|
|
21
|
+
if (id == null) {
|
|
22
|
+
return EMPTY;
|
|
23
|
+
}
|
|
24
|
+
// The entity alone does not say the whole record is loaded — the list
|
|
25
|
+
// endpoints merge their own list-shaped rows into the same store. Only a
|
|
26
|
+
// completed detail load does, which is also exactly when the page's
|
|
27
|
+
// cache-guarded fetch issues no request and this is the sole seed.
|
|
28
|
+
if (fetchInvoicingSubscriptionFormPage.match(action) &&
|
|
29
|
+
state.editInvoicingSubscriptionDetailViewState.recordStateById[id]
|
|
30
|
+
?.fetchState !== 'Completed') {
|
|
31
|
+
return EMPTY;
|
|
32
|
+
}
|
|
33
|
+
// Nothing to seed from on a cold open; the detail response triggers this
|
|
34
|
+
// again once it lands.
|
|
35
|
+
const subscription = id != null ? getInvoicingSubscriptionById(state, id) : undefined;
|
|
15
36
|
if (subscription == null) {
|
|
16
37
|
return EMPTY;
|
|
17
38
|
}
|
|
@@ -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/esm/view/invoicing/invoicingDunningCaseListView/invoicingDunningCaseListViewState.js
CHANGED
|
@@ -22,11 +22,11 @@ export const INVOICING_DUNNING_CASE_COLUMNS = [
|
|
|
22
22
|
];
|
|
23
23
|
export const ALL_INVOICING_DUNNING_CASE_TAB_IDS = [
|
|
24
24
|
'all',
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
25
|
+
'retrying',
|
|
26
|
+
'scheduled',
|
|
27
|
+
'manual_review',
|
|
28
|
+
'paused',
|
|
29
|
+
'resolved',
|
|
30
|
+
'cancelled',
|
|
31
31
|
];
|
|
32
32
|
export const toInvoicingDunningCaseTabId = (v) => stringToUnion(v, ALL_INVOICING_DUNNING_CASE_TAB_IDS);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The form's own write. Keeps the status: an edit on top of a seeded draft is
|
|
3
|
+
* still the record's draft, and an edit made before the record arrived must not
|
|
4
|
+
* pass for one.
|
|
5
|
+
*/
|
|
6
|
+
export function applyInvoicingFormDraftEdit(draftById, id, localData) {
|
|
7
|
+
draftById[id] = {
|
|
8
|
+
fetchState: draftById[id]?.fetchState ?? 'Not-Started',
|
|
9
|
+
localData,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Fill the draft from the loaded record, once. A second seed is ignored so a
|
|
14
|
+
* late refetch cannot overwrite edits in progress — but a draft the form wrote
|
|
15
|
+
* before the record landed is replaced, since it was never the record's.
|
|
16
|
+
*/
|
|
17
|
+
export function applyInvoicingFormDraftSeed(draftById, id, localData) {
|
|
18
|
+
if (draftById[id]?.fetchState === 'Completed') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
draftById[id] = { fetchState: 'Completed', localData };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Record that the fetch failed, so the form renders instead of waiting behind a
|
|
25
|
+
* skeleton that will never resolve.
|
|
26
|
+
*
|
|
27
|
+
* Mirrors the seed: a draft holding the record is left alone, since a
|
|
28
|
+
* background refetch failing is no reason to discard it, and anything else is
|
|
29
|
+
* marked failed. A draft the form wrote before the record landed keeps its
|
|
30
|
+
* values — there is no reason to throw away what the user typed — but stops
|
|
31
|
+
* counting as still loading, and a later successful fetch can still seed it.
|
|
32
|
+
*/
|
|
33
|
+
export function applyInvoicingFormDraftFailure(draftById, id, blankLocalData) {
|
|
34
|
+
const existing = draftById[id];
|
|
35
|
+
if (existing == null) {
|
|
36
|
+
draftById[id] = { fetchState: 'Error', localData: blankLocalData };
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (existing.fetchState === 'Completed') {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
existing.fetchState = 'Error';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The record behind this draft was just saved and is about to be refetched.
|
|
46
|
+
* Keep the values on screen — dropping them would blank the form for the round
|
|
47
|
+
* trip, and the form remounts on the response anyway — but stop counting them
|
|
48
|
+
* as the loaded record's, so that response is allowed to replace them.
|
|
49
|
+
*/
|
|
50
|
+
export function applyInvoicingFormDraftSaved(draftById, id) {
|
|
51
|
+
const existing = draftById[id];
|
|
52
|
+
if (existing == null) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
existing.fetchState = 'Not-Started';
|
|
56
|
+
}
|
|
57
|
+
/** The draft's values, or the blank form when nothing is drafted yet. */
|
|
58
|
+
export function getInvoicingFormDraftLocalData(draftById, id, blankLocalData) {
|
|
59
|
+
if (id == null) {
|
|
60
|
+
return blankLocalData;
|
|
61
|
+
}
|
|
62
|
+
return draftById[id]?.localData ?? blankLocalData;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether the form should still be showing its skeleton — the record has
|
|
66
|
+
* neither reached the draft nor failed trying.
|
|
67
|
+
*
|
|
68
|
+
* Settles once and stays settled, which is what keeps a later refetch from
|
|
69
|
+
* swapping a form the user is typing into back for a skeleton. Create mode
|
|
70
|
+
* (no id) has no record to wait for.
|
|
71
|
+
*/
|
|
72
|
+
export function isInvoicingFormDraftPending(draftById, id) {
|
|
73
|
+
if (id == null) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const fetchState = draftById[id]?.fetchState;
|
|
77
|
+
return fetchState !== 'Completed' && fetchState !== 'Error';
|
|
78
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createSelector } from '@reduxjs/toolkit';
|
|
2
|
+
import { toInvoicingQboAccountMappingSlot, } from '../../../entity/invoicing/invoicingCommonPayload';
|
|
1
3
|
import { getAccountList, getNestedAccountListHierarchy, } from '../../accountList/accountListSelector';
|
|
2
4
|
import { getClassList, getNestedClassListHierarchy, } from '../../classList/classListSelector';
|
|
3
5
|
// The shared bucket, fetched with the same arguments as the other consumers so
|
|
@@ -82,14 +84,20 @@ export const getInvoicingCatalogAccountingLookup = (state) => ({
|
|
|
82
84
|
classByQboId: (qboId) => invoicingQboClassByQboId(state, qboId),
|
|
83
85
|
});
|
|
84
86
|
/**
|
|
85
|
-
* Accounts
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
87
|
+
* Accounts as the invoicing pickers need them, from the shared
|
|
88
|
+
* accounting-service list, so whatever another screen already loaded is reused.
|
|
89
|
+
* `qboId` is the submitted value because that is what the invoicing API stores;
|
|
90
|
+
* an account without one cannot be posted, so it is dropped rather than offered.
|
|
91
|
+
*
|
|
92
|
+
* Memoized on the two slices it reads: the pickers and the lists derived from
|
|
93
|
+
* them are handed straight to forms, and a fresh array on every store change
|
|
94
|
+
* re-seeds a form the user is editing.
|
|
90
95
|
*/
|
|
91
|
-
const
|
|
92
|
-
|
|
96
|
+
export const getInvoicingQboAccountOptions = createSelector([
|
|
97
|
+
(state) => state.accountState,
|
|
98
|
+
(state) => state.accountListState,
|
|
99
|
+
], (accountState, accountListState) => {
|
|
100
|
+
const { accounts } = getAccountList(accountState, accountListState, INVOICING_ACCOUNT_LIST_KEY);
|
|
93
101
|
return accounts.flatMap((account) => {
|
|
94
102
|
if (account.qboId == null || account.isDeleted === true) {
|
|
95
103
|
return [];
|
|
@@ -102,7 +110,7 @@ const invoicingAccountingOptions = (state) => {
|
|
|
102
110
|
},
|
|
103
111
|
];
|
|
104
112
|
});
|
|
105
|
-
};
|
|
113
|
+
});
|
|
106
114
|
/**
|
|
107
115
|
* Normalises an account-type name for comparison. The config's mapping kinds
|
|
108
116
|
* name QuickBooks types the way QuickBooks displays them ("Other Current
|
|
@@ -138,6 +146,46 @@ export const invoicingAccountsForQboAccountTypes = (accounts, accountTypes) => {
|
|
|
138
146
|
return accounts.filter((account) => account.accountType != null &&
|
|
139
147
|
wanted.has(normalizeAccountTypeName(account.accountType)));
|
|
140
148
|
};
|
|
149
|
+
/**
|
|
150
|
+
* The account-mapping slot a catalog price point's income account shares with
|
|
151
|
+
* the settings page, and the one a discount's account shares with it.
|
|
152
|
+
*/
|
|
153
|
+
export const INVOICING_INCOME_ACCOUNT_MAPPING_SLOT = 'default_income_account';
|
|
154
|
+
export const INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT = 'discounts_account';
|
|
155
|
+
/**
|
|
156
|
+
* The account types a slot falls back to when the config carries no kind for
|
|
157
|
+
* it. QuickBooks posts a product, a price and a discount line to an income
|
|
158
|
+
* account; offering every account instead would let a save be rejected.
|
|
159
|
+
*/
|
|
160
|
+
export const INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES = 'Income';
|
|
161
|
+
/**
|
|
162
|
+
* The QuickBooks ids a mapping slot's picker may offer, from the account types
|
|
163
|
+
* the config gives that slot — so every picker filling the same slot offers the
|
|
164
|
+
* same accounts, and widening one stays a config change.
|
|
165
|
+
*
|
|
166
|
+
* Ids rather than a filtered tree: the pickers flatten the tree into options by
|
|
167
|
+
* walking each node's children, so dropping a parent that is not itself of the
|
|
168
|
+
* wanted type would drop its matching descendants with it.
|
|
169
|
+
*
|
|
170
|
+
* Matched on the slot rather than the code as written, since a config naming
|
|
171
|
+
* the kind `discounts_account_id` means the same slot. A slot the config does
|
|
172
|
+
* not carry falls back to `defaultAccountTypes` rather than to every account.
|
|
173
|
+
*/
|
|
174
|
+
export const invoicingQboAccountIdsForMappingSlot = (accounts, accountMappingKindOptions, mappingSlot, defaultAccountTypes) => {
|
|
175
|
+
const accountTypes = accountMappingKindOptions.find((kindOption) => toInvoicingQboAccountMappingSlot(kindOption.code) === mappingSlot)?.accountTypes ?? defaultAccountTypes;
|
|
176
|
+
return invoicingAccountsForQboAccountTypes(accounts, accountTypes).map((account) => account.id);
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Stable empties for a tenant with no QBO connection.
|
|
180
|
+
*
|
|
181
|
+
* `?? []` builds a fresh array per call, and the invoicing forms re-seed their
|
|
182
|
+
* fields whenever their option lists change identity — so a form open against a
|
|
183
|
+
* disconnected tenant would reset itself on every unrelated store change, until
|
|
184
|
+
* the first edit disarms the re-seed.
|
|
185
|
+
*/
|
|
186
|
+
export const NO_INVOICING_ACCOUNT_HIERARCHY = [];
|
|
187
|
+
export const NO_INVOICING_CLASS_HIERARCHY = [];
|
|
188
|
+
export const NO_INVOICING_ACCOUNT_IDS = [];
|
|
141
189
|
/** Classes as the invoicing pickers need them, from the shared class list. */
|
|
142
190
|
const invoicingAccountingClassOptions = (state) => {
|
|
143
191
|
const { classes } = getClassList(state.classState, state.classListState);
|
|
@@ -159,7 +207,7 @@ export const getInvoicingQboView = (state) => {
|
|
|
159
207
|
const qboState = state.invoicingQboViewState;
|
|
160
208
|
const accountList = state.accountListState.byReportId[INVOICING_ACCOUNT_LIST_KEY];
|
|
161
209
|
return {
|
|
162
|
-
accounts:
|
|
210
|
+
accounts: getInvoicingQboAccountOptions(state),
|
|
163
211
|
accountsFetchState: {
|
|
164
212
|
fetchState: accountList.fetchState,
|
|
165
213
|
error: accountList.error,
|
|
@@ -1 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
// Only an override opens the feedback popup. Silent accepts and retractions post
|
|
2
|
+
// in the background, so they must not drive the popup's loader or its toast.
|
|
3
|
+
export const isPromptedRecommendationFeedback = (outcome) => outcome === 'override_outside' || outcome === 'override_ranked';
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createSlice } from '@reduxjs/toolkit';
|
|
2
2
|
import { clearAll } from '../../rootActions';
|
|
3
|
+
import { isPromptedRecommendationFeedback, } from './recommendationPayload';
|
|
3
4
|
import { getEntityRecommendationKey, initialEntityRecommendationState, } from './recommendationState';
|
|
4
5
|
export const initialState = {
|
|
6
|
+
feedbackSubmitState: { fetchState: 'Not-Started', error: undefined },
|
|
5
7
|
recommendationByEntity: {},
|
|
6
8
|
};
|
|
7
9
|
const recommendation = createSlice({
|
|
@@ -131,13 +133,31 @@ const recommendation = createSlice({
|
|
|
131
133
|
};
|
|
132
134
|
},
|
|
133
135
|
},
|
|
134
|
-
//
|
|
135
|
-
// override
|
|
136
|
-
//
|
|
137
|
-
//
|
|
136
|
+
// Human feedback on a recommendation (accept, ranked override or outside
|
|
137
|
+
// override). Handled by submitRecommendationFeedbackEpic, which POSTs to the
|
|
138
|
+
// accounting recommendation-feedback endpoint. The categorization itself
|
|
139
|
+
// never waits on this: only the feedback popup reads the submit state.
|
|
138
140
|
submitRecommendationFeedback: {
|
|
139
|
-
reducer() {
|
|
140
|
-
|
|
141
|
+
reducer(draft, action) {
|
|
142
|
+
if (!isPromptedRecommendationFeedback(action.payload.outcome)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
draft.feedbackSubmitState = {
|
|
146
|
+
fetchState: 'In-Progress',
|
|
147
|
+
error: undefined,
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
prepare(args) {
|
|
151
|
+
return { payload: args };
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
submitRecommendationFeedbackCompleted: {
|
|
155
|
+
reducer(draft, action) {
|
|
156
|
+
const { succeeded, error } = action.payload;
|
|
157
|
+
draft.feedbackSubmitState = {
|
|
158
|
+
fetchState: succeeded ? 'Completed' : 'Error',
|
|
159
|
+
error,
|
|
160
|
+
};
|
|
141
161
|
},
|
|
142
162
|
prepare(args) {
|
|
143
163
|
return { payload: args };
|
|
@@ -150,7 +170,7 @@ const recommendation = createSlice({
|
|
|
150
170
|
});
|
|
151
171
|
},
|
|
152
172
|
});
|
|
153
|
-
export const { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, submitRecommendationFeedback, } = recommendation.actions;
|
|
173
|
+
export const { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, submitRecommendationFeedback, submitRecommendationFeedbackCompleted, } = recommendation.actions;
|
|
154
174
|
export default recommendation.reducer;
|
|
155
175
|
const doUpdateRecommendationForEntity = (draft, entityId, entityType, payload) => {
|
|
156
176
|
const key = getEntityRecommendationKey(entityType, entityId);
|