@zeniai/client-epic-state 5.3.5 → 5.3.7-betaRD0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/lib/entity/customerIncome/customerIncomeTrend.d.ts +18 -2
  2. package/lib/entity/customerIncome/customerIncomeTrend.js +65 -5
  3. package/lib/entity/insights/insightSummary.d.ts +3 -0
  4. package/lib/entity/insights/insightSummaryPayload.js +2 -0
  5. package/lib/entity/invoicing/invoice/invoicePayload.d.ts +1 -0
  6. package/lib/entity/invoicing/invoice/invoicePayload.js +3 -0
  7. package/lib/entity/invoicing/invoice/invoiceState.d.ts +2 -0
  8. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  9. package/lib/entity/snackbar/snackbarTypes.js +1 -0
  10. package/lib/entity/tenant/epic/fetchAllTenantsEpic.js +45 -38
  11. package/lib/entity/vendorExpense/vendorSpendTrendFilterTabType.d.ts +2 -2
  12. package/lib/epic.d.ts +4 -1
  13. package/lib/epic.js +6 -0
  14. package/lib/esm/entity/customerIncome/customerIncomeTrend.js +63 -4
  15. package/lib/esm/entity/insights/insightSummaryPayload.js +2 -0
  16. package/lib/esm/entity/invoicing/invoice/invoicePayload.js +3 -0
  17. package/lib/esm/entity/snackbar/snackbarTypes.js +1 -0
  18. package/lib/esm/entity/tenant/epic/fetchAllTenantsEpic.js +46 -39
  19. package/lib/esm/epic.js +6 -0
  20. package/lib/esm/index.js +5 -2
  21. package/lib/esm/init.js +14 -3
  22. package/lib/esm/reducer.js +3 -0
  23. package/lib/esm/sessionEndpointsLoader.js +7 -0
  24. package/lib/esm/view/customerSummaryView/customerSummaryViewPayload.js +1 -0
  25. package/lib/esm/view/customerSummaryView/customerSummaryViewReducer.js +197 -0
  26. package/lib/esm/view/customerSummaryView/customerSummaryViewSelector.js +35 -0
  27. package/lib/esm/view/customerSummaryView/customerSummaryViewState.js +10 -0
  28. package/lib/esm/view/customerSummaryView/epic/fetchCustomerSummaryDetailEpic.js +30 -0
  29. package/lib/esm/view/customerSummaryView/epic/fetchCustomerSummaryListEpic.js +27 -0
  30. package/lib/esm/view/customerSummaryView/epic/saveCustomerSummaryDetailEpic.js +44 -0
  31. package/lib/esm/view/expenseAutomationView/epics/accountRecon/fetchReconciliationViewEpic.js +2 -1
  32. package/lib/esm/view/trend/fetchIncomeTrendEpic.js +29 -6
  33. package/lib/esm/view/trend/trendReducer.js +4 -2
  34. package/lib/index.d.ts +9 -2
  35. package/lib/index.js +73 -51
  36. package/lib/init.d.ts +9 -0
  37. package/lib/init.js +17 -4
  38. package/lib/reducer.d.ts +1 -0
  39. package/lib/reducer.js +3 -0
  40. package/lib/rootStateTypes.d.ts +2 -0
  41. package/lib/sessionEndpointsLoader.d.ts +9 -0
  42. package/lib/sessionEndpointsLoader.js +11 -0
  43. package/lib/tsconfig.mocks.tsbuildinfo +1 -0
  44. package/lib/tsconfig.typecheck.tsbuildinfo +1 -0
  45. package/lib/view/commonVendorView/vendorsTabVendorView/vendorsTabVendorViewReducer.d.ts +2 -2
  46. package/lib/view/customerSummaryView/customerSummaryViewPayload.d.ts +38 -0
  47. package/lib/view/customerSummaryView/customerSummaryViewPayload.js +2 -0
  48. package/lib/view/customerSummaryView/customerSummaryViewReducer.d.ts +32 -0
  49. package/lib/view/customerSummaryView/customerSummaryViewReducer.js +202 -0
  50. package/lib/view/customerSummaryView/customerSummaryViewSelector.d.ts +20 -0
  51. package/lib/view/customerSummaryView/customerSummaryViewSelector.js +43 -0
  52. package/lib/view/customerSummaryView/customerSummaryViewState.d.ts +63 -0
  53. package/lib/view/customerSummaryView/customerSummaryViewState.js +13 -0
  54. package/lib/view/customerSummaryView/epic/fetchCustomerSummaryDetailEpic.d.ts +9 -0
  55. package/lib/view/customerSummaryView/epic/fetchCustomerSummaryDetailEpic.js +34 -0
  56. package/lib/view/customerSummaryView/epic/fetchCustomerSummaryListEpic.d.ts +7 -0
  57. package/lib/view/customerSummaryView/epic/fetchCustomerSummaryListEpic.js +31 -0
  58. package/lib/view/customerSummaryView/epic/saveCustomerSummaryDetailEpic.d.ts +8 -0
  59. package/lib/view/customerSummaryView/epic/saveCustomerSummaryDetailEpic.js +48 -0
  60. package/lib/view/expenseAutomationView/epics/accountRecon/fetchReconciliationViewEpic.js +2 -1
  61. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  62. package/lib/view/trend/fetchIncomeTrendEpic.d.ts +1 -1
  63. package/lib/view/trend/fetchIncomeTrendEpic.js +28 -5
  64. package/lib/view/trend/trendReducer.d.ts +8 -1
  65. package/lib/view/trend/trendReducer.js +4 -2
  66. package/lib/zeniAPI.d.ts +2 -2
  67. package/package.json +26 -1
@@ -9,7 +9,9 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
10
10
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
11
  };
12
- var _CustomerIncomeTrend_customer, _CustomerIncomeTrend_timePeriod, _CustomerIncomeTrend_incomeTrend, _CustomerIncomeTrend_insights;
12
+ var _CustomerIncomeTrend_customer, _CustomerIncomeTrend_filter, _CustomerIncomeTrend_timePeriod, _CustomerIncomeTrend_incomeTrend, _CustomerIncomeTrend_insights;
13
+ import { dateNow } from '../../zeniDayJS';
14
+ import { getFilteredExpenseTrend } from '../vendorExpense/vendorExpenseTrend';
13
15
  import { getCustomerIncomeTrendKey } from './customerIncomeTrendKey';
14
16
  export class CustomerIncomeTrend {
15
17
  get balancesKind() {
@@ -19,7 +21,18 @@ export class CustomerIncomeTrend {
19
21
  return 'ascending_date';
20
22
  }
21
23
  get monthly() {
22
- return __classPrivateFieldGet(this, _CustomerIncomeTrend_incomeTrend, "f").months ?? [];
24
+ const months = __classPrivateFieldGet(this, _CustomerIncomeTrend_incomeTrend, "f").months ?? [];
25
+ if (__classPrivateFieldGet(this, _CustomerIncomeTrend_filter, "f") == null) {
26
+ return months;
27
+ }
28
+ const { filter, firstMonthOfFY, last30DaysRevenue, last90DaysRevenue } = __classPrivateFieldGet(this, _CustomerIncomeTrend_filter, "f");
29
+ if (filter === 'last_30_days') {
30
+ return getLastNDaysMonths(months, last30DaysRevenue, 30);
31
+ }
32
+ if (filter === 'last_90_days') {
33
+ return getLastNDaysMonths(months, last90DaysRevenue, 90);
34
+ }
35
+ return getFilteredExpenseTrend({ ...__classPrivateFieldGet(this, _CustomerIncomeTrend_incomeTrend, "f"), last30DaysMonths: null, last90DaysMonths: null }, filter, firstMonthOfFY);
23
36
  }
24
37
  get quarterly() {
25
38
  return __classPrivateFieldGet(this, _CustomerIncomeTrend_incomeTrend, "f").quarters ?? [];
@@ -48,8 +61,17 @@ export class CustomerIncomeTrend {
48
61
  get key() {
49
62
  return getCustomerIncomeTrendKey(__classPrivateFieldGet(this, _CustomerIncomeTrend_customer, "f").id, __classPrivateFieldGet(this, _CustomerIncomeTrend_timePeriod, "f"));
50
63
  }
51
- constructor({ customer, timePeriod, incomeTrend, insights, }) {
64
+ withFilter(filter) {
65
+ return new CustomerIncomeTrend({
66
+ customer: __classPrivateFieldGet(this, _CustomerIncomeTrend_customer, "f"),
67
+ timePeriod: __classPrivateFieldGet(this, _CustomerIncomeTrend_timePeriod, "f"),
68
+ incomeTrend: __classPrivateFieldGet(this, _CustomerIncomeTrend_incomeTrend, "f"),
69
+ insights: __classPrivateFieldGet(this, _CustomerIncomeTrend_insights, "f"),
70
+ }, filter);
71
+ }
72
+ constructor({ customer, timePeriod, incomeTrend, insights }, filter) {
52
73
  _CustomerIncomeTrend_customer.set(this, void 0);
74
+ _CustomerIncomeTrend_filter.set(this, void 0);
53
75
  _CustomerIncomeTrend_timePeriod.set(this, void 0);
54
76
  _CustomerIncomeTrend_incomeTrend.set(this, void 0);
55
77
  _CustomerIncomeTrend_insights.set(this, void 0);
@@ -57,9 +79,46 @@ export class CustomerIncomeTrend {
57
79
  __classPrivateFieldSet(this, _CustomerIncomeTrend_timePeriod, timePeriod, "f");
58
80
  __classPrivateFieldSet(this, _CustomerIncomeTrend_incomeTrend, incomeTrend, "f");
59
81
  __classPrivateFieldSet(this, _CustomerIncomeTrend_insights, insights, "f");
82
+ __classPrivateFieldSet(this, _CustomerIncomeTrend_filter, filter, "f");
60
83
  }
61
84
  }
62
- _CustomerIncomeTrend_customer = new WeakMap(), _CustomerIncomeTrend_timePeriod = new WeakMap(), _CustomerIncomeTrend_incomeTrend = new WeakMap(), _CustomerIncomeTrend_insights = new WeakMap();
85
+ _CustomerIncomeTrend_customer = new WeakMap(), _CustomerIncomeTrend_filter = new WeakMap(), _CustomerIncomeTrend_timePeriod = new WeakMap(), _CustomerIncomeTrend_incomeTrend = new WeakMap(), _CustomerIncomeTrend_insights = new WeakMap();
86
+ /**
87
+ * Months covering the last `n` days, oldest month cut so the slices add up to the precomputed
88
+ * last-n-days total. Port of accounting `SpendTrendReport._prepare_last_n_days_time_slice`
89
+ * (Vendors gets these slices from the spend_trend API; income_trend has no equivalent).
90
+ * The cut slice is clamped at 0: the total (gl_entries, daily) and the months (live QBO) can disagree.
91
+ */
92
+ export const getLastNDaysMonths = (months, lastNDaysAmount, n) => {
93
+ const findMonth = (start) => months.find((month) => month.startDate.isSame(start, 'day'));
94
+ const withAmount = (month, amount) => ({
95
+ ...month,
96
+ balance: { ...month.balance, amount: Math.max(amount, 0) },
97
+ });
98
+ const now = dateNow();
99
+ let monthStart = now.startOf('month');
100
+ let daysCollected = now.date();
101
+ const currentMonth = findMonth(monthStart);
102
+ // The current month alone covers the n days (e.g. the 31st for last 30 days): it is the whole total.
103
+ if (daysCollected >= n) {
104
+ return currentMonth != null
105
+ ? [withAmount(currentMonth, lastNDaysAmount)]
106
+ : [];
107
+ }
108
+ const slices = currentMonth != null ? [currentMonth] : [];
109
+ let total = currentMonth?.balance.amount ?? 0;
110
+ while (daysCollected < n) {
111
+ monthStart = monthStart.subtract(1, 'month');
112
+ daysCollected += monthStart.daysInMonth();
113
+ const month = findMonth(monthStart);
114
+ if (month != null) {
115
+ const slice = daysCollected > n ? withAmount(month, lastNDaysAmount - total) : month;
116
+ total += slice.balance.amount;
117
+ slices.unshift(slice);
118
+ }
119
+ }
120
+ return slices;
121
+ };
63
122
  /**
64
123
  * Returns an average amount for the list of COABalances
65
124
  * @param coaBalances
@@ -5,5 +5,7 @@ export const mapInsightsPayloadtoInsightsState = (insightsPayload) => {
5
5
  title: insight.title ?? '',
6
6
  summary: insight.summary ?? '',
7
7
  icon: toZeniUrl(insight.icon?.url ?? ''),
8
+ changeType: insight.change_type,
9
+ percentChange: insight.percent_change,
8
10
  }));
9
11
  };
@@ -14,6 +14,9 @@ const mapLineItem = (payload, currencyCode, currencySymbol) => ({
14
14
  : undefined,
15
15
  entityDescription: payload.entity_description,
16
16
  entityType: payload.entity_type,
17
+ grossAmount: payload.gross_amount != null
18
+ ? toAmount(payload.gross_amount, currencyCode, currencySymbol)
19
+ : undefined,
17
20
  id: payload.id,
18
21
  isTaxed: payload.is_taxed,
19
22
  itemLevelDiscountAmount: payload.item_level_discount_amount != null
@@ -67,6 +67,7 @@ const ALL_SNACKBAR_MESSAGE_SECTIONS = [
67
67
  'bill_pay_marked_as_paid',
68
68
  'zeni_account_accept_term',
69
69
  'update_vendor',
70
+ 'update_customer',
70
71
  'data_refresh_update',
71
72
  'delete_bank_account',
72
73
  'create_bank_account',
@@ -1,7 +1,8 @@
1
- import { from, of } from 'rxjs';
1
+ import { defer, from, of } from 'rxjs';
2
2
  import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { toMonth } from '../../../commonStateTypes/timePeriod';
4
4
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
5
+ import { getSessionEndpointsLoader } from '../../../sessionEndpointsLoader';
5
6
  import { initializeSelectedPeriodForTenants } from '../../../view/expenseAutomationView/expenseAutomationViewReducer';
6
7
  import { date, dateNow } from '../../../zeniDayJS';
7
8
  import { updateCompanies } from '../../company/companyReducer';
@@ -10,48 +11,54 @@ export const fetchAllTenantsEpic = (actions$, state, zeniAPI) => {
10
11
  const queryParam = {
11
12
  is_include_companies_in_response: true,
12
13
  };
13
- return actions$.pipe(filter(fetchAllTenants.match), switchMap((action) => zeniAPI
14
- .getJSON(`${zeniAPI.apiEndPoints.tenantMicroServiceBaseUrl}/1.0/users/${action.payload.userId}/tenants?query=${encodeURIComponent(JSON.stringify(queryParam))}`, Object.assign({ 'zeni-user-id': action.payload.userId }, { 'zeni-tenant-id': '' }, action.payload.zeniSessionId != null
15
- ? { 'zeni-session-id': action.payload.zeniSessionId }
16
- : {}))
17
- .pipe(mergeMap((response) => {
18
- const currentTenantId = state.value.tenantState.currentTenantId;
19
- if (isSuccessResponse(response) && response.data != null) {
20
- const defaultSelectedPeriodsForTenants = getDefaultSelectedPeriodsForTenants(response.data);
21
- let actions = [
22
- updateTenantsSuccess(response.data),
23
- initializeSelectedPeriodForTenants({
24
- selectedPeriodByTenantId: defaultSelectedPeriodsForTenants,
25
- }),
26
- ];
27
- if (response.data.companies != null) {
28
- let companiesPayload = response.data.companies;
29
- if (currentTenantId != null) {
30
- // filter out this tenant ID related company detail
31
- // Find companyID for this tenant
32
- const currentTenantPayload = response.data.tenants.find((tenant) => tenant.tenant_id === currentTenantId);
33
- if (currentTenantPayload != null) {
34
- companiesPayload = response.data.companies.filter((company) => company.company_id !== currentTenantPayload.company_id);
35
- }
36
- else {
37
- console.warn('Selected tenant record not found in response', currentTenantId);
38
- }
39
- }
40
- actions = [
41
- updateCompanies({
42
- payload: companiesPayload,
43
- schema: {},
14
+ return actions$.pipe(filter(fetchAllTenants.match), switchMap((action) => {
15
+ const fetchTenants$ = defer(() => zeniAPI.getJSON(`${zeniAPI.apiEndPoints.tenantMicroServiceBaseUrl}/1.0/users/${action.payload.userId}/tenants?query=${encodeURIComponent(JSON.stringify(queryParam))}`, Object.assign({ 'zeni-user-id': action.payload.userId }, { 'zeni-tenant-id': '' }, action.payload.zeniSessionId != null
16
+ ? { 'zeni-session-id': action.payload.zeniSessionId }
17
+ : {}))).pipe(mergeMap((response) => {
18
+ const currentTenantId = state.value.tenantState.currentTenantId;
19
+ if (isSuccessResponse(response) && response.data != null) {
20
+ const defaultSelectedPeriodsForTenants = getDefaultSelectedPeriodsForTenants(response.data);
21
+ let actions = [
22
+ updateTenantsSuccess(response.data),
23
+ initializeSelectedPeriodForTenants({
24
+ selectedPeriodByTenantId: defaultSelectedPeriodsForTenants,
44
25
  }),
45
- ...actions,
46
26
  ];
27
+ if (response.data.companies != null) {
28
+ let companiesPayload = response.data.companies;
29
+ if (currentTenantId != null) {
30
+ // filter out this tenant ID related company detail
31
+ // Find companyID for this tenant
32
+ const currentTenantPayload = response.data.tenants.find((tenant) => tenant.tenant_id === currentTenantId);
33
+ if (currentTenantPayload != null) {
34
+ companiesPayload = response.data.companies.filter((company) => company.company_id !== currentTenantPayload.company_id);
35
+ }
36
+ else {
37
+ console.warn('Selected tenant record not found in response', currentTenantId);
38
+ }
39
+ }
40
+ actions = [
41
+ updateCompanies({
42
+ payload: companiesPayload,
43
+ schema: {},
44
+ }),
45
+ ...actions,
46
+ ];
47
+ }
48
+ return from(actions);
47
49
  }
48
- return from(actions);
49
- }
50
- else {
51
- return of(updateTenantsFailure(response.status));
50
+ else {
51
+ return of(updateTenantsFailure(response.status));
52
+ }
53
+ }), catchError((error) => of(updateTenantsFailure(createZeniAPIStatus('Unexpected error', 'Fetch All Tenants REST API call errored out' +
54
+ JSON.stringify(error))))));
55
+ const sessionEndpointsLoader = getSessionEndpointsLoader();
56
+ if (sessionEndpointsLoader == null) {
57
+ return fetchTenants$;
52
58
  }
53
- }), catchError((error) => of(updateTenantsFailure(createZeniAPIStatus('Unexpected error', 'Fetch All Tenants REST API call errored out' +
54
- JSON.stringify(error))))))));
59
+ return defer(() => from(sessionEndpointsLoader(action.payload.userId, action.payload.zeniSessionId))).pipe(switchMap(() => fetchTenants$), catchError((error) => of(updateTenantsFailure(createZeniAPIStatus('Unexpected error', 'Loading session endpoints errored out: ' +
60
+ JSON.stringify(error))))));
61
+ }));
55
62
  };
56
63
  export const defaultMonthYearPeriod = {
57
64
  month: toMonth(dateNow().get('month') + 1),
package/lib/esm/epic.js CHANGED
@@ -178,6 +178,9 @@ import { fetchCreditAgentMacroEpic, } from './view/creditAgentView/epics/fetchCr
178
178
  import { saveCreditAgentMacroEpic, } from './view/creditAgentView/epics/saveCreditAgentMacroEpic';
179
179
  import { scheduleTenantCreditScoreCronEpic, } from './view/creditAgentView/epics/scheduleTenantCreditScoreCronEpic';
180
180
  import { updateCardProfileEpic, } from './view/creditAgentView/epics/updateCardProfileEpic';
181
+ import { fetchCustomerSummaryDetailEpic, } from './view/customerSummaryView/epic/fetchCustomerSummaryDetailEpic';
182
+ import { fetchCustomerSummaryListEpic, } from './view/customerSummaryView/epic/fetchCustomerSummaryListEpic';
183
+ import { saveCustomerSummaryDetailEpic, } from './view/customerSummaryView/epic/saveCustomerSummaryDetailEpic';
181
184
  import { fetchDashboardEpic, } from './view/dashboard/dashboardEpic';
182
185
  import { fetchDashboardLayoutEpic, } from './view/dashboardLayout/fetchDashboardLayoutEpic';
183
186
  import { updateDashboardLayoutEpic, } from './view/dashboardLayout/updateDashboardLayoutEpic';
@@ -1003,6 +1006,8 @@ const ALL_EPICS = [
1003
1006
  fetchCreditAccountRepaymentEpic,
1004
1007
  fetchCreditAgentMacroEpic,
1005
1008
  fetchCurrencyConversionValueEpic,
1009
+ fetchCustomerSummaryDetailEpic,
1010
+ fetchCustomerSummaryListEpic,
1006
1011
  fetchDashboardEpic,
1007
1012
  fetchDashboardLayoutEpic,
1008
1013
  fetchDebitCardSummaryEpic,
@@ -1401,6 +1406,7 @@ const ALL_EPICS = [
1401
1406
  saveCompanyPassportDetailsEpic,
1402
1407
  saveConnectorCredentialsEpic,
1403
1408
  saveCreditAgentMacroEpic,
1409
+ saveCustomerSummaryDetailEpic,
1404
1410
  saveExpenseAutomationReconciliationDetailsEpic,
1405
1411
  saveExpenseAutomationReconciliationReviewEpic,
1406
1412
  saveExternalConnectionEpic,
package/lib/esm/index.js CHANGED
@@ -90,7 +90,7 @@ import { getUserRoleMap, hasAICFOAccess, hasAdminLevelAccess, hasAnyCompanyRole,
90
90
  import { updateVendors } from './entity/vendor/vendorReducer';
91
91
  import { defaultVendorCurrency, } from './entity/vendor/vendorState';
92
92
  import { VendorExpenseTrend } from './entity/vendorExpense/vendorExpenseTrend';
93
- import initialize, { disconnectPusher, initializePusher, initializeWithNewStore, injectFeatureEpics, injectFeatureModules, updateZeniAPIClientConfig, zeniAPIClientConfig, } from './init';
93
+ import initialize, { disconnectPusher, initializePusher, initializeWithNewStore, injectFeatureEpics, injectFeatureModules, setSessionEndpointsLoader, updateZeniAPIClientConfig, updateZeniAPIEndpoints, zeniAPIClientConfig, } from './init';
94
94
  import { isAccessDeniedError, isInvalidSessionError, isSuccessResponse, } from './responsePayload';
95
95
  import { clearAll } from './rootActions';
96
96
  import { fetchAccountList } from './view/accountList/accountListReducer';
@@ -485,7 +485,7 @@ export { updateCreditAgentMacro, updateCreditAgentRow, updateCreditAgentRows, }
485
485
  export { getCreditAgentRows, getCreditAgentMacroEntity, } from './entity/creditAgent/creditAgentSelector';
486
486
  export { saveJeAccountSettings, saveJeAccountSettingsLocalData };
487
487
  export default initialize;
488
- export { initializePusher, disconnectPusher, Dayjs, dateInLocal as zeniDateInLocal, date as zeniDate, dateLocal, dateNow, getBusinessDayOfDate, formatZeniDateFY as formatZeniDate, getFYMonths, getFYQuarterAndYear, getStartOfAndEndOfTimeframeForFY, getActualPeriodOfFYQtr, getActualPeriodOfFY, getLocalTimezone, addPeriod, subtractPeriod, setLocalTimezone, updateZeniDateLocaleID, toZeniUrl, toZeniUrlWithoutBaseURL, updateZeniAPIClientConfig, zeniAPIClientConfig, injectFeatureEpics, injectFeatureModules, initializeWithNewStore, getTimeframeTickTag, isIncompleteMonthPresentInTimeframeTick, getLastMonthOfFYQuarter, getLastMonthOfFYYear, getMonthIndex, dateFromYearMonthDate, getMinDate, };
488
+ export { initializePusher, disconnectPusher, Dayjs, dateInLocal as zeniDateInLocal, date as zeniDate, dateLocal, dateNow, getBusinessDayOfDate, formatZeniDateFY as formatZeniDate, getFYMonths, getFYQuarterAndYear, getStartOfAndEndOfTimeframeForFY, getActualPeriodOfFYQtr, getActualPeriodOfFY, getLocalTimezone, addPeriod, subtractPeriod, setLocalTimezone, updateZeniDateLocaleID, toZeniUrl, toZeniUrlWithoutBaseURL, updateZeniAPIClientConfig, updateZeniAPIEndpoints, setSessionEndpointsLoader, zeniAPIClientConfig, injectFeatureEpics, injectFeatureModules, initializeWithNewStore, getTimeframeTickTag, isIncompleteMonthPresentInTimeframeTick, getLastMonthOfFYQuarter, getLastMonthOfFYYear, getMonthIndex, dateFromYearMonthDate, getMinDate, };
489
489
  export { toAmount };
490
490
  export { toReportID, toReportFormatStrict, isCashFlowOrBalanceSheetReport, isBalanceSheetReport, isBalanceSheetClassesViewReport, isBalanceSheetProjectViewReport, isCashFlowClassesViewReport, isCashFlowProjectViewReport, isCashFlowReport, isGroupedCashFlowReport, isGroupedBalanceSheetReport, isDashboardReport, isDashboardClassesViewReport, isDashboardProjectViewReport, isPAndLReport, isPAndLClassesViewReport, isPAndLProjectViewReport, isAgingReport, isOpExByVendorReport, isFluxAnalysisOpExReport, };
491
491
  export { toCOABalanceTimeframe, };
@@ -872,3 +872,6 @@ export { getInvoicingOnboardingGateView } from './view/invoicing/invoicingConfig
872
872
  // narrow a JEScheduleRow before reading baseTransaction / recurringAmount.
873
873
  export { getExpenseAutomationJEOneTimeAccrualDetailView, getJEScheduleRowType, isAccruedScheduleRow, isAiAccountantSchedule, isExistingTabRow, isOneTimeAccrualRow, isRenderableOneTimeAccrual, isPendingReviewRow, } from './view/expenseAutomationView/selectors/jeSchedulesViewSelector';
874
874
  export { getActualMatchingAmount, getActualMatchingReversalDate, getActualMatchingMatchModalConfirmStatus, getCloseMatchAfterIgnoreMatchId, getCloseMatchAfterUndo, getCloseMatchAfterUndoMatchId, parseActualMatchingMatchId, getExpenseAutomationActualMatchingMatchView, getExpenseAutomationActualMatchingView, isAutoReversedAccrual, isManuallyReversedAccrual, isPendingReviewAccrual, } from './view/expenseAutomationView/selectors/actualMatchingViewSelector';
875
+ export { fetchCustomerSummaryDetail, fetchCustomerSummaryList, resetCustomerSummaryDetailUiState, saveCustomerSummaryDetail, updateCustomerAllTransactionsTabData, updateCustomerDetailSelectedTab, updateCustomerSummaryRevenueSelection, updateCustomerSummaryScrollYOffset, updateCustomerSummarySortUiState, updateCustomerTrendFilter, } from './view/customerSummaryView/customerSummaryViewReducer';
876
+ export { getCustomerSummaryAllTransactionsTabData, getCustomerSummaryDetail, getCustomerSummaryList, getCustomerSummarySelectedTabType, getCustomerSummaryTrendFilter, isCustomersTabVisible, } from './view/customerSummaryView/customerSummaryViewSelector';
877
+ export { CUSTOMER_REVENUE_KEYS } from './view/customerSummaryView/customerSummaryViewState';
package/lib/esm/init.js CHANGED
@@ -2,6 +2,7 @@ import configureNewStore from './configureStore';
2
2
  import { ZeniAPIClient } from './zeniAPI';
3
3
  import { setZeniAPIClient, zeniAPI } from './zeniAPIClientState';
4
4
  export { zeniAPI } from './zeniAPIClientState';
5
+ export { setSessionEndpointsLoader } from './sessionEndpointsLoader';
5
6
  export { disconnectPusher, initializePusher, pusher } from './pusherClient';
6
7
  export let store;
7
8
  let initialized = false;
@@ -13,11 +14,10 @@ let initialized = false;
13
14
  */
14
15
  export default function initialize(restEndPoints, userId, sessionId, tenantId, isSandboxEnv, onSessionInvalidated) {
15
16
  if (initialized) {
16
- console.warn(`Already initialized with url ${JSON.stringify(zeniAPI.apiEndPoints)}`);
17
+ console.warn('client-epic-state is already initialized');
17
18
  return store;
18
19
  }
19
20
  initialized = true;
20
- console.info(`initializing with ${JSON.stringify(restEndPoints)}`);
21
21
  initialize3rdPartyExtensions();
22
22
  setZeniAPIClient(new ZeniAPIClient(restEndPoints, userId, sessionId, tenantId, onSessionInvalidated));
23
23
  store = configureNewStore(zeniAPI, isSandboxEnv);
@@ -35,6 +35,18 @@ export { injectFeatureModules, injectFeatureEpics } from './configureStore';
35
35
  export function updateZeniAPIClientConfig(userId, sessionId, tenantId) {
36
36
  zeniAPI.update(userId, sessionId, tenantId);
37
37
  }
38
+ /**
39
+ * Replace the REST endpoints the API client calls. Epics read the endpoints
40
+ * at request time, so every request made after this uses the new ones.
41
+ * Note: This version SHOULD BE used only in the client side.
42
+ * @param restEndPoints
43
+ */
44
+ export function updateZeniAPIEndpoints(restEndPoints) {
45
+ if (zeniAPI == null) {
46
+ throw new Error('Call initialize() before updateZeniAPIEndpoints()');
47
+ }
48
+ zeniAPI.apiEndPoints = restEndPoints;
49
+ }
38
50
  /**
39
51
  * Returns current Zeni REST API client config.
40
52
  * @returns
@@ -61,7 +73,6 @@ export function zeniAPIClientConfig() {
61
73
  * @param tenantId tenant id
62
74
  */
63
75
  export async function initializeWithNewStore(restEndPoints, userId, sessionId, tenantId) {
64
- console.info(`initializeWithNewStore with ${JSON.stringify(restEndPoints)}`);
65
76
  initialize3rdPartyExtensions();
66
77
  const zeniHttpClient = new ZeniAPIClient(restEndPoints, userId, sessionId, tenantId);
67
78
  const store = configureNewStore(zeniHttpClient, false);
@@ -116,6 +116,7 @@ import companyTaskManagerView, { initialState as initialCompanyTaskManagerViewSt
116
116
  import companyView, { initialState as initialCompanyViewState, } from './view/companyView/companyViewReducer';
117
117
  import createTransferEntry, { initialState as initialCreateTransferEntryState, } from './view/createTransferEntry/createTransferEntryReducer';
118
118
  import creditAgentView, { initialState as initialCreditAgentViewState, } from './view/creditAgentView/creditAgentViewReducer';
119
+ import customerSummaryView, { initialState as initialCustomerSummaryViewState, } from './view/customerSummaryView/customerSummaryViewReducer';
119
120
  import dashboard, { initialState as initialDashboardState, } from './view/dashboard/dashboardReducer';
120
121
  import dashboardLayout, { initialState as initialDashboardLayoutState, } from './view/dashboardLayout/dashboardLayoutReducer';
121
122
  import expenseAutomationView, { initialState as initialExpenseAutomationViewState, } from './view/expenseAutomationView/expenseAutomationViewReducer';
@@ -545,6 +546,7 @@ const initialViewsState = {
545
546
  vendorFirstReviewViewState: initialVendorFirstReviewViewState,
546
547
  vendorGlobalReviewViewState: initialVendorGlobalReviewViewState,
547
548
  vendorListState: initialVendorListState,
549
+ customerSummaryViewState: initialCustomerSummaryViewState,
548
550
  vendorFiling1099ListState: initialVendorFiling1099ListState,
549
551
  vendorFiling1099UploadDetailsState: initialVendorFiling1099UploadDetails,
550
552
  vendorsTabVendorViewState: initialVendorsTabVendorViewReducer,
@@ -833,6 +835,7 @@ const viewReducers = {
833
835
  vendorFirstReviewViewState: vendorFirstReviewView,
834
836
  vendorGlobalReviewViewState: vendorGlobalReviewView,
835
837
  vendorListState: vendorList,
838
+ customerSummaryViewState: customerSummaryView,
836
839
  vendorFiling1099ListState: vendorFiling1099List,
837
840
  vendorFiling1099UploadDetailsState: vendorFiling1099UploadDetails,
838
841
  vendorsTabVendorViewState: vendorsTabVendorView,
@@ -0,0 +1,7 @@
1
+ let sessionEndpointsLoader;
2
+ export function setSessionEndpointsLoader(loader) {
3
+ sessionEndpointsLoader = loader;
4
+ }
5
+ export function getSessionEndpointsLoader() {
6
+ return sessionEndpointsLoader;
7
+ }
@@ -0,0 +1,197 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+ import { clearAll } from '../../rootActions';
3
+ import { CUSTOMER_REVENUE_KEYS, } from './customerSummaryViewState';
4
+ export const initialState = {
5
+ customers: [],
6
+ detailById: {},
7
+ fetchState: 'Not-Started',
8
+ searchString: '',
9
+ selectedTabType: 'income_transactions',
10
+ totalCount: 0,
11
+ trendFilter: 'last_12_months',
12
+ uiState: {
13
+ pageToken: null,
14
+ revenueSelection: 'year_to_date_revenue',
15
+ scrollYOffset: 0,
16
+ sortKey: 'year_to_date_revenue',
17
+ sortOrder: 'descending',
18
+ },
19
+ allTransactionsTabData: undefined,
20
+ detailCustomerId: undefined,
21
+ error: undefined,
22
+ };
23
+ export const toCustomerSummary = (payload) => ({
24
+ id: payload.customer_id,
25
+ qboId: payload.qbo_id ?? undefined,
26
+ name: payload.customer_name ?? '',
27
+ balance: payload.balance ?? 0,
28
+ currencyCode: payload.currency_code ?? undefined,
29
+ currencySymbol: payload.currency_symbol ?? undefined,
30
+ revenue: Object.fromEntries(CUSTOMER_REVENUE_KEYS.map((key) => [key, payload[key] ?? 0])),
31
+ account: payload.account?.account_id != null
32
+ ? {
33
+ id: payload.account.account_id,
34
+ name: payload.account.account_name ?? '',
35
+ }
36
+ : undefined,
37
+ accountingClass: payload.accounting_class?.accounting_class_id != null
38
+ ? {
39
+ id: payload.accounting_class.accounting_class_id,
40
+ name: payload.accounting_class.accounting_class_name ?? '',
41
+ }
42
+ : undefined,
43
+ });
44
+ const getDetail = (draft, customerId) => {
45
+ var _a;
46
+ return ((_a = draft.detailById)[customerId] ?? (_a[customerId] = {
47
+ fetchState: 'Not-Started',
48
+ saveState: 'Not-Started',
49
+ }));
50
+ };
51
+ const customerSummaryView = createSlice({
52
+ name: 'customerSummaryView',
53
+ initialState,
54
+ reducers: {
55
+ fetchCustomerSummaryList: {
56
+ prepare(keepExistingListItems, searchString = '', pageToken = null) {
57
+ return { payload: { keepExistingListItems, pageToken, searchString } };
58
+ },
59
+ reducer(draft, action) {
60
+ const { keepExistingListItems, pageToken, searchString } = action.payload;
61
+ // Load-more while a page is in flight: keep that page (the epic drops this fetch too).
62
+ if (keepExistingListItems && draft.fetchState === 'In-Progress') {
63
+ return;
64
+ }
65
+ if (!keepExistingListItems) {
66
+ draft.customers = [];
67
+ draft.uiState.scrollYOffset = 0;
68
+ }
69
+ draft.searchString = searchString;
70
+ draft.uiState.pageToken = pageToken;
71
+ draft.fetchState = 'In-Progress';
72
+ draft.error = undefined;
73
+ },
74
+ },
75
+ updateCustomerSummaryList: {
76
+ prepare(response, keepExistingListItems) {
77
+ return { payload: { keepExistingListItems, response } };
78
+ },
79
+ reducer(draft, action) {
80
+ const { response, keepExistingListItems } = action.payload;
81
+ const customers = (response.data?.customers ?? []).map(toCustomerSummary);
82
+ const existingIds = new Set(draft.customers.map(({ id }) => id));
83
+ draft.customers = keepExistingListItems
84
+ ? [
85
+ ...draft.customers,
86
+ ...customers.filter(({ id }) => !existingIds.has(id)),
87
+ ]
88
+ : customers;
89
+ draft.totalCount = response.data?.total_count ?? 0;
90
+ draft.uiState.pageToken = response.data?.query?.page_token ?? null;
91
+ draft.fetchState = 'Completed';
92
+ },
93
+ },
94
+ updateCustomerSummaryListFailure(draft, action) {
95
+ draft.fetchState = 'Error';
96
+ draft.error = action.payload;
97
+ // No more pages after a failure, so load-more doesn't keep retrying.
98
+ draft.uiState.pageToken = null;
99
+ },
100
+ updateCustomerSummarySortUiState: {
101
+ prepare(sortKey, sortOrder) {
102
+ return { payload: { sortKey, sortOrder } };
103
+ },
104
+ reducer(draft, action) {
105
+ draft.uiState.sortKey = action.payload.sortKey;
106
+ draft.uiState.sortOrder = action.payload.sortOrder;
107
+ },
108
+ },
109
+ updateCustomerSummaryRevenueSelection(draft, action) {
110
+ draft.uiState.revenueSelection = action.payload;
111
+ },
112
+ updateCustomerSummaryScrollYOffset(draft, action) {
113
+ draft.uiState.scrollYOffset = action.payload;
114
+ },
115
+ fetchCustomerSummaryDetail(draft, action) {
116
+ const detail = getDetail(draft, action.payload);
117
+ detail.fetchState = 'In-Progress';
118
+ detail.error = undefined;
119
+ },
120
+ updateCustomerSummaryDetail: {
121
+ prepare(customerId, customer) {
122
+ return { payload: { customer, customerId } };
123
+ },
124
+ reducer(draft, action) {
125
+ const detail = getDetail(draft, action.payload.customerId);
126
+ detail.customer = toCustomerSummary(action.payload.customer);
127
+ detail.fetchState = 'Completed';
128
+ // Keep the list row in sync after an edit.
129
+ const index = draft.customers.findIndex(({ id }) => id === action.payload.customerId);
130
+ if (index >= 0) {
131
+ draft.customers[index] = {
132
+ ...draft.customers[index],
133
+ account: detail.customer.account,
134
+ accountingClass: detail.customer.accountingClass,
135
+ };
136
+ }
137
+ },
138
+ },
139
+ updateCustomerSummaryDetailFailure: {
140
+ prepare(customerId, status) {
141
+ return { payload: { customerId, status } };
142
+ },
143
+ reducer(draft, action) {
144
+ const detail = getDetail(draft, action.payload.customerId);
145
+ detail.fetchState = 'Error';
146
+ detail.error = action.payload.status;
147
+ },
148
+ },
149
+ saveCustomerSummaryDetail: {
150
+ prepare(customerId, changes) {
151
+ return { payload: { changes, customerId } };
152
+ },
153
+ reducer(draft, action) {
154
+ getDetail(draft, action.payload.customerId).saveState = 'In-Progress';
155
+ },
156
+ },
157
+ updateCustomerSummarySaveState: {
158
+ prepare(customerId, saveState) {
159
+ return { payload: { customerId, saveState } };
160
+ },
161
+ reducer(draft, action) {
162
+ getDetail(draft, action.payload.customerId).saveState =
163
+ action.payload.saveState;
164
+ },
165
+ },
166
+ updateCustomerAllTransactionsTabData(draft, action) {
167
+ draft.allTransactionsTabData = action.payload;
168
+ },
169
+ updateCustomerDetailSelectedTab(draft, action) {
170
+ draft.selectedTabType = action.payload;
171
+ },
172
+ updateCustomerTrendFilter(draft, action) {
173
+ draft.trendFilter = action.payload;
174
+ },
175
+ // Detail page opened for `customerId`: a different customer starts from the default filter / tab /
176
+ // transactions window / save state; the same one (e.g. back from a transaction) keeps them.
177
+ resetCustomerSummaryDetailUiState(draft, action) {
178
+ if (draft.detailCustomerId === action.payload) {
179
+ return;
180
+ }
181
+ draft.detailCustomerId = action.payload;
182
+ draft.trendFilter = initialState.trendFilter;
183
+ draft.selectedTabType = initialState.selectedTabType;
184
+ draft.allTransactionsTabData = undefined;
185
+ Object.values(draft.detailById).forEach((detail) => {
186
+ detail.saveState = 'Not-Started';
187
+ });
188
+ },
189
+ },
190
+ extraReducers: (builder) => {
191
+ builder.addCase(clearAll, (draft) => {
192
+ Object.assign(draft, initialState);
193
+ });
194
+ },
195
+ });
196
+ export const { fetchCustomerSummaryList, updateCustomerSummaryList, updateCustomerSummaryListFailure, updateCustomerSummarySortUiState, updateCustomerSummaryRevenueSelection, updateCustomerSummaryScrollYOffset, fetchCustomerSummaryDetail, updateCustomerSummaryDetail, updateCustomerSummaryDetailFailure, saveCustomerSummaryDetail, updateCustomerSummarySaveState, updateCustomerDetailSelectedTab, updateCustomerAllTransactionsTabData, updateCustomerTrendFilter, resetCustomerSummaryDetailUiState, } = customerSummaryView.actions;
197
+ export default customerSummaryView.reducer;
@@ -0,0 +1,35 @@
1
+ import { createSelector } from '@reduxjs/toolkit';
2
+ import { getIsAccountingClassesEnabled } from '../../entity/tenant/tenantSelector';
3
+ import { getNestedAccountListHierarchy } from '../accountList/accountListSelector';
4
+ import { getNestedClassListHierarchy } from '../classList/classListSelector';
5
+ const isLoadingFetchState = (fetchState) => fetchState === 'Not-Started' || fetchState === 'In-Progress';
6
+ const NOT_STARTED_DETAIL = {
7
+ fetchState: 'Not-Started',
8
+ saveState: 'Not-Started',
9
+ };
10
+ export const getCustomerSummaryList = (state) => state.customerSummaryViewState;
11
+ export const getCustomerSummaryDetail = createSelector((state, customerId) => state.customerSummaryViewState.detailById[customerId], (state) => state.accountListState, (state) => state.classListState, getIsAccountingClassesEnabled, (detail, accountListState, classListState, isAccountingClassesEnabled) => ({
12
+ ...(detail ?? NOT_STARTED_DETAIL),
13
+ accountsHierarchyList: getNestedAccountListHierarchy(accountListState, 'accountList'),
14
+ classHierarchyList: getNestedClassListHierarchy(classListState),
15
+ isAccountingClassesEnabled,
16
+ isReferenceDataLoading: isLoadingFetchState(accountListState.byReportId.accountList.fetchState) ||
17
+ (isAccountingClassesEnabled &&
18
+ isLoadingFetchState(classListState.fetchState)),
19
+ }));
20
+ export const getCustomerSummarySelectedTabType = (state) => state.customerSummaryViewState.selectedTabType;
21
+ export const getCustomerSummaryAllTransactionsTabData = (state) => state.customerSummaryViewState.allTransactionsTabData;
22
+ export const getCustomerSummaryTrendFilter = (state) => state.customerSummaryViewState.trendFilter;
23
+ // Roles holding the customers / customers.read_only scopes (tenant role_scopes_v32). Same roles as the
24
+ // Vendors tab today, kept separate so AR access can diverge; bookkeeping_user is read-only.
25
+ export const isCustomersTabVisible = (roleMap) => [
26
+ 'admin',
27
+ 'bookkeeping_admin',
28
+ 'bookkeeping_user',
29
+ 'ap_analyst',
30
+ 'controller',
31
+ 'customer_success_manager',
32
+ 'reviewer',
33
+ 'tax_lead',
34
+ 'preparer',
35
+ ].some((role) => roleMap?.[role] === true);
@@ -0,0 +1,10 @@
1
+ export const CUSTOMER_REVENUE_KEYS = [
2
+ 'last_90_days_revenue',
3
+ 'last_30_days_revenue',
4
+ 'year_to_date_revenue',
5
+ 'last_month_revenue',
6
+ 'last_3_months_revenue',
7
+ 'last_6_months_revenue',
8
+ 'last_12_months_revenue',
9
+ 'previous_year_revenue',
10
+ ];
@@ -0,0 +1,30 @@
1
+ import { concat, from, of } from 'rxjs';
2
+ import { catchError, filter, map, mergeMap } from 'rxjs/operators';
3
+ import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
4
+ import { fetchAccountList } from '../../accountList/accountListReducer';
5
+ import { fetchClassList } from '../../classList/classListReducer';
6
+ import { fetchCustomerSummaryDetail, updateCustomerSummaryDetail, updateCustomerSummaryDetailFailure, } from '../customerSummaryViewReducer';
7
+ export const fetchCustomerSummaryDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchCustomerSummaryDetail.match), mergeMap(({ payload: customerId }) => {
8
+ // Category / class pickers on the detail page need the account and class lists.
9
+ const { accountListState, classListState } = state$.value;
10
+ const listActions = [];
11
+ const accountList = accountListState.byReportId.accountList;
12
+ if (!accountList.hasValidState() &&
13
+ accountList.fetchState !== 'In-Progress') {
14
+ listActions.push(fetchAccountList('accountList', true));
15
+ }
16
+ if (!classListState.hasValidState() &&
17
+ classListState.fetchState !== 'In-Progress') {
18
+ listActions.push(fetchClassList());
19
+ }
20
+ const customer$ = zeniAPI
21
+ .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/customers/summary/${encodeURIComponent(customerId)}`)
22
+ .pipe(map((response) => {
23
+ const customer = response.data?.customers?.[0];
24
+ return isSuccessResponse(response) && customer != null
25
+ ? updateCustomerSummaryDetail(customerId, customer)
26
+ : updateCustomerSummaryDetailFailure(customerId, response.status);
27
+ }), catchError((error) => of(updateCustomerSummaryDetailFailure(customerId, createZeniAPIStatus('Unexpected Error', 'Fetch customer summary REST API call errored out' +
28
+ JSON.stringify(error))))));
29
+ return concat(from(listActions), customer$);
30
+ }));