@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
@@ -3,7 +3,6 @@ import { toClassBasePayload } from '../../../entity/class/classPayload';
3
3
  import { blankInvoicingCatalogSubFamily, isTieredCatalogPricingModel, } from './invoicingCatalogItemFormHelper';
4
4
  const CURRENCY_CODE = 'USD';
5
5
  const trimOrUndefined = (value) => value.trim() !== '' ? value.trim() : undefined;
6
- const trimOrNull = (value) => value.trim() !== '' ? value.trim() : null;
7
6
  const toProductCode = (value) => value
8
7
  .trim()
9
8
  .toLowerCase()
@@ -91,13 +90,24 @@ const buildSubFamilyPayload = (localData, subFamily, clearEmptyValues, lookup) =
91
90
  /** `unit` is the item's unit of measure, not a currency; `CURRENCY_CODE` above prices Price points. */
92
91
  const DEFAULT_CATALOG_ITEM_UNIT = 'USD';
93
92
  /**
94
- * Build the parent catalog-item request body from the form draft. Creates omit
95
- * both parent and price point IDs; edits retain returned price point IDs.
93
+ * Build the parent catalog-item request body from the form draft. Every field
94
+ * the form holds is sent, so a value cleared on the form is cleared on the
95
+ * item: the service reads an absent key as "leave it alone", which is why
96
+ * `description` travels as an empty string and `applicable_items` as an empty
97
+ * list rather than being dropped. Fields the form does not collect
98
+ * (`external_name`, `item_family_id`, `metadata`, …) stay off the body so an
99
+ * edit cannot overwrite them with a value nobody chose.
96
100
  */
97
101
  export const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate, lookup) => {
98
- const description = trimOrUndefined(localData.description);
99
102
  const productCode = trimOrUndefined(localData.productCode) ?? toProductCode(localData.name);
100
103
  const itemPayload = {
104
+ // `applicable_items` only means anything while the item is restricted;
105
+ // sending the empty list otherwise is what clears a restriction that was
106
+ // just lifted.
107
+ applicable_items: localData.itemApplicability === 'restricted'
108
+ ? localData.applicableItems
109
+ : [],
110
+ description: trimOrUndefined(localData.description) ?? '',
101
111
  enabled_for_checkout: true,
102
112
  enabled_in_portal: true,
103
113
  is_giftable: false,
@@ -107,41 +117,63 @@ export const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate,
107
117
  name: localData.name.trim(),
108
118
  product_code: productCode,
109
119
  type: localData.catalogType,
120
+ // The form no longer collects a unit; an item that already has one keeps it.
121
+ unit: trimOrUndefined(localData.unit) ?? DEFAULT_CATALOG_ITEM_UNIT,
110
122
  };
111
- if (description != null) {
112
- itemPayload.description = description;
113
- }
114
- // The form no longer collects a unit; an item that already has one keeps it.
115
- itemPayload.unit =
116
- trimOrUndefined(localData.unit) ?? DEFAULT_CATALOG_ITEM_UNIT;
117
- if (localData.itemApplicability === 'restricted' &&
118
- localData.applicableItems.length > 0) {
119
- itemPayload.applicable_items = localData.applicableItems;
123
+ // The external id is assigned by the billing service and shown read-only, so
124
+ // it is echoed back only when the item already has one: sending the blank a
125
+ // create starts with would clear an id assigned in the meantime.
126
+ const externalId = trimOrUndefined(localData.externalId);
127
+ if (externalId != null) {
128
+ itemPayload.external_id = externalId;
120
129
  }
121
130
  const subFamilies = localData.subFamilies.length > 0
122
131
  ? localData.subFamilies
123
132
  : [blankInvoicingCatalogSubFamily()];
124
- itemPayload.sub_family_details = isCreate
125
- ? subFamilies.map((subFamily) => buildSubFamilyPayload(localData, subFamily, false, lookup))
126
- : subFamilies.map((subFamily) => {
127
- const priceCode = trimOrUndefined(subFamily.priceCode) ?? toProductCode(subFamily.name);
128
- const detail = {
129
- description: trimOrNull(subFamily.description),
130
- invoice_notes: trimOrNull(subFamily.invoiceNotes),
131
- name: subFamily.name.trim(),
132
- price_code: priceCode,
133
- show_description_in_invoices: subFamily.showDescriptionInInvoices,
134
- };
135
- // Edits send a narrower body than creates, but the accounting mapping
136
- // is editable on this form, so leaving it out dropped the account and
137
- // class the user had just picked. `null` rather than omitted when it is
138
- // empty: an absent key on an update reads as "leave it alone", which
139
- // would make a cleared mapping impossible to save — the same reason
140
- // `description` and `invoice_notes` go through `trimOrNull`.
141
- detail.accounting_detail =
142
- buildAccountingDetail(subFamily, lookup) ?? null;
143
- const id = trimOrUndefined(subFamily.id);
144
- return id != null ? { ...detail, id } : detail;
145
- });
133
+ if (isCreate) {
134
+ itemPayload.sub_families = subFamilies.map((subFamily) => buildSubFamilyPayload(localData, subFamily, false, lookup));
135
+ return itemPayload;
136
+ }
137
+ // Only price points the service has already stored travel in an update: it
138
+ // addresses them by id, and a blank one has nothing to address. A price point
139
+ // added on the form is created by its own request once the item is saved,
140
+ // rather than riding along as a row the update cannot place.
141
+ itemPayload.sub_families = subFamilies
142
+ .filter((subFamily) => subFamily.id !== '')
143
+ .map((subFamily) => {
144
+ // Edits send the same body a create does. The narrower hand-built one
145
+ // this replaces had to be kept in step by hand and was not: it dropped
146
+ // `accounting_detail` once, and `billing_cycles`, `price`,
147
+ // `pricing_model`, `period`, the trial fields, `proration_type`,
148
+ // `free_quantity`, `is_taxable` and `tiers` were never added at all, so
149
+ // editing any of them looked saved and changed nothing.
150
+ const detail = buildSubFamilyPayload(localData, subFamily, true, lookup);
151
+ // `null` rather than omitted when empty: an absent key on an update
152
+ // reads as "leave it alone", which would make a cleared mapping
153
+ // impossible to save — the same reason `description` and
154
+ // `invoice_notes` are cleared rather than dropped above.
155
+ detail.accounting_detail = detail.accounting_detail ?? null;
156
+ // Never blank: the filter above leaves only price points the service has
157
+ // already stored, which is the only kind an update can address.
158
+ return { ...detail, id: subFamily.id };
159
+ });
146
160
  return itemPayload;
147
161
  };
162
+ /**
163
+ * The bodies that create the price points added on an edit form, one per
164
+ * request to `/catalog/products`.
165
+ *
166
+ * An update cannot carry them: it addresses price points by id, and these have
167
+ * none until the service assigns one. They are bound to their item by
168
+ * `item_id`, so this needs the saved item's id and runs after the item itself
169
+ * is saved.
170
+ *
171
+ * Empty values are left off rather than sent as `null`: there is nothing on a
172
+ * price point that does not exist yet to clear.
173
+ */
174
+ export const localDataToCreateInvoicingCatalogProductPayloads = (localData, itemId, lookup) => localData.subFamilies
175
+ .filter((subFamily) => subFamily.id === '')
176
+ .map((subFamily) => ({
177
+ ...buildSubFamilyPayload(localData, subFamily, false, lookup),
178
+ item_id: itemId,
179
+ }));
@@ -1,5 +1,6 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
2
  import { clearAll } from '../../../rootActions';
3
+ import { applyInvoicingFormDraftEdit, applyInvoicingFormDraftFailure, applyInvoicingFormDraftSaved, applyInvoicingFormDraftSeed, } from '../invoicingFormDraft';
3
4
  import { initialInvoicingCatalogItemFormLocalData } from './invoicingCatalogItemFormHelper';
4
5
  export const initialState = {
5
6
  fetchState: 'Not-Started',
@@ -21,7 +22,7 @@ const editInvoicingCatalogItemDetailView = createSlice({
21
22
  updateInvoicingCatalogItemFormDraft(draft, action) {
22
23
  const { id, localData } = action.payload;
23
24
  if (id != null) {
24
- draft.editDraftById[id] = localData;
25
+ applyInvoicingFormDraftEdit(draft.editDraftById, id, localData);
25
26
  }
26
27
  else {
27
28
  draft.newItemDraft = localData;
@@ -42,6 +43,9 @@ const editInvoicingCatalogItemDetailView = createSlice({
42
43
  draft.error = undefined;
43
44
  draft.savedItemId = action.payload.itemId;
44
45
  draft.savedKind = action.payload.kind;
46
+ // An edit is refetched on success; keep the saved values on screen until
47
+ // that lands, and let it replace them.
48
+ applyInvoicingFormDraftSaved(draft.editDraftById, action.payload.itemId);
45
49
  },
46
50
  submitInvoicingCatalogItemFormFailure(draft, action) {
47
51
  draft.fetchState = 'Error';
@@ -64,6 +68,7 @@ const editInvoicingCatalogItemDetailView = createSlice({
64
68
  fetchState: 'Error',
65
69
  error: action.payload.error,
66
70
  };
71
+ applyInvoicingFormDraftFailure(draft.editDraftById, action.payload.id, initialInvoicingCatalogItemFormLocalData);
67
72
  },
68
73
  fetchInvoicingCatalogItemDetailPage: {
69
74
  reducer() {
@@ -79,14 +84,10 @@ const editInvoicingCatalogItemDetailView = createSlice({
79
84
  };
80
85
  },
81
86
  },
82
- /**
83
- * Seed the draft from the loaded record, once. Skipped when a draft already
84
- * exists so a late-arriving refetch cannot overwrite in-progress edits.
85
- */
87
+ /** Fill the draft from the loaded record — see `applyInvoicingFormDraftSeed`. */
86
88
  seedInvoicingCatalogItemFormDraft(draft, action) {
87
- if (draft.editDraftById[action.payload.id] == null) {
88
- draft.editDraftById[action.payload.id] = action.payload.localData;
89
- }
89
+ const { id, localData } = action.payload;
90
+ applyInvoicingFormDraftSeed(draft.editDraftById, id, localData);
90
91
  },
91
92
  runInvoicingCatalogPlanAction(draft, action) {
92
93
  draft.actionStateById[action.payload.id] = {
@@ -125,13 +126,23 @@ const editInvoicingCatalogItemDetailView = createSlice({
125
126
  error: action.payload.error,
126
127
  };
127
128
  },
128
- resetEditInvoicingCatalogItemDetailView(draft) {
129
+ /**
130
+ * Discard a form's draft — the catalog item's own when `id` is given,
131
+ * the create draft otherwise. Scoped to the one form on purpose: another
132
+ * catalog item's half-finished edit is not this form's to throw away.
133
+ * Clearing every draft at once is what `clearAll` is for.
134
+ */
135
+ resetEditInvoicingCatalogItemDetailView(draft, action) {
129
136
  draft.fetchState = 'Not-Started';
130
137
  draft.error = undefined;
131
138
  draft.savedItemId = undefined;
132
139
  draft.savedKind = undefined;
133
- draft.editDraftById = {};
134
- draft.newItemDraft = { ...initialInvoicingCatalogItemFormLocalData };
140
+ const id = action.payload?.id;
141
+ if (id == null) {
142
+ draft.newItemDraft = { ...initialInvoicingCatalogItemFormLocalData };
143
+ return;
144
+ }
145
+ delete draft.editDraftById[id];
135
146
  },
136
147
  },
137
148
  extraReducers: (builder) => {
@@ -2,7 +2,8 @@ import { createSelector } from '@reduxjs/toolkit';
2
2
  import { reduceAllFetchState } from '../../../commonStateTypes/reduceFetchState';
3
3
  import { getInvoicingPlanById, getInvoicingPlansByIds, } from '../../../entity/invoicing/plan/planSelector';
4
4
  import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
5
- import { getInvoicingQboView, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
5
+ import { getInvoicingFormDraftLocalData, isInvoicingFormDraftPending, } from '../invoicingFormDraft';
6
+ import { INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES, INVOICING_INCOME_ACCOUNT_MAPPING_SLOT, NO_INVOICING_ACCOUNT_HIERARCHY, NO_INVOICING_ACCOUNT_IDS, NO_INVOICING_CLASS_HIERARCHY, getInvoicingQboAccountOptions, getInvoicingQboView, invoicingQboAccountIdsForMappingSlot, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
6
7
  import { INVOICING_CATALOG_APPLICABILITY_VALUES, INVOICING_CATALOG_PRICING_MODEL_VALUES, INVOICING_CATALOG_PRORATION_VALUES, INVOICING_CATALOG_TIER_TYPE_VALUES, initialInvoicingCatalogItemFormLocalData, } from './invoicingCatalogItemFormHelper';
7
8
  export const getEditInvoicingCatalogItemDetailViewState = (state) => ({
8
9
  fetchState: state.editInvoicingCatalogItemDetailViewState.fetchState,
@@ -43,6 +44,21 @@ const NOT_STARTED = {
43
44
  error: undefined,
44
45
  fetchState: 'Not-Started',
45
46
  };
47
+ /**
48
+ * The QuickBooks ids a price point's income account may be picked from.
49
+ *
50
+ * Memoized, and collapsing to one shared empty array while QBO is disconnected:
51
+ * the pages turn this into their picker options and re-seed their form defaults
52
+ * when those change, so a fresh array on every store change would reset the form
53
+ * under the user — including mid-save.
54
+ */
55
+ const getInvoicingCatalogIncomeAccountIds = createSelector([
56
+ getInvoicingQboAccountOptions,
57
+ (state) => getInvoicingConfigView(state).qboAccountMappingKindOptions,
58
+ isInvoicingQboConnected,
59
+ ], (accounts, qboAccountMappingKindOptions, isQboConnected) => isQboConnected
60
+ ? invoicingQboAccountIdsForMappingSlot(accounts, qboAccountMappingKindOptions, INVOICING_INCOME_ACCOUNT_MAPPING_SLOT, INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES)
61
+ : NO_INVOICING_ACCOUNT_IDS);
46
62
  export const getInvoicingCatalogItemFormView = (state, id) => {
47
63
  const { editDraftById, error, fetchState, newItemDraft, savedItemId, savedKind, } = state.editInvoicingCatalogItemDetailViewState;
48
64
  const plan = id != null ? getInvoicingPlanById(state.invoicingPlanState, id) : undefined;
@@ -52,7 +68,7 @@ export const getInvoicingCatalogItemFormView = (state, id) => {
52
68
  // lands. Until then the page is showing its skeleton, so the blank shape here
53
69
  // is never rendered.
54
70
  const localData = id != null
55
- ? (editDraftById[id] ?? initialInvoicingCatalogItemFormLocalData)
71
+ ? getInvoicingFormDraftLocalData(editDraftById, id, initialInvoicingCatalogItemFormLocalData)
56
72
  : newItemDraft;
57
73
  const allApplicableItemOptions = getInvoicingApplicableCatalogItemOptions(state);
58
74
  const applicableItemOptions = id != null
@@ -90,9 +106,12 @@ export const getInvoicingCatalogItemFormView = (state, id) => {
90
106
  catalogProductLifecycleActionOptions,
91
107
  recordState,
92
108
  fieldHelp: invoicingFieldHelpForGroup(state, 'catalog'),
109
+ isFormLoading: isInvoicingAccountingPickerDataLoading(state) ||
110
+ isInvoicingFormDraftPending(editDraftById, id),
93
111
  isQboPickerDataLoading: isInvoicingAccountingPickerDataLoading(state),
94
- qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? [],
95
- qboClassHierarchy: qboView?.nestedClassHierarchy ?? [],
112
+ qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? NO_INVOICING_ACCOUNT_HIERARCHY,
113
+ qboClassHierarchy: qboView?.nestedClassHierarchy ?? NO_INVOICING_CLASS_HIERARCHY,
114
+ qboIncomeAccountIds: getInvoicingCatalogIncomeAccountIds(state),
96
115
  applicabilityValues: INVOICING_CATALOG_APPLICABILITY_VALUES,
97
116
  applicableItemOptions,
98
117
  localData,
@@ -3,13 +3,7 @@
3
3
  * for pricing model, tier type, proration or applicability yet, so these stay
4
4
  * here until it does. Everything else on this form comes from `/1.0/config`.
5
5
  */
6
- export const INVOICING_CATALOG_PRICING_MODEL_VALUES = [
7
- 'flat_fee',
8
- 'per_unit',
9
- 'tiered',
10
- 'volume',
11
- 'stairstep',
12
- ];
6
+ export const INVOICING_CATALOG_PRICING_MODEL_VALUES = ['flat_fee', 'per_unit', 'tiered', 'volume', 'stairstep'];
13
7
  export const INVOICING_CATALOG_TIER_TYPE_VALUES = [
14
8
  'per_unit',
15
9
  'flat_fee',
@@ -1,4 +1,4 @@
1
- import { from } from 'rxjs';
1
+ import { forkJoin, from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { mergeInvoicingPlanSubFamily, updateInvoicingPlans, } from '../../../entity/invoicing/plan/planReducer';
4
4
  import { updateInvoicingProducts } from '../../../entity/invoicing/product/productReducer';
@@ -7,7 +7,7 @@ import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
7
7
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
8
8
  import { addInvoicingCatalogItem } from '../invoicingCatalogListView/invoicingCatalogListViewReducer';
9
9
  import { getInvoicingCatalogAccountingLookup } from '../invoicingQboView/invoicingQboViewSelector';
10
- import { localDataToSaveInvoicingCatalogItemPayload, } from './editInvoicingCatalogItemDetailViewPayload';
10
+ import { localDataToCreateInvoicingCatalogProductPayloads, localDataToSaveInvoicingCatalogItemPayload, } from './editInvoicingCatalogItemDetailViewPayload';
11
11
  import { submitInvoicingCatalogItemForm, submitInvoicingCatalogItemFormFailure, submitInvoicingCatalogItemFormSuccess, } from './editInvoicingCatalogItemDetailViewReducer';
12
12
  export const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter((action) => submitInvoicingCatalogItemForm.match(action)), mergeMap((action) => {
13
13
  const { id } = action.payload;
@@ -17,8 +17,9 @@ export const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actio
17
17
  // class become the records the service stores. Price points ride along
18
18
  // nested, so the detail page and the create/edit form share one request.
19
19
  const viewState = state.editInvoicingCatalogItemDetailViewState;
20
- const localData = (id != null ? viewState.editDraftById[id] : viewState.newItemDraft) ??
21
- viewState.newItemDraft;
20
+ const localData = (id != null
21
+ ? viewState.editDraftById[id]?.localData
22
+ : viewState.newItemDraft) ?? viewState.newItemDraft;
22
23
  const data = localDataToSaveInvoicingCatalogItemPayload(localData, id == null, accountingLookup);
23
24
  const baseUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0`;
24
25
  const path = id != null
@@ -80,7 +81,20 @@ export const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actio
80
81
  }));
81
82
  return resultActions;
82
83
  };
83
- return completeSave([]);
84
+ // Price points added on an edit form cannot ride in the update:
85
+ // it addresses them by id and they have none yet. They are created
86
+ // against the item that was just saved, and their responses join
87
+ // the same success — one of them failing is the whole save
88
+ // failing, since the item on screen would otherwise claim price
89
+ // points the service never stored. A create needs none of this:
90
+ // its price points are nested in the item body.
91
+ const newProductPayloads = id != null
92
+ ? localDataToCreateInvoicingCatalogProductPayloads(localData, savedItem.id, accountingLookup)
93
+ : [];
94
+ if (newProductPayloads.length === 0) {
95
+ return completeSave([]);
96
+ }
97
+ return forkJoin(newProductPayloads.map((productPayload) => zeniAPI.postAndGetJSON(`${baseUrl}/catalog/products`, productPayload))).pipe(mergeMap((productResponses) => completeSave(productResponses)));
84
98
  }
85
99
  return from([
86
100
  submitInvoicingCatalogItemFormFailure(response.status),
@@ -1,16 +1,39 @@
1
1
  import { EMPTY, of } from 'rxjs';
2
2
  import { filter, mergeMap, withLatestFrom } from 'rxjs/operators';
3
3
  import { getInvoicingPlanById } from '../../../entity/invoicing/plan/planSelector';
4
- import { seedInvoicingCatalogItemFormDraft, updateInvoicingCatalogItemDetail, } from './editInvoicingCatalogItemDetailViewReducer';
4
+ import { fetchInvoicingCatalogItemDetailPage, seedInvoicingCatalogItemFormDraft, updateInvoicingCatalogItemDetail, } from './editInvoicingCatalogItemDetailViewReducer';
5
5
  import { entityToInvoicingCatalogItemFormLocalData } from './invoicingCatalogItemFormHelper';
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 seedInvoicingCatalogItemFormDraftEpic = (actions$, state$) => actions$.pipe(filter(updateInvoicingCatalogItemDetail.match), withLatestFrom(state$), mergeMap(([action, state]) => {
13
- const { id } = action.payload;
16
+ export const seedInvoicingCatalogItemFormDraftEpic = (actions$, state$) => actions$.pipe(filter((action) => updateInvoicingCatalogItemDetail.match(action) ||
17
+ fetchInvoicingCatalogItemDetailPage.match(action)), withLatestFrom(state$), mergeMap(([action, state]) => {
18
+ const id = action.payload.id;
19
+ if (id == null) {
20
+ return EMPTY;
21
+ }
22
+ // The entity alone does not say the whole record is loaded — the list
23
+ // endpoints merge their own list-shaped rows into the same store. Only a
24
+ // completed detail load does, which is also exactly when the page's
25
+ // cache-guarded fetch issues no request and this is the sole seed.
26
+ // A forced refetch is the caller asking for fresh data: the response will
27
+ // seed, and seeding first from the entity as it stands would mark the
28
+ // draft loaded and lock that response out.
29
+ if (fetchInvoicingCatalogItemDetailPage.match(action) &&
30
+ (action.payload.cacheOverride === true ||
31
+ state.editInvoicingCatalogItemDetailViewState.recordStateById[id]
32
+ ?.fetchState !== 'Completed')) {
33
+ return EMPTY;
34
+ }
35
+ // Nothing to seed from on a cold open; the detail response triggers this
36
+ // again once it lands.
14
37
  const plan = getInvoicingPlanById(state.invoicingPlanState, id);
15
38
  if (plan == null) {
16
39
  return EMPTY;
@@ -1,9 +1,13 @@
1
+ import { toAccountPayload } from '../../../entity/account/accountPayload';
1
2
  /**
2
3
  * Build the create/update discount request body from the form draft (snake_case
3
4
  * matching the `/discounts` schema). On create the coupon id is part of the
4
5
  * body; on edit it is the URL param, so it is omitted here.
6
+ *
7
+ * `accountByQboId` resolves the discounts-account picker back to the account
8
+ * record the body carries, the same way the catalog and QBO settings bodies do.
5
9
  */
6
- export const localDataToSaveInvoicingCouponPayload = (localData, isCreate) => {
10
+ export const localDataToSaveInvoicingCouponPayload = (localData, isCreate, accountByQboId) => {
7
11
  const data = {
8
12
  apply_on: localData.applyOn,
9
13
  discount_type: localData.discountType,
@@ -54,5 +58,16 @@ export const localDataToSaveInvoicingCouponPayload = (localData, isCreate) => {
54
58
  if (invoiceNotes !== '') {
55
59
  data.invoice_notes = invoiceNotes;
56
60
  }
61
+ // Omitted rather than sent as `null` when there is none: the service reads a
62
+ // blank discounts account as "not given" and leaves what it has, which is
63
+ // what keeps a form that never showed the picker — QBO disconnected — from
64
+ // clearing an account set elsewhere.
65
+ const discountsAccountId = localData.discountsAccountId.trim();
66
+ const discountsAccount = discountsAccountId !== ''
67
+ ? accountByQboId?.(discountsAccountId)
68
+ : undefined;
69
+ if (discountsAccount != null) {
70
+ data.discounts_account = toAccountPayload(discountsAccount);
71
+ }
57
72
  return data;
58
73
  };
@@ -1,5 +1,6 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
2
  import { clearAll } from '../../../rootActions';
3
+ import { applyInvoicingFormDraftEdit, applyInvoicingFormDraftFailure, applyInvoicingFormDraftSaved, applyInvoicingFormDraftSeed, } from '../invoicingFormDraft';
3
4
  import { initialInvoicingCouponFormLocalData, } from './editInvoicingCouponDetailViewState';
4
5
  export const initialState = {
5
6
  fetchState: 'Not-Started',
@@ -20,7 +21,7 @@ const editInvoicingCouponDetailView = createSlice({
20
21
  updateInvoicingCouponFormDraft(draft, action) {
21
22
  const { id, localData } = action.payload;
22
23
  if (id != null) {
23
- draft.editDraftById[id] = localData;
24
+ applyInvoicingFormDraftEdit(draft.editDraftById, id, localData);
24
25
  }
25
26
  else {
26
27
  draft.newCouponDraft = localData;
@@ -50,6 +51,9 @@ const editInvoicingCouponDetailView = createSlice({
50
51
  draft.fetchState = 'Completed';
51
52
  draft.error = undefined;
52
53
  draft.savedCouponId = action.payload.couponId;
54
+ // An edit is refetched on success; keep the saved values on screen until
55
+ // that lands, and let it replace them.
56
+ applyInvoicingFormDraftSaved(draft.editDraftById, action.payload.couponId);
53
57
  },
54
58
  submitInvoicingCouponFormFailure(draft, action) {
55
59
  draft.fetchState = 'Error';
@@ -85,15 +89,12 @@ const editInvoicingCouponDetailView = createSlice({
85
89
  fetchState: 'Error',
86
90
  error: action.payload.status,
87
91
  };
92
+ applyInvoicingFormDraftFailure(draft.editDraftById, action.payload.id, initialInvoicingCouponFormLocalData);
88
93
  },
89
- /**
90
- * Seed the draft from the loaded record, once. Skipped when a draft already
91
- * exists so a late refetch cannot overwrite in-progress edits.
92
- */
94
+ /** Fill the draft from the loaded record — see `applyInvoicingFormDraftSeed`. */
93
95
  seedInvoicingCouponFormDraft(draft, action) {
94
- if (draft.editDraftById[action.payload.id] == null) {
95
- draft.editDraftById[action.payload.id] = action.payload.localData;
96
- }
96
+ const { id, localData } = action.payload;
97
+ applyInvoicingFormDraftSeed(draft.editDraftById, id, localData);
97
98
  },
98
99
  runInvoicingDiscountAction(draft, action) {
99
100
  draft.actionStateById[action.payload.id] = {
@@ -113,12 +114,22 @@ const editInvoicingCouponDetailView = createSlice({
113
114
  error: action.payload.error,
114
115
  };
115
116
  },
116
- resetEditInvoicingCouponDetailView(draft) {
117
+ /**
118
+ * Discard a form's draft — the coupon's own when `id` is given,
119
+ * the create draft otherwise. Scoped to the one form on purpose: another
120
+ * coupon's half-finished edit is not this form's to throw away.
121
+ * Clearing every draft at once is what `clearAll` is for.
122
+ */
123
+ resetEditInvoicingCouponDetailView(draft, action) {
117
124
  draft.fetchState = 'Not-Started';
118
125
  draft.error = undefined;
119
126
  draft.savedCouponId = undefined;
120
- draft.editDraftById = {};
121
- draft.newCouponDraft = { ...initialInvoicingCouponFormLocalData };
127
+ const id = action.payload?.id;
128
+ if (id == null) {
129
+ draft.newCouponDraft = { ...initialInvoicingCouponFormLocalData };
130
+ return;
131
+ }
132
+ delete draft.editDraftById[id];
122
133
  },
123
134
  },
124
135
  extraReducers: (builder) => {
@@ -1,13 +1,32 @@
1
+ import { createSelector } from '@reduxjs/toolkit';
1
2
  import { reduceAllFetchState } from '../../../commonStateTypes/reduceFetchState';
2
3
  import { getInvoicingCouponById } from '../../../entity/invoicing/coupon/couponSelector';
3
4
  import { toInvoicingFormOption, } from '../../../entity/invoicing/invoicingCommonPayload';
4
5
  import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
5
- import { initialInvoicingCouponFormLocalData, } from './editInvoicingCouponDetailViewState';
6
+ import { getInvoicingFormDraftLocalData, isInvoicingFormDraftPending, } from '../invoicingFormDraft';
7
+ import { INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES, INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT, NO_INVOICING_ACCOUNT_HIERARCHY, NO_INVOICING_ACCOUNT_IDS, getInvoicingQboAccountOptions, getInvoicingQboView, invoicingQboAccountByQboId, invoicingQboAccountIdsForMappingSlot, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
8
+ import { getInvoicingSettings, getInvoicingSettingsFetchState, } from '../settingsView/settingsViewSelector';
9
+ import { initialInvoicingCouponFormLocalData, invoicingCouponDiscountsAccountIdForCreate, } from './editInvoicingCouponDetailViewState';
6
10
  /**
7
11
  * Bundled view for the discount create/edit form: the save fetch state/error,
8
12
  * the id of the coupon last saved, and — when a `couponId` is passed (edit
9
13
  * mode) — the existing coupon entity to prefill the form.
10
14
  */
15
+ /**
16
+ * The QuickBooks ids the discounts account may be picked from.
17
+ *
18
+ * Memoized, and collapsing to one shared empty array while QBO is disconnected:
19
+ * the form turns this into its picker options and re-seeds its defaults when
20
+ * those change, so a fresh array on every store change would reset the form
21
+ * under the user until their first edit.
22
+ */
23
+ const getInvoicingCouponDiscountsAccountIds = createSelector([
24
+ getInvoicingQboAccountOptions,
25
+ (state) => getInvoicingConfigView(state).qboAccountMappingKindOptions,
26
+ isInvoicingQboConnected,
27
+ ], (accounts, qboAccountMappingKindOptions, isQboConnected) => isQboConnected
28
+ ? invoicingQboAccountIdsForMappingSlot(accounts, qboAccountMappingKindOptions, INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT, INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES)
29
+ : NO_INVOICING_ACCOUNT_IDS);
11
30
  const NOT_STARTED = {
12
31
  error: undefined,
13
32
  fetchState: 'Not-Started',
@@ -23,6 +42,9 @@ export const getEditInvoicingCouponDetailViewState = (state, couponId) => {
23
42
  fetchState: 'Not-Started',
24
43
  })
25
44
  : { error: undefined, fetchState: 'Completed' };
45
+ const coupon = couponId != null
46
+ ? getInvoicingCouponById(state.invoicingCouponState, couponId)
47
+ : undefined;
26
48
  return {
27
49
  actionState: couponId != null
28
50
  ? (state.editInvoicingCouponDetailViewState.actionStateById[couponId] ??
@@ -36,9 +58,9 @@ export const getEditInvoicingCouponDetailViewState = (state, couponId) => {
36
58
  { error: undefined, fetchState: configFetchState },
37
59
  ]),
38
60
  recordState,
39
- coupon: couponId != null
40
- ? getInvoicingCouponById(state.invoicingCouponState, couponId)
41
- : undefined,
61
+ isFormLoading: isInvoicingFormDraftPending(state.editInvoicingCouponDetailViewState.editDraftById, couponId),
62
+ coupon,
63
+ discountsAccountName: invoicingQboAccountByQboId(state, coupon?.discountsAccountId)?.accountName,
42
64
  savedCouponId,
43
65
  fetchState,
44
66
  error,
@@ -58,16 +80,57 @@ export const getInvoicingCouponFormView = (state, couponId) => {
58
80
  // `seedInvoicingCouponFormDraftEpic` when the detail load lands. Until then
59
81
  // the page is showing its skeleton.
60
82
  const localData = couponId != null
61
- ? (editDraftById[couponId] ?? initialInvoicingCouponFormLocalData)
83
+ ? getInvoicingFormDraftLocalData(editDraftById, couponId, initialInvoicingCouponFormLocalData)
62
84
  : newCouponDraft;
63
85
  const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, } = getInvoicingConfigView(state);
86
+ const isQboConnected = isInvoicingQboConnected(state);
87
+ const qboView = isQboConnected ? getInvoicingQboView(state) : undefined;
88
+ const isQboPickerDataLoading = isInvoicingAccountingPickerDataLoading(state);
89
+ const settingsFetchState = getInvoicingSettingsFetchState(state).fetchState;
90
+ // Only a create reads the settings, and only until they arrive: an `Error`
91
+ // is a create that starts on no account, not a page that never renders.
92
+ const isCreateSeedPending = couponId == null &&
93
+ (settingsFetchState === 'Not-Started' ||
94
+ settingsFetchState === 'In-Progress');
64
95
  return {
65
96
  ...base,
66
97
  applyOnOptions: discountApplyOnOptions.map(toInvoicingFormOption),
67
98
  discountTypeOptions: discountTypeOptions.map(toInvoicingFormOption),
68
99
  durationTypeOptions: discountDurationTypeOptions.map(toInvoicingFormOption),
69
- localData,
100
+ isQboConnected,
101
+ isQboPickerDataLoading,
102
+ // Widened past the record and config the detail page waits on: the form
103
+ // also renders from the account list, and seeds a create from the settings
104
+ // discounts account. A form that mounted before those landed would offer an
105
+ // empty picker and save a coupon with no account of its own — not wrong,
106
+ // since blank means "use the settings account", but not what the user was
107
+ // shown either. Widened here rather than on the base view, so the detail
108
+ // page does not start waiting on lists it never renders.
109
+ //
110
+ // Only while they are still coming, though, and the settings only on a
111
+ // create: a coupon saves fine without a discounts account, so neither is
112
+ // allowed to hold the form behind a skeleton once it has failed.
113
+ pageFetchState: reduceAllFetchState([
114
+ base.pageFetchState,
115
+ {
116
+ error: undefined,
117
+ fetchState: isQboPickerDataLoading || isCreateSeedPending
118
+ ? 'In-Progress'
119
+ : 'Completed',
120
+ },
121
+ ]),
122
+ localData: couponId != null
123
+ ? localData
124
+ : {
125
+ ...localData,
126
+ // Seeded from settings only while the draft has none of its own, so
127
+ // the picker shows the account a create would post against before
128
+ // the user touches it, and stops seeding the moment they pick.
129
+ discountsAccountId: invoicingCouponDiscountsAccountIdForCreate(localData.discountsAccountId, getInvoicingSettings(state)?.qbo?.account_mappings),
130
+ },
70
131
  mode: couponId != null ? 'edit' : 'create',
71
132
  periodUnitOptions: billingPeriodUnitOptions.map(toInvoicingFormOption),
133
+ qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? NO_INVOICING_ACCOUNT_HIERARCHY,
134
+ qboIncomeAccountIds: getInvoicingCouponDiscountsAccountIds(state),
72
135
  };
73
136
  };