@zeniai/client-epic-state 5.2.11 → 5.2.13-beta0ND

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 (60) hide show
  1. package/lib/entity/invoicing/invoicingCommonPayload.d.ts +27 -29
  2. package/lib/entity/invoicing/invoicingCommonPayload.js +37 -75
  3. package/lib/entity/invoicing/product/productPayload.d.ts +10 -0
  4. package/lib/entity/invoicing/product/productPayload.js +14 -2
  5. package/lib/epic.d.ts +3 -2
  6. package/lib/epic.js +12 -5
  7. package/lib/esm/entity/invoicing/invoicingCommonPayload.js +33 -73
  8. package/lib/esm/entity/invoicing/product/productPayload.js +14 -2
  9. package/lib/esm/epic.js +8 -1
  10. package/lib/esm/index.js +7 -6
  11. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +25 -9
  12. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +6 -1
  13. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +40 -0
  14. package/lib/esm/view/invoicing/settingsView/epics/connectInvoicingStripeEpic.js +27 -0
  15. package/lib/esm/view/invoicing/settingsView/epics/disconnectInvoicingStripeEpic.js +19 -0
  16. package/lib/esm/view/invoicing/settingsView/epics/fetchInvoicingSettingsEpic.js +19 -0
  17. package/lib/esm/view/invoicing/settingsView/epics/saveInvoicingSettingsEpic.js +39 -0
  18. package/lib/esm/view/invoicing/settingsView/epics/settingsViewActionType.js +1 -0
  19. package/lib/esm/view/invoicing/settingsView/epics/submitInvoicingQboSettingsEpic.js +25 -0
  20. package/lib/esm/view/invoicing/settingsView/invoicingQboSettingsFormConfig.js +48 -0
  21. package/lib/esm/view/invoicing/settingsView/settingsViewReducer.js +25 -1
  22. package/lib/esm/view/invoicing/settingsView/settingsViewSelector.js +41 -0
  23. package/lib/esm/view/recommendation/recommendationReducer.js +13 -1
  24. package/lib/esm/view/recommendation/submitRecommendationFeedbackEpic.js +52 -0
  25. package/lib/index.d.ts +10 -7
  26. package/lib/index.js +36 -28
  27. package/lib/view/expenseAutomationView/types/reconciliationViewState.d.ts +1 -1
  28. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.d.ts +12 -1
  29. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +25 -9
  30. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +6 -1
  31. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +10 -1
  32. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +43 -1
  33. package/lib/view/invoicing/settingsView/epics/connectInvoicingStripeEpic.d.ts +6 -0
  34. package/lib/view/invoicing/settingsView/epics/connectInvoicingStripeEpic.js +31 -0
  35. package/lib/view/invoicing/settingsView/epics/disconnectInvoicingStripeEpic.d.ts +6 -0
  36. package/lib/view/invoicing/settingsView/epics/disconnectInvoicingStripeEpic.js +23 -0
  37. package/lib/view/invoicing/settingsView/epics/fetchInvoicingSettingsEpic.d.ts +6 -0
  38. package/lib/view/invoicing/settingsView/epics/fetchInvoicingSettingsEpic.js +23 -0
  39. package/lib/view/invoicing/settingsView/epics/saveInvoicingSettingsEpic.d.ts +6 -0
  40. package/lib/view/invoicing/settingsView/epics/saveInvoicingSettingsEpic.js +43 -0
  41. package/lib/view/invoicing/settingsView/epics/settingsViewActionType.d.ts +5 -0
  42. package/lib/view/invoicing/settingsView/epics/settingsViewActionType.js +2 -0
  43. package/lib/view/invoicing/settingsView/epics/submitInvoicingQboSettingsEpic.d.ts +11 -0
  44. package/lib/view/invoicing/settingsView/epics/submitInvoicingQboSettingsEpic.js +29 -0
  45. package/lib/view/invoicing/settingsView/invoicingQboSettingsFormConfig.d.ts +34 -0
  46. package/lib/view/invoicing/settingsView/invoicingQboSettingsFormConfig.js +53 -0
  47. package/lib/view/invoicing/settingsView/settingsViewReducer.d.ts +4 -1
  48. package/lib/view/invoicing/settingsView/settingsViewReducer.js +26 -2
  49. package/lib/view/invoicing/settingsView/settingsViewSelector.d.ts +35 -0
  50. package/lib/view/invoicing/settingsView/settingsViewSelector.js +43 -1
  51. package/lib/view/invoicing/settingsView/settingsViewState.d.ts +8 -0
  52. package/lib/view/recommendation/recommendationPayload.d.ts +34 -0
  53. package/lib/view/recommendation/recommendationReducer.d.ts +2 -1
  54. package/lib/view/recommendation/recommendationReducer.js +14 -2
  55. package/lib/view/recommendation/submitRecommendationFeedbackEpic.d.ts +7 -0
  56. package/lib/view/recommendation/submitRecommendationFeedbackEpic.js +56 -0
  57. package/package.json +1 -1
  58. package/lib/esm/view/invoicing/settingsView/settingsViewEpics.js +0 -92
  59. package/lib/view/invoicing/settingsView/settingsViewEpics.d.ts +0 -12
  60. package/lib/view/invoicing/settingsView/settingsViewEpics.js +0 -99
@@ -1,6 +1,7 @@
1
1
  import { toAmount } from '../../../commonStateTypes/amount';
2
2
  import { defaultCustomerCurrency } from '../../../commonStateTypes/currencyHelper';
3
3
  import { date } from '../../../zeniDayJS';
4
+ import { firstSelectableInvoicingId } from '../invoicingCommonPayload';
4
5
  // ── Payload → State mappers (snake_case wire shape → camelCase domain shape) ──
5
6
  export const mapInvoicingPlanTierPayloadToState = (payload, currencyCode, currencySymbol) => ({
6
7
  price: toAmount(payload.price, currencyCode, currencySymbol),
@@ -10,11 +11,22 @@ export const mapInvoicingPlanTierPayloadToState = (payload, currencyCode, curren
10
11
  pricingType: payload.pricing_type,
11
12
  });
12
13
  const mapInvoicingAccountingDetailPayloadToState = (payload) => ({
13
- accountingCategory1: payload.accounting_category_1,
14
+ // The QuickBooks id first in both, for the reason `firstSelectableInvoicingId`
15
+ // documents: the form's pickers are keyed on it, so reading the internal id
16
+ // would leave the field blank over a mapping that exists.
17
+ accountingCategory1: firstSelectableInvoicingId([
18
+ payload.accounting_class?.qbo_id,
19
+ payload.accounting_class?.accounting_class_id,
20
+ payload.accounting_category_1,
21
+ ]),
14
22
  accountingCategory2: payload.accounting_category_2,
15
23
  accountingCategory3: payload.accounting_category_3,
16
24
  accountingCategory4: payload.accounting_category_4,
17
- accountingCode: payload.accounting_code,
25
+ accountingCode: firstSelectableInvoicingId([
26
+ payload.account?.qbo_id,
27
+ payload.account?.account_id,
28
+ payload.accounting_code,
29
+ ]),
18
30
  sku: payload.sku,
19
31
  });
20
32
  export const mapInvoicingProductPayloadToState = (payload) => {
package/lib/esm/epic.js CHANGED
@@ -310,8 +310,12 @@ import { issueCreditNoteEpic } from './view/invoicing/issueCreditNote/issueCredi
310
310
  import { runInvoicingPaymentActionEpic, } from './view/invoicing/paymentAction/paymentActionEpic';
311
311
  import { addPromotionalCreditsEpic, } from './view/invoicing/promotionalCreditAction/promotionalCreditActionEpic';
312
312
  import { recordPaymentEpic, } from './view/invoicing/recordPayment/recordPaymentEpic';
313
+ import { connectInvoicingStripeEpic } from './view/invoicing/settingsView/epics/connectInvoicingStripeEpic';
314
+ import { disconnectInvoicingStripeEpic } from './view/invoicing/settingsView/epics/disconnectInvoicingStripeEpic';
315
+ import { fetchInvoicingSettingsEpic } from './view/invoicing/settingsView/epics/fetchInvoicingSettingsEpic';
316
+ import { saveInvoicingSettingsEpic } from './view/invoicing/settingsView/epics/saveInvoicingSettingsEpic';
317
+ import { submitInvoicingQboSettingsEpic } from './view/invoicing/settingsView/epics/submitInvoicingQboSettingsEpic';
313
318
  import { initializeInvoicingBrandingAddressEpic, submitInvoicingBrandingFormEpic, } from './view/invoicing/settingsView/invoicingBrandingFormEpics';
314
- import { connectInvoicingStripeEpic, disconnectInvoicingStripeEpic, fetchInvoicingSettingsEpic, saveInvoicingSettingsEpic, } from './view/invoicing/settingsView/settingsViewEpics';
315
319
  import { runInvoicingSubscriptionActionEpic, } from './view/invoicing/subscriptionAction/subscriptionActionEpic';
316
320
  import { fetchMonthClosePerformanceTrendEpic, } from './view/monthEndCloseChecksView/epics/fetchMonthClosePerformanceTrendEpic';
317
321
  import { fetchMonthEndCloseChecksEpic, } from './view/monthEndCloseChecksView/epics/fetchMonthEndCloseChecksEpic';
@@ -386,6 +390,7 @@ import { fetchEntityRecommendationsByTransactionIdEpic, } from './view/recommend
386
390
  import { fetchEntityRecommendationsForLineUpdateEpic, } from './view/recommendation/fetchEntityRecommendationsForLineUpdateEpic';
387
391
  import { fetchRecommendationByEntityIdEpic, } from './view/recommendation/fetchRecommendationByEntityIdEpic';
388
392
  import { fetchRecommendationByEntityNameEpic, } from './view/recommendation/fetchRecommendationByEntityNameEpic';
393
+ import { submitRecommendationFeedbackEpic, } from './view/recommendation/submitRecommendationFeedbackEpic';
389
394
  import { fetchReferralsEpic, } from './view/referralView/epics/fetchReferralsEpic';
390
395
  import { fetchRewardsPlanEpic, } from './view/referralView/epics/fetchRewardsPlanEpic';
391
396
  import { resendReferralInviteEpic, } from './view/referralView/epics/resendReferralInviteEpic';
@@ -1357,7 +1362,9 @@ const ALL_EPICS = [
1357
1362
  submitFeedbackEpic,
1358
1363
  submitIntlVerificationEpic,
1359
1364
  submitInvoicingBrandingFormEpic,
1365
+ submitInvoicingQboSettingsEpic,
1360
1366
  submitQuestionEpic,
1367
+ submitRecommendationFeedbackEpic,
1361
1368
  syncInvoicingSubscriptionCouponsEpic,
1362
1369
  syncTabsAfterAutomatchEpic,
1363
1370
  toggleReportUIOptionForecastModeEpic,
package/lib/esm/index.js CHANGED
@@ -194,11 +194,11 @@ import { getExpenseAutomationTransactionView, getLastTransferEntryReplacement, }
194
194
  import { applyTransactionFilters } from './view/expenseAutomationView/transactionFilterHelpers';
195
195
  import { TRANSACTION_FILTER_CATEGORIES, } from './view/expenseAutomationView/transactionFilterTypes';
196
196
  import { DEFAULT_COMPLETED_SUB_TAB, toCompletedSubTab, } from './view/expenseAutomationView/types/completedSubTab';
197
- import { DEFAULT_TRANSACTION_GROUP, TRANSACTION_GROUPS, toTransactionGroup, } from './view/expenseAutomationView/types/transactionGroup';
198
197
  import { toJEScheduleMainTab as toExpenseAutomationJEScheduleMainTab, toJEScheduleSortKey as toExpenseAutomationJEScheduleSortKey, } from './view/expenseAutomationView/types/jeSchedulesViewState';
199
198
  import { BATCH_FILE_STATUSES, isUnmatchedTabFileStatus, toBatchFileStatus, toBatchStatusValue, toBulkUploadPhase, toBulkUploadResultsTab, toBulkUploadSortKey, toMatchSource, toMissingReceiptsTab, } from './view/expenseAutomationView/types/missingReceiptsViewState';
200
199
  import { toMissingReceiptsSortKey as toExpenseAutomationMissingReceiptsSortKey, } from './view/expenseAutomationView/types/missingReceiptsViewState';
201
200
  import { toReconciliationTabsType, } from './view/expenseAutomationView/types/reconciliationViewState';
201
+ import { DEFAULT_TRANSACTION_GROUP, TRANSACTION_GROUPS, toTransactionGroup, } from './view/expenseAutomationView/types/transactionGroup';
202
202
  import { toTransactionsTabKey as toExpenseAutomationTransactionsTabKey, toTransactionsSortKey, } from './view/expenseAutomationView/types/transactionsViewState';
203
203
  import { fetchRegisteredInterests, notifyMeForFeature, } from './view/featureNotificationView/featureNotificationViewReducer';
204
204
  import { getFeatureNotificationView, getRegisteredInterests, getRegisteredInterestsByFeature, isFeatureInterestRegistered, } from './view/featureNotificationView/featureNotificationViewSelector';
@@ -264,7 +264,7 @@ import { fetchProfitAndLossForTimeframeProjectView, fetchProfitAndLossProjectVie
264
264
  import { getProfitAndLossProjectView } from './view/profitAndLossProjectView/profitAndLossProjectViewSelector';
265
265
  import { fetchProjectList } from './view/projectList/projectListReducer';
266
266
  import { getProjectList, } from './view/projectList/projectListSelector';
267
- import { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityId, fetchRecommendationByEntityName, } from './view/recommendation/recommendationReducer';
267
+ import { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityId, fetchRecommendationByEntityName, submitRecommendationFeedback, } from './view/recommendation/recommendationReducer';
268
268
  import { DEFAULT_REFERRER_AMOUNT, clearReferrals, fetchReferrals, fetchRewardsPlan, resendReferralInvite, saveReferralFormDataInLocalStore, sendReferralInvite, updateReferViewed, updateReferralListSortUiState, } from './view/referralView/referralReducer';
269
269
  import { getInviteFormView, getReferralListView, getRewardsPlanCard, } from './view/referralView/referralSelector';
270
270
  import { AmountStatusTypes, StatusTypes, toReferralListViewSortKeyType, } from './view/referralView/referralState';
@@ -629,7 +629,7 @@ export { getSpendManagementEffectiveListPeriod, getSelectedCompanyOfficer, showB
629
629
  export { canSendMonthEndEmailReport, shouldEnableCalendarPickerForLastReportSent, };
630
630
  export { getInternationalSubConfigCodeKey, BILL_NEW_PAYMENT_METHODS, NEW_INTERNATIONAL_METHOD_SUBTEXT, SWIFT_OUR_INTERNATIONAL_METHOD_SUBTEXT, LOCAL_CURRENCY_INTERNATIONAL_METHOD_SUBTEXT, };
631
631
  export { getVendorList, updateSortUiState, updatePageToken, updateYTDSelectionUIState, isColumnYTDSpend, isVendorsTabVisible, toVendorReportIDType, getVendorFiling1099List, fetchVendorsFiling1099Download, };
632
- export { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityId, fetchRecommendationByEntityName, };
632
+ export { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityId, fetchRecommendationByEntityName, submitRecommendationFeedback, };
633
633
  export { fetchOwnerList };
634
634
  export { getZeniAccountsConfigDetail, getZeniAccountList, fetchZeniAccountList, fetchAccountList, createCheckingAccount, fetchPaymentAccountList, unlinkPaymentAccount, getDepositAccountDetail, getAllDepositAccounts, getDepositAccountDetailForPDF, fetchDepositAccount, fetchZeniAccountsConfig, updateTransferMoneyLocalData, updateTransferToLocalData, updateDepositToLocalData, transferMoney, getTransferDetail, clearTransferDetail, clearReviewTransferDetail, fetchReviewTransferDetail, fetchDepositAccountDetail, depositCheck, updateCheckDepositLocalData, clearCheckDeposit, getCheckDepositDetail, updateDepositAccount, fetchDepositAccountHistorySuccess, fetchDepositAccountHistoryFailure, fetchZeniAccStatementPage, getZeniAccStatements, };
635
635
  export { fetchDepositAccountTransactionList, getDepositAccountTransactionList, };
@@ -849,15 +849,16 @@ export { getInvoicingCatalogItemDetail } from './view/invoicing/invoicingCatalog
849
849
  export { getInvoicingPlanById } from './entity/invoicing/plan/planSelector';
850
850
  export { getInvoicingProductById } from './entity/invoicing/product/productSelector';
851
851
  // ── Invoicing: settings ───────────────────────────────────────────────────────
852
- export { connectInvoicingStripe, disconnectInvoicingStripe, fetchInvoicingSettings, resetInvoicingBrandingFormDraft, resetSaveInvoicingSettings, saveInvoicingSettings, submitInvoicingBrandingForm, updateInvoicingBrandingFormDraft, updateInvoicingSettings, } from './view/invoicing/settingsView/settingsViewReducer';
853
- export { getInvoicingBrandingFormView, getInvoicingSettings, getInvoicingSettingsFetchState, getInvoicingSettingsHubView, getInvoicingSettingsView, } from './view/invoicing/settingsView/settingsViewSelector';
852
+ export { connectInvoicingStripe, disconnectInvoicingStripe, fetchInvoicingSettings, resetInvoicingBrandingFormDraft, resetInvoicingQboSettingsDraft, resetSaveInvoicingSettings, saveInvoicingSettings, submitInvoicingBrandingForm, submitInvoicingQboSettings, updateInvoicingBrandingFormDraft, updateInvoicingQboSettingsDraft, updateInvoicingSettings, } from './view/invoicing/settingsView/settingsViewReducer';
853
+ export { getInvoicingBrandingFormView, getInvoicingQboSettingsFormView, getInvoicingSettings, getInvoicingSettingsFetchState, getInvoicingSettingsHubView, getInvoicingSettingsView, } from './view/invoicing/settingsView/settingsViewSelector';
854
+ export { entityToInvoicingQboSettingsLocalData, initialInvoicingQboSettingsLocalData, invoicingQboSettingsLocalDataToPayload, } from './view/invoicing/settingsView/invoicingQboSettingsFormConfig';
854
855
  export { INVOICING_BUSINESS_ADDRESS_TYPE, entityToInvoicingBrandingFormLocalData, initialInvoicingBrandingFormLocalData, } from './view/invoicing/settingsView/invoicingBrandingFormConfig';
855
856
  export { isStripeConnectedFromSettings } from './view/invoicing/settingsView/invoicingSettingsHelpers';
856
857
  // ── Invoicing: QBO integration ────────────────────────────────────────────────
857
858
  export { fetchInvoicingAccountingPickers, fetchInvoicingQboConnection, fetchInvoicingQboSyncHealth, retryInvoicingQboSyncTask, startInvoicingQboBackfill, } from './view/invoicing/invoicingQboView/invoicingQboViewReducer';
858
859
  export { INVOICING_INCOME_ACCOUNT_TYPES, invoicingAccountsForQboAccountTypes, } from './view/invoicing/invoicingQboView/invoicingQboViewSelector';
859
860
  export { getInvoicingQboView } from './view/invoicing/invoicingQboView/invoicingQboViewSelector';
860
- export { INVOICING_QBO_CUSTOMER_MATCHING_MODES, getInvoicingQboAccountMappingId, toInvoicingNamedOptionsByKey, toInvoicingQboCustomerMatchingMode, withInvoicingQboAccountMappingId, } from './entity/invoicing/invoicingCommonPayload';
861
+ export { INVOICING_QBO_CUSTOMER_MATCHING_MODES, getInvoicingQboAccountMappingId, toInvoicingNamedOptionsByKey, toInvoicingQboCustomerMatchingMode, } from './entity/invoicing/invoicingCommonPayload';
861
862
  // ── Invoicing: config ─────────────────────────────────────────────────────────
862
863
  export { acceptInvoicingTerms, enableInvoicing, fetchInvoicingConfig, resetInvoicingConfigActionStates, updateInvoicingConfig, } from './view/invoicing/invoicingConfigView/invoicingConfigViewReducer';
863
864
  export { getInvoicingConfigView } from './view/invoicing/invoicingConfigView/invoicingConfigViewSelector';
@@ -1,3 +1,5 @@
1
+ import { toAccountPayload } from '../../../entity/account/accountPayload';
2
+ import { toClassBasePayload } from '../../../entity/class/classPayload';
1
3
  import { blankInvoicingCatalogSubFamily, isTieredCatalogPricingModel, } from './invoicingCatalogItemFormConfig';
2
4
  const CURRENCY_CODE = 'USD';
3
5
  const trimOrUndefined = (value) => value.trim() !== '' ? value.trim() : undefined;
@@ -7,20 +9,26 @@ const toProductCode = (value) => value
7
9
  .toLowerCase()
8
10
  .replace(/[^a-z0-9]+/g, '-')
9
11
  .replace(/^-|-$/g, '');
10
- const buildAccountingDetail = (subFamily) => {
12
+ const buildAccountingDetail = (subFamily, lookup) => {
11
13
  const detail = {};
12
14
  if (subFamily.sku.trim() !== '') {
13
15
  detail.sku = subFamily.sku.trim();
14
16
  }
15
- if (subFamily.accountingCode.trim() !== '') {
16
- detail.accounting_code = subFamily.accountingCode.trim();
17
+ // The account and class travel as whole objects rather than bare codes, built
18
+ // by the same helpers the transaction and vendor payloads use. An id that no
19
+ // longer resolves is dropped: sending a code the service cannot expand is how
20
+ // a mapping silently lands on the wrong account.
21
+ const account = lookup.accountByQboId(subFamily.accountingCode.trim());
22
+ if (account != null) {
23
+ detail.account = toAccountPayload(account);
17
24
  }
18
- if (subFamily.accountingCategory.trim() !== '') {
19
- detail.accounting_category_1 = subFamily.accountingCategory.trim();
25
+ const accountingClass = lookup.classByQboId(subFamily.accountingCategory.trim());
26
+ if (accountingClass != null) {
27
+ detail.accounting_class = toClassBasePayload(accountingClass);
20
28
  }
21
29
  return Object.keys(detail).length > 0 ? detail : undefined;
22
30
  };
23
- const buildSubFamilyPayload = (localData, subFamily, clearEmptyValues) => {
31
+ const buildSubFamilyPayload = (localData, subFamily, clearEmptyValues, lookup) => {
24
32
  const isAddon = localData.catalogType === 'addon';
25
33
  const tiered = isTieredCatalogPricingModel(subFamily.pricingModel);
26
34
  const name = subFamily.name.trim();
@@ -74,7 +82,7 @@ const buildSubFamilyPayload = (localData, subFamily, clearEmptyValues) => {
74
82
  starting_unit: Number(tier.startingUnit) || 1,
75
83
  }));
76
84
  }
77
- const accountingDetail = buildAccountingDetail(subFamily);
85
+ const accountingDetail = buildAccountingDetail(subFamily, lookup);
78
86
  if (accountingDetail != null) {
79
87
  payload.accounting_detail = accountingDetail;
80
88
  }
@@ -86,7 +94,7 @@ const DEFAULT_CATALOG_ITEM_UNIT = 'USD';
86
94
  * Build the parent catalog-item request body from the form draft. Creates omit
87
95
  * both parent and sub-family IDs; edits retain returned sub-family IDs.
88
96
  */
89
- export const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate) => {
97
+ export const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate, lookup) => {
90
98
  const description = trimOrUndefined(localData.description);
91
99
  const productCode = trimOrUndefined(localData.productCode) ?? toProductCode(localData.name);
92
100
  const itemPayload = {
@@ -114,7 +122,7 @@ export const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate)
114
122
  ? localData.subFamilies
115
123
  : [blankInvoicingCatalogSubFamily()];
116
124
  itemPayload.sub_family_details = isCreate
117
- ? subFamilies.map((subFamily) => buildSubFamilyPayload(localData, subFamily, false))
125
+ ? subFamilies.map((subFamily) => buildSubFamilyPayload(localData, subFamily, false, lookup))
118
126
  : subFamilies.map((subFamily) => {
119
127
  const priceCode = trimOrUndefined(subFamily.priceCode) ?? toProductCode(subFamily.name);
120
128
  const detail = {
@@ -124,6 +132,14 @@ export const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate)
124
132
  price_code: priceCode,
125
133
  show_description_in_invoices: subFamily.showDescriptionInInvoices,
126
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;
127
143
  const id = trimOrUndefined(subFamily.id);
128
144
  return id != null ? { ...detail, id } : detail;
129
145
  });
@@ -6,6 +6,7 @@ import { apiErrorSnackbarVariables } from '../../../entity/snackbar/snackbarApiE
6
6
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
7
7
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
8
8
  import { addInvoicingCatalogItem } from '../invoicingCatalogListView/invoicingCatalogListViewReducer';
9
+ import { invoicingQboAccountByQboId, invoicingQboClassByQboId, } from '../invoicingQboView/invoicingQboViewSelector';
9
10
  import { localDataToSaveInvoicingCatalogItemPayload, } from './editInvoicingCatalogItemDetailViewPayload';
10
11
  import { saveInvoicingCatalogItem, submitInvoicingCatalogItemForm, submitInvoicingCatalogItemFormFailure, submitInvoicingCatalogItemFormSuccess, } from './editInvoicingCatalogItemDetailViewReducer';
11
12
  export const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter((action) => submitInvoicingCatalogItemForm.match(action) ||
@@ -20,7 +21,11 @@ export const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actio
20
21
  const viewState = state$.value.editInvoicingCatalogItemDetailViewState;
21
22
  const localData = (id != null ? viewState.editDraftById[id] : viewState.newItemDraft) ??
22
23
  viewState.newItemDraft;
23
- data = localDataToSaveInvoicingCatalogItemPayload(localData, id == null);
24
+ const state = state$.value;
25
+ data = localDataToSaveInvoicingCatalogItemPayload(localData, id == null, {
26
+ accountByQboId: (qboId) => invoicingQboAccountByQboId(state, qboId),
27
+ classByQboId: (qboId) => invoicingQboClassByQboId(state, qboId),
28
+ });
24
29
  }
25
30
  const baseUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0`;
26
31
  const path = id != null
@@ -37,6 +37,46 @@ export const INVOICING_INCOME_ACCOUNT_TYPES = [
37
37
  'income',
38
38
  'other_income',
39
39
  ];
40
+ /**
41
+ * The account behind a picker value. The pickers submit the QuickBooks id, so
42
+ * that is what callers hold; the payload builders need the whole entity to send
43
+ * `toAccountPayload`, which is why the lookup lives here next to the options.
44
+ */
45
+ export const invoicingQboAccountByQboId = (state, qboId) => {
46
+ if (qboId == null || qboId === '') {
47
+ return undefined;
48
+ }
49
+ // `getAccountList` throws when a listed id is missing from the entity slice.
50
+ // That is survivable when looking one account up — the caller falls back to
51
+ // the id it already had — but not when it happens inside an epic, where the
52
+ // throw would take the whole request with it.
53
+ try {
54
+ const { accounts } = getAccountList(state.accountState, state.accountListState, INVOICING_ACCOUNT_LIST_KEY);
55
+ // Matched on any id the account is known by. The pickers submit `qboId`,
56
+ // but a mapping that arrived from the service can name the account by its
57
+ // platform id instead, and an account we cannot resolve is a mapping that
58
+ // cannot be sent as a record.
59
+ return accounts.find((account) => account.qboId === qboId ||
60
+ account.accountId === qboId ||
61
+ account.coaId === qboId);
62
+ }
63
+ catch {
64
+ return undefined;
65
+ }
66
+ };
67
+ /** The class behind a picker value, for the same reason as the account above. */
68
+ export const invoicingQboClassByQboId = (state, qboId) => {
69
+ if (qboId == null || qboId === '') {
70
+ return undefined;
71
+ }
72
+ try {
73
+ const { classes } = getClassList(state.classState, state.classListState);
74
+ return classes.find((accountingClass) => accountingClass.qboId === qboId);
75
+ }
76
+ catch {
77
+ return undefined;
78
+ }
79
+ };
40
80
  /**
41
81
  * Accounts and classes as the invoicing pickers need them. Both come from the
42
82
  * shared accounting-service lists, so whatever another screen already loaded
@@ -0,0 +1,27 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
4
+ import { connectInvoicingStripe, fetchInvoicingSettings, invoicingStripeConnectionFailure, updateInvoicingStripeConnection, } from '../settingsViewReducer';
5
+ export const connectInvoicingStripeEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(connectInvoicingStripe.match), mergeMap((action) => {
6
+ const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/settings/stripe-connect/setup`;
7
+ const { api_key, publishable_key, webhook_secret } = action.payload;
8
+ const body = {
9
+ api_key,
10
+ publishable_key,
11
+ ...(webhook_secret != null && webhook_secret !== ''
12
+ ? { webhook_secret }
13
+ : {}),
14
+ };
15
+ return zeniAPI
16
+ .postAndGetJSON(apiUrl, body)
17
+ .pipe(switchMap((response) => {
18
+ if (isSuccessResponse(response) && response.data != null) {
19
+ return of(updateInvoicingStripeConnection({
20
+ stripe_connected: response.data.stripe_connected,
21
+ publishable_key: response.data.publishable_key,
22
+ }), fetchInvoicingSettings());
23
+ }
24
+ return of(invoicingStripeConnectionFailure(response.status));
25
+ }), catchError((error) => of(invoicingStripeConnectionFailure(createZeniAPIStatus('Unexpected Error', 'Stripe connect setup REST API call errored out ' +
26
+ JSON.stringify(error))))));
27
+ }));
@@ -0,0 +1,19 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
4
+ import { disconnectInvoicingStripe, fetchInvoicingSettings, invoicingStripeConnectionFailure, updateInvoicingStripeConnection, } from '../settingsViewReducer';
5
+ export const disconnectInvoicingStripeEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(disconnectInvoicingStripe.match), mergeMap(() => {
6
+ const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/settings/stripe-connect/callback`;
7
+ return zeniAPI
8
+ .deleteAndGetJSON(apiUrl)
9
+ .pipe(switchMap((response) => {
10
+ if (isSuccessResponse(response)) {
11
+ return of(updateInvoicingStripeConnection({
12
+ stripe_connected: false,
13
+ publishable_key: '',
14
+ }), fetchInvoicingSettings());
15
+ }
16
+ return of(invoicingStripeConnectionFailure(response.status));
17
+ }), catchError((error) => of(invoicingStripeConnectionFailure(createZeniAPIStatus('Unexpected Error', 'Stripe disconnect REST API call errored out ' +
18
+ JSON.stringify(error))))));
19
+ }));
@@ -0,0 +1,19 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, switchMap } from 'rxjs/operators';
3
+ import { updateAddresses } from '../../../../entity/address/addressReducer';
4
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
5
+ import { fetchInvoicingSettings, updateInvoicingSettings, updateInvoicingSettingsFailure, } from '../settingsViewReducer';
6
+ export const fetchInvoicingSettingsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchInvoicingSettings.match), switchMap(() => {
7
+ const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/settings`;
8
+ return zeniAPI.getJSON(apiUrl).pipe(switchMap((response) => {
9
+ if (isSuccessResponse(response) && response.data != null) {
10
+ const address = response.data.address;
11
+ return [
12
+ ...(address != null ? [updateAddresses([address])] : []),
13
+ updateInvoicingSettings(response.data),
14
+ ];
15
+ }
16
+ return of(updateInvoicingSettingsFailure(response.status));
17
+ }), catchError((error) => of(updateInvoicingSettingsFailure(createZeniAPIStatus('Unexpected Error', 'Fetch settings REST API call errored out ' +
18
+ JSON.stringify(error))))));
19
+ }));
@@ -0,0 +1,39 @@
1
+ import { from } from 'rxjs';
2
+ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { apiErrorSnackbarVariables } from '../../../../entity/snackbar/snackbarApiError';
4
+ import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
5
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
6
+ import { saveInvoicingSettings, saveInvoicingSettingsFailure, saveInvoicingSettingsSuccess, } from '../settingsViewReducer';
7
+ export const saveInvoicingSettingsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(saveInvoicingSettings.match), mergeMap((action) => {
8
+ const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/settings`;
9
+ const { settings, snackbarSection } = action.payload;
10
+ /** `status` surfaces the platform's error text; omit it to keep the generic copy. */
11
+ const snackbar = (messageText, status) => snackbarSection == null
12
+ ? []
13
+ : [
14
+ openSnackbar({
15
+ messageSection: snackbarSection,
16
+ messageText,
17
+ type: messageText === 'success' ? 'success' : 'error',
18
+ variables: apiErrorSnackbarVariables(status),
19
+ }),
20
+ ];
21
+ return zeniAPI
22
+ .putAndGetJSON(apiUrl, { ...settings })
23
+ .pipe(switchMap((response) => {
24
+ if (isSuccessResponse(response) && response.data != null) {
25
+ return from([
26
+ saveInvoicingSettingsSuccess(response.data),
27
+ ...snackbar('success'),
28
+ ]);
29
+ }
30
+ return from([
31
+ saveInvoicingSettingsFailure(response.status),
32
+ ...snackbar('failed', response.status),
33
+ ]);
34
+ }), catchError((error) => from([
35
+ saveInvoicingSettingsFailure(createZeniAPIStatus('Unexpected Error', 'Save settings REST API call errored out ' +
36
+ JSON.stringify(error))),
37
+ ...snackbar('failed'),
38
+ ])));
39
+ }));
@@ -0,0 +1,25 @@
1
+ import { EMPTY, of } from 'rxjs';
2
+ import { filter, mergeMap, withLatestFrom } from 'rxjs/operators';
3
+ import { invoicingQboAccountByQboId } from '../../invoicingQboView/invoicingQboViewSelector';
4
+ import { invoicingQboSettingsLocalDataToPayload } from '../invoicingQboSettingsFormConfig';
5
+ import { saveInvoicingSettings, submitInvoicingQboSettings, } from '../settingsViewReducer';
6
+ /**
7
+ * Saves the QBO local data. The screen fires this without a payload — the local
8
+ * data is already in the store, so sending it back through the action would make
9
+ * the page the carrier for state it does not own. Converting to the request body
10
+ * happens here too, keeping the wire shape out of the screen.
11
+ */
12
+ export const submitInvoicingQboSettingsEpic = (actions$, state$) => actions$.pipe(filter(submitInvoicingQboSettings.match), withLatestFrom(state$), mergeMap(([, state]) => {
13
+ const { qboDraft } = state.invoicingSettingsViewState;
14
+ // No local data means the settings response has not landed, so there is
15
+ // nothing the user could have edited yet.
16
+ if (qboDraft == null) {
17
+ return EMPTY;
18
+ }
19
+ return of(saveInvoicingSettings({
20
+ settings: {
21
+ qbo: invoicingQboSettingsLocalDataToPayload(qboDraft, (accountId) => invoicingQboAccountByQboId(state, accountId)),
22
+ },
23
+ snackbarSection: 'invoicing_qbo_settings_save',
24
+ }));
25
+ }));
@@ -0,0 +1,48 @@
1
+ import { getInvoicingQboAccountMappingId, toInvoicingQboAccountMappingSlot, toInvoicingQboAccountMappingsPayload, } from '../../../entity/invoicing/invoicingCommonPayload';
2
+ export const initialInvoicingQboSettingsLocalData = {
3
+ accountIdByMappingCode: {},
4
+ autoCreateItems: false,
5
+ closedPeriodVoidAsCreditMemo: false,
6
+ customerMatching: '',
7
+ isSyncEnabled: false,
8
+ syncStartDate: '',
9
+ trackingCategoriesEnabled: false,
10
+ };
11
+ /** The settings response as the page edits it. */
12
+ export const entityToInvoicingQboSettingsLocalData = (qbo) => {
13
+ const accountMappings = qbo?.account_mappings ?? {};
14
+ const accountIdByMappingCode = {};
15
+ // Keyed by the slot the payload used, which is the config code the page will
16
+ // ask for — the reader tolerates the older `_id` form on the way in.
17
+ Object.keys(accountMappings).forEach((key) => {
18
+ const accountId = getInvoicingQboAccountMappingId(accountMappings, key);
19
+ if (accountId != null) {
20
+ accountIdByMappingCode[toInvoicingQboAccountMappingSlot(key)] = accountId;
21
+ }
22
+ });
23
+ return {
24
+ accountIdByMappingCode,
25
+ autoCreateItems: qbo?.auto_create_items === true,
26
+ closedPeriodVoidAsCreditMemo: qbo?.closed_period_void_as_credit_memo === true,
27
+ customerMatching: qbo?.customer_matching ?? '',
28
+ isSyncEnabled: qbo?.enabled === true,
29
+ syncStartDate: qbo?.sync_start_date ?? '',
30
+ trackingCategoriesEnabled: qbo?.tracking_categories_enabled === true,
31
+ };
32
+ };
33
+ /**
34
+ * The local data as the request body. `accountForId` turns each picked id into
35
+ * the whole account the service stores; it comes from the epic, which is where
36
+ * the account list is in reach.
37
+ */
38
+ export const invoicingQboSettingsLocalDataToPayload = (localData, accountForId) => {
39
+ return {
40
+ account_mappings: toInvoicingQboAccountMappingsPayload(localData.accountIdByMappingCode, accountForId),
41
+ auto_create_items: localData.autoCreateItems,
42
+ closed_period_void_as_credit_memo: localData.closedPeriodVoidAsCreditMemo,
43
+ customer_matching: localData.customerMatching,
44
+ enabled: localData.isSyncEnabled,
45
+ sync_start_date: localData.syncStartDate !== '' ? localData.syncStartDate : null,
46
+ tracking_categories_enabled: localData.trackingCategoriesEnabled,
47
+ };
48
+ };
@@ -1,11 +1,16 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
2
  import { clearAll } from '../../../rootActions';
3
+ import { entityToInvoicingQboSettingsLocalData, } from './invoicingQboSettingsFormConfig';
3
4
  export const initialState = {
4
5
  fetchState: 'Not-Started',
5
6
  error: undefined,
6
7
  saveFetchState: { fetchState: 'Not-Started', error: undefined },
7
8
  stripeFetchState: { fetchState: 'Not-Started', error: undefined },
8
9
  settings: undefined,
10
+ // Listed so `clearAll` resets them: an in-progress draft left behind by a
11
+ // tenant switch would be shown and saved against the tenant switched to.
12
+ brandingDraft: undefined,
13
+ qboDraft: undefined,
9
14
  hasValidState() {
10
15
  return this.fetchState === 'Completed' && this.settings != null;
11
16
  },
@@ -22,6 +27,11 @@ const settingsView = createSlice({
22
27
  draft.settings = action.payload;
23
28
  draft.fetchState = 'Completed';
24
29
  draft.error = undefined;
30
+ // Seed the QBO page's local data from the response. An edit already in
31
+ // progress wins: a refetch behind the user must not discard it.
32
+ if (draft.qboDraft == null) {
33
+ draft.qboDraft = entityToInvoicingQboSettingsLocalData(action.payload.qbo);
34
+ }
25
35
  },
26
36
  updateInvoicingSettingsFailure(draft, action) {
27
37
  draft.fetchState = 'Error';
@@ -36,6 +46,9 @@ const settingsView = createSlice({
36
46
  saveInvoicingSettingsSuccess(draft, action) {
37
47
  draft.settings = action.payload;
38
48
  draft.saveFetchState = { fetchState: 'Completed', error: undefined };
49
+ // Re-seeded from what was just persisted, so the page keeps working off
50
+ // local data while showing the server's version of it.
51
+ draft.qboDraft = entityToInvoicingQboSettingsLocalData(action.payload.qbo);
39
52
  },
40
53
  saveInvoicingSettingsFailure(draft, action) {
41
54
  draft.saveFetchState = { fetchState: 'Error', error: action.payload };
@@ -54,6 +67,17 @@ const settingsView = createSlice({
54
67
  resetInvoicingBrandingFormDraft(draft) {
55
68
  draft.brandingDraft = undefined;
56
69
  },
70
+ updateInvoicingQboSettingsDraft(draft, action) {
71
+ draft.qboDraft = action.payload.localData;
72
+ },
73
+ // Trigger only — handled by submitInvoicingQboSettingsEpic, which reads the
74
+ // local data and converts it to the request body.
75
+ submitInvoicingQboSettings(draft) {
76
+ draft.saveFetchState = { fetchState: 'In-Progress', error: undefined };
77
+ },
78
+ resetInvoicingQboSettingsDraft(draft) {
79
+ draft.qboDraft = undefined;
80
+ },
57
81
  connectInvoicingStripe: {
58
82
  reducer(draft) {
59
83
  draft.stripeFetchState = { fetchState: 'In-Progress', error: undefined };
@@ -85,5 +109,5 @@ const settingsView = createSlice({
85
109
  });
86
110
  },
87
111
  });
88
- export const { fetchInvoicingSettings, updateInvoicingSettings, updateInvoicingSettingsFailure, saveInvoicingSettings, saveInvoicingSettingsSuccess, saveInvoicingSettingsFailure, resetSaveInvoicingSettings, updateInvoicingBrandingFormDraft, submitInvoicingBrandingForm, resetInvoicingBrandingFormDraft, connectInvoicingStripe, disconnectInvoicingStripe, updateInvoicingStripeConnection, invoicingStripeConnectionFailure, } = settingsView.actions;
112
+ export const { fetchInvoicingSettings, updateInvoicingSettings, updateInvoicingSettingsFailure, saveInvoicingSettings, saveInvoicingSettingsSuccess, saveInvoicingSettingsFailure, resetSaveInvoicingSettings, updateInvoicingBrandingFormDraft, submitInvoicingBrandingForm, resetInvoicingBrandingFormDraft, updateInvoicingQboSettingsDraft, submitInvoicingQboSettings, resetInvoicingQboSettingsDraft, connectInvoicingStripe, disconnectInvoicingStripe, updateInvoicingStripeConnection, invoicingStripeConnectionFailure, } = settingsView.actions;
89
113
  export default settingsView.reducer;
@@ -1,7 +1,9 @@
1
1
  import { getInvoicingQboAccountMappingId, } from '../../../entity/invoicing/invoicingCommonPayload';
2
2
  import { getFormattedAddress } from '../../addressView/addressViewSelector';
3
3
  import { getInvoicingConfigView } from '../invoicingConfigView/invoicingConfigViewSelector';
4
+ import { getInvoicingQboView, isInvoicingAccountingPickerDataLoading, } from '../invoicingQboView/invoicingQboViewSelector';
4
5
  import { INVOICING_BUSINESS_ADDRESS_TYPE, entityToInvoicingBrandingFormLocalData, } from './invoicingBrandingFormConfig';
6
+ import { initialInvoicingQboSettingsLocalData, } from './invoicingQboSettingsFormConfig';
5
7
  /**
6
8
  * Shared primitive: the raw invoicing settings. Consumed internally by other
7
9
  * CES selectors (create-invoice, customer-detail, overview) and by non-settings
@@ -92,6 +94,45 @@ export const getInvoicingSettingsHubView = (state) => {
92
94
  status,
93
95
  };
94
96
  };
97
+ /**
98
+ * Bundled view for the QBO configuration page: the draft it edits, the accounts
99
+ * and option lists its dropdowns render, the connection and sync health it
100
+ * reports, and the fetch states that decide between skeleton, form and error.
101
+ *
102
+ * `localData` is the draft once the user has edited, and the settings response
103
+ * before that — so the page has one field to read and CES owns when it becomes
104
+ * available, rather than the page copying settings into local state.
105
+ */
106
+ export const getInvoicingQboSettingsFormView = (state) => {
107
+ const { error, fetchState, qboDraft, saveFetchState, settings } = state.invoicingSettingsViewState;
108
+ const { fetchState: configFetchState, qboAccountMappingKindOptions, qboCustomerMatchingOptions, qboSyncStatusOptions, } = getInvoicingConfigView(state);
109
+ const qboView = getInvoicingQboView(state);
110
+ return {
111
+ accountMappingKindOptions: qboAccountMappingKindOptions,
112
+ accounts: qboView.accounts,
113
+ backfillState: qboView.backfillState,
114
+ connection: qboView.connection,
115
+ configFetchState,
116
+ customerMatchingOptions: qboCustomerMatchingOptions,
117
+ // The dropdowns cannot be built before both the settings and the accounts
118
+ // land, so the page waits on either rather than rendering empty pickers.
119
+ isLoading: fetchState === 'Not-Started' ||
120
+ fetchState === 'In-Progress' ||
121
+ isInvoicingAccountingPickerDataLoading(state),
122
+ isSyncEnabledSaved: settings?.qbo?.enabled === true,
123
+ // Seeded in the reducer when the settings response lands, so this is a plain
124
+ // read rather than a second place that decides what the page starts from.
125
+ localData: qboDraft ?? initialInvoicingQboSettingsLocalData,
126
+ nestedAccountHierarchy: qboView.nestedAccountHierarchy,
127
+ retryFetchStateByTaskId: qboView.retryFetchStateByTaskId,
128
+ saveState: saveFetchState.fetchState === 'Not-Started' && error != null
129
+ ? { fetchState, error }
130
+ : saveFetchState,
131
+ settingsFetchState: fetchState,
132
+ syncHealth: qboView.syncHealth,
133
+ syncStatusOptions: qboSyncStatusOptions,
134
+ };
135
+ };
95
136
  /**
96
137
  * Bundled view for the branding settings form: the draft `localData` (seeded
97
138
  * from the existing settings until the user edits), the business-address label
@@ -131,6 +131,18 @@ const recommendation = createSlice({
131
131
  };
132
132
  },
133
133
  },
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.
138
+ submitRecommendationFeedback: {
139
+ reducer() {
140
+ // Fire-and-forget: no state mutation; the epic performs the POST.
141
+ },
142
+ prepare(args) {
143
+ return { payload: args };
144
+ },
145
+ },
134
146
  },
135
147
  extraReducers: (builder) => {
136
148
  builder.addCase(clearAll, (draft) => {
@@ -138,7 +150,7 @@ const recommendation = createSlice({
138
150
  });
139
151
  },
140
152
  });
141
- export const { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, } = recommendation.actions;
153
+ export const { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, submitRecommendationFeedback, } = recommendation.actions;
142
154
  export default recommendation.reducer;
143
155
  const doUpdateRecommendationForEntity = (draft, entityId, entityType, payload) => {
144
156
  const key = getEntityRecommendationKey(entityType, entityId);