@zeniai/client-epic-state 5.2.10 → 5.2.11-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 (74) 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/companyMonthEndReportView/companyMonthEndReportViewReducer.js +6 -2
  12. package/lib/esm/view/companyMonthEndReportView/companyMonthEndReportViewState.js +4 -0
  13. package/lib/esm/view/companyMonthEndReportView/epics/saveCompanyMonthEndReportEpic.js +10 -6
  14. package/lib/esm/view/companyMonthEndReportView/epics/sendCompanyMonthEndReportEpic.js +9 -5
  15. package/lib/esm/view/companyMonthEndReportView/monthEndTemplateData.js +32 -0
  16. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +25 -9
  17. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +6 -1
  18. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +40 -0
  19. package/lib/esm/view/invoicing/settingsView/epics/connectInvoicingStripeEpic.js +27 -0
  20. package/lib/esm/view/invoicing/settingsView/epics/disconnectInvoicingStripeEpic.js +19 -0
  21. package/lib/esm/view/invoicing/settingsView/epics/fetchInvoicingSettingsEpic.js +19 -0
  22. package/lib/esm/view/invoicing/settingsView/epics/saveInvoicingSettingsEpic.js +39 -0
  23. package/lib/esm/view/invoicing/settingsView/epics/settingsViewActionType.js +1 -0
  24. package/lib/esm/view/invoicing/settingsView/epics/submitInvoicingQboSettingsEpic.js +25 -0
  25. package/lib/esm/view/invoicing/settingsView/invoicingQboSettingsFormConfig.js +48 -0
  26. package/lib/esm/view/invoicing/settingsView/settingsViewReducer.js +25 -1
  27. package/lib/esm/view/invoicing/settingsView/settingsViewSelector.js +41 -0
  28. package/lib/esm/view/recommendation/recommendationReducer.js +16 -1
  29. package/lib/esm/view/recommendation/submitRecommendationFeedbackEpic.js +52 -0
  30. package/lib/index.d.ts +12 -8
  31. package/lib/index.js +36 -28
  32. package/lib/view/companyMonthEndReportView/companyMonthEndReportViewPayload.d.ts +8 -0
  33. package/lib/view/companyMonthEndReportView/companyMonthEndReportViewReducer.d.ts +6 -2
  34. package/lib/view/companyMonthEndReportView/companyMonthEndReportViewReducer.js +6 -2
  35. package/lib/view/companyMonthEndReportView/companyMonthEndReportViewState.d.ts +2 -0
  36. package/lib/view/companyMonthEndReportView/companyMonthEndReportViewState.js +4 -0
  37. package/lib/view/companyMonthEndReportView/epics/saveCompanyMonthEndReportEpic.js +10 -6
  38. package/lib/view/companyMonthEndReportView/epics/sendCompanyMonthEndReportEpic.js +9 -5
  39. package/lib/view/companyMonthEndReportView/monthEndTemplateData.d.ts +16 -0
  40. package/lib/view/companyMonthEndReportView/monthEndTemplateData.js +36 -0
  41. package/lib/view/expenseAutomationView/types/reconciliationViewState.d.ts +1 -1
  42. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.d.ts +12 -1
  43. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +25 -9
  44. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +6 -1
  45. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +10 -1
  46. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +43 -1
  47. package/lib/view/invoicing/settingsView/epics/connectInvoicingStripeEpic.d.ts +6 -0
  48. package/lib/view/invoicing/settingsView/epics/connectInvoicingStripeEpic.js +31 -0
  49. package/lib/view/invoicing/settingsView/epics/disconnectInvoicingStripeEpic.d.ts +6 -0
  50. package/lib/view/invoicing/settingsView/epics/disconnectInvoicingStripeEpic.js +23 -0
  51. package/lib/view/invoicing/settingsView/epics/fetchInvoicingSettingsEpic.d.ts +6 -0
  52. package/lib/view/invoicing/settingsView/epics/fetchInvoicingSettingsEpic.js +23 -0
  53. package/lib/view/invoicing/settingsView/epics/saveInvoicingSettingsEpic.d.ts +6 -0
  54. package/lib/view/invoicing/settingsView/epics/saveInvoicingSettingsEpic.js +43 -0
  55. package/lib/view/invoicing/settingsView/epics/settingsViewActionType.d.ts +5 -0
  56. package/lib/view/invoicing/settingsView/epics/settingsViewActionType.js +2 -0
  57. package/lib/view/invoicing/settingsView/epics/submitInvoicingQboSettingsEpic.d.ts +11 -0
  58. package/lib/view/invoicing/settingsView/epics/submitInvoicingQboSettingsEpic.js +29 -0
  59. package/lib/view/invoicing/settingsView/invoicingQboSettingsFormConfig.d.ts +34 -0
  60. package/lib/view/invoicing/settingsView/invoicingQboSettingsFormConfig.js +53 -0
  61. package/lib/view/invoicing/settingsView/settingsViewReducer.d.ts +4 -1
  62. package/lib/view/invoicing/settingsView/settingsViewReducer.js +26 -2
  63. package/lib/view/invoicing/settingsView/settingsViewSelector.d.ts +35 -0
  64. package/lib/view/invoicing/settingsView/settingsViewSelector.js +43 -1
  65. package/lib/view/invoicing/settingsView/settingsViewState.d.ts +8 -0
  66. package/lib/view/recommendation/recommendationPayload.d.ts +34 -0
  67. package/lib/view/recommendation/recommendationReducer.d.ts +2 -1
  68. package/lib/view/recommendation/recommendationReducer.js +17 -2
  69. package/lib/view/recommendation/submitRecommendationFeedbackEpic.d.ts +7 -0
  70. package/lib/view/recommendation/submitRecommendationFeedbackEpic.js +56 -0
  71. package/package.json +1 -1
  72. package/lib/esm/view/invoicing/settingsView/settingsViewEpics.js +0 -92
  73. package/lib/view/invoicing/settingsView/settingsViewEpics.d.ts +0 -12
  74. 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';
@@ -95,7 +95,10 @@ const companyMonthEndReportView = createSlice({
95
95
  },
96
96
  },
97
97
  saveCompanyMonthEndReport: {
98
- prepare(recipientEmailList, companyId, subject, isPreview, fileId, zeniRequestsHtml, tenantId) {
98
+ // The trailing pair is an object, not two more positional optionals: both
99
+ // are optional and neither is a distinct type, so a swapped call would
100
+ // compile silently.
101
+ prepare(recipientEmailList, companyId, subject, isPreview, fileId, zeniRequestsHtml, options) {
99
102
  return {
100
103
  payload: {
101
104
  recipientEmailList,
@@ -104,7 +107,8 @@ const companyMonthEndReportView = createSlice({
104
107
  isPreview,
105
108
  fileId,
106
109
  zeniRequestsHtml,
107
- tenantId,
110
+ tenantId: options?.tenantId,
111
+ agentInsightEdits: options?.agentInsightEdits,
108
112
  },
109
113
  };
110
114
  },
@@ -18,6 +18,10 @@ export const toCompanyMonthEndReportData = (data) => {
18
18
  : undefined,
19
19
  draftUserId: data.draft_user_id,
20
20
  zeniRequestsData: data.zeni_requests_data ?? [],
21
+ agentInsightsEditedBy: data.agent_insights_edited_by ?? undefined,
22
+ agentInsightsEditedAt: data.agent_insights_edited_at != null
23
+ ? date(data.agent_insights_edited_at)
24
+ : undefined,
21
25
  };
22
26
  };
23
27
  export function toMonthEndHistoricData(data) {
@@ -6,9 +6,10 @@ import { getAttachmentFileByFileId } from '../../../entity/file/fileSelector';
6
6
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
7
7
  import { createZeniAPIStatus, isSuccessStatus } from '../../../responsePayload';
8
8
  import { dateNow, getBusinessDayOfDate } from '../../../zeniDayJS';
9
+ import { buildMonthEndTemplateData } from '../monthEndTemplateData';
9
10
  import { saveCompanyMonthEndReport, updateCompanyMonthEndReportDataOnSaveSuccess, updateStatusForCompanyMonthEndReportOnSaveFailure, } from '../companyMonthEndReportViewReducer';
10
11
  export const saveCompanyMonthEndReportEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(saveCompanyMonthEndReport.match), mergeMap((action) => {
11
- const { companyId, recipientEmailList, subject, isPreview, fileId, zeniRequestsHtml, tenantId, } = action.payload;
12
+ const { agentInsightEdits, companyId, recipientEmailList, subject, isPreview, fileId, zeniRequestsHtml, tenantId, } = action.payload;
12
13
  const state = state$.value;
13
14
  const company = state.companyState.companiesById[companyId];
14
15
  const companyName = state.companyState.companiesById[companyId].companyInfo.name;
@@ -27,15 +28,18 @@ export const saveCompanyMonthEndReportEpic = (actions$, state$, zeniAPI) => acti
27
28
  }
28
29
  }
29
30
  const payload = {
30
- template_data: {
31
- ...state.companyMonthEndReportViewState.monthEndReportDataByCompanyId[companyId].companyMonthEndReportData.templateData,
32
- subject,
33
- },
31
+ template_data: buildMonthEndTemplateData(state.companyMonthEndReportViewState.monthEndReportDataByCompanyId[companyId].companyMonthEndReportData.templateData, subject, agentInsightEdits),
34
32
  recipient_email_list: recipientEmailList,
35
33
  tenant_receiver_email_list: state.companyMonthEndReportViewState.monthEndReportDataByCompanyId[companyId].companyMonthEndReportData.tenantReceiverEmailList,
36
34
  last_report_sent_business_day: lastReportSentBusinessDay,
37
35
  last_report_sent_date: lastReportSentDate.format(DEFAULT_DATE_FORMAT),
38
- zeni_requests_html: zeniRequestsHtml,
36
+ // Three-way, because the server distinguishes all three: an empty string means the user
37
+ // cleared the box and is sent as an explicit null, a value is sent as-is, and undefined
38
+ // means this save was not about Zeni Requests at all — the key is omitted so the draft's
39
+ // existing text is left alone.
40
+ ...(zeniRequestsHtml !== undefined && {
41
+ zeni_requests_html: zeniRequestsHtml === '' ? null : zeniRequestsHtml,
42
+ }),
39
43
  files: filePayload,
40
44
  };
41
45
  const query = { is_preview: isPreview };
@@ -7,6 +7,7 @@ import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
7
7
  import { createZeniAPIStatus, isSuccessStatus } from '../../../responsePayload';
8
8
  import { dateNow, getBusinessDayOfDate } from '../../../zeniDayJS';
9
9
  import { updateMetaDataOnSendMonthEndReport } from '../../companyView/companyViewReducer';
10
+ import { buildMonthEndTemplateData } from '../monthEndTemplateData';
10
11
  import { sendCompanyMonthEndReport, updateStatusForSendCompanyMonthEndReport, } from '../companyMonthEndReportViewReducer';
11
12
  export const sendCompanyMonthEndReportEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(sendCompanyMonthEndReport.match), mergeMap((action) => {
12
13
  const { companyId, fileId, tenantId } = action.payload;
@@ -31,15 +32,18 @@ export const sendCompanyMonthEndReportEpic = (actions$, state$, zeniAPI) => acti
31
32
  }
32
33
  }
33
34
  const payload = {
34
- template_data: {
35
- ...state.companyMonthEndReportViewState.monthEndReportDataByCompanyId[companyId].companyMonthEndReportData.templateData,
36
- subject,
37
- },
35
+ template_data: buildMonthEndTemplateData(state.companyMonthEndReportViewState.monthEndReportDataByCompanyId[companyId].companyMonthEndReportData.templateData, subject),
38
36
  recipient_email_list: recipientEmailList,
39
37
  tenant_receiver_email_list: state.companyMonthEndReportViewState.monthEndReportDataByCompanyId[companyId].companyMonthEndReportData.tenantReceiverEmailList,
40
38
  last_report_sent_business_day: lastReportSentBusinessDay,
41
39
  last_report_sent_date: lastReportSentDate.format(DEFAULT_DATE_FORMAT),
42
- zeni_requests_html: zeniRequestsHtml,
40
+ // Three-way, because the server distinguishes all three: an empty string means the user
41
+ // cleared the box and is sent as an explicit null, a value is sent as-is, and undefined
42
+ // means this save was not about Zeni Requests at all — the key is omitted so the draft's
43
+ // existing text is left alone.
44
+ ...(zeniRequestsHtml !== undefined && {
45
+ zeni_requests_html: zeniRequestsHtml === '' ? null : zeniRequestsHtml,
46
+ }),
43
47
  files: filePayload,
44
48
  };
45
49
  return zeniAPI
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The AI CFO (V2) month-end email renders its subject line from this merge var
3
+ * rather than from the generic `subject` key, so a subject typed in the cockpit
4
+ * has to be written here too or the sent email keeps the AI's original subject.
5
+ */
6
+ export const AGENT_INSIGHTS_SUBJECT_KEY = 'agent_insights_subject';
7
+ /**
8
+ * Build the `template_data` sent to the month-end email endpoint.
9
+ *
10
+ * Controller edits to the AI-generated insight fields are merged over the
11
+ * template data held in the store. The server sanitizes and persists whichever
12
+ * `agent_insights_*` keys are present, and the send path renders from the
13
+ * persisted draft — so only a save (`is_preview: false`) makes an edit stick.
14
+ */
15
+ export const buildMonthEndTemplateData = (templateData, subject, agentInsightEdits) => {
16
+ const merged = {
17
+ ...templateData,
18
+ ...agentInsightEdits,
19
+ subject,
20
+ };
21
+ // Only for AI CFO drafts (the key is absent on legacy ones), never over an
22
+ // explicit edit to the field itself, and never with an empty value — on send
23
+ // the subject comes from store state and can be absent, which must not blank
24
+ // the AI's subject line.
25
+ if (AGENT_INSIGHTS_SUBJECT_KEY in merged &&
26
+ agentInsightEdits?.[AGENT_INSIGHTS_SUBJECT_KEY] == null &&
27
+ subject != null &&
28
+ subject !== '') {
29
+ merged[AGENT_INSIGHTS_SUBJECT_KEY] = subject;
30
+ }
31
+ return merged;
32
+ };
@@ -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
+ };