@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.
Files changed (196) hide show
  1. package/lib/entity/billPay/billTransaction/billTransactionReducer.d.ts +5 -1
  2. package/lib/entity/billPay/billTransaction/billTransactionReducer.js +17 -2
  3. package/lib/entity/invoicing/coupon/couponPayload.d.ts +8 -0
  4. package/lib/entity/invoicing/coupon/couponPayload.js +8 -0
  5. package/lib/entity/invoicing/coupon/couponState.d.ts +6 -0
  6. package/lib/entity/invoicing/dunningCase/dunningCaseTypes.d.ts +1 -1
  7. package/lib/entity/invoicing/plan/planSelector.d.ts +20 -0
  8. package/lib/entity/invoicing/plan/planSelector.js +42 -0
  9. package/lib/entity/invoicing/product/productPayload.d.ts +8 -1
  10. package/lib/entity/invoicing/product/productPayload.js +4 -1
  11. package/lib/entity/invoicing/product/productState.d.ts +16 -0
  12. package/lib/entity/jeSchedules/jeSchedulesReducer.d.ts +4 -1
  13. package/lib/entity/jeSchedules/jeSchedulesReducer.js +79 -5
  14. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  15. package/lib/entity/snackbar/snackbarTypes.js +1 -0
  16. package/lib/entity/task/taskState.d.ts +1 -1
  17. package/lib/epic.d.ts +4 -1
  18. package/lib/epic.js +10 -4
  19. package/lib/esm/entity/billPay/billTransaction/billTransactionReducer.js +16 -1
  20. package/lib/esm/entity/invoicing/coupon/couponPayload.js +8 -0
  21. package/lib/esm/entity/invoicing/plan/planSelector.js +39 -0
  22. package/lib/esm/entity/invoicing/product/productPayload.js +4 -1
  23. package/lib/esm/entity/jeSchedules/jeSchedulesReducer.js +78 -4
  24. package/lib/esm/entity/snackbar/snackbarTypes.js +1 -0
  25. package/lib/esm/epic.js +10 -4
  26. package/lib/esm/index.js +8 -7
  27. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +3 -2
  28. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/fetchFailedJePostingsEpic.js +39 -0
  29. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.js +14 -1
  30. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulesEpic.js +10 -13
  31. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/initializeJeScheduleLocalDataEpic.js +38 -17
  32. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/retryJeSchedulesEpic.js +17 -1
  33. package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +15 -4
  34. package/lib/esm/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +6 -0
  35. package/lib/esm/view/expenseAutomationView/reducers/jeSchedulesViewReducer.js +74 -1
  36. package/lib/esm/view/expenseAutomationView/reducers/reconciliationViewReducer.js +80 -53
  37. package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +10 -0
  38. package/lib/esm/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +11 -2
  39. package/lib/esm/view/expenseAutomationView/selectors/jeSchedulesViewSelector.js +49 -14
  40. package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +28 -11
  41. package/lib/esm/view/invoicing/createInvoice/createInvoiceSelector.js +28 -1
  42. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +67 -35
  43. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.js +22 -11
  44. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +23 -4
  45. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/invoicingCatalogItemFormHelper.js +1 -7
  46. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +19 -5
  47. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/seedInvoicingCatalogItemFormDraftEpic.js +30 -7
  48. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewPayload.js +16 -1
  49. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewReducer.js +22 -11
  50. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +69 -6
  51. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.js +22 -0
  52. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/fetchInvoicingCouponDetailPageEpic.js +22 -4
  53. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +18 -4
  54. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/seedInvoicingCouponFormDraftEpic.js +29 -6
  55. package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer.js +28 -12
  56. package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.js +37 -2
  57. package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/initializeInvoicingCustomerAddressEpic.js +43 -17
  58. package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/saveInvoicingCustomerEpic.js +1 -1
  59. package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/seedInvoicingCustomerFormDraftEpic.js +29 -6
  60. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.js +30 -14
  61. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +143 -8
  62. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionFormPageEpic.js +4 -1
  63. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/initializeInvoicingSubscriptionAddressEpic.js +44 -18
  64. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +1 -1
  65. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/seedInvoicingSubscriptionFormDraftEpic.js +29 -8
  66. package/lib/esm/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewSelector.js +12 -0
  67. package/lib/esm/view/invoicing/invoicingDunningCaseListView/invoicingDunningCaseListViewState.js +6 -6
  68. package/lib/esm/view/invoicing/invoicingFormDraft.js +78 -0
  69. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +57 -9
  70. package/lib/esm/view/recommendation/recommendationPayload.js +3 -1
  71. package/lib/esm/view/recommendation/recommendationReducer.js +27 -7
  72. package/lib/esm/view/recommendation/recommendationSelector.js +1 -0
  73. package/lib/esm/view/recommendation/submitRecommendationFeedbackEpic.js +20 -8
  74. package/lib/esm/view/spendManagement/billPay/billDetailView/billDetailViewReducer.js +87 -1
  75. package/lib/esm/view/spendManagement/billPay/billDetailView/billDetailViewSelector.js +1 -0
  76. package/lib/esm/view/spendManagement/billPay/billDetailView/billDetailViewState.js +4 -0
  77. package/lib/esm/view/spendManagement/billPay/billDetailView/epics/fetchBillRecommendationsEpic.js +97 -0
  78. package/lib/esm/view/spendManagement/billPay/billDetailView/epics/fetchRecommendationsForBill.js +85 -0
  79. package/lib/esm/view/spendManagement/billPay/editBillView/editBillViewReducer.js +18 -1
  80. package/lib/esm/view/spendManagement/billPay/editBillView/editBillViewSelector.js +3 -0
  81. package/lib/esm/view/spendManagement/billPay/editBillView/epics/applyBillRecommendationsToLocalStoreEpic.js +95 -0
  82. package/lib/index.d.ts +9 -8
  83. package/lib/index.js +54 -48
  84. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +3 -2
  85. package/lib/view/expenseAutomationView/epics/jeSchedule/fetchFailedJePostingsEpic.d.ts +20 -0
  86. package/lib/view/expenseAutomationView/epics/jeSchedule/fetchFailedJePostingsEpic.js +43 -0
  87. package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.d.ts +2 -2
  88. package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.js +13 -0
  89. package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulesEpic.js +8 -11
  90. package/lib/view/expenseAutomationView/epics/jeSchedule/initializeJeScheduleLocalDataEpic.js +38 -17
  91. package/lib/view/expenseAutomationView/epics/jeSchedule/retryJeSchedulesEpic.d.ts +2 -2
  92. package/lib/view/expenseAutomationView/epics/jeSchedule/retryJeSchedulesEpic.js +17 -1
  93. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +15 -4
  94. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  95. package/lib/view/expenseAutomationView/payload/jeSchedulesPayload.d.ts +18 -0
  96. package/lib/view/expenseAutomationView/payload/transactionCategorizationPayload.d.ts +18 -7
  97. package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +6 -0
  98. package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.d.ts +10 -3
  99. package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.js +76 -3
  100. package/lib/view/expenseAutomationView/reducers/reconciliationViewReducer.js +80 -53
  101. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +10 -0
  102. package/lib/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.d.ts +3 -0
  103. package/lib/view/expenseAutomationView/selectorTypes/jeSchedulesSelectorTypes.d.ts +3 -0
  104. package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +8 -4
  105. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.d.ts +1 -0
  106. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +12 -2
  107. package/lib/view/expenseAutomationView/selectors/jeSchedulesViewSelector.js +49 -14
  108. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +27 -10
  109. package/lib/view/expenseAutomationView/types/jeSchedulesViewState.d.ts +16 -0
  110. package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +8 -5
  111. package/lib/view/invoicing/createInvoice/createInvoiceSelector.d.ts +7 -2
  112. package/lib/view/invoicing/createInvoice/createInvoiceSelector.js +28 -1
  113. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.d.ts +46 -5
  114. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +69 -36
  115. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.d.ts +3 -1
  116. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.js +22 -11
  117. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +17 -0
  118. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +22 -3
  119. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewState.d.ts +2 -1
  120. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/invoicingCatalogItemFormHelper.d.ts +2 -2
  121. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/invoicingCatalogItemFormHelper.js +1 -7
  122. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +17 -3
  123. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/seedInvoicingCatalogItemFormDraftEpic.d.ts +10 -6
  124. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/seedInvoicingCatalogItemFormDraftEpic.js +29 -6
  125. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewPayload.d.ts +5 -1
  126. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewPayload.js +16 -1
  127. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewReducer.d.ts +3 -1
  128. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewReducer.js +22 -11
  129. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.d.ts +46 -0
  130. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +68 -5
  131. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.d.ts +21 -1
  132. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.js +24 -1
  133. package/lib/view/invoicing/editInvoicingCouponDetailView/fetchInvoicingCouponDetailPageEpic.d.ts +9 -5
  134. package/lib/view/invoicing/editInvoicingCouponDetailView/fetchInvoicingCouponDetailPageEpic.js +22 -4
  135. package/lib/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +18 -4
  136. package/lib/view/invoicing/editInvoicingCouponDetailView/seedInvoicingCouponFormDraftEpic.d.ts +10 -6
  137. package/lib/view/invoicing/editInvoicingCouponDetailView/seedInvoicingCouponFormDraftEpic.js +28 -5
  138. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer.d.ts +5 -1
  139. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer.js +29 -13
  140. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.d.ts +16 -7
  141. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.js +37 -2
  142. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewState.d.ts +8 -1
  143. package/lib/view/invoicing/editInvoicingCustomerDetailView/initializeInvoicingCustomerAddressEpic.d.ts +15 -10
  144. package/lib/view/invoicing/editInvoicingCustomerDetailView/initializeInvoicingCustomerAddressEpic.js +42 -16
  145. package/lib/view/invoicing/editInvoicingCustomerDetailView/saveInvoicingCustomerEpic.js +1 -1
  146. package/lib/view/invoicing/editInvoicingCustomerDetailView/seedInvoicingCustomerFormDraftEpic.d.ts +10 -6
  147. package/lib/view/invoicing/editInvoicingCustomerDetailView/seedInvoicingCustomerFormDraftEpic.js +28 -5
  148. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.d.ts +5 -1
  149. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.js +31 -15
  150. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.d.ts +79 -12
  151. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +145 -9
  152. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewState.d.ts +8 -1
  153. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionFormPageEpic.js +4 -1
  154. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/initializeInvoicingSubscriptionAddressEpic.d.ts +15 -11
  155. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/initializeInvoicingSubscriptionAddressEpic.js +42 -16
  156. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +1 -1
  157. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/seedInvoicingSubscriptionFormDraftEpic.d.ts +10 -6
  158. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/seedInvoicingSubscriptionFormDraftEpic.js +28 -7
  159. package/lib/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewSelector.d.ts +10 -0
  160. package/lib/view/invoicing/invoicingCatalogListView/invoicingCatalogListViewSelector.js +12 -0
  161. package/lib/view/invoicing/invoicingDunningCaseListView/invoicingDunningCaseListViewReducer.d.ts +1 -1
  162. package/lib/view/invoicing/invoicingDunningCaseListView/invoicingDunningCaseListViewState.d.ts +2 -2
  163. package/lib/view/invoicing/invoicingDunningCaseListView/invoicingDunningCaseListViewState.js +6 -6
  164. package/lib/view/invoicing/invoicingFormDraft.d.ts +63 -0
  165. package/lib/view/invoicing/invoicingFormDraft.js +86 -0
  166. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +49 -0
  167. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +59 -10
  168. package/lib/view/invoicing/invoicingSubscriptionListView/invoicingSubscriptionListViewReducer.d.ts +1 -1
  169. package/lib/view/invoicing/invoicingSubscriptionListView/invoicingSubscriptionListViewState.d.ts +1 -1
  170. package/lib/view/recommendation/recommendationPayload.d.ts +1 -0
  171. package/lib/view/recommendation/recommendationPayload.js +5 -0
  172. package/lib/view/recommendation/recommendationReducer.d.ts +7 -1
  173. package/lib/view/recommendation/recommendationReducer.js +28 -8
  174. package/lib/view/recommendation/recommendationSelector.d.ts +2 -0
  175. package/lib/view/recommendation/recommendationSelector.js +3 -1
  176. package/lib/view/recommendation/recommendationState.d.ts +2 -1
  177. package/lib/view/recommendation/submitRecommendationFeedbackEpic.d.ts +2 -2
  178. package/lib/view/recommendation/submitRecommendationFeedbackEpic.js +18 -6
  179. package/lib/view/spendManagement/billPay/billDetailView/billDetailViewPayload.d.ts +19 -0
  180. package/lib/view/spendManagement/billPay/billDetailView/billDetailViewReducer.d.ts +10 -1
  181. package/lib/view/spendManagement/billPay/billDetailView/billDetailViewReducer.js +88 -2
  182. package/lib/view/spendManagement/billPay/billDetailView/billDetailViewSelector.d.ts +1 -0
  183. package/lib/view/spendManagement/billPay/billDetailView/billDetailViewSelector.js +1 -0
  184. package/lib/view/spendManagement/billPay/billDetailView/billDetailViewState.d.ts +7 -0
  185. package/lib/view/spendManagement/billPay/billDetailView/billDetailViewState.js +4 -0
  186. package/lib/view/spendManagement/billPay/billDetailView/epics/fetchBillRecommendationsEpic.d.ts +37 -0
  187. package/lib/view/spendManagement/billPay/billDetailView/epics/fetchBillRecommendationsEpic.js +104 -0
  188. package/lib/view/spendManagement/billPay/billDetailView/epics/fetchRecommendationsForBill.d.ts +24 -0
  189. package/lib/view/spendManagement/billPay/billDetailView/epics/fetchRecommendationsForBill.js +92 -0
  190. package/lib/view/spendManagement/billPay/editBillView/editBillViewReducer.d.ts +4 -1
  191. package/lib/view/spendManagement/billPay/editBillView/editBillViewReducer.js +19 -2
  192. package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.d.ts +1 -0
  193. package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.js +3 -0
  194. package/lib/view/spendManagement/billPay/editBillView/epics/applyBillRecommendationsToLocalStoreEpic.d.ts +18 -0
  195. package/lib/view/spendManagement/billPay/editBillView/epics/applyBillRecommendationsToLocalStoreEpic.js +102 -0
  196. 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[invoicingSubscriptionID] ??
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
- 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);
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
- coupons: getInvoicingCouponListItems(state),
70
- 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),
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, of } from 'rxjs';
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 an existing subscription's saved shipping
11
- * address (looked up in the shared address store by `shippingAddressId`, same as
12
- * the customer billing-address flow).
13
- * Runs both when the subscription detail loads and on an explicit initialize
14
- * trigger. Skips when an in-progress draft already exists, so an address the
15
- * user just entered survives navigating between the form and the Address screen.
16
- * Mirrors `initializeInvoicingCustomerAddressEpic`.
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)), mergeMap((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 existingDraft = rootState.addressViewState.newAddressState?.[INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE]?.addressToCreate;
23
- if (existingDraft != null || id == null) {
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
- return address != null
31
- ? of(newAddressInLocalStore({
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
- : EMPTY;
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
  }));
@@ -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 moment the detail fetch lands, so
8
- * the form has its values before it mounts. The reducer ignores the seed when a
9
- * draft already exists, which is what stops a background refetch from
10
- * overwriting edits in progress.
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), withLatestFrom(state$), mergeMap(([action, state]) => {
13
- const { id } = action.payload;
14
- const subscription = getInvoicingSubscriptionById(state, id);
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,
@@ -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
- 'Retrying',
26
- 'Scheduled',
27
- 'Manual Review',
28
- 'Paused',
29
- 'Resolved',
30
- 'Cancelled',
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 and classes as the invoicing pickers need them. Both come from the
86
- * shared accounting-service lists, so whatever another screen already loaded
87
- * is reused. `qboId` is the submitted value because that is what the invoicing
88
- * API stores; an account without one cannot be posted, so it is dropped rather
89
- * than offered.
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 invoicingAccountingOptions = (state) => {
92
- const { accounts } = getAccountList(state.accountState, state.accountListState, INVOICING_ACCOUNT_LIST_KEY);
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: invoicingAccountingOptions(state),
210
+ accounts: getInvoicingQboAccountOptions(state),
163
211
  accountsFetchState: {
164
212
  fetchState: accountList.fetchState,
165
213
  error: accountList.error,
@@ -1 +1,3 @@
1
- export {};
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
- // Fire-and-forget human feedback on a recommendation (accept, ranked
135
- // override or outside override). Handled by submitRecommendationFeedbackEpic
136
- // which POSTs to the accounting recommendation-feedback endpoint. No state
137
- // mutation: the categorization UX must not depend on the feedback round-trip.
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
- // Fire-and-forget: no state mutation; the epic performs the POST.
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);
@@ -11,3 +11,4 @@ export const getRecommendationsForEntity = (state, entityId, entityType) => {
11
11
  recommendations,
12
12
  };
13
13
  };
14
+ export const getRecommendationFeedbackSubmitState = (state) => state.entityRecommendationState.feedbackSubmitState;