@zeniai/client-epic-state 5.1.91 → 5.1.92-beta0ND

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/lib/entity/snackbar/snackbarApiError.d.ts +8 -0
  2. package/lib/entity/snackbar/snackbarApiError.js +15 -0
  3. package/lib/epic.d.ts +2 -1
  4. package/lib/epic.js +2 -1
  5. package/lib/esm/entity/snackbar/snackbarApiError.js +11 -0
  6. package/lib/esm/epic.js +2 -1
  7. package/lib/esm/index.js +3 -3
  8. package/lib/esm/view/invoicing/createCreditNote/createCreditNoteEpic.js +2 -0
  9. package/lib/esm/view/invoicing/createInvoice/createInvoiceEpic.js +2 -0
  10. package/lib/esm/view/invoicing/discountAction/discountActionEpic.js +6 -3
  11. package/lib/esm/view/invoicing/dunningAction/dunningActionEpic.js +6 -3
  12. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +5 -4
  13. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +3 -0
  14. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +2 -0
  15. package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/saveInvoicingCustomerEpic.js +2 -0
  16. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +2 -0
  17. package/lib/esm/view/invoicing/invoiceAction/invoiceActionEpic.js +7 -3
  18. package/lib/esm/view/invoicing/invoicingQboView/epics/retryInvoicingQboSyncTaskEpic.js +2 -0
  19. package/lib/esm/view/invoicing/invoicingQboView/epics/startInvoicingQboBackfillEpic.js +2 -0
  20. package/lib/esm/view/invoicing/issueCreditNote/issueCreditNoteEpic.js +2 -0
  21. package/lib/esm/view/invoicing/recordPayment/recordPaymentEpic.js +3 -0
  22. package/lib/esm/view/invoicing/settingsView/settingsViewEpics.js +6 -3
  23. package/lib/esm/view/invoicing/settingsView/settingsViewSelector.js +52 -0
  24. package/lib/esm/view/invoicing/subscriptionAction/subscriptionActionEpic.js +6 -3
  25. package/lib/esm/view/recommendation/recommendationReducer.js +17 -1
  26. package/lib/esm/view/recommendation/submitRecommendationFeedbackEpic.js +52 -0
  27. package/lib/esm/view/vendorTabView/vendorTabViewReducer.js +1 -1
  28. package/lib/index.d.ts +5 -5
  29. package/lib/index.js +26 -24
  30. package/lib/view/invoicing/createCreditNote/createCreditNoteEpic.js +2 -0
  31. package/lib/view/invoicing/createInvoice/createInvoiceEpic.js +2 -0
  32. package/lib/view/invoicing/discountAction/discountActionEpic.js +5 -2
  33. package/lib/view/invoicing/dunningAction/dunningActionEpic.js +5 -2
  34. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +5 -4
  35. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +3 -0
  36. package/lib/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +2 -0
  37. package/lib/view/invoicing/editInvoicingCustomerDetailView/saveInvoicingCustomerEpic.js +2 -0
  38. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +2 -0
  39. package/lib/view/invoicing/invoiceAction/invoiceActionEpic.js +6 -2
  40. package/lib/view/invoicing/invoicingQboView/epics/retryInvoicingQboSyncTaskEpic.js +2 -0
  41. package/lib/view/invoicing/invoicingQboView/epics/startInvoicingQboBackfillEpic.js +2 -0
  42. package/lib/view/invoicing/issueCreditNote/issueCreditNoteEpic.js +2 -0
  43. package/lib/view/invoicing/recordPayment/recordPaymentEpic.js +3 -0
  44. package/lib/view/invoicing/settingsView/settingsViewEpics.js +5 -2
  45. package/lib/view/invoicing/settingsView/settingsViewSelector.d.ts +41 -1
  46. package/lib/view/invoicing/settingsView/settingsViewSelector.js +55 -1
  47. package/lib/view/invoicing/subscriptionAction/subscriptionActionEpic.js +5 -2
  48. package/lib/view/recommendation/recommendationPayload.d.ts +34 -0
  49. package/lib/view/recommendation/recommendationReducer.d.ts +2 -1
  50. package/lib/view/recommendation/recommendationReducer.js +18 -2
  51. package/lib/view/recommendation/submitRecommendationFeedbackEpic.d.ts +7 -0
  52. package/lib/view/recommendation/submitRecommendationFeedbackEpic.js +56 -0
  53. package/package.json +1 -1
@@ -1,8 +1,9 @@
1
1
  import { from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateInvoicingCoupons } from '../../../entity/invoicing/coupon/couponReducer';
4
+ import { apiErrorSnackbarVariables } from '../../../entity/snackbar/snackbarApiError';
4
5
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
5
- import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
6
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../responsePayload';
6
7
  import { runInvoicingDiscountAction, runInvoicingDiscountActionFailure, runInvoicingDiscountActionSuccess, } from './discountActionReducer';
7
8
  /** Snackbar copy per action kind; unlisted kinds run without one. */
8
9
  const SNACKBAR_SECTION_BY_KIND = {
@@ -14,13 +15,15 @@ export const runInvoicingDiscountActionEpic = (actions$, _state$, zeniAPI) => ac
14
15
  const { action: kind, id } = action.payload;
15
16
  const requestBody = { action: kind };
16
17
  const snackbarSection = SNACKBAR_SECTION_BY_KIND[kind];
17
- const snackbar = (messageText) => snackbarSection == null
18
+ /** `status` surfaces the platform's error text; omit it to keep the generic copy. */
19
+ const snackbar = (messageText, status) => snackbarSection == null
18
20
  ? []
19
21
  : [
20
22
  openSnackbar({
21
23
  messageSection: snackbarSection,
22
24
  messageText,
23
25
  type: messageText === 'success' ? 'success' : 'error',
26
+ variables: apiErrorSnackbarVariables(status),
24
27
  }),
25
28
  ];
26
29
  const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/discounts/${encodeURIComponent(id)}/actions`;
@@ -39,7 +42,7 @@ export const runInvoicingDiscountActionEpic = (actions$, _state$, zeniAPI) => ac
39
42
  error: response.status,
40
43
  id,
41
44
  }),
42
- ...snackbar('failed'),
45
+ ...snackbar('failed', response.status),
43
46
  ]);
44
47
  }), catchError((error) => from([
45
48
  runInvoicingDiscountActionFailure({
@@ -1,8 +1,9 @@
1
1
  import { from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateInvoicingDunningCases } from '../../../entity/invoicing/dunningCase/dunningCaseReducer';
4
+ import { apiErrorSnackbarVariables } from '../../../entity/snackbar/snackbarApiError';
4
5
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
5
- import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
6
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../responsePayload';
6
7
  import { runInvoicingDunningAction, runInvoicingDunningActionFailure, runInvoicingDunningActionSuccess, } from './dunningActionReducer';
7
8
  /**
8
9
  * Snackbar copy is per action kind; anything not listed here runs without one.
@@ -15,13 +16,15 @@ const SNACKBAR_SECTION_BY_KIND = {
15
16
  export const runInvoicingDunningActionEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(runInvoicingDunningAction.match), mergeMap((action) => {
16
17
  const { action: kind, body, id } = action.payload;
17
18
  const snackbarSection = SNACKBAR_SECTION_BY_KIND[kind];
18
- const snackbar = (messageText) => snackbarSection == null
19
+ /** `status` surfaces the platform's error text; omit it to keep the generic copy. */
20
+ const snackbar = (messageText, status) => snackbarSection == null
19
21
  ? []
20
22
  : [
21
23
  openSnackbar({
22
24
  messageSection: snackbarSection,
23
25
  messageText,
24
26
  type: messageText === 'success' ? 'success' : 'error',
27
+ variables: apiErrorSnackbarVariables(status),
25
28
  }),
26
29
  ];
27
30
  const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/dunning/${encodeURIComponent(id)}/${kind}`;
@@ -40,7 +43,7 @@ export const runInvoicingDunningActionEpic = (actions$, _state$, zeniAPI) => act
40
43
  error: response.status,
41
44
  id,
42
45
  }),
43
- ...snackbar('failed'),
46
+ ...snackbar('failed', response.status),
44
47
  ]);
45
48
  }), catchError((error) => from([
46
49
  runInvoicingDunningActionFailure({
@@ -80,6 +80,8 @@ const buildSubFamilyPayload = (localData, subFamily, clearEmptyValues) => {
80
80
  }
81
81
  return payload;
82
82
  };
83
+ /** `unit` is the item's unit of measure, not a currency; `CURRENCY_CODE` above prices sub-families. */
84
+ const DEFAULT_CATALOG_ITEM_UNIT = 'USD';
83
85
  /**
84
86
  * Build the parent catalog-item request body from the form draft. Creates omit
85
87
  * both parent and sub-family IDs; edits retain returned sub-family IDs.
@@ -101,10 +103,9 @@ export const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate)
101
103
  if (description != null) {
102
104
  itemPayload.description = description;
103
105
  }
104
- const unit = trimOrUndefined(localData.unit);
105
- if (unit != null) {
106
- itemPayload.unit = unit;
107
- }
106
+ // The form no longer collects a unit; an item that already has one keeps it.
107
+ itemPayload.unit =
108
+ trimOrUndefined(localData.unit) ?? DEFAULT_CATALOG_ITEM_UNIT;
108
109
  if (localData.itemApplicability === 'restricted' &&
109
110
  localData.applicableItems.length > 0) {
110
111
  itemPayload.applicable_items = localData.applicableItems;
@@ -2,6 +2,7 @@ import { forkJoin, from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { mergeInvoicingPlanSubFamily, updateInvoicingPlans, } from '../../../entity/invoicing/plan/planReducer';
4
4
  import { updateInvoicingProducts } from '../../../entity/invoicing/product/productReducer';
5
+ import { apiErrorSnackbarVariables } from '../../../entity/snackbar/snackbarApiError';
5
6
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
6
7
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
7
8
  import { addInvoicingCatalogItem } from '../invoicingCatalogListView/invoicingCatalogListViewReducer';
@@ -45,6 +46,7 @@ export const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actio
45
46
  messageSection: 'invoicing_catalog_item_saved',
46
47
  messageText: 'failed',
47
48
  type: 'error',
49
+ variables: apiErrorSnackbarVariables(failedProductResponse.status),
48
50
  }),
49
51
  ]);
50
52
  }
@@ -94,6 +96,7 @@ export const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actio
94
96
  messageSection: 'invoicing_catalog_item_saved',
95
97
  messageText: 'failed',
96
98
  type: 'error',
99
+ variables: apiErrorSnackbarVariables(response.status),
97
100
  }),
98
101
  ]);
99
102
  }), catchError((error) => from([
@@ -1,6 +1,7 @@
1
1
  import { from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateInvoicingCoupons } from '../../../entity/invoicing/coupon/couponReducer';
4
+ import { apiErrorSnackbarVariables } from '../../../entity/snackbar/snackbarApiError';
4
5
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
5
6
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
6
7
  import { addInvoicingCouponToList } from '../couponView/couponViewReducer';
@@ -57,6 +58,7 @@ export const saveInvoicingCouponEpic = (actions$, state$, zeniAPI) => actions$.p
57
58
  messageSection: 'invoicing_coupon_saved',
58
59
  messageText: 'failed',
59
60
  type: 'error',
61
+ variables: apiErrorSnackbarVariables(response.status),
60
62
  }),
61
63
  ]);
62
64
  }), catchError((error) => from([
@@ -3,6 +3,7 @@ import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateAddresses } from '../../../entity/address/addressReducer';
4
4
  import { extractInvoicingCustomerAddresses } from '../../../entity/invoicing/invoicingCustomer/invoicingCustomerPayload';
5
5
  import { updateInvoicingCustomers } from '../../../entity/invoicing/invoicingCustomer/invoicingCustomerReducer';
6
+ import { apiErrorSnackbarVariables } from '../../../entity/snackbar/snackbarApiError';
6
7
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
7
8
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
8
9
  import { resetNewAddressDataInLocalStore } from '../../addressView/addressViewReducer';
@@ -59,6 +60,7 @@ export const saveInvoicingCustomerEpic = (actions$, state$, zeniAPI) => actions$
59
60
  messageSection: 'invoicing_customer_saved',
60
61
  messageText: 'failed',
61
62
  type: 'error',
63
+ variables: apiErrorSnackbarVariables(response.status),
62
64
  }),
63
65
  ]);
64
66
  }), catchError((error) => from([
@@ -3,6 +3,7 @@ import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateAddresses } from '../../../entity/address/addressReducer';
4
4
  import { extractInvoicingSubscriptionAddresses } from '../../../entity/invoicing/invoicingSubscription/invoicingSubscriptionPayload';
5
5
  import { updateInvoicingSubscriptions } from '../../../entity/invoicing/invoicingSubscription/invoicingSubscriptionReducer';
6
+ import { apiErrorSnackbarVariables } from '../../../entity/snackbar/snackbarApiError';
6
7
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
7
8
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
8
9
  import { resetNewAddressDataInLocalStore } from '../../addressView/addressViewReducer';
@@ -45,6 +46,7 @@ export const saveInvoicingSubscriptionEpic = (actions$, state$, zeniAPI) => acti
45
46
  messageSection: 'invoicing_subscription_saved',
46
47
  messageText: 'failed',
47
48
  type: 'error',
49
+ variables: apiErrorSnackbarVariables(response.status),
48
50
  }),
49
51
  ];
50
52
  }
@@ -3,8 +3,9 @@ import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateAddresses } from '../../../entity/address/addressReducer';
4
4
  import { extractInvoicingInvoiceAddresses } from '../../../entity/invoicing/invoice/invoicePayload';
5
5
  import { updateInvoices } from '../../../entity/invoicing/invoice/invoiceReducer';
6
+ import { apiErrorSnackbarVariables } from '../../../entity/snackbar/snackbarApiError';
6
7
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
7
- import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
8
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../responsePayload';
8
9
  import { runInvoicingInvoiceAction, runInvoicingInvoiceActionFailure, runInvoicingInvoiceActionSuccess, updateInvoicingInvoice, } from './invoiceActionReducer';
9
10
  /** Snackbar copy per action kind; unlisted kinds run without one. */
10
11
  const SNACKBAR_SECTION_BY_KIND = {
@@ -16,13 +17,15 @@ export const runInvoicingInvoiceActionEpic = (actions$, _state$, zeniAPI) => act
16
17
  action.payload.action !== 'issue-credit-note'), mergeMap((action) => {
17
18
  const { action: kind, body, invoiceId } = action.payload;
18
19
  const snackbarSection = SNACKBAR_SECTION_BY_KIND[kind];
19
- const snackbar = (messageText) => snackbarSection == null
20
+ /** `status` surfaces the platform's error text; omit it to keep the generic copy. */
21
+ const snackbar = (messageText, status) => snackbarSection == null
20
22
  ? []
21
23
  : [
22
24
  openSnackbar({
23
25
  messageSection: snackbarSection,
24
26
  messageText,
25
27
  type: messageText === 'success' ? 'success' : 'error',
28
+ variables: apiErrorSnackbarVariables(status),
26
29
  }),
27
30
  ];
28
31
  const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/invoices/${encodeURIComponent(invoiceId)}/${kind}`;
@@ -42,7 +45,7 @@ export const runInvoicingInvoiceActionEpic = (actions$, _state$, zeniAPI) => act
42
45
  error: response.status,
43
46
  invoiceId,
44
47
  }),
45
- ...snackbar('failed'),
48
+ ...snackbar('failed', response.status),
46
49
  ]);
47
50
  }), catchError((error) => from([
48
51
  runInvoicingInvoiceActionFailure({
@@ -80,6 +83,7 @@ export const updateInvoicingInvoiceEpic = (actions$, _state$, zeniAPI) => action
80
83
  messageSection: 'invoicing_invoice_saved',
81
84
  messageText: 'failed',
82
85
  type: 'error',
86
+ variables: apiErrorSnackbarVariables(response.status),
83
87
  }),
84
88
  ]);
85
89
  }), catchError((error) => from([
@@ -1,5 +1,6 @@
1
1
  import { from, of } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
+ import { apiErrorSnackbarVariables } from '../../../../entity/snackbar/snackbarApiError';
3
4
  import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
4
5
  import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
5
6
  import { fetchInvoicingQboSyncHealth, retryInvoicingQboSyncTask, retryInvoicingQboSyncTaskFailure, retryInvoicingQboSyncTaskSuccess, } from '../invoicingQboViewReducer';
@@ -28,6 +29,7 @@ export const retryInvoicingQboSyncTaskEpic = (actions$, _state$, zeniAPI) => act
28
29
  messageSection: 'invoicing_qbo_sync_health_retry',
29
30
  messageText: 'failed',
30
31
  type: 'error',
32
+ variables: apiErrorSnackbarVariables(response.status),
31
33
  }),
32
34
  ]);
33
35
  }), catchError((error) => from([
@@ -1,5 +1,6 @@
1
1
  import { from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { apiErrorSnackbarVariables } from '../../../../entity/snackbar/snackbarApiError';
3
4
  import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
4
5
  import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
5
6
  import { fetchInvoicingQboSyncHealth, startInvoicingQboBackfill, startInvoicingQboBackfillFailure, startInvoicingQboBackfillSuccess, } from '../invoicingQboViewReducer';
@@ -27,6 +28,7 @@ export const startInvoicingQboBackfillEpic = (actions$, _state$, zeniAPI) => act
27
28
  messageSection: 'invoicing_qbo_backfill',
28
29
  messageText: 'failed',
29
30
  type: 'error',
31
+ variables: apiErrorSnackbarVariables(response.status),
30
32
  }),
31
33
  ]);
32
34
  }), catchError((error) => from([
@@ -1,6 +1,7 @@
1
1
  import { from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateInvoicingCreditNotes } from '../../../entity/invoicing/creditNote/creditNoteReducer';
4
+ import { apiErrorSnackbarVariables } from '../../../entity/snackbar/snackbarApiError';
4
5
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
5
6
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
6
7
  import { fetchInvoiceDetail } from '../invoiceDetail/invoiceDetailReducer';
@@ -32,6 +33,7 @@ export const issueCreditNoteEpic = (actions$, state$, zeniAPI) => actions$.pipe(
32
33
  messageSection: 'invoicing_credit_note_saved',
33
34
  messageText: 'failed',
34
35
  type: 'error',
36
+ variables: apiErrorSnackbarVariables(response.status),
35
37
  }),
36
38
  ]);
37
39
  }), catchError((error) => from([
@@ -2,6 +2,7 @@ import { from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateInvoices } from '../../../entity/invoicing/invoice/invoiceReducer';
4
4
  import { updateInvoicingTransactions } from '../../../entity/invoicing/invoicingTransaction/invoicingTransactionReducer';
5
+ import { apiErrorSnackbarVariables } from '../../../entity/snackbar/snackbarApiError';
5
6
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
6
7
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
7
8
  import { fetchInvoiceDetail } from '../invoiceDetail/invoiceDetailReducer';
@@ -40,6 +41,7 @@ export const recordPaymentEpic = (actions$, state$, zeniAPI) => actions$.pipe(fi
40
41
  messageSection: 'invoicing_payment_recorded',
41
42
  messageText: 'failed',
42
43
  type: 'error',
44
+ variables: apiErrorSnackbarVariables(response.status),
43
45
  }),
44
46
  ]);
45
47
  }), catchError((error) => from([
@@ -82,6 +84,7 @@ export const recordPaymentEpic = (actions$, state$, zeniAPI) => actions$.pipe(fi
82
84
  messageSection: 'invoicing_payment_recorded',
83
85
  messageText: 'failed',
84
86
  type: 'error',
87
+ variables: apiErrorSnackbarVariables(response.status),
85
88
  }),
86
89
  ]);
87
90
  }), catchError((error) => from([
@@ -1,8 +1,9 @@
1
1
  import { from, of } from 'rxjs';
2
2
  import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { updateAddresses } from '../../../entity/address/addressReducer';
4
+ import { apiErrorSnackbarVariables } from '../../../entity/snackbar/snackbarApiError';
4
5
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
5
- import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
6
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../responsePayload';
6
7
  import { connectInvoicingStripe, disconnectInvoicingStripe, fetchInvoicingSettings, invoicingStripeConnectionFailure, saveInvoicingSettings, saveInvoicingSettingsFailure, saveInvoicingSettingsSuccess, updateInvoicingSettings, updateInvoicingSettingsFailure, updateInvoicingStripeConnection, } from './settingsViewReducer';
7
8
  export const fetchInvoicingSettingsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchInvoicingSettings.match), switchMap(() => {
8
9
  const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/settings`;
@@ -59,13 +60,15 @@ export const disconnectInvoicingStripeEpic = (actions$, _state$, zeniAPI) => act
59
60
  export const saveInvoicingSettingsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(saveInvoicingSettings.match), mergeMap((action) => {
60
61
  const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/settings`;
61
62
  const { settings, snackbarSection } = action.payload;
62
- const snackbar = (messageText) => snackbarSection == null
63
+ /** `status` surfaces the platform's error text; omit it to keep the generic copy. */
64
+ const snackbar = (messageText, status) => snackbarSection == null
63
65
  ? []
64
66
  : [
65
67
  openSnackbar({
66
68
  messageSection: snackbarSection,
67
69
  messageText,
68
70
  type: messageText === 'success' ? 'success' : 'error',
71
+ variables: apiErrorSnackbarVariables(status),
69
72
  }),
70
73
  ];
71
74
  return zeniAPI
@@ -79,7 +82,7 @@ export const saveInvoicingSettingsEpic = (actions$, _state$, zeniAPI) => actions
79
82
  }
80
83
  return from([
81
84
  saveInvoicingSettingsFailure(response.status),
82
- ...snackbar('failed'),
85
+ ...snackbar('failed', response.status),
83
86
  ]);
84
87
  }), catchError((error) => from([
85
88
  saveInvoicingSettingsFailure(createZeniAPIStatus('Unexpected Error', 'Save settings REST API call errored out ' +
@@ -1,3 +1,4 @@
1
+ import { getInvoicingQboAccountMappingId, } from '../../../entity/invoicing/invoicingCommonPayload';
1
2
  import { getFormattedAddress } from '../../addressView/addressViewSelector';
2
3
  import { getInvoicingConfigView } from '../invoicingConfigView/invoicingConfigViewSelector';
3
4
  import { INVOICING_BUSINESS_ADDRESS_TYPE, entityToInvoicingBrandingFormLocalData, } from './invoicingBrandingFormConfig';
@@ -30,6 +31,57 @@ export const getInvoicingSettingsView = (state) => {
30
31
  stripeConnectState: stripeFetchState,
31
32
  };
32
33
  };
34
+ /**
35
+ * Gates the Enable Invoicing action: payments must be able to land (Stripe
36
+ * connected *and* accepting charges) and the QBO accounting step must be
37
+ * finished, so an enabled tenant can always book what it invoices.
38
+ *
39
+ * Deliberately not part of `deriveInvoicingOnboardingStatus`, which routes the
40
+ * module off the config flags alone. Folding these in there would (a) demote a
41
+ * live tenant to the settings tree the moment an integration broke, stranding
42
+ * its existing invoices, and (b) deadlock the route gate, which fetches only the
43
+ * config — never the settings or QBO connection these read.
44
+ */
45
+ export function deriveInvoicingEnablePrerequisites(settings, isQboConnected, qboAccountMappingKindOptions) {
46
+ const isStripeConnected = settings?.stripe_connected === true &&
47
+ settings?.stripe_account?.charges_enabled === true;
48
+ const accountMappings = settings?.qbo?.account_mappings ?? undefined;
49
+ // An empty option list means the config has not landed yet — treating that as
50
+ // configured would let `every` pass vacuously and unlock the button early.
51
+ const isQboConfigured = isQboConnected &&
52
+ qboAccountMappingKindOptions.length > 0 &&
53
+ qboAccountMappingKindOptions.every((option) => getInvoicingQboAccountMappingId(accountMappings, option.code) != null);
54
+ return {
55
+ canEnable: isStripeConnected && isQboConfigured,
56
+ isQboConfigured,
57
+ isStripeConnected,
58
+ };
59
+ }
60
+ /**
61
+ * The settings hub's composite of everything it derives from the store: whether
62
+ * to offer Enable Invoicing at all, the in-flight flag for that action, and the
63
+ * two integration prerequisites kept separate so the hub can name the step still
64
+ * outstanding instead of showing a dead button.
65
+ *
66
+ * `canEnableInvoicing` folds in the onboarding step, so the screen does not need
67
+ * to re-check it: the footer is only offered mid-onboarding and only once both
68
+ * integrations are ready.
69
+ *
70
+ * Deliberately not folded into `getInvoicingConfigView`: that selector gates
71
+ * every invoicing route and stays a pure read of the config slice. The settings
72
+ * and QBO slices read here are only fetched once a settings screen is mounted.
73
+ */
74
+ export const getInvoicingSettingsHubView = (state) => {
75
+ const { enableState, qboAccountMappingKindOptions, status } = getInvoicingConfigView(state);
76
+ const { canEnable, isQboConfigured, isStripeConnected } = deriveInvoicingEnablePrerequisites(state.invoicingSettingsViewState.settings, state.invoicingQboViewState.connection?.connected === true, qboAccountMappingKindOptions);
77
+ return {
78
+ canEnableInvoicing: status === 'settings' && canEnable,
79
+ isEnablingInvoicing: enableState.fetchState === 'In-Progress',
80
+ isQboConfigured,
81
+ isStripeConnected,
82
+ status,
83
+ };
84
+ };
33
85
  /**
34
86
  * Bundled view for the branding settings form: the draft `localData` (seeded
35
87
  * from the existing settings until the user edits), the business-address label
@@ -3,8 +3,9 @@ import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateAddresses } from '../../../entity/address/addressReducer';
4
4
  import { extractInvoicingSubscriptionAddresses } from '../../../entity/invoicing/invoicingSubscription/invoicingSubscriptionPayload';
5
5
  import { updateInvoicingSubscriptions } from '../../../entity/invoicing/invoicingSubscription/invoicingSubscriptionReducer';
6
+ import { apiErrorSnackbarVariables } from '../../../entity/snackbar/snackbarApiError';
6
7
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
7
- import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
8
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../responsePayload';
8
9
  import { runInvoicingSubscriptionAction, runInvoicingSubscriptionActionFailure, runInvoicingSubscriptionActionSuccess, } from './subscriptionActionReducer';
9
10
  /**
10
11
  * Each lifecycle action reports with its own copy. Keyed loosely because the
@@ -24,13 +25,15 @@ export const runInvoicingSubscriptionActionEpic = (actions$, _state$, zeniAPI) =
24
25
  const { action: kind, body, id } = action.payload;
25
26
  const requestBody = { action: kind, ...(body ?? {}) };
26
27
  const snackbarSection = SNACKBAR_SECTION_BY_ACTION[kind];
27
- const snackbar = (messageText) => snackbarSection == null
28
+ /** `status` surfaces the platform's error text; omit it to keep the generic copy. */
29
+ const snackbar = (messageText, status) => snackbarSection == null
28
30
  ? []
29
31
  : [
30
32
  openSnackbar({
31
33
  messageSection: snackbarSection,
32
34
  messageText,
33
35
  type: messageText === 'success' ? 'success' : 'error',
36
+ variables: apiErrorSnackbarVariables(status),
34
37
  }),
35
38
  ];
36
39
  const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/subscriptions/${encodeURIComponent(id)}/actions`;
@@ -50,7 +53,7 @@ export const runInvoicingSubscriptionActionEpic = (actions$, _state$, zeniAPI) =
50
53
  error: response.status,
51
54
  id,
52
55
  }),
53
- ...snackbar('failed'),
56
+ ...snackbar('failed', response.status),
54
57
  ]);
55
58
  }), catchError((error) => from([
56
59
  runInvoicingSubscriptionActionFailure({
@@ -134,6 +134,22 @@ const recommendation = createSlice({
134
134
  clearRecommendation(draft) {
135
135
  Object.assign(draft, initialState);
136
136
  },
137
+ // Fire-and-forget human feedback on a recommendation (accept, ranked
138
+ // override or outside override). Handled by submitRecommendationFeedbackEpic
139
+ // which POSTs to the accounting recommendation-feedback endpoint. No state
140
+ // mutation: the categorization UX must not depend on the feedback round-trip.
141
+ submitRecommendationFeedback: {
142
+ reducer(
143
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
144
+ _draft,
145
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
146
+ _action) {
147
+ // do nothing
148
+ },
149
+ prepare(args) {
150
+ return { payload: args };
151
+ },
152
+ },
137
153
  },
138
154
  extraReducers: (builder) => {
139
155
  builder.addCase(clearAll, (draft) => {
@@ -141,7 +157,7 @@ const recommendation = createSlice({
141
157
  });
142
158
  },
143
159
  });
144
- export const { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, clearRecommendation, } = recommendation.actions;
160
+ export const { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, clearRecommendation, submitRecommendationFeedback, } = recommendation.actions;
145
161
  export default recommendation.reducer;
146
162
  const doUpdateRecommendationForEntity = (draft, entityId, entityType, payload) => {
147
163
  const key = getEntityRecommendationKey(entityType, entityId);
@@ -0,0 +1,52 @@
1
+ import { EMPTY } from 'rxjs';
2
+ import { catchError, filter, mergeMap } from 'rxjs/operators';
3
+ import { isSuccessStatus } from '../../responsePayload';
4
+ import { submitRecommendationFeedback } from './recommendationReducer';
5
+ const toRequestBody = (payload) => ({
6
+ line_id: payload.lineId ?? null,
7
+ dimension: payload.dimension,
8
+ recommendation_set_id: payload.recommendationSetId ?? null,
9
+ top_pick_source: payload.topPickSource ?? null,
10
+ shown_recommendations: (payload.shownRecommendations ?? []).map((recommendation) => ({
11
+ source: recommendation.source,
12
+ rank: recommendation.rank,
13
+ value: {
14
+ name: recommendation.value.name,
15
+ id: recommendation.value.id ?? null,
16
+ integration_id: recommendation.value.integrationId ?? null,
17
+ },
18
+ confidence: recommendation.confidence ?? null,
19
+ reasoning: recommendation.reasoning ?? null,
20
+ cot: recommendation.cot ?? [],
21
+ })),
22
+ selected: {
23
+ name: payload.selected.name,
24
+ id: payload.selected.id ?? null,
25
+ integration_id: payload.selected.integrationId ?? null,
26
+ is_new_entity: payload.selected.isNewEntity ?? false,
27
+ },
28
+ matched_source: payload.matchedSource ?? null,
29
+ matched_rank: payload.matchedRank ?? null,
30
+ outcome: payload.outcome,
31
+ reason_text: payload.reasonText ?? null,
32
+ skipped: payload.skipped ?? false,
33
+ retracts_feedback_id: payload.retractsFeedbackId ?? null,
34
+ features: payload.features ?? null,
35
+ });
36
+ // Fire-and-forget: the epic never re-emits an action. Feedback delivery is a
37
+ // side-effect that must not disrupt the categorization UX, so both the
38
+ // success and error paths complete silently (errors are logged for debugging).
39
+ export const submitRecommendationFeedbackEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(submitRecommendationFeedback.match), mergeMap((action) => {
40
+ const { transactionId } = action.payload;
41
+ return zeniAPI
42
+ .postAndGetJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/transactions/${transactionId}/recommendation-feedback`, toRequestBody(action.payload))
43
+ .pipe(mergeMap((response) => {
44
+ if (!isSuccessStatus(response)) {
45
+ console.error('submitRecommendationFeedbackEpic: recommendation feedback was not recorded', response.status);
46
+ }
47
+ return EMPTY;
48
+ }), catchError((error) => {
49
+ console.error('submitRecommendationFeedbackEpic: failed to submit recommendation feedback', error);
50
+ return EMPTY;
51
+ }));
52
+ }));
@@ -53,5 +53,5 @@ const vendorTabView = createSlice({
53
53
  });
54
54
  },
55
55
  });
56
- export const { clearVendorTabView, fetchVendorTabView, updateVendorTabViewTab, } = vendorTabView.actions;
56
+ export const { clearVendorTabView, fetchVendorTabView, updateVendorTabViewTab } = vendorTabView.actions;
57
57
  export default vendorTabView.reducer;
package/lib/index.d.ts CHANGED
@@ -137,7 +137,7 @@ import { getTaskGroupById } from './entity/taskGroup/taskGroupSelector';
137
137
  import { TaskGroupState } from './entity/taskGroup/taskGroupState';
138
138
  import { TaskGroupTemplate } from './entity/taskGroupTemplate/taskGroupTemplateState';
139
139
  import { DoSignInPayload, deleteConnection, doMagicLinkSignIn, doSignIn, doSignOut, fetchActiveTenant, fetchAllTenants, fetchExcludedResources as fetchExcludedResourcesForTenant, fetchExternalConnections as fetchExternalConnectionsForTenant, fetchSubscriptionSummaryForTenant, initEmailConnectOAuth, resendVerifyDeviceOTP, resetSignInState, saveAPIKeyConnection, saveConnectorCredentials, saveExternalConnection as saveExternalConnectionForTenant, saveOAuthConnection, sendEmailMagicLinkToUser, sendSessionHeartbeat, toExternalIntegrationType, toExternalSupportedTool, updateCurrentTenant, updateSignInState, verifyDeviceWithTwoFA } from './entity/tenant/tenantReducer';
140
- import { CurrentTenant, TenantBaseView, TenantCoreDetailsView, TenantView, TenantsBaseOrdered, TenantsOrdered, DEFAULT_RECEIPTS_EMAIL, getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getReceiptsEmail, getTenantBaseById, getTenantBaseViewForTenantId, getTenantBaseViewForTenantView, getTenantsBaseByCheckInDateSelector, getTenantsByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, isOtherProductsEnabledForTenant, isTenantBankingOnly, isTenantBookkeepingEnabled, isTenantCardsOnly, isTenantUsingZeniCOA, isZeniDomainTenant, toTenantCoreDetailsView } from './entity/tenant/tenantSelector';
140
+ import { CurrentTenant, DEFAULT_RECEIPTS_EMAIL, TenantBaseView, TenantCoreDetailsView, TenantView, TenantsBaseOrdered, TenantsOrdered, getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getReceiptsEmail, getTenantBaseById, getTenantBaseViewForTenantId, getTenantBaseViewForTenantView, getTenantsBaseByCheckInDateSelector, getTenantsByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, isOtherProductsEnabledForTenant, isTenantBankingOnly, isTenantBookkeepingEnabled, isTenantCardsOnly, isTenantUsingZeniCOA, isZeniDomainTenant, toTenantCoreDetailsView } from './entity/tenant/tenantSelector';
141
141
  import { CapitalizableAccount, CapitalizableAccounts, CapitalizableClassificationType, CapitalizableMatchPattern, Connection, LoggedInUser, RoleResource, Tenant, TenantProductSettings } from './entity/tenant/tenantState';
142
142
  import { ToastNotificationPayload } from './entity/toastNotification/toastNotificationPayload';
143
143
  import { pushToastNotification } from './entity/toastNotification/toastNotificationReducer';
@@ -393,7 +393,7 @@ import { ProfitAndLossProjectViewReport } from './view/profitAndLossProjectView/
393
393
  import { ProfitAndLossProjectViewUIState } from './view/profitAndLossProjectView/profitAndLossProjectViewState';
394
394
  import { fetchProjectList } from './view/projectList/projectListReducer';
395
395
  import { ProjectListSelectorView, getProjectList } from './view/projectList/projectListSelector';
396
- import { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityId, fetchRecommendationByEntityName } from './view/recommendation/recommendationReducer';
396
+ import { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityId, fetchRecommendationByEntityName, submitRecommendationFeedback } from './view/recommendation/recommendationReducer';
397
397
  import { DEFAULT_REFERRER_AMOUNT, clearReferrals, fetchReferrals, fetchRewardsPlan, resendReferralInvite, saveReferralFormDataInLocalStore, sendReferralInvite, updateReferViewed, updateReferralListSortUiState } from './view/referralView/referralReducer';
398
398
  import { ReferralListSelectorView, RewardsPlanCardReport, getInviteFormView, getReferralListView, getRewardsPlanCard } from './view/referralView/referralSelector';
399
399
  import { AmountStatusTypes, InviteCompanyLocalData, ReferralAmountStatus, ReferralInvitation, ReferralListViewSortKey, ReferralStatus, ReferralViewState, ReferralViewUIState, RewardsPlanData, StatusTypes, toReferralListViewSortKeyType } from './view/referralView/referralState';
@@ -878,7 +878,7 @@ export { Filing1099TinType };
878
878
  export { BillPayCardReport };
879
879
  export { ReimbursementCardReport };
880
880
  export { VendorListViewSortKey, VendorViewUIState, VendorListSelectorView, VendorListQuery, getVendorList, VendorListView, updateSortUiState, updatePageToken, updateYTDSelectionUIState, VendorListViewYTDSpendSortKey, isColumnYTDSpend, isVendorsTabVisible, VendorViewType, VendorReportIDType, toVendorReportIDType, VendorFiling1099ListSelectorView, getVendorFiling1099List, VendorFiling1099ListViewSortKey, VendorFiling1099ViewUIState, VendorFiling1099DownloadData, fetchVendorsFiling1099Download, Type1099Download, VendorFiling1099DownloadDataForType, };
881
- export { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityId, fetchRecommendationByEntityName, };
881
+ export { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityId, fetchRecommendationByEntityName, submitRecommendationFeedback, };
882
882
  export type { FetchEntityRecommendationsForLineUpdatePayload, RecommendationLineUpdateFlowType, } from './view/recommendation/recommendationReducer';
883
883
  export { fetchOwnerList };
884
884
  export { ZeniAccountsConfigSelectorView, getZeniAccountsConfigDetail, ZeniAccountListSelectorView, getZeniAccountList, fetchZeniAccountList, fetchAccountList, DepositAccount, createCheckingAccount, fetchPaymentAccountList, unlinkPaymentAccount, getDepositAccountDetail, DepositAccountDetailSelectorView, DepositAccountListSelectorView, getAllDepositAccounts, getDepositAccountDetailForPDF, DepositAccountDetailForPDF, fetchDepositAccount, fetchZeniAccountsConfig, AccountHistoryWithBalances, TransferDetailLocalData, ReviewTransferDetail, updateTransferMoneyLocalData, updateTransferToLocalData, updateDepositToLocalData, transferMoney, getTransferDetail, TransferDetailSelectorView, clearTransferDetail, clearReviewTransferDetail, fetchReviewTransferDetail, fetchDepositAccountDetail, DepositAccountTransaction, DepositAccountWithAutoTransferRules, DepositTransactionStatusCode, DepositTransactionStatus, CheckDepositLocalData, depositCheck, updateCheckDepositLocalData, clearCheckDeposit, CheckDepositSelectorView, getCheckDepositDetail, updateDepositAccount, fetchDepositAccountHistorySuccess, fetchDepositAccountHistoryFailure, DepositAccountUpdateType, ZeniAccountSummaryWithBalance, fetchZeniAccStatementPage, getZeniAccStatements, ZeniAccStatementsSelectorView, ZeniAccountPaymentMethod, };
@@ -1211,8 +1211,8 @@ export { getInvoicingPlanById } from './entity/invoicing/plan/planSelector';
1211
1211
  export { getInvoicingProductById } from './entity/invoicing/product/productSelector';
1212
1212
  export type { InvoicingCatalogItemDetailView } from './view/invoicing/invoicingCatalogItemDetailView/invoicingCatalogItemDetailViewSelector';
1213
1213
  export { clearInvoicingSettings, connectInvoicingStripe, disconnectInvoicingStripe, fetchInvoicingSettings, resetInvoicingBrandingFormDraft, resetSaveInvoicingSettings, saveInvoicingSettings, submitInvoicingBrandingForm, updateInvoicingBrandingFormDraft, updateInvoicingSettings, } from './view/invoicing/settingsView/settingsViewReducer';
1214
- export { getInvoicingBrandingFormView, getInvoicingSettings, getInvoicingSettingsFetchState, getInvoicingSettingsView, } from './view/invoicing/settingsView/settingsViewSelector';
1215
- export type { InvoicingBrandingFormView, InvoicingSettingsView, } from './view/invoicing/settingsView/settingsViewSelector';
1214
+ export { getInvoicingBrandingFormView, getInvoicingSettings, getInvoicingSettingsFetchState, getInvoicingSettingsHubView, getInvoicingSettingsView, } from './view/invoicing/settingsView/settingsViewSelector';
1215
+ export type { InvoicingBrandingFormView, InvoicingSettingsHubView, InvoicingSettingsView, } from './view/invoicing/settingsView/settingsViewSelector';
1216
1216
  export type { InvoicingBrandingFormLocalData } from './view/invoicing/settingsView/invoicingBrandingFormConfig';
1217
1217
  export { INVOICING_BUSINESS_ADDRESS_TYPE, entityToInvoicingBrandingFormLocalData, initialInvoicingBrandingFormLocalData, } from './view/invoicing/settingsView/invoicingBrandingFormConfig';
1218
1218
  export { isStripeConnectedFromSettings } from './view/invoicing/settingsView/invoicingSettingsHelpers';