@zeniai/client-epic-state 5.2.11 → 5.2.13-beta0ND
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +13 -1
- package/lib/esm/view/recommendation/submitRecommendationFeedbackEpic.js +52 -0
- package/lib/index.d.ts +10 -7
- package/lib/index.js +36 -28
- 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 +14 -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';
|
|
@@ -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
|
+
};
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { createSlice } from '@reduxjs/toolkit';
|
|
2
2
|
import { clearAll } from '../../../rootActions';
|
|
3
|
+
import { entityToInvoicingQboSettingsLocalData, } from './invoicingQboSettingsFormConfig';
|
|
3
4
|
export const initialState = {
|
|
4
5
|
fetchState: 'Not-Started',
|
|
5
6
|
error: undefined,
|
|
6
7
|
saveFetchState: { fetchState: 'Not-Started', error: undefined },
|
|
7
8
|
stripeFetchState: { fetchState: 'Not-Started', error: undefined },
|
|
8
9
|
settings: undefined,
|
|
10
|
+
// Listed so `clearAll` resets them: an in-progress draft left behind by a
|
|
11
|
+
// tenant switch would be shown and saved against the tenant switched to.
|
|
12
|
+
brandingDraft: undefined,
|
|
13
|
+
qboDraft: undefined,
|
|
9
14
|
hasValidState() {
|
|
10
15
|
return this.fetchState === 'Completed' && this.settings != null;
|
|
11
16
|
},
|
|
@@ -22,6 +27,11 @@ const settingsView = createSlice({
|
|
|
22
27
|
draft.settings = action.payload;
|
|
23
28
|
draft.fetchState = 'Completed';
|
|
24
29
|
draft.error = undefined;
|
|
30
|
+
// Seed the QBO page's local data from the response. An edit already in
|
|
31
|
+
// progress wins: a refetch behind the user must not discard it.
|
|
32
|
+
if (draft.qboDraft == null) {
|
|
33
|
+
draft.qboDraft = entityToInvoicingQboSettingsLocalData(action.payload.qbo);
|
|
34
|
+
}
|
|
25
35
|
},
|
|
26
36
|
updateInvoicingSettingsFailure(draft, action) {
|
|
27
37
|
draft.fetchState = 'Error';
|
|
@@ -36,6 +46,9 @@ const settingsView = createSlice({
|
|
|
36
46
|
saveInvoicingSettingsSuccess(draft, action) {
|
|
37
47
|
draft.settings = action.payload;
|
|
38
48
|
draft.saveFetchState = { fetchState: 'Completed', error: undefined };
|
|
49
|
+
// Re-seeded from what was just persisted, so the page keeps working off
|
|
50
|
+
// local data while showing the server's version of it.
|
|
51
|
+
draft.qboDraft = entityToInvoicingQboSettingsLocalData(action.payload.qbo);
|
|
39
52
|
},
|
|
40
53
|
saveInvoicingSettingsFailure(draft, action) {
|
|
41
54
|
draft.saveFetchState = { fetchState: 'Error', error: action.payload };
|
|
@@ -54,6 +67,17 @@ const settingsView = createSlice({
|
|
|
54
67
|
resetInvoicingBrandingFormDraft(draft) {
|
|
55
68
|
draft.brandingDraft = undefined;
|
|
56
69
|
},
|
|
70
|
+
updateInvoicingQboSettingsDraft(draft, action) {
|
|
71
|
+
draft.qboDraft = action.payload.localData;
|
|
72
|
+
},
|
|
73
|
+
// Trigger only — handled by submitInvoicingQboSettingsEpic, which reads the
|
|
74
|
+
// local data and converts it to the request body.
|
|
75
|
+
submitInvoicingQboSettings(draft) {
|
|
76
|
+
draft.saveFetchState = { fetchState: 'In-Progress', error: undefined };
|
|
77
|
+
},
|
|
78
|
+
resetInvoicingQboSettingsDraft(draft) {
|
|
79
|
+
draft.qboDraft = undefined;
|
|
80
|
+
},
|
|
57
81
|
connectInvoicingStripe: {
|
|
58
82
|
reducer(draft) {
|
|
59
83
|
draft.stripeFetchState = { fetchState: 'In-Progress', error: undefined };
|
|
@@ -85,5 +109,5 @@ const settingsView = createSlice({
|
|
|
85
109
|
});
|
|
86
110
|
},
|
|
87
111
|
});
|
|
88
|
-
export const { fetchInvoicingSettings, updateInvoicingSettings, updateInvoicingSettingsFailure, saveInvoicingSettings, saveInvoicingSettingsSuccess, saveInvoicingSettingsFailure, resetSaveInvoicingSettings, updateInvoicingBrandingFormDraft, submitInvoicingBrandingForm, resetInvoicingBrandingFormDraft, connectInvoicingStripe, disconnectInvoicingStripe, updateInvoicingStripeConnection, invoicingStripeConnectionFailure, } = settingsView.actions;
|
|
112
|
+
export const { fetchInvoicingSettings, updateInvoicingSettings, updateInvoicingSettingsFailure, saveInvoicingSettings, saveInvoicingSettingsSuccess, saveInvoicingSettingsFailure, resetSaveInvoicingSettings, updateInvoicingBrandingFormDraft, submitInvoicingBrandingForm, resetInvoicingBrandingFormDraft, updateInvoicingQboSettingsDraft, submitInvoicingQboSettings, resetInvoicingQboSettingsDraft, connectInvoicingStripe, disconnectInvoicingStripe, updateInvoicingStripeConnection, invoicingStripeConnectionFailure, } = settingsView.actions;
|
|
89
113
|
export default settingsView.reducer;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { getInvoicingQboAccountMappingId, } from '../../../entity/invoicing/invoicingCommonPayload';
|
|
2
2
|
import { getFormattedAddress } from '../../addressView/addressViewSelector';
|
|
3
3
|
import { getInvoicingConfigView } from '../invoicingConfigView/invoicingConfigViewSelector';
|
|
4
|
+
import { getInvoicingQboView, isInvoicingAccountingPickerDataLoading, } from '../invoicingQboView/invoicingQboViewSelector';
|
|
4
5
|
import { INVOICING_BUSINESS_ADDRESS_TYPE, entityToInvoicingBrandingFormLocalData, } from './invoicingBrandingFormConfig';
|
|
6
|
+
import { initialInvoicingQboSettingsLocalData, } from './invoicingQboSettingsFormConfig';
|
|
5
7
|
/**
|
|
6
8
|
* Shared primitive: the raw invoicing settings. Consumed internally by other
|
|
7
9
|
* CES selectors (create-invoice, customer-detail, overview) and by non-settings
|
|
@@ -92,6 +94,45 @@ export const getInvoicingSettingsHubView = (state) => {
|
|
|
92
94
|
status,
|
|
93
95
|
};
|
|
94
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* Bundled view for the QBO configuration page: the draft it edits, the accounts
|
|
99
|
+
* and option lists its dropdowns render, the connection and sync health it
|
|
100
|
+
* reports, and the fetch states that decide between skeleton, form and error.
|
|
101
|
+
*
|
|
102
|
+
* `localData` is the draft once the user has edited, and the settings response
|
|
103
|
+
* before that — so the page has one field to read and CES owns when it becomes
|
|
104
|
+
* available, rather than the page copying settings into local state.
|
|
105
|
+
*/
|
|
106
|
+
export const getInvoicingQboSettingsFormView = (state) => {
|
|
107
|
+
const { error, fetchState, qboDraft, saveFetchState, settings } = state.invoicingSettingsViewState;
|
|
108
|
+
const { fetchState: configFetchState, qboAccountMappingKindOptions, qboCustomerMatchingOptions, qboSyncStatusOptions, } = getInvoicingConfigView(state);
|
|
109
|
+
const qboView = getInvoicingQboView(state);
|
|
110
|
+
return {
|
|
111
|
+
accountMappingKindOptions: qboAccountMappingKindOptions,
|
|
112
|
+
accounts: qboView.accounts,
|
|
113
|
+
backfillState: qboView.backfillState,
|
|
114
|
+
connection: qboView.connection,
|
|
115
|
+
configFetchState,
|
|
116
|
+
customerMatchingOptions: qboCustomerMatchingOptions,
|
|
117
|
+
// The dropdowns cannot be built before both the settings and the accounts
|
|
118
|
+
// land, so the page waits on either rather than rendering empty pickers.
|
|
119
|
+
isLoading: fetchState === 'Not-Started' ||
|
|
120
|
+
fetchState === 'In-Progress' ||
|
|
121
|
+
isInvoicingAccountingPickerDataLoading(state),
|
|
122
|
+
isSyncEnabledSaved: settings?.qbo?.enabled === true,
|
|
123
|
+
// Seeded in the reducer when the settings response lands, so this is a plain
|
|
124
|
+
// read rather than a second place that decides what the page starts from.
|
|
125
|
+
localData: qboDraft ?? initialInvoicingQboSettingsLocalData,
|
|
126
|
+
nestedAccountHierarchy: qboView.nestedAccountHierarchy,
|
|
127
|
+
retryFetchStateByTaskId: qboView.retryFetchStateByTaskId,
|
|
128
|
+
saveState: saveFetchState.fetchState === 'Not-Started' && error != null
|
|
129
|
+
? { fetchState, error }
|
|
130
|
+
: saveFetchState,
|
|
131
|
+
settingsFetchState: fetchState,
|
|
132
|
+
syncHealth: qboView.syncHealth,
|
|
133
|
+
syncStatusOptions: qboSyncStatusOptions,
|
|
134
|
+
};
|
|
135
|
+
};
|
|
95
136
|
/**
|
|
96
137
|
* Bundled view for the branding settings form: the draft `localData` (seeded
|
|
97
138
|
* from the existing settings until the user edits), the business-address label
|
|
@@ -131,6 +131,18 @@ const recommendation = createSlice({
|
|
|
131
131
|
};
|
|
132
132
|
},
|
|
133
133
|
},
|
|
134
|
+
// Fire-and-forget human feedback on a recommendation (accept, ranked
|
|
135
|
+
// override or outside override). Handled by submitRecommendationFeedbackEpic
|
|
136
|
+
// which POSTs to the accounting recommendation-feedback endpoint. No state
|
|
137
|
+
// mutation: the categorization UX must not depend on the feedback round-trip.
|
|
138
|
+
submitRecommendationFeedback: {
|
|
139
|
+
reducer() {
|
|
140
|
+
// Fire-and-forget: no state mutation; the epic performs the POST.
|
|
141
|
+
},
|
|
142
|
+
prepare(args) {
|
|
143
|
+
return { payload: args };
|
|
144
|
+
},
|
|
145
|
+
},
|
|
134
146
|
},
|
|
135
147
|
extraReducers: (builder) => {
|
|
136
148
|
builder.addCase(clearAll, (draft) => {
|
|
@@ -138,7 +150,7 @@ const recommendation = createSlice({
|
|
|
138
150
|
});
|
|
139
151
|
},
|
|
140
152
|
});
|
|
141
|
-
export const { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, } = recommendation.actions;
|
|
153
|
+
export const { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, submitRecommendationFeedback, } = recommendation.actions;
|
|
142
154
|
export default recommendation.reducer;
|
|
143
155
|
const doUpdateRecommendationForEntity = (draft, entityId, entityType, payload) => {
|
|
144
156
|
const key = getEntityRecommendationKey(entityType, entityId);
|