@zeniai/client-epic-state 5.1.48 → 5.1.50-beta3RR

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.
@@ -102,6 +102,7 @@ export interface AiCfoAnswerPayload {
102
102
  artifact_host_url?: string | null;
103
103
  artifact_title?: string | null;
104
104
  follow_up_questions?: string[];
105
+ suggested_reply?: string | null;
105
106
  table?: AiCfoVisualizationPayload | null;
106
107
  }
107
108
  export interface ChatSessionPayload {
@@ -112,6 +112,7 @@ function chatHistoryToQA(history) {
112
112
  followUpQuestions: Array.isArray(parsedAgentMessage.follow_up_questions)
113
113
  ? parsedAgentMessage.follow_up_questions
114
114
  : [],
115
+ suggestedReply: parsedAgentMessage.suggested_reply ?? undefined,
115
116
  assumptions: parsedAgentMessage.assumptions ?? '',
116
117
  insights: parsedAgentMessage.insights ?? '',
117
118
  summarizedTitle: parsedAgentMessage.summarized_title ?? undefined,
@@ -351,7 +352,7 @@ const toAiCfoVisualizationData = (data) => {
351
352
  };
352
353
  };
353
354
  const toResponseBlockType = (answer, userId) => {
354
- const { metadata, state, type, text, visualization, table, follow_up_questions, assumptions, insights, summarized_title, artifact, artifact_title, artifact_host_url, } = answer;
355
+ const { metadata, state, type, text, visualization, table, follow_up_questions, suggested_reply, assumptions, insights, summarized_title, artifact, artifact_title, artifact_host_url, } = answer;
355
356
  const { chat_session_id, message_id, timestamp, chain_of_thought_action_steps: chainOfThoughtActionSteps = [], } = metadata;
356
357
  return {
357
358
  messageId: message_id,
@@ -365,6 +366,7 @@ const toResponseBlockType = (answer, userId) => {
365
366
  visualization: visualization != null ? (0, exports.toAiCfoVisualization)(visualization) : undefined,
366
367
  table: table != null ? (0, exports.toAiCfoVisualization)(table) : undefined,
367
368
  followUpQuestions: follow_up_questions ?? [],
369
+ suggestedReply: suggested_reply ?? undefined,
368
370
  assumptions: assumptions ?? '',
369
371
  insights: insights ?? '',
370
372
  summarizedTitle: summarized_title ?? undefined,
@@ -163,6 +163,7 @@ export interface AnswerContentBlockBase {
163
163
  artifact?: string;
164
164
  artifactHostUrl?: string;
165
165
  artifactTitle?: string;
166
+ suggestedReply?: string;
166
167
  summarizedTitle?: string;
167
168
  table?: AiCfoVisualization;
168
169
  visualization?: AiCfoVisualization;
@@ -108,6 +108,7 @@ function chatHistoryToQA(history) {
108
108
  followUpQuestions: Array.isArray(parsedAgentMessage.follow_up_questions)
109
109
  ? parsedAgentMessage.follow_up_questions
110
110
  : [],
111
+ suggestedReply: parsedAgentMessage.suggested_reply ?? undefined,
111
112
  assumptions: parsedAgentMessage.assumptions ?? '',
112
113
  insights: parsedAgentMessage.insights ?? '',
113
114
  summarizedTitle: parsedAgentMessage.summarized_title ?? undefined,
@@ -346,7 +347,7 @@ const toAiCfoVisualizationData = (data) => {
346
347
  };
347
348
  };
348
349
  const toResponseBlockType = (answer, userId) => {
349
- const { metadata, state, type, text, visualization, table, follow_up_questions, assumptions, insights, summarized_title, artifact, artifact_title, artifact_host_url, } = answer;
350
+ const { metadata, state, type, text, visualization, table, follow_up_questions, suggested_reply, assumptions, insights, summarized_title, artifact, artifact_title, artifact_host_url, } = answer;
350
351
  const { chat_session_id, message_id, timestamp, chain_of_thought_action_steps: chainOfThoughtActionSteps = [], } = metadata;
351
352
  return {
352
353
  messageId: message_id,
@@ -360,6 +361,7 @@ const toResponseBlockType = (answer, userId) => {
360
361
  visualization: visualization != null ? toAiCfoVisualization(visualization) : undefined,
361
362
  table: table != null ? toAiCfoVisualization(table) : undefined,
362
363
  followUpQuestions: follow_up_questions ?? [],
364
+ suggestedReply: suggested_reply ?? undefined,
363
365
  assumptions: assumptions ?? '',
364
366
  insights: insights ?? '',
365
367
  summarizedTitle: summarized_title ?? undefined,
@@ -4,11 +4,95 @@ import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
4
4
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
5
5
  import { getCurrentTenant } from '../../../entity/tenant/tenantSelector';
6
6
  import { removeTransactionCategorization, updateTransactionIsMiscategorized, } from '../../../entity/transaction/transactionReducer';
7
- import { createZeniAPIStatus, isSuccessStatus, } from '../../../responsePayload';
7
+ import { getSupportedTransactionById } from '../../../entity/transaction/transactionSelector';
8
+ import { createZeniAPIStatus, isSuccessResponse, isSuccessStatus, } from '../../../responsePayload';
8
9
  import { updateStatusForTransactionNotMiscategorizedUpdateForCategorization, updateTotalCountForTransactionCategorization, } from '../../expenseAutomationView/reducers/transactionsViewReducer';
9
10
  import { markTransactionAsNotMiscategorized, updateStatusConsolidatedSaveTransactionDetail, updateStatusForTransactionNotMiscategorizedUpdate, } from '../transactionDetailReducer';
10
11
  import { getTransactionDetailKey } from '../transactionDetailState';
11
12
  export const markTransactionAsNotMiscategorizedEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(markTransactionAsNotMiscategorized.match), mergeMap((action) => {
13
+ if (action.payload.isExpenseAutomationEnabled === true) {
14
+ const transactionKey = getTransactionDetailKey(action.payload.transactionId);
15
+ const transaction = getSupportedTransactionById(state$.value.transactionState, transactionKey);
16
+ const lineIds = (action.payload.lineIds?.length ?? 0) > 0
17
+ ? action.payload.lineIds
18
+ : (transaction?.lines ?? []).map((line) => line.id);
19
+ return zeniAPI
20
+ .putAndGetJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/expense-automation/mark-as-reviewed`, {
21
+ transactions_reviewed: [
22
+ {
23
+ transaction_id: action.payload.transactionId.id,
24
+ line_ids: lineIds,
25
+ },
26
+ ],
27
+ })
28
+ .pipe(mergeMap((response) => {
29
+ if (isSuccessResponse(response)) {
30
+ const actions = [];
31
+ actions.push(updateStatusForTransactionNotMiscategorizedUpdate(action.payload.transactionId, 'Completed', undefined));
32
+ const txnId = action.payload.transactionId.id;
33
+ const reviewedFailed = response.data?.transactions_reviewed_failed ?? [];
34
+ const isTransactionFailed = reviewedFailed.some((t) => t.transaction_id === txnId);
35
+ if (!isTransactionFailed) {
36
+ actions.push(updateTransactionIsMiscategorized(transactionKey, false));
37
+ const { expenseAutomationViewState, expenseAutomationTransactionsViewState, } = state$.value;
38
+ const { transactionCategorizationView, selectedTransactionCategorizationTab, } = expenseAutomationTransactionsViewState;
39
+ const currentTenant = getCurrentTenant(state$.value);
40
+ const { selectedPeriodByTenantId } = expenseAutomationViewState;
41
+ const selectedPeriod = selectedPeriodByTenantId[currentTenant.tenantId];
42
+ const autoTabLineItems = transactionCategorizationView.autoCategorized
43
+ .transactionReviewLocalDataById[txnId]
44
+ ?.transactionReviewLocalData.tabSpecificLineItems
45
+ .autoCategorized ?? [];
46
+ const hasAutoLines = autoTabLineItems.length > 0;
47
+ const { uiState } = transactionCategorizationView[selectedTransactionCategorizationTab];
48
+ const removeIdsFromPeriod = selectedTransactionCategorizationTab === 'review'
49
+ ? [txnId]
50
+ : hasAutoLines
51
+ ? []
52
+ : [txnId];
53
+ actions.push(updateStatusForTransactionNotMiscategorizedUpdateForCategorization(selectedTransactionCategorizationTab, [txnId], removeIdsFromPeriod, selectedPeriod, 'Completed'));
54
+ if (!hasAutoLines) {
55
+ actions.push(removeTransactionCategorization({
56
+ transactionIds: [txnId],
57
+ }));
58
+ }
59
+ if (removeIdsFromPeriod.length > 0) {
60
+ const totalCountByPeriod = uiState.totalCount[toMonthYearPeriodId(selectedPeriod)];
61
+ const updatedTotalCount = totalCountByPeriod === 0
62
+ ? totalCountByPeriod
63
+ : totalCountByPeriod - 1;
64
+ actions.push(updateTotalCountForTransactionCategorization({
65
+ selectedPeriod,
66
+ selectedTab: selectedTransactionCategorizationTab,
67
+ totalCount: updatedTotalCount,
68
+ }));
69
+ }
70
+ }
71
+ actions.push(updateStatusConsolidatedSaveTransactionDetail({
72
+ transactionId: action.payload.transactionId,
73
+ fetchState: 'Completed',
74
+ }));
75
+ if (action.payload.transactionSaveUpdates.anyBulkUpdates === true) {
76
+ actions.push(openSnackbar({
77
+ messageSection: 'transactionDetails_updatingPastTransactions',
78
+ messageText: 'success',
79
+ type: 'success',
80
+ }));
81
+ }
82
+ else {
83
+ actions.push(openSnackbar({
84
+ messageSection: 'transactionDetails_transactionUpdated',
85
+ messageText: 'success',
86
+ type: 'success',
87
+ }));
88
+ }
89
+ return from(actions);
90
+ }
91
+ else {
92
+ return of(updateStatusForTransactionNotMiscategorizedUpdate(action.payload.transactionId, 'Error', response.status));
93
+ }
94
+ }), catchError((error) => of(updateStatusForTransactionNotMiscategorizedUpdate(action.payload.transactionId, 'Error', createZeniAPIStatus('Unexpected Error', `Transaction mark as reviewed REST API call errored out ${error}`)))));
95
+ }
12
96
  return zeniAPI
13
97
  .putAndGetJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/transactions/${action.payload.transactionId.id}/mark-miscategorized-as-correct`, preparePayload(action.payload.transactionId))
14
98
  .pipe(mergeMap((response) => {
@@ -16,44 +100,7 @@ export const markTransactionAsNotMiscategorizedEpic = (actions$, state$, zeniAPI
16
100
  const actions = [];
17
101
  actions.push(updateStatusForTransactionNotMiscategorizedUpdate(action.payload.transactionId, 'Completed', undefined));
18
102
  const transactionKey = getTransactionDetailKey(action.payload.transactionId);
19
- // Mark this transaction as not miscategorized anymore
20
103
  actions.push(updateTransactionIsMiscategorized(transactionKey, false));
21
- if (action.payload.isExpenseAutomationEnabled === true) {
22
- const { expenseAutomationViewState, expenseAutomationTransactionsViewState, } = state$.value;
23
- const { transactionCategorizationView, selectedTransactionCategorizationTab, } = expenseAutomationTransactionsViewState;
24
- const currentTenant = getCurrentTenant(state$.value);
25
- const { selectedPeriodByTenantId } = expenseAutomationViewState;
26
- const selectedPeriod = selectedPeriodByTenantId[currentTenant.tenantId];
27
- const txnId = action.payload.transactionId.id;
28
- const autoTabLineItems = transactionCategorizationView.autoCategorized
29
- .transactionReviewLocalDataById[txnId]
30
- ?.transactionReviewLocalData.tabSpecificLineItems
31
- .autoCategorized ?? [];
32
- const hasAutoLines = autoTabLineItems.length > 0;
33
- const { uiState } = transactionCategorizationView[selectedTransactionCategorizationTab];
34
- const removeIdsFromPeriod = selectedTransactionCategorizationTab === 'review'
35
- ? [txnId]
36
- : hasAutoLines
37
- ? []
38
- : [txnId];
39
- actions.push(updateStatusForTransactionNotMiscategorizedUpdateForCategorization(selectedTransactionCategorizationTab, [txnId], removeIdsFromPeriod, selectedPeriod, 'Completed'));
40
- if (!hasAutoLines) {
41
- actions.push(removeTransactionCategorization({
42
- transactionIds: [txnId],
43
- }));
44
- }
45
- if (removeIdsFromPeriod.length > 0) {
46
- const totalCountByPeriod = uiState.totalCount[toMonthYearPeriodId(selectedPeriod)];
47
- const updatedTotalCount = totalCountByPeriod === 0
48
- ? totalCountByPeriod
49
- : totalCountByPeriod - 1;
50
- actions.push(updateTotalCountForTransactionCategorization({
51
- selectedPeriod,
52
- selectedTab: selectedTransactionCategorizationTab,
53
- totalCount: updatedTotalCount,
54
- }));
55
- }
56
- }
57
104
  actions.push(updateStatusConsolidatedSaveTransactionDetail({
58
105
  transactionId: action.payload.transactionId,
59
106
  fetchState: 'Completed',
@@ -66,7 +113,6 @@ export const markTransactionAsNotMiscategorizedEpic = (actions$, state$, zeniAPI
66
113
  }));
67
114
  }
68
115
  else {
69
- //Finally everything done for this transaction
70
116
  actions.push(openSnackbar({
71
117
  messageSection: 'transactionDetails_transactionUpdated',
72
118
  messageText: 'success',
@@ -10,7 +10,6 @@ import { injectCOTFlagsIntoTransactionPayload, toTransactionPayload, } from '../
10
10
  import { toVendorTransactionPayload, toVendorTransactionWithCustomerPayload, } from '../../../entity/transaction/payloadTypes/vendorTransactionPayload';
11
11
  import { isCustomerTransaction, } from '../../../entity/transaction/stateTypes/customerTransaction';
12
12
  import { isTransferTransaction, } from '../../../entity/transaction/stateTypes/otherTransaction';
13
- import { toJEAndDepositTransactionTypeStrict, toVendorTransactionTypeStrict, } from '../../../entity/transaction/stateTypes/transactionType';
14
13
  import { isVendorTransaction, } from '../../../entity/transaction/stateTypes/vendorTransaction';
15
14
  import toSupportedTransaction from '../../../entity/transaction/toSupportedTransaction';
16
15
  import { getTransactionWithCOT } from '../../../entity/transaction/transactionHelper';
@@ -20,12 +19,12 @@ import { updateVendors } from '../../../entity/vendor/vendorReducer';
20
19
  import { getVendorByVendorId } from '../../../entity/vendor/vendorSelector';
21
20
  import { createZeniAPIStatus, isActionNotAllowed, isSuccessResponse, } from '../../../responsePayload';
22
21
  import { getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, } from '../../../entity/tenant/tenantSelector';
23
- import { getUncategorizedAccounts } from '../../accountList/accountListSelector';
22
+ import { getUncategorizedAccounts, } from '../../accountList/accountListSelector';
24
23
  import { getPendingReviewLineIdsFromTransaction } from '../../expenseAutomationView/helpers/transactionCategorizationLocalDataHelper';
25
24
  import { syncTransactionCategorizationFromDetailSave } from '../../expenseAutomationView/reducers/transactionsViewReducer';
26
25
  import { initializeTransactionDetailLocalData, markTransactionAsNotMiscategorized, updateStatusConsolidatedSaveTransactionDetail, updateTransactionDetail, updateTransactionDetailSaveStatus, updateTransactionStateIfUpdateActionNotAllowed, } from '../transactionDetailReducer';
27
26
  import { getTransactionDetailKey, } from '../transactionDetailState';
28
- import { isTransactionUncategorized } from './saveTransactionHelper';
27
+ import { isAccountUncategorized } from '../transactionDetailLocalDataHelper';
29
28
  export const updateTransactionDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(updateTransactionDetail.match), mergeMap((action) => {
30
29
  const { transactionState, vendorState, transactionDetailState } = state$.value;
31
30
  const isAccountingProjectsEnabled = getIsAccountingProjectsEnabled(state$.value);
@@ -36,24 +35,6 @@ export const updateTransactionDetailEpic = (actions$, state$, zeniAPI) => action
36
35
  if (isSuccessResponse(response) && response.data != null) {
37
36
  const transactionIdResp = toTransactionID(response.data.transaction);
38
37
  const transactionDetailKeyForResponse = getTransactionDetailKey(transactionIdResp);
39
- const tranDetail = recordGet(transactionDetailState.transactionDetailById, transactionDetailKeyForResponse, undefined);
40
- const tranVendorUpdates = tranDetail?.transactionDetailLocalData.vendorUpdates;
41
- let vendorUpdates = undefined;
42
- if (tranVendorUpdates?.qboId != null) {
43
- const vendor = getVendorByVendorId(vendorState, tranVendorUpdates.qboId);
44
- if (vendor != null) {
45
- vendorUpdates = {
46
- id: vendor?.qboId,
47
- name: vendor?.name,
48
- };
49
- }
50
- else if (tranVendorUpdates.name != null) {
51
- vendorUpdates = {
52
- id: tranVendorUpdates.qboId,
53
- name: tranVendorUpdates.name,
54
- };
55
- }
56
- }
57
38
  const actions = [];
58
39
  if (response.data.vendors != null) {
59
40
  actions.push(updateVendors(response.data.vendors, 'merge'));
@@ -66,30 +47,14 @@ export const updateTransactionDetailEpic = (actions$, state$, zeniAPI) => action
66
47
  vendorId: response.data.vendors?.[0]?.vendor_id,
67
48
  }));
68
49
  actions.push(updateTransactionDetailSaveStatus(transactionIdResp, 'Completed'));
69
- let callMarkTransactionAsNotMiscategorized = true;
70
- // NOTE: If tran is vendorType OR JE OR Deposit transaction and there is no vendor chosen on this tranaction while saving,
71
- // then we need to skip mark-miscategorized-as-correct api call
72
- if (toVendorTransactionTypeStrict(response.data.transaction.transaction_type) != null ||
73
- toJEAndDepositTransactionTypeStrict(response.data.transaction.transaction_type) != null) {
74
- const isAccountingClassesEnabled = getIsAccountingClassesEnabled(state$.value);
75
- const isAnyClassMissing = isAccountingClassesEnabled &&
76
- checkIfClassMissingForAnyLineItem(response.data.transaction);
77
- const isVendorOrCustomerMissingInAnyLine = toJEAndDepositTransactionTypeStrict(response.data.transaction.transaction_type) != null &&
78
- checkIfVendorOrCustomerMissingForAnyLineItem(response.data.transaction);
79
- if ((vendorUpdates?.id == null &&
80
- isVendorOrCustomerMissingInAnyLine == true) ||
81
- isAnyClassMissing == true) {
82
- callMarkTransactionAsNotMiscategorized = false;
83
- }
84
- }
85
50
  const uncategorizedIncomeExpense = getUncategorizedAccounts(state$.value.accountState, state$.value.accountListState, 'accountList');
86
51
  const transaction = toSupportedTransaction(response.data.transaction);
87
- const isTransactionUnCategorized = isTransactionUncategorized(transaction, uncategorizedIncomeExpense);
52
+ const isAccountingClassesEnabled = getIsAccountingClassesEnabled(state$.value);
53
+ const eligibleLineIds = getLineIdsEligibleForReview(transaction, uncategorizedIncomeExpense, isAccountingClassesEnabled);
88
54
  if (action.payload.transactionSaveUpdates.markAsMiscatUpdates ===
89
55
  true &&
90
- callMarkTransactionAsNotMiscategorized === true &&
91
- isTransactionUnCategorized === false) {
92
- actions.push(markTransactionAsNotMiscategorized(transactionIdResp, action.payload.transactionSaveUpdates, action.payload.isExpenseAutomationEnabled));
56
+ eligibleLineIds.length > 0) {
57
+ actions.push(markTransactionAsNotMiscategorized(transactionIdResp, action.payload.transactionSaveUpdates, action.payload.isExpenseAutomationEnabled, eligibleLineIds));
93
58
  }
94
59
  else {
95
60
  actions.push(updateStatusConsolidatedSaveTransactionDetail({
@@ -299,31 +264,25 @@ export const toTransactionUpdatePayload = (updates, detail, cotTransactionTracki
299
264
  // Inject COT flags into the payload
300
265
  return injectCOTFlagsIntoTransactionPayload(basePayload, transaction);
301
266
  };
302
- const checkIfClassMissingForAnyLineItem = (transactionPayload) => {
303
- let isAnyClassMissing = false;
304
- if (transactionPayload.lines != null) {
305
- transactionPayload.lines.forEach((line) => {
306
- if (line.line_detail?.class
307
- ?.accounting_class_id == null) {
308
- isAnyClassMissing = true;
309
- }
310
- });
311
- }
312
- return isAnyClassMissing;
313
- };
314
- const checkIfVendorOrCustomerMissingForAnyLineItem = (transactionPayload) => {
315
- let isAnyVendorOrCustomerMissing = false;
316
- if (transactionPayload.lines != null) {
317
- transactionPayload.lines.forEach((line) => {
318
- if (line.line_detail
319
- ?.vendor == null &&
320
- line.line_detail
321
- ?.customer == null) {
322
- isAnyVendorOrCustomerMissing = true;
323
- }
324
- });
325
- }
326
- return isAnyVendorOrCustomerMissing;
267
+ const getLineIdsEligibleForReview = (transaction, uncategorizedIncomeExpense, isAccountingClassesEnabled) => {
268
+ return (transaction.lines ?? [])
269
+ .filter((line) => {
270
+ if (line.type !== 'transaction_with_account_and_class_line' &&
271
+ line.type !== 'journal_entry_transaction_line' &&
272
+ line.type !== 'transaction_with_product_or_service_line') {
273
+ return false;
274
+ }
275
+ const typedLine = line;
276
+ const hasCategory = !isAccountUncategorized(uncategorizedIncomeExpense, typedLine.account.qboId);
277
+ const hasClass = !isAccountingClassesEnabled || typedLine.class != null;
278
+ const hasAmount = (typedLine.amount?.amount ?? 0) !== 0;
279
+ const hasEntity = line.type !== 'journal_entry_transaction_line' ||
280
+ typedLine.vendor != null ||
281
+ typedLine.customer != null;
282
+ return hasCategory && hasClass && hasAmount && hasEntity;
283
+ })
284
+ .map((line) => line.id)
285
+ .filter(Boolean);
327
286
  };
328
287
  // Converts a locally-created line item (from edit mode) into the wire shape
329
288
  // expected by the categorize endpoint. Sending id='' signals the backend to
@@ -190,12 +190,13 @@ const transactionDetail = createSlice({
190
190
  };
191
191
  }
192
192
  },
193
- prepare(transactionId, transactionSaveUpdates, isExpenseAutomationEnabled) {
193
+ prepare(transactionId, transactionSaveUpdates, isExpenseAutomationEnabled, lineIds) {
194
194
  return {
195
195
  payload: {
196
196
  transactionId,
197
197
  transactionSaveUpdates,
198
198
  isExpenseAutomationEnabled,
199
+ lineIds,
199
200
  },
200
201
  };
201
202
  },
@@ -7,11 +7,95 @@ const timePeriod_1 = require("../../../commonStateTypes/timePeriod");
7
7
  const snackbarReducer_1 = require("../../../entity/snackbar/snackbarReducer");
8
8
  const tenantSelector_1 = require("../../../entity/tenant/tenantSelector");
9
9
  const transactionReducer_1 = require("../../../entity/transaction/transactionReducer");
10
+ const transactionSelector_1 = require("../../../entity/transaction/transactionSelector");
10
11
  const responsePayload_1 = require("../../../responsePayload");
11
12
  const transactionsViewReducer_1 = require("../../expenseAutomationView/reducers/transactionsViewReducer");
12
13
  const transactionDetailReducer_1 = require("../transactionDetailReducer");
13
14
  const transactionDetailState_1 = require("../transactionDetailState");
14
15
  const markTransactionAsNotMiscategorizedEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(transactionDetailReducer_1.markTransactionAsNotMiscategorized.match), (0, operators_1.mergeMap)((action) => {
16
+ if (action.payload.isExpenseAutomationEnabled === true) {
17
+ const transactionKey = (0, transactionDetailState_1.getTransactionDetailKey)(action.payload.transactionId);
18
+ const transaction = (0, transactionSelector_1.getSupportedTransactionById)(state$.value.transactionState, transactionKey);
19
+ const lineIds = (action.payload.lineIds?.length ?? 0) > 0
20
+ ? action.payload.lineIds
21
+ : (transaction?.lines ?? []).map((line) => line.id);
22
+ return zeniAPI
23
+ .putAndGetJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/expense-automation/mark-as-reviewed`, {
24
+ transactions_reviewed: [
25
+ {
26
+ transaction_id: action.payload.transactionId.id,
27
+ line_ids: lineIds,
28
+ },
29
+ ],
30
+ })
31
+ .pipe((0, operators_1.mergeMap)((response) => {
32
+ if ((0, responsePayload_1.isSuccessResponse)(response)) {
33
+ const actions = [];
34
+ actions.push((0, transactionDetailReducer_1.updateStatusForTransactionNotMiscategorizedUpdate)(action.payload.transactionId, 'Completed', undefined));
35
+ const txnId = action.payload.transactionId.id;
36
+ const reviewedFailed = response.data?.transactions_reviewed_failed ?? [];
37
+ const isTransactionFailed = reviewedFailed.some((t) => t.transaction_id === txnId);
38
+ if (!isTransactionFailed) {
39
+ actions.push((0, transactionReducer_1.updateTransactionIsMiscategorized)(transactionKey, false));
40
+ const { expenseAutomationViewState, expenseAutomationTransactionsViewState, } = state$.value;
41
+ const { transactionCategorizationView, selectedTransactionCategorizationTab, } = expenseAutomationTransactionsViewState;
42
+ const currentTenant = (0, tenantSelector_1.getCurrentTenant)(state$.value);
43
+ const { selectedPeriodByTenantId } = expenseAutomationViewState;
44
+ const selectedPeriod = selectedPeriodByTenantId[currentTenant.tenantId];
45
+ const autoTabLineItems = transactionCategorizationView.autoCategorized
46
+ .transactionReviewLocalDataById[txnId]
47
+ ?.transactionReviewLocalData.tabSpecificLineItems
48
+ .autoCategorized ?? [];
49
+ const hasAutoLines = autoTabLineItems.length > 0;
50
+ const { uiState } = transactionCategorizationView[selectedTransactionCategorizationTab];
51
+ const removeIdsFromPeriod = selectedTransactionCategorizationTab === 'review'
52
+ ? [txnId]
53
+ : hasAutoLines
54
+ ? []
55
+ : [txnId];
56
+ actions.push((0, transactionsViewReducer_1.updateStatusForTransactionNotMiscategorizedUpdateForCategorization)(selectedTransactionCategorizationTab, [txnId], removeIdsFromPeriod, selectedPeriod, 'Completed'));
57
+ if (!hasAutoLines) {
58
+ actions.push((0, transactionReducer_1.removeTransactionCategorization)({
59
+ transactionIds: [txnId],
60
+ }));
61
+ }
62
+ if (removeIdsFromPeriod.length > 0) {
63
+ const totalCountByPeriod = uiState.totalCount[(0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod)];
64
+ const updatedTotalCount = totalCountByPeriod === 0
65
+ ? totalCountByPeriod
66
+ : totalCountByPeriod - 1;
67
+ actions.push((0, transactionsViewReducer_1.updateTotalCountForTransactionCategorization)({
68
+ selectedPeriod,
69
+ selectedTab: selectedTransactionCategorizationTab,
70
+ totalCount: updatedTotalCount,
71
+ }));
72
+ }
73
+ }
74
+ actions.push((0, transactionDetailReducer_1.updateStatusConsolidatedSaveTransactionDetail)({
75
+ transactionId: action.payload.transactionId,
76
+ fetchState: 'Completed',
77
+ }));
78
+ if (action.payload.transactionSaveUpdates.anyBulkUpdates === true) {
79
+ actions.push((0, snackbarReducer_1.openSnackbar)({
80
+ messageSection: 'transactionDetails_updatingPastTransactions',
81
+ messageText: 'success',
82
+ type: 'success',
83
+ }));
84
+ }
85
+ else {
86
+ actions.push((0, snackbarReducer_1.openSnackbar)({
87
+ messageSection: 'transactionDetails_transactionUpdated',
88
+ messageText: 'success',
89
+ type: 'success',
90
+ }));
91
+ }
92
+ return (0, rxjs_1.from)(actions);
93
+ }
94
+ else {
95
+ return (0, rxjs_1.of)((0, transactionDetailReducer_1.updateStatusForTransactionNotMiscategorizedUpdate)(action.payload.transactionId, 'Error', response.status));
96
+ }
97
+ }), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, transactionDetailReducer_1.updateStatusForTransactionNotMiscategorizedUpdate)(action.payload.transactionId, 'Error', (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', `Transaction mark as reviewed REST API call errored out ${error}`)))));
98
+ }
15
99
  return zeniAPI
16
100
  .putAndGetJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/transactions/${action.payload.transactionId.id}/mark-miscategorized-as-correct`, preparePayload(action.payload.transactionId))
17
101
  .pipe((0, operators_1.mergeMap)((response) => {
@@ -19,44 +103,7 @@ const markTransactionAsNotMiscategorizedEpic = (actions$, state$, zeniAPI) => ac
19
103
  const actions = [];
20
104
  actions.push((0, transactionDetailReducer_1.updateStatusForTransactionNotMiscategorizedUpdate)(action.payload.transactionId, 'Completed', undefined));
21
105
  const transactionKey = (0, transactionDetailState_1.getTransactionDetailKey)(action.payload.transactionId);
22
- // Mark this transaction as not miscategorized anymore
23
106
  actions.push((0, transactionReducer_1.updateTransactionIsMiscategorized)(transactionKey, false));
24
- if (action.payload.isExpenseAutomationEnabled === true) {
25
- const { expenseAutomationViewState, expenseAutomationTransactionsViewState, } = state$.value;
26
- const { transactionCategorizationView, selectedTransactionCategorizationTab, } = expenseAutomationTransactionsViewState;
27
- const currentTenant = (0, tenantSelector_1.getCurrentTenant)(state$.value);
28
- const { selectedPeriodByTenantId } = expenseAutomationViewState;
29
- const selectedPeriod = selectedPeriodByTenantId[currentTenant.tenantId];
30
- const txnId = action.payload.transactionId.id;
31
- const autoTabLineItems = transactionCategorizationView.autoCategorized
32
- .transactionReviewLocalDataById[txnId]
33
- ?.transactionReviewLocalData.tabSpecificLineItems
34
- .autoCategorized ?? [];
35
- const hasAutoLines = autoTabLineItems.length > 0;
36
- const { uiState } = transactionCategorizationView[selectedTransactionCategorizationTab];
37
- const removeIdsFromPeriod = selectedTransactionCategorizationTab === 'review'
38
- ? [txnId]
39
- : hasAutoLines
40
- ? []
41
- : [txnId];
42
- actions.push((0, transactionsViewReducer_1.updateStatusForTransactionNotMiscategorizedUpdateForCategorization)(selectedTransactionCategorizationTab, [txnId], removeIdsFromPeriod, selectedPeriod, 'Completed'));
43
- if (!hasAutoLines) {
44
- actions.push((0, transactionReducer_1.removeTransactionCategorization)({
45
- transactionIds: [txnId],
46
- }));
47
- }
48
- if (removeIdsFromPeriod.length > 0) {
49
- const totalCountByPeriod = uiState.totalCount[(0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod)];
50
- const updatedTotalCount = totalCountByPeriod === 0
51
- ? totalCountByPeriod
52
- : totalCountByPeriod - 1;
53
- actions.push((0, transactionsViewReducer_1.updateTotalCountForTransactionCategorization)({
54
- selectedPeriod,
55
- selectedTab: selectedTransactionCategorizationTab,
56
- totalCount: updatedTotalCount,
57
- }));
58
- }
59
- }
60
107
  actions.push((0, transactionDetailReducer_1.updateStatusConsolidatedSaveTransactionDetail)({
61
108
  transactionId: action.payload.transactionId,
62
109
  fetchState: 'Completed',
@@ -69,7 +116,6 @@ const markTransactionAsNotMiscategorizedEpic = (actions$, state$, zeniAPI) => ac
69
116
  }));
70
117
  }
71
118
  else {
72
- //Finally everything done for this transaction
73
119
  actions.push((0, snackbarReducer_1.openSnackbar)({
74
120
  messageSection: 'transactionDetails_transactionUpdated',
75
121
  messageText: 'success',
@@ -16,7 +16,6 @@ const transactionPayload_1 = require("../../../entity/transaction/payloadTypes/t
16
16
  const vendorTransactionPayload_1 = require("../../../entity/transaction/payloadTypes/vendorTransactionPayload");
17
17
  const customerTransaction_1 = require("../../../entity/transaction/stateTypes/customerTransaction");
18
18
  const otherTransaction_1 = require("../../../entity/transaction/stateTypes/otherTransaction");
19
- const transactionType_1 = require("../../../entity/transaction/stateTypes/transactionType");
20
19
  const vendorTransaction_1 = require("../../../entity/transaction/stateTypes/vendorTransaction");
21
20
  const toSupportedTransaction_1 = __importDefault(require("../../../entity/transaction/toSupportedTransaction"));
22
21
  const transactionHelper_1 = require("../../../entity/transaction/transactionHelper");
@@ -31,7 +30,7 @@ const transactionCategorizationLocalDataHelper_1 = require("../../expenseAutomat
31
30
  const transactionsViewReducer_1 = require("../../expenseAutomationView/reducers/transactionsViewReducer");
32
31
  const transactionDetailReducer_1 = require("../transactionDetailReducer");
33
32
  const transactionDetailState_1 = require("../transactionDetailState");
34
- const saveTransactionHelper_1 = require("./saveTransactionHelper");
33
+ const transactionDetailLocalDataHelper_1 = require("../transactionDetailLocalDataHelper");
35
34
  const updateTransactionDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(transactionDetailReducer_1.updateTransactionDetail.match), (0, operators_1.mergeMap)((action) => {
36
35
  const { transactionState, vendorState, transactionDetailState } = state$.value;
37
36
  const isAccountingProjectsEnabled = (0, tenantSelector_1.getIsAccountingProjectsEnabled)(state$.value);
@@ -42,24 +41,6 @@ const updateTransactionDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe
42
41
  if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
43
42
  const transactionIdResp = (0, transactionIDPayload_1.toTransactionID)(response.data.transaction);
44
43
  const transactionDetailKeyForResponse = (0, transactionDetailState_1.getTransactionDetailKey)(transactionIdResp);
45
- const tranDetail = (0, get_1.default)(transactionDetailState.transactionDetailById, transactionDetailKeyForResponse, undefined);
46
- const tranVendorUpdates = tranDetail?.transactionDetailLocalData.vendorUpdates;
47
- let vendorUpdates = undefined;
48
- if (tranVendorUpdates?.qboId != null) {
49
- const vendor = (0, vendorSelector_1.getVendorByVendorId)(vendorState, tranVendorUpdates.qboId);
50
- if (vendor != null) {
51
- vendorUpdates = {
52
- id: vendor?.qboId,
53
- name: vendor?.name,
54
- };
55
- }
56
- else if (tranVendorUpdates.name != null) {
57
- vendorUpdates = {
58
- id: tranVendorUpdates.qboId,
59
- name: tranVendorUpdates.name,
60
- };
61
- }
62
- }
63
44
  const actions = [];
64
45
  if (response.data.vendors != null) {
65
46
  actions.push((0, vendorReducer_1.updateVendors)(response.data.vendors, 'merge'));
@@ -72,30 +53,14 @@ const updateTransactionDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe
72
53
  vendorId: response.data.vendors?.[0]?.vendor_id,
73
54
  }));
74
55
  actions.push((0, transactionDetailReducer_1.updateTransactionDetailSaveStatus)(transactionIdResp, 'Completed'));
75
- let callMarkTransactionAsNotMiscategorized = true;
76
- // NOTE: If tran is vendorType OR JE OR Deposit transaction and there is no vendor chosen on this tranaction while saving,
77
- // then we need to skip mark-miscategorized-as-correct api call
78
- if ((0, transactionType_1.toVendorTransactionTypeStrict)(response.data.transaction.transaction_type) != null ||
79
- (0, transactionType_1.toJEAndDepositTransactionTypeStrict)(response.data.transaction.transaction_type) != null) {
80
- const isAccountingClassesEnabled = (0, tenantSelector_1.getIsAccountingClassesEnabled)(state$.value);
81
- const isAnyClassMissing = isAccountingClassesEnabled &&
82
- checkIfClassMissingForAnyLineItem(response.data.transaction);
83
- const isVendorOrCustomerMissingInAnyLine = (0, transactionType_1.toJEAndDepositTransactionTypeStrict)(response.data.transaction.transaction_type) != null &&
84
- checkIfVendorOrCustomerMissingForAnyLineItem(response.data.transaction);
85
- if ((vendorUpdates?.id == null &&
86
- isVendorOrCustomerMissingInAnyLine == true) ||
87
- isAnyClassMissing == true) {
88
- callMarkTransactionAsNotMiscategorized = false;
89
- }
90
- }
91
56
  const uncategorizedIncomeExpense = (0, accountListSelector_1.getUncategorizedAccounts)(state$.value.accountState, state$.value.accountListState, 'accountList');
92
57
  const transaction = (0, toSupportedTransaction_1.default)(response.data.transaction);
93
- const isTransactionUnCategorized = (0, saveTransactionHelper_1.isTransactionUncategorized)(transaction, uncategorizedIncomeExpense);
58
+ const isAccountingClassesEnabled = (0, tenantSelector_1.getIsAccountingClassesEnabled)(state$.value);
59
+ const eligibleLineIds = getLineIdsEligibleForReview(transaction, uncategorizedIncomeExpense, isAccountingClassesEnabled);
94
60
  if (action.payload.transactionSaveUpdates.markAsMiscatUpdates ===
95
61
  true &&
96
- callMarkTransactionAsNotMiscategorized === true &&
97
- isTransactionUnCategorized === false) {
98
- actions.push((0, transactionDetailReducer_1.markTransactionAsNotMiscategorized)(transactionIdResp, action.payload.transactionSaveUpdates, action.payload.isExpenseAutomationEnabled));
62
+ eligibleLineIds.length > 0) {
63
+ actions.push((0, transactionDetailReducer_1.markTransactionAsNotMiscategorized)(transactionIdResp, action.payload.transactionSaveUpdates, action.payload.isExpenseAutomationEnabled, eligibleLineIds));
99
64
  }
100
65
  else {
101
66
  actions.push((0, transactionDetailReducer_1.updateStatusConsolidatedSaveTransactionDetail)({
@@ -307,31 +272,25 @@ const toTransactionUpdatePayload = (updates, detail, cotTransactionTracking, isA
307
272
  return (0, transactionPayload_1.injectCOTFlagsIntoTransactionPayload)(basePayload, transaction);
308
273
  };
309
274
  exports.toTransactionUpdatePayload = toTransactionUpdatePayload;
310
- const checkIfClassMissingForAnyLineItem = (transactionPayload) => {
311
- let isAnyClassMissing = false;
312
- if (transactionPayload.lines != null) {
313
- transactionPayload.lines.forEach((line) => {
314
- if (line.line_detail?.class
315
- ?.accounting_class_id == null) {
316
- isAnyClassMissing = true;
317
- }
318
- });
319
- }
320
- return isAnyClassMissing;
321
- };
322
- const checkIfVendorOrCustomerMissingForAnyLineItem = (transactionPayload) => {
323
- let isAnyVendorOrCustomerMissing = false;
324
- if (transactionPayload.lines != null) {
325
- transactionPayload.lines.forEach((line) => {
326
- if (line.line_detail
327
- ?.vendor == null &&
328
- line.line_detail
329
- ?.customer == null) {
330
- isAnyVendorOrCustomerMissing = true;
331
- }
332
- });
333
- }
334
- return isAnyVendorOrCustomerMissing;
275
+ const getLineIdsEligibleForReview = (transaction, uncategorizedIncomeExpense, isAccountingClassesEnabled) => {
276
+ return (transaction.lines ?? [])
277
+ .filter((line) => {
278
+ if (line.type !== 'transaction_with_account_and_class_line' &&
279
+ line.type !== 'journal_entry_transaction_line' &&
280
+ line.type !== 'transaction_with_product_or_service_line') {
281
+ return false;
282
+ }
283
+ const typedLine = line;
284
+ const hasCategory = !(0, transactionDetailLocalDataHelper_1.isAccountUncategorized)(uncategorizedIncomeExpense, typedLine.account.qboId);
285
+ const hasClass = !isAccountingClassesEnabled || typedLine.class != null;
286
+ const hasAmount = (typedLine.amount?.amount ?? 0) !== 0;
287
+ const hasEntity = line.type !== 'journal_entry_transaction_line' ||
288
+ typedLine.vendor != null ||
289
+ typedLine.customer != null;
290
+ return hasCategory && hasClass && hasAmount && hasEntity;
291
+ })
292
+ .map((line) => line.id)
293
+ .filter(Boolean);
335
294
  };
336
295
  // Converts a locally-created line item (from edit mode) into the wire shape
337
296
  // expected by the categorize endpoint. Sending id='' signals the backend to
@@ -38,10 +38,11 @@ export declare const downloadAccountingProviderAttachment: import("@reduxjs/tool
38
38
  error: ZeniAPIStatus<Record<string, unknown>> | undefined;
39
39
  }, "transactionDetail/updateTransactionDetailSaveStatus", never, never>, clearTransactionDetailSaveStatus: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[transactionId: TransactionID], {
40
40
  transactionId: TransactionID;
41
- }, "transactionDetail/clearTransactionDetailSaveStatus", never, never>, markTransactionAsNotMiscategorized: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[transactionId: TransactionID, transactionSaveUpdates: TransactionSaveUpdates, isExpenseAutomationEnabled: boolean], {
41
+ }, "transactionDetail/clearTransactionDetailSaveStatus", never, never>, markTransactionAsNotMiscategorized: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[transactionId: TransactionID, transactionSaveUpdates: TransactionSaveUpdates, isExpenseAutomationEnabled: boolean, lineIds?: string[] | undefined], {
42
42
  transactionId: TransactionID;
43
43
  transactionSaveUpdates: TransactionSaveUpdates;
44
44
  isExpenseAutomationEnabled: boolean;
45
+ lineIds: string[] | undefined;
45
46
  }, "transactionDetail/markTransactionAsNotMiscategorized", never, never>, deleteTransactionAttachment: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[attachmentFileName: string, transactionId: TransactionID], {
46
47
  attachmentFileName: string;
47
48
  transactionId: TransactionID;
@@ -197,12 +197,13 @@ const transactionDetail = (0, toolkit_1.createSlice)({
197
197
  };
198
198
  }
199
199
  },
200
- prepare(transactionId, transactionSaveUpdates, isExpenseAutomationEnabled) {
200
+ prepare(transactionId, transactionSaveUpdates, isExpenseAutomationEnabled, lineIds) {
201
201
  return {
202
202
  payload: {
203
203
  transactionId,
204
204
  transactionSaveUpdates,
205
205
  isExpenseAutomationEnabled,
206
+ lineIds,
206
207
  },
207
208
  };
208
209
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.1.48",
3
+ "version": "5.1.50-beta3RR",
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",