@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.
- package/lib/entity/invoicing/invoicingCommonPayload.d.ts +27 -29
- package/lib/entity/invoicing/invoicingCommonPayload.js +37 -75
- package/lib/entity/invoicing/product/productPayload.d.ts +10 -0
- package/lib/entity/invoicing/product/productPayload.js +14 -2
- package/lib/epic.d.ts +3 -2
- package/lib/epic.js +12 -5
- package/lib/esm/entity/invoicing/invoicingCommonPayload.js +33 -73
- package/lib/esm/entity/invoicing/product/productPayload.js +14 -2
- package/lib/esm/epic.js +8 -1
- package/lib/esm/index.js +7 -6
- package/lib/esm/view/companyMonthEndReportView/companyMonthEndReportViewReducer.js +6 -2
- package/lib/esm/view/companyMonthEndReportView/companyMonthEndReportViewState.js +4 -0
- package/lib/esm/view/companyMonthEndReportView/epics/saveCompanyMonthEndReportEpic.js +10 -6
- package/lib/esm/view/companyMonthEndReportView/epics/sendCompanyMonthEndReportEpic.js +9 -5
- package/lib/esm/view/companyMonthEndReportView/monthEndTemplateData.js +32 -0
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +25 -9
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +6 -1
- package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +40 -0
- package/lib/esm/view/invoicing/settingsView/epics/connectInvoicingStripeEpic.js +27 -0
- package/lib/esm/view/invoicing/settingsView/epics/disconnectInvoicingStripeEpic.js +19 -0
- package/lib/esm/view/invoicing/settingsView/epics/fetchInvoicingSettingsEpic.js +19 -0
- package/lib/esm/view/invoicing/settingsView/epics/saveInvoicingSettingsEpic.js +39 -0
- package/lib/esm/view/invoicing/settingsView/epics/settingsViewActionType.js +1 -0
- package/lib/esm/view/invoicing/settingsView/epics/submitInvoicingQboSettingsEpic.js +25 -0
- package/lib/esm/view/invoicing/settingsView/invoicingQboSettingsFormConfig.js +48 -0
- package/lib/esm/view/invoicing/settingsView/settingsViewReducer.js +25 -1
- package/lib/esm/view/invoicing/settingsView/settingsViewSelector.js +41 -0
- package/lib/esm/view/recommendation/recommendationReducer.js +16 -1
- package/lib/esm/view/recommendation/submitRecommendationFeedbackEpic.js +52 -0
- package/lib/index.d.ts +12 -8
- package/lib/index.js +36 -28
- package/lib/view/companyMonthEndReportView/companyMonthEndReportViewPayload.d.ts +8 -0
- package/lib/view/companyMonthEndReportView/companyMonthEndReportViewReducer.d.ts +6 -2
- package/lib/view/companyMonthEndReportView/companyMonthEndReportViewReducer.js +6 -2
- package/lib/view/companyMonthEndReportView/companyMonthEndReportViewState.d.ts +2 -0
- package/lib/view/companyMonthEndReportView/companyMonthEndReportViewState.js +4 -0
- package/lib/view/companyMonthEndReportView/epics/saveCompanyMonthEndReportEpic.js +10 -6
- package/lib/view/companyMonthEndReportView/epics/sendCompanyMonthEndReportEpic.js +9 -5
- package/lib/view/companyMonthEndReportView/monthEndTemplateData.d.ts +16 -0
- package/lib/view/companyMonthEndReportView/monthEndTemplateData.js +36 -0
- package/lib/view/expenseAutomationView/types/reconciliationViewState.d.ts +1 -1
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.d.ts +12 -1
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +25 -9
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +6 -1
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +10 -1
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +43 -1
- package/lib/view/invoicing/settingsView/epics/connectInvoicingStripeEpic.d.ts +6 -0
- package/lib/view/invoicing/settingsView/epics/connectInvoicingStripeEpic.js +31 -0
- package/lib/view/invoicing/settingsView/epics/disconnectInvoicingStripeEpic.d.ts +6 -0
- package/lib/view/invoicing/settingsView/epics/disconnectInvoicingStripeEpic.js +23 -0
- package/lib/view/invoicing/settingsView/epics/fetchInvoicingSettingsEpic.d.ts +6 -0
- package/lib/view/invoicing/settingsView/epics/fetchInvoicingSettingsEpic.js +23 -0
- package/lib/view/invoicing/settingsView/epics/saveInvoicingSettingsEpic.d.ts +6 -0
- package/lib/view/invoicing/settingsView/epics/saveInvoicingSettingsEpic.js +43 -0
- package/lib/view/invoicing/settingsView/epics/settingsViewActionType.d.ts +5 -0
- package/lib/view/invoicing/settingsView/epics/settingsViewActionType.js +2 -0
- package/lib/view/invoicing/settingsView/epics/submitInvoicingQboSettingsEpic.d.ts +11 -0
- package/lib/view/invoicing/settingsView/epics/submitInvoicingQboSettingsEpic.js +29 -0
- package/lib/view/invoicing/settingsView/invoicingQboSettingsFormConfig.d.ts +34 -0
- package/lib/view/invoicing/settingsView/invoicingQboSettingsFormConfig.js +53 -0
- package/lib/view/invoicing/settingsView/settingsViewReducer.d.ts +4 -1
- package/lib/view/invoicing/settingsView/settingsViewReducer.js +26 -2
- package/lib/view/invoicing/settingsView/settingsViewSelector.d.ts +35 -0
- package/lib/view/invoicing/settingsView/settingsViewSelector.js +43 -1
- package/lib/view/invoicing/settingsView/settingsViewState.d.ts +8 -0
- package/lib/view/recommendation/recommendationPayload.d.ts +34 -0
- package/lib/view/recommendation/recommendationReducer.d.ts +2 -1
- package/lib/view/recommendation/recommendationReducer.js +17 -2
- package/lib/view/recommendation/submitRecommendationFeedbackEpic.d.ts +7 -0
- package/lib/view/recommendation/submitRecommendationFeedbackEpic.js +56 -0
- package/package.json +1 -1
- package/lib/esm/view/invoicing/settingsView/settingsViewEpics.js +0 -92
- package/lib/view/invoicing/settingsView/settingsViewEpics.d.ts +0 -12
- 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
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
});
|
package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js
CHANGED
|
@@ -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
|
-
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
};
|