@zeniai/client-epic-state 5.2.5 → 5.2.7-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 (40) hide show
  1. package/lib/epic.d.ts +2 -1
  2. package/lib/epic.js +4 -4
  3. package/lib/esm/epic.js +4 -4
  4. package/lib/esm/index.js +4 -3
  5. package/lib/esm/view/accountList/accountListReducer.js +1 -0
  6. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +8 -0
  7. package/lib/esm/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.js +29 -0
  8. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewPayload.js +0 -23
  9. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewReducer.js +3 -40
  10. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +124 -6
  11. package/lib/esm/view/recommendation/recommendationReducer.js +16 -1
  12. package/lib/esm/view/recommendation/submitRecommendationFeedbackEpic.js +52 -0
  13. package/lib/index.d.ts +7 -4
  14. package/lib/index.js +29 -26
  15. package/lib/view/accountList/accountListReducer.js +1 -0
  16. package/lib/view/accountList/accountListState.d.ts +1 -1
  17. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +9 -0
  18. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +8 -0
  19. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.d.ts +5 -0
  20. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.js +33 -0
  21. package/lib/view/invoicing/invoicingQboView/epics/invoicingQboViewActionType.d.ts +4 -2
  22. package/lib/view/invoicing/invoicingQboView/invoicingQboViewPayload.d.ts +0 -26
  23. package/lib/view/invoicing/invoicingQboView/invoicingQboViewPayload.js +0 -24
  24. package/lib/view/invoicing/invoicingQboView/invoicingQboViewReducer.d.ts +2 -10
  25. package/lib/view/invoicing/invoicingQboView/invoicingQboViewReducer.js +4 -41
  26. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +43 -6
  27. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +128 -7
  28. package/lib/view/invoicing/invoicingQboView/invoicingQboViewState.d.ts +1 -5
  29. package/lib/view/recommendation/recommendationPayload.d.ts +34 -0
  30. package/lib/view/recommendation/recommendationReducer.d.ts +2 -1
  31. package/lib/view/recommendation/recommendationReducer.js +17 -2
  32. package/lib/view/recommendation/submitRecommendationFeedbackEpic.d.ts +7 -0
  33. package/lib/view/recommendation/submitRecommendationFeedbackEpic.js +56 -0
  34. package/package.json +1 -1
  35. package/lib/esm/view/invoicing/invoicingQboView/epics/fetchInvoicingQboAccountsEpic.js +0 -25
  36. package/lib/esm/view/invoicing/invoicingQboView/epics/fetchInvoicingQboClassesEpic.js +0 -15
  37. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingQboAccountsEpic.d.ts +0 -6
  38. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingQboAccountsEpic.js +0 -29
  39. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingQboClassesEpic.d.ts +0 -6
  40. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingQboClassesEpic.js +0 -19
@@ -1,15 +1,11 @@
1
1
  "use strict";
2
2
  var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.startInvoicingQboBackfillFailure = exports.startInvoicingQboBackfillSuccess = exports.startInvoicingQboBackfill = exports.retryInvoicingQboSyncTaskFailure = exports.retryInvoicingQboSyncTaskSuccess = exports.retryInvoicingQboSyncTask = exports.updateInvoicingQboSyncHealthFailure = exports.updateInvoicingQboSyncHealth = exports.fetchInvoicingQboSyncHealth = exports.updateInvoicingQboClassesFailure = exports.updateInvoicingQboClasses = exports.fetchInvoicingQboClasses = exports.updateInvoicingQboAccountsFailure = exports.updateInvoicingQboAccounts = exports.fetchInvoicingQboAccounts = exports.updateInvoicingQboConnectionFailure = exports.updateInvoicingQboConnection = exports.fetchInvoicingQboConnection = exports.initialState = void 0;
4
+ exports.startInvoicingQboBackfillFailure = exports.startInvoicingQboBackfillSuccess = exports.startInvoicingQboBackfill = exports.retryInvoicingQboSyncTaskFailure = exports.retryInvoicingQboSyncTaskSuccess = exports.retryInvoicingQboSyncTask = exports.updateInvoicingQboSyncHealthFailure = exports.updateInvoicingQboSyncHealth = exports.fetchInvoicingQboSyncHealth = exports.fetchInvoicingAccountingPickers = exports.updateInvoicingQboConnectionFailure = exports.updateInvoicingQboConnection = exports.fetchInvoicingQboConnection = exports.initialState = void 0;
5
5
  const toolkit_1 = require("@reduxjs/toolkit");
6
6
  const rootActions_1 = require("../../../rootActions");
7
7
  exports.initialState = {
8
- accountsByAccountTypes: {},
9
- accountsFetchStateByAccountTypes: {},
10
8
  backfillFetchState: { fetchState: 'Not-Started', error: undefined },
11
- classes: [],
12
- classesFetchState: { fetchState: 'Not-Started', error: undefined },
13
9
  connection: undefined,
14
10
  connectionFetchState: { fetchState: 'Not-Started', error: undefined },
15
11
  retryFetchStateByTaskId: {},
@@ -33,41 +29,8 @@ const invoicingQboView = (0, toolkit_1.createSlice)({
33
29
  updateInvoicingQboConnectionFailure(draft, action) {
34
30
  draft.connectionFetchState = { fetchState: 'Error', error: action.payload };
35
31
  },
36
- fetchInvoicingQboAccounts: {
37
- reducer(draft, action) {
38
- draft.accountsFetchStateByAccountTypes[action.payload.accountTypes] = {
39
- fetchState: 'In-Progress',
40
- error: undefined,
41
- };
42
- },
43
- prepare(accountTypes) {
44
- return { payload: { accountTypes } };
45
- },
46
- },
47
- updateInvoicingQboAccounts(draft, action) {
48
- draft.accountsByAccountTypes[action.payload.accountTypes] =
49
- action.payload.accounts;
50
- draft.accountsFetchStateByAccountTypes[action.payload.accountTypes] = {
51
- fetchState: 'Completed',
52
- error: undefined,
53
- };
54
- },
55
- updateInvoicingQboAccountsFailure(draft, action) {
56
- draft.accountsFetchStateByAccountTypes[action.payload.accountTypes] = {
57
- fetchState: 'Error',
58
- error: action.payload.status,
59
- };
60
- },
61
- fetchInvoicingQboClasses(draft) {
62
- draft.classesFetchState = { fetchState: 'In-Progress', error: undefined };
63
- },
64
- updateInvoicingQboClasses(draft, action) {
65
- draft.classes = action.payload;
66
- draft.classesFetchState = { fetchState: 'Completed', error: undefined };
67
- },
68
- updateInvoicingQboClassesFailure(draft, action) {
69
- draft.classesFetchState = { fetchState: 'Error', error: action.payload };
70
- },
32
+ // No state of its own: the epic decides which shared lists to load.
33
+ fetchInvoicingAccountingPickers() { },
71
34
  fetchInvoicingQboSyncHealth(draft) {
72
35
  draft.syncHealthFetchState = {
73
36
  fetchState: 'In-Progress',
@@ -131,5 +94,5 @@ const invoicingQboView = (0, toolkit_1.createSlice)({
131
94
  });
132
95
  },
133
96
  });
134
- _a = invoicingQboView.actions, exports.fetchInvoicingQboConnection = _a.fetchInvoicingQboConnection, exports.updateInvoicingQboConnection = _a.updateInvoicingQboConnection, exports.updateInvoicingQboConnectionFailure = _a.updateInvoicingQboConnectionFailure, exports.fetchInvoicingQboAccounts = _a.fetchInvoicingQboAccounts, exports.updateInvoicingQboAccounts = _a.updateInvoicingQboAccounts, exports.updateInvoicingQboAccountsFailure = _a.updateInvoicingQboAccountsFailure, exports.fetchInvoicingQboClasses = _a.fetchInvoicingQboClasses, exports.updateInvoicingQboClasses = _a.updateInvoicingQboClasses, exports.updateInvoicingQboClassesFailure = _a.updateInvoicingQboClassesFailure, exports.fetchInvoicingQboSyncHealth = _a.fetchInvoicingQboSyncHealth, exports.updateInvoicingQboSyncHealth = _a.updateInvoicingQboSyncHealth, exports.updateInvoicingQboSyncHealthFailure = _a.updateInvoicingQboSyncHealthFailure, exports.retryInvoicingQboSyncTask = _a.retryInvoicingQboSyncTask, exports.retryInvoicingQboSyncTaskSuccess = _a.retryInvoicingQboSyncTaskSuccess, exports.retryInvoicingQboSyncTaskFailure = _a.retryInvoicingQboSyncTaskFailure, exports.startInvoicingQboBackfill = _a.startInvoicingQboBackfill, exports.startInvoicingQboBackfillSuccess = _a.startInvoicingQboBackfillSuccess, exports.startInvoicingQboBackfillFailure = _a.startInvoicingQboBackfillFailure;
97
+ _a = invoicingQboView.actions, exports.fetchInvoicingQboConnection = _a.fetchInvoicingQboConnection, exports.updateInvoicingQboConnection = _a.updateInvoicingQboConnection, exports.updateInvoicingQboConnectionFailure = _a.updateInvoicingQboConnectionFailure, exports.fetchInvoicingAccountingPickers = _a.fetchInvoicingAccountingPickers, exports.fetchInvoicingQboSyncHealth = _a.fetchInvoicingQboSyncHealth, exports.updateInvoicingQboSyncHealth = _a.updateInvoicingQboSyncHealth, exports.updateInvoicingQboSyncHealthFailure = _a.updateInvoicingQboSyncHealthFailure, exports.retryInvoicingQboSyncTask = _a.retryInvoicingQboSyncTask, exports.retryInvoicingQboSyncTaskSuccess = _a.retryInvoicingQboSyncTaskSuccess, exports.retryInvoicingQboSyncTaskFailure = _a.retryInvoicingQboSyncTaskFailure, exports.startInvoicingQboBackfill = _a.startInvoicingQboBackfill, exports.startInvoicingQboBackfillSuccess = _a.startInvoicingQboBackfillSuccess, exports.startInvoicingQboBackfillFailure = _a.startInvoicingQboBackfillFailure;
135
98
  exports.default = invoicingQboView.reducer;
@@ -1,21 +1,58 @@
1
+ import { NestedAccountHierarchyForReport } from '../../../commonStateTypes/accountView/nestedAccountID';
2
+ import { NestedClassHierarchyForReport } from '../../../commonStateTypes/classesView/nestedClassID';
1
3
  import { FetchStateAndError } from '../../../commonStateTypes/common';
4
+ import { AccountType } from '../../../entity/account/accountState';
2
5
  import { RootState } from '../../../rootStateTypes';
3
- import { InvoicingQboAccountOption, InvoicingQboConnection, InvoicingQboSyncHealth } from './invoicingQboViewPayload';
6
+ import { InvoicingQboConnection, InvoicingQboSyncHealth } from './invoicingQboViewPayload';
7
+ export declare const INVOICING_ACCOUNT_LIST_KEY: "invoicingAccountList";
8
+ /** Whether QBO is connected, without building the option lists to find out. */
9
+ export declare const isInvoicingQboConnected: (state: RootState) => boolean;
10
+ /**
11
+ * Account types that can back invoicing revenue. Named here so screens ask for
12
+ * "income accounts" rather than repeating the accounting service's vocabulary.
13
+ */
14
+ export declare const INVOICING_INCOME_ACCOUNT_TYPES: readonly AccountType[];
15
+ /** An account or class as the invoicing pickers render and submit it. */
16
+ export interface InvoicingAccountingOption {
17
+ id: string;
18
+ name: string;
19
+ accountType?: string;
20
+ }
4
21
  export interface InvoicingQboView {
5
- accountsByAccountTypes: Record<string, InvoicingQboAccountOption[]>;
6
- accountsFetchStateByAccountTypes: Record<string, FetchStateAndError>;
22
+ accounts: InvoicingAccountingOption[];
23
+ accountsFetchState: FetchStateAndError;
7
24
  backfillState: FetchStateAndError;
8
- classes: InvoicingQboAccountOption[];
25
+ classes: InvoicingAccountingOption[];
9
26
  classesFetchState: FetchStateAndError;
10
27
  connectionFetchState: FetchStateAndError;
28
+ /**
29
+ * The account and class trees the grouped pickers render. The flat option
30
+ * lists above stay for callers that only need id and name.
31
+ */
32
+ nestedAccountHierarchy: NestedAccountHierarchyForReport[];
33
+ nestedClassHierarchy: NestedClassHierarchyForReport[];
11
34
  retryFetchStateByTaskId: Record<string, FetchStateAndError>;
12
35
  syncHealthFetchState: FetchStateAndError;
13
36
  connection?: InvoicingQboConnection;
14
37
  syncHealth?: InvoicingQboSyncHealth;
15
38
  }
39
+ /**
40
+ * The accounts valid for one QBO mapping kind, whose `accountTypes` is a
41
+ * comma-separated list of QuickBooks type names. An unrecognised list yields no
42
+ * accounts rather than every account: offering a bank account where income is
43
+ * required would post a mapping QuickBooks rejects.
44
+ */
45
+ export declare const invoicingAccountsForQboAccountTypes: (accounts: InvoicingAccountingOption[], accountTypes: string) => InvoicingAccountingOption[];
46
+ /**
47
+ * The account tree narrowed to the given account types. The tree itself carries
48
+ * no account type, so the flat account list supplies that and the matching
49
+ * `qboId`s are kept — which is why callers that only need "income accounts, as a
50
+ * tree" do not have to hold a second flat list to filter with.
51
+ */
52
+ export declare const invoicingAccountHierarchyForTypes: (state: RootState, accountTypes: readonly AccountType[]) => NestedAccountHierarchyForReport[];
16
53
  /**
17
54
  * Single bundled view for the QBO integration settings screen — the
18
- * connection, per-account-types account options, classes, sync health, and the
19
- * fetch/action states for each of them.
55
+ * connection, the account and class options, sync health, and the fetch/action
56
+ * states for each of them.
20
57
  */
21
58
  export declare const getInvoicingQboView: (state: RootState) => InvoicingQboView;
@@ -1,19 +1,140 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getInvoicingQboView = void 0;
3
+ exports.getInvoicingQboView = exports.invoicingAccountHierarchyForTypes = exports.invoicingAccountsForQboAccountTypes = exports.INVOICING_INCOME_ACCOUNT_TYPES = exports.isInvoicingQboConnected = exports.INVOICING_ACCOUNT_LIST_KEY = void 0;
4
+ const accountListSelector_1 = require("../../accountList/accountListSelector");
5
+ const classListSelector_1 = require("../../classList/classListSelector");
6
+ // Its own bucket: invoicing needs the AP/AR exclusions that the vendor and
7
+ // transaction screens sharing `accountList` do not pass.
8
+ exports.INVOICING_ACCOUNT_LIST_KEY = 'invoicingAccountList';
9
+ /** Whether QBO is connected, without building the option lists to find out. */
10
+ const isInvoicingQboConnected = (state) => state.invoicingQboViewState.connection?.connected === true;
11
+ exports.isInvoicingQboConnected = isInvoicingQboConnected;
12
+ /**
13
+ * Account types that can back invoicing revenue. Named here so screens ask for
14
+ * "income accounts" rather than repeating the accounting service's vocabulary.
15
+ */
16
+ exports.INVOICING_INCOME_ACCOUNT_TYPES = [
17
+ 'income',
18
+ 'other_income',
19
+ ];
20
+ /**
21
+ * Accounts and classes as the invoicing pickers need them. Both come from the
22
+ * shared accounting-service lists, so whatever another screen already loaded
23
+ * is reused. `qboId` is the submitted value because that is what the invoicing
24
+ * API stores; an account without one cannot be posted, so it is dropped rather
25
+ * than offered.
26
+ */
27
+ const invoicingAccountingOptions = (state, accountTypes) => {
28
+ const { accounts } = (0, accountListSelector_1.getAccountList)(state.accountState, state.accountListState, exports.INVOICING_ACCOUNT_LIST_KEY);
29
+ return accounts.flatMap((account) => {
30
+ if (account.qboId == null || account.isDeleted === true) {
31
+ return [];
32
+ }
33
+ if (accountTypes != null &&
34
+ (account.accountType == null ||
35
+ !accountTypes.includes(account.accountType))) {
36
+ return [];
37
+ }
38
+ return [
39
+ {
40
+ id: String(account.qboId),
41
+ name: account.accountName,
42
+ accountType: account.accountType,
43
+ },
44
+ ];
45
+ });
46
+ };
47
+ /**
48
+ * Normalises an account-type name for comparison. The config's mapping kinds
49
+ * name QuickBooks types the way QuickBooks displays them ("Other Current
50
+ * Asset", "Expense"), while the accounting service uses snake_case and
51
+ * sometimes pluralises ("other_current_assets", "expenses"). Comparing the
52
+ * normalised, de-pluralised forms keeps a picker from silently emptying because
53
+ * of a trailing "s".
54
+ */
55
+ const normalizeAccountTypeName = (value) => {
56
+ const normalized = value
57
+ .trim()
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9]+/g, '_');
60
+ // Singularise so "Other Current Liability" meets `other_current_liabilities`
61
+ // and "Expense" meets `expenses`. `-ies` needs its own case: stripping the
62
+ // trailing "s" alone would leave "liabilitie", matching nothing.
63
+ return normalized.replace(/ies$/, 'y').replace(/([^s])s$/, '$1');
64
+ };
65
+ /**
66
+ * The accounts valid for one QBO mapping kind, whose `accountTypes` is a
67
+ * comma-separated list of QuickBooks type names. An unrecognised list yields no
68
+ * accounts rather than every account: offering a bank account where income is
69
+ * required would post a mapping QuickBooks rejects.
70
+ */
71
+ const invoicingAccountsForQboAccountTypes = (accounts, accountTypes) => {
72
+ const wanted = new Set(accountTypes
73
+ .split(',')
74
+ .map(normalizeAccountTypeName)
75
+ .filter((accountType) => accountType !== ''));
76
+ if (wanted.size === 0) {
77
+ return accounts;
78
+ }
79
+ return accounts.filter((account) => account.accountType != null &&
80
+ wanted.has(normalizeAccountTypeName(account.accountType)));
81
+ };
82
+ exports.invoicingAccountsForQboAccountTypes = invoicingAccountsForQboAccountTypes;
83
+ /** Classes as the invoicing pickers need them, from the shared class list. */
84
+ const invoicingAccountingClassOptions = (state) => {
85
+ const { classes } = (0, classListSelector_1.getClassList)(state.classState, state.classListState);
86
+ return classes.flatMap((accountingClass) => accountingClass.qboId == null || accountingClass.isDeleted === true
87
+ ? []
88
+ : [
89
+ {
90
+ id: String(accountingClass.qboId),
91
+ name: accountingClass.className,
92
+ },
93
+ ]);
94
+ };
95
+ /**
96
+ * The account tree narrowed to the given account types. The tree itself carries
97
+ * no account type, so the flat account list supplies that and the matching
98
+ * `qboId`s are kept — which is why callers that only need "income accounts, as a
99
+ * tree" do not have to hold a second flat list to filter with.
100
+ */
101
+ const invoicingAccountHierarchyForTypes = (state, accountTypes) => {
102
+ const allowedQboIds = new Set(invoicingAccountingOptions(state, accountTypes).map((account) => account.id));
103
+ const keepMatching = (nodes) => nodes.flatMap((node) => {
104
+ const children = keepMatching(node.children);
105
+ const isMatch = node.qboId != null && allowedQboIds.has(String(node.qboId));
106
+ // A parent that is not itself a match is still kept when it holds one, so
107
+ // the tree's shape survives the filter.
108
+ if (!isMatch && children.length === 0) {
109
+ return [];
110
+ }
111
+ return [{ ...node, children }];
112
+ });
113
+ return keepMatching((0, accountListSelector_1.getNestedAccountListHierarchy)(state.accountListState, exports.INVOICING_ACCOUNT_LIST_KEY));
114
+ };
115
+ exports.invoicingAccountHierarchyForTypes = invoicingAccountHierarchyForTypes;
4
116
  /**
5
117
  * Single bundled view for the QBO integration settings screen — the
6
- * connection, per-account-types account options, classes, sync health, and the
7
- * fetch/action states for each of them.
118
+ * connection, the account and class options, sync health, and the fetch/action
119
+ * states for each of them.
8
120
  */
9
121
  const getInvoicingQboView = (state) => {
10
122
  const qboState = state.invoicingQboViewState;
123
+ const accountList = state.accountListState.byReportId[exports.INVOICING_ACCOUNT_LIST_KEY];
11
124
  return {
12
- accountsByAccountTypes: qboState.accountsByAccountTypes,
13
- accountsFetchStateByAccountTypes: qboState.accountsFetchStateByAccountTypes,
125
+ accounts: invoicingAccountingOptions(state),
126
+ accountsFetchState: {
127
+ fetchState: accountList.fetchState,
128
+ error: accountList.error,
129
+ },
130
+ nestedAccountHierarchy: (0, accountListSelector_1.getNestedAccountListHierarchy)(state.accountListState, exports.INVOICING_ACCOUNT_LIST_KEY),
131
+ nestedClassHierarchy: (0, classListSelector_1.getNestedClassListHierarchy)(state.classListState),
14
132
  backfillState: qboState.backfillFetchState,
15
- classes: qboState.classes,
16
- classesFetchState: qboState.classesFetchState,
133
+ classes: invoicingAccountingClassOptions(state),
134
+ classesFetchState: {
135
+ fetchState: state.classListState.fetchState,
136
+ error: state.classListState.error,
137
+ },
17
138
  connection: qboState.connection,
18
139
  connectionFetchState: qboState.connectionFetchState,
19
140
  retryFetchStateByTaskId: qboState.retryFetchStateByTaskId,
@@ -1,11 +1,7 @@
1
1
  import { FetchStateAndError } from '../../../commonStateTypes/common';
2
- import { InvoicingQboAccountOption, InvoicingQboConnection, InvoicingQboSyncHealth } from './invoicingQboViewPayload';
2
+ import { InvoicingQboConnection, InvoicingQboSyncHealth } from './invoicingQboViewPayload';
3
3
  export interface InvoicingQboViewState {
4
- accountsByAccountTypes: Record<string, InvoicingQboAccountOption[]>;
5
- accountsFetchStateByAccountTypes: Record<string, FetchStateAndError>;
6
4
  backfillFetchState: FetchStateAndError;
7
- classes: InvoicingQboAccountOption[];
8
- classesFetchState: FetchStateAndError;
9
5
  connectionFetchState: FetchStateAndError;
10
6
  retryFetchStateByTaskId: Record<string, FetchStateAndError>;
11
7
  syncHealthFetchState: FetchStateAndError;
@@ -12,4 +12,38 @@ interface FetchRecommendationPayloadWithCOT {
12
12
  recommendations: RecommendationPayloadWithCOT[];
13
13
  }
14
14
  export type FetchRecommendationResponseWithCOT = ZeniAPIResponse<FetchRecommendationPayloadWithCOT>;
15
+ export type RecommendationFeedbackDimension = 'vendor' | 'category' | 'class';
16
+ export type RecommendationFeedbackSource = 'primary' | 'alternate' | 'shadow' | 'qbo';
17
+ export type RecommendationFeedbackOutcome = 'accepted' | 'override_ranked' | 'override_outside' | 'retracted';
18
+ export interface RecommendationFeedbackValue {
19
+ name: string;
20
+ id?: string | null;
21
+ integrationId?: string | null;
22
+ }
23
+ export interface ShownRecommendationForFeedback {
24
+ rank: number;
25
+ source: RecommendationFeedbackSource;
26
+ value: RecommendationFeedbackValue;
27
+ confidence?: number | null;
28
+ cot?: string[];
29
+ reasoning?: string | null;
30
+ }
31
+ export interface SubmitRecommendationFeedbackPayload {
32
+ dimension: RecommendationFeedbackDimension;
33
+ outcome: RecommendationFeedbackOutcome;
34
+ selected: RecommendationFeedbackValue & {
35
+ isNewEntity?: boolean;
36
+ };
37
+ transactionId: string;
38
+ features?: Record<string, unknown> | null;
39
+ lineId?: string | null;
40
+ matchedRank?: number | null;
41
+ matchedSource?: RecommendationFeedbackSource | null;
42
+ reasonText?: string | null;
43
+ recommendationSetId?: string | null;
44
+ retractsFeedbackId?: string | null;
45
+ shownRecommendations?: ShownRecommendationForFeedback[];
46
+ skipped?: boolean;
47
+ topPickSource?: RecommendationFeedbackSource | null;
48
+ }
15
49
  export {};
@@ -5,6 +5,7 @@ import { TransactionID } from '../../entity/transaction/stateTypes/transactionID
5
5
  import { SupportedTransaction } from '../../entity/transaction/transactionState';
6
6
  import { ZeniAPIStatus } from '../../responsePayload';
7
7
  import { TransactionsTab } from '../expenseAutomationView/types/transactionsViewState';
8
+ import { SubmitRecommendationFeedbackPayload } from './recommendationPayload';
8
9
  import { EntityRecommendationState } from './recommendationState';
9
10
  export type RecommendationLineUpdateFlowType = 'categorization' | 'detail';
10
11
  type FetchEntityRecommendationsForLineUpdateCommonPayload = {
@@ -57,6 +58,6 @@ export declare const fetchEntityRecommendationsByTransactionId: import("@reduxjs
57
58
  }, "recommendation/updateRecommendationForEntityFailure">, indicateReadyToInitializeRecommendation: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[entityIdOrName: string, entityType: EntityType], {
58
59
  entityIdOrName: string;
59
60
  entityType: EntityType;
60
- }, "recommendation/indicateReadyToInitializeRecommendation", never, never>;
61
+ }, "recommendation/indicateReadyToInitializeRecommendation", never, never>, clearRecommendation: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"recommendation/clearRecommendation">, submitRecommendationFeedback: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[args: SubmitRecommendationFeedbackPayload], SubmitRecommendationFeedbackPayload, "recommendation/submitRecommendationFeedback", never, never>;
61
62
  declare const _default: import("redux").Reducer<EntityRecommendationState>;
62
63
  export default _default;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.indicateReadyToInitializeRecommendation = exports.updateRecommendationForEntityFailure = exports.updateRecommendationForEntity = exports.fetchRecommendationByEntityId = exports.fetchRecommendationByEntityName = exports.fetchEntityRecommendationsForLineUpdate = exports.fetchEntityRecommendationsByTransactionId = exports.initialState = void 0;
4
+ exports.submitRecommendationFeedback = exports.clearRecommendation = exports.indicateReadyToInitializeRecommendation = exports.updateRecommendationForEntityFailure = exports.updateRecommendationForEntity = exports.fetchRecommendationByEntityId = exports.fetchRecommendationByEntityName = exports.fetchEntityRecommendationsForLineUpdate = exports.fetchEntityRecommendationsByTransactionId = exports.initialState = void 0;
5
5
  const toolkit_1 = require("@reduxjs/toolkit");
6
6
  const rootActions_1 = require("../../rootActions");
7
7
  const recommendationState_1 = require("./recommendationState");
@@ -135,6 +135,21 @@ const recommendation = (0, toolkit_1.createSlice)({
135
135
  };
136
136
  },
137
137
  },
138
+ clearRecommendation(draft) {
139
+ Object.assign(draft, exports.initialState);
140
+ },
141
+ // Fire-and-forget human feedback on a recommendation (accept, ranked
142
+ // override or outside override). Handled by submitRecommendationFeedbackEpic
143
+ // which POSTs to the accounting recommendation-feedback endpoint. No state
144
+ // mutation: the categorization UX must not depend on the feedback round-trip.
145
+ submitRecommendationFeedback: {
146
+ reducer() {
147
+ // Fire-and-forget: no state mutation; the epic performs the POST.
148
+ },
149
+ prepare(args) {
150
+ return { payload: args };
151
+ },
152
+ },
138
153
  },
139
154
  extraReducers: (builder) => {
140
155
  builder.addCase(rootActions_1.clearAll, (draft) => {
@@ -142,7 +157,7 @@ const recommendation = (0, toolkit_1.createSlice)({
142
157
  });
143
158
  },
144
159
  });
145
- _a = recommendation.actions, exports.fetchEntityRecommendationsByTransactionId = _a.fetchEntityRecommendationsByTransactionId, exports.fetchEntityRecommendationsForLineUpdate = _a.fetchEntityRecommendationsForLineUpdate, exports.fetchRecommendationByEntityName = _a.fetchRecommendationByEntityName, exports.fetchRecommendationByEntityId = _a.fetchRecommendationByEntityId, exports.updateRecommendationForEntity = _a.updateRecommendationForEntity, exports.updateRecommendationForEntityFailure = _a.updateRecommendationForEntityFailure, exports.indicateReadyToInitializeRecommendation = _a.indicateReadyToInitializeRecommendation;
160
+ _a = recommendation.actions, exports.fetchEntityRecommendationsByTransactionId = _a.fetchEntityRecommendationsByTransactionId, exports.fetchEntityRecommendationsForLineUpdate = _a.fetchEntityRecommendationsForLineUpdate, exports.fetchRecommendationByEntityName = _a.fetchRecommendationByEntityName, exports.fetchRecommendationByEntityId = _a.fetchRecommendationByEntityId, exports.updateRecommendationForEntity = _a.updateRecommendationForEntity, exports.updateRecommendationForEntityFailure = _a.updateRecommendationForEntityFailure, exports.indicateReadyToInitializeRecommendation = _a.indicateReadyToInitializeRecommendation, exports.clearRecommendation = _a.clearRecommendation, exports.submitRecommendationFeedback = _a.submitRecommendationFeedback;
146
161
  exports.default = recommendation.reducer;
147
162
  const doUpdateRecommendationForEntity = (draft, entityId, entityType, payload) => {
148
163
  const key = (0, recommendationState_1.getEntityRecommendationKey)(entityType, entityId);
@@ -0,0 +1,7 @@
1
+ import { ActionsObservable, StateObservable } from 'redux-observable';
2
+ import { Observable } from 'rxjs';
3
+ import { RootState } from '../../reducer';
4
+ import { ZeniAPI } from '../../zeniAPI';
5
+ import { submitRecommendationFeedback } from './recommendationReducer';
6
+ export type ActionType = ReturnType<typeof submitRecommendationFeedback>;
7
+ export declare const submitRecommendationFeedbackEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.submitRecommendationFeedbackEpic = void 0;
4
+ const rxjs_1 = require("rxjs");
5
+ const operators_1 = require("rxjs/operators");
6
+ const responsePayload_1 = require("../../responsePayload");
7
+ const recommendationReducer_1 = require("./recommendationReducer");
8
+ const toRequestBody = (payload) => ({
9
+ line_id: payload.lineId ?? null,
10
+ dimension: payload.dimension,
11
+ recommendation_set_id: payload.recommendationSetId ?? null,
12
+ top_pick_source: payload.topPickSource ?? null,
13
+ shown_recommendations: (payload.shownRecommendations ?? []).map((recommendation) => ({
14
+ source: recommendation.source,
15
+ rank: recommendation.rank,
16
+ value: {
17
+ name: recommendation.value.name,
18
+ id: recommendation.value.id ?? null,
19
+ integration_id: recommendation.value.integrationId ?? null,
20
+ },
21
+ confidence: recommendation.confidence ?? null,
22
+ reasoning: recommendation.reasoning ?? null,
23
+ cot: recommendation.cot ?? [],
24
+ })),
25
+ selected: {
26
+ name: payload.selected.name,
27
+ id: payload.selected.id ?? null,
28
+ integration_id: payload.selected.integrationId ?? null,
29
+ is_new_entity: payload.selected.isNewEntity ?? false,
30
+ },
31
+ matched_source: payload.matchedSource ?? null,
32
+ matched_rank: payload.matchedRank ?? null,
33
+ outcome: payload.outcome,
34
+ reason_text: payload.reasonText ?? null,
35
+ skipped: payload.skipped ?? false,
36
+ retracts_feedback_id: payload.retractsFeedbackId ?? null,
37
+ features: payload.features ?? null,
38
+ });
39
+ // Fire-and-forget: the epic never re-emits an action. Feedback delivery is a
40
+ // side-effect that must not disrupt the categorization UX, so both the
41
+ // success and error paths complete silently (errors are logged for debugging).
42
+ const submitRecommendationFeedbackEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(recommendationReducer_1.submitRecommendationFeedback.match), (0, operators_1.mergeMap)((action) => {
43
+ const { transactionId } = action.payload;
44
+ return zeniAPI
45
+ .postAndGetJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/transactions/${transactionId}/recommendation-feedback`, toRequestBody(action.payload))
46
+ .pipe((0, operators_1.mergeMap)((response) => {
47
+ if (!(0, responsePayload_1.isSuccessStatus)(response)) {
48
+ console.error('submitRecommendationFeedbackEpic: recommendation feedback was not recorded', response.status);
49
+ }
50
+ return rxjs_1.EMPTY;
51
+ }), (0, operators_1.catchError)((error) => {
52
+ console.error('submitRecommendationFeedbackEpic: failed to submit recommendation feedback', error);
53
+ return rxjs_1.EMPTY;
54
+ }));
55
+ }));
56
+ exports.submitRecommendationFeedbackEpic = submitRecommendationFeedbackEpic;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.2.5",
3
+ "version": "5.2.7-beta0ND",
4
4
  "description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/esm/index.js",
@@ -1,25 +0,0 @@
1
- import { of } from 'rxjs';
2
- import { catchError, filter, map, mergeMap } from 'rxjs/operators';
3
- import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
4
- import { mapInvoicingQboAccountOptions, } from '../invoicingQboViewPayload';
5
- import { fetchInvoicingQboAccounts, updateInvoicingQboAccounts, updateInvoicingQboAccountsFailure, } from '../invoicingQboViewReducer';
6
- export const fetchInvoicingQboAccountsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchInvoicingQboAccounts.match), mergeMap((action) => {
7
- const { accountTypes } = action.payload;
8
- const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/qbo/accounts?account_types=${encodeURIComponent(accountTypes)}`;
9
- return zeniAPI.getJSON(apiUrl).pipe(map((response) => {
10
- if (isSuccessResponse(response)) {
11
- return updateInvoicingQboAccounts({
12
- accountTypes,
13
- accounts: mapInvoicingQboAccountOptions(response.data?.accounts ?? []),
14
- });
15
- }
16
- return updateInvoicingQboAccountsFailure({
17
- accountTypes,
18
- status: response.status,
19
- });
20
- }), catchError((error) => of(updateInvoicingQboAccountsFailure({
21
- accountTypes,
22
- status: createZeniAPIStatus('Unexpected Error', 'Fetch QBO accounts REST API call errored out ' +
23
- JSON.stringify(error)),
24
- }))));
25
- }));
@@ -1,15 +0,0 @@
1
- import { of } from 'rxjs';
2
- import { catchError, filter, map, switchMap } from 'rxjs/operators';
3
- import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
4
- import { mapInvoicingQboAccountOptions, } from '../invoicingQboViewPayload';
5
- import { fetchInvoicingQboClasses, updateInvoicingQboClasses, updateInvoicingQboClassesFailure, } from '../invoicingQboViewReducer';
6
- export const fetchInvoicingQboClassesEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchInvoicingQboClasses.match), switchMap(() => {
7
- const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/qbo/classes`;
8
- return zeniAPI.getJSON(apiUrl).pipe(map((response) => {
9
- if (isSuccessResponse(response)) {
10
- return updateInvoicingQboClasses(mapInvoicingQboAccountOptions(response.data?.accounts ?? []));
11
- }
12
- return updateInvoicingQboClassesFailure(response.status);
13
- }), catchError((error) => of(updateInvoicingQboClassesFailure(createZeniAPIStatus('Unexpected Error', 'Fetch QBO classes REST API call errored out ' +
14
- JSON.stringify(error))))));
15
- }));
@@ -1,6 +0,0 @@
1
- import { ActionsObservable, StateObservable } from 'redux-observable';
2
- import { Observable } from 'rxjs';
3
- import { RootState } from '../../../../rootStateTypes';
4
- import { ZeniAPI } from '../../../../zeniAPI';
5
- import { InvoicingQboViewActionType } from './invoicingQboViewActionType';
6
- export declare const fetchInvoicingQboAccountsEpic: (actions$: ActionsObservable<InvoicingQboViewActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<InvoicingQboViewActionType>;
@@ -1,29 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fetchInvoicingQboAccountsEpic = void 0;
4
- const rxjs_1 = require("rxjs");
5
- const operators_1 = require("rxjs/operators");
6
- const responsePayload_1 = require("../../../../responsePayload");
7
- const invoicingQboViewPayload_1 = require("../invoicingQboViewPayload");
8
- const invoicingQboViewReducer_1 = require("../invoicingQboViewReducer");
9
- const fetchInvoicingQboAccountsEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(invoicingQboViewReducer_1.fetchInvoicingQboAccounts.match), (0, operators_1.mergeMap)((action) => {
10
- const { accountTypes } = action.payload;
11
- const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/qbo/accounts?account_types=${encodeURIComponent(accountTypes)}`;
12
- return zeniAPI.getJSON(apiUrl).pipe((0, operators_1.map)((response) => {
13
- if ((0, responsePayload_1.isSuccessResponse)(response)) {
14
- return (0, invoicingQboViewReducer_1.updateInvoicingQboAccounts)({
15
- accountTypes,
16
- accounts: (0, invoicingQboViewPayload_1.mapInvoicingQboAccountOptions)(response.data?.accounts ?? []),
17
- });
18
- }
19
- return (0, invoicingQboViewReducer_1.updateInvoicingQboAccountsFailure)({
20
- accountTypes,
21
- status: response.status,
22
- });
23
- }), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, invoicingQboViewReducer_1.updateInvoicingQboAccountsFailure)({
24
- accountTypes,
25
- status: (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch QBO accounts REST API call errored out ' +
26
- JSON.stringify(error)),
27
- }))));
28
- }));
29
- exports.fetchInvoicingQboAccountsEpic = fetchInvoicingQboAccountsEpic;
@@ -1,6 +0,0 @@
1
- import { ActionsObservable, StateObservable } from 'redux-observable';
2
- import { Observable } from 'rxjs';
3
- import { RootState } from '../../../../rootStateTypes';
4
- import { ZeniAPI } from '../../../../zeniAPI';
5
- import { InvoicingQboViewActionType } from './invoicingQboViewActionType';
6
- export declare const fetchInvoicingQboClassesEpic: (actions$: ActionsObservable<InvoicingQboViewActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<InvoicingQboViewActionType>;
@@ -1,19 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fetchInvoicingQboClassesEpic = void 0;
4
- const rxjs_1 = require("rxjs");
5
- const operators_1 = require("rxjs/operators");
6
- const responsePayload_1 = require("../../../../responsePayload");
7
- const invoicingQboViewPayload_1 = require("../invoicingQboViewPayload");
8
- const invoicingQboViewReducer_1 = require("../invoicingQboViewReducer");
9
- const fetchInvoicingQboClassesEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(invoicingQboViewReducer_1.fetchInvoicingQboClasses.match), (0, operators_1.switchMap)(() => {
10
- const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/qbo/classes`;
11
- return zeniAPI.getJSON(apiUrl).pipe((0, operators_1.map)((response) => {
12
- if ((0, responsePayload_1.isSuccessResponse)(response)) {
13
- return (0, invoicingQboViewReducer_1.updateInvoicingQboClasses)((0, invoicingQboViewPayload_1.mapInvoicingQboAccountOptions)(response.data?.accounts ?? []));
14
- }
15
- return (0, invoicingQboViewReducer_1.updateInvoicingQboClassesFailure)(response.status);
16
- }), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, invoicingQboViewReducer_1.updateInvoicingQboClassesFailure)((0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch QBO classes REST API call errored out ' +
17
- JSON.stringify(error))))));
18
- }));
19
- exports.fetchInvoicingQboClassesEpic = fetchInvoicingQboClassesEpic;