@zeniai/client-epic-state 5.2.35-beta0MM → 5.2.35-beta10MM

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 (89) hide show
  1. package/lib/entity/actualMatching/actualMatchingPayload.d.ts +7 -1
  2. package/lib/entity/actualMatching/actualMatchingPayload.js +19 -12
  3. package/lib/entity/actualMatching/actualMatchingReducer.d.ts +3 -2
  4. package/lib/entity/actualMatching/actualMatchingReducer.js +3 -3
  5. package/lib/entity/actualMatching/actualMatchingState.d.ts +3 -2
  6. package/lib/entity/aiCfo/aiCfoReducer.js +78 -4
  7. package/lib/entity/aiCfo/aiCfoState.d.ts +16 -0
  8. package/lib/entity/jeSchedules/jeSchedulesPayload.d.ts +2 -0
  9. package/lib/entity/jeSchedules/jeSchedulesPayload.js +4 -0
  10. package/lib/entity/jeSchedules/jeSchedulesState.d.ts +2 -0
  11. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  12. package/lib/entity/snackbar/snackbarTypes.js +2 -0
  13. package/lib/esm/entity/actualMatching/actualMatchingPayload.js +18 -12
  14. package/lib/esm/entity/actualMatching/actualMatchingReducer.js +3 -3
  15. package/lib/esm/entity/aiCfo/aiCfoReducer.js +79 -5
  16. package/lib/esm/entity/jeSchedules/jeSchedulesPayload.js +4 -0
  17. package/lib/esm/entity/snackbar/snackbarTypes.js +2 -0
  18. package/lib/esm/index.js +6 -6
  19. package/lib/esm/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingEpic.js +3 -5
  20. package/lib/esm/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingMatchEpic.js +3 -2
  21. package/lib/esm/view/expenseAutomationView/epics/actualMatching/matchAndReverseAccrualsEpic.js +80 -25
  22. package/lib/esm/view/expenseAutomationView/epics/actualMatching/refreshActualMatchingList.js +16 -0
  23. package/lib/esm/view/expenseAutomationView/epics/actualMatching/searchMatchAccrualsEpic.js +16 -6
  24. package/lib/esm/view/expenseAutomationView/epics/actualMatching/searchMatchTransactionsEpic.js +12 -11
  25. package/lib/esm/view/expenseAutomationView/epics/actualMatching/undoReversalAccrualsEpic.js +50 -26
  26. package/lib/esm/view/expenseAutomationView/epics/common/fetchAllExpenseAutomationTabsEpic.js +5 -9
  27. package/lib/esm/view/expenseAutomationView/epics/common/refreshExpenseAutomationCurrentTabEpic.js +1 -9
  28. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.js +13 -3
  29. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.js +34 -16
  30. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/switchJESchedulesTabEpic.js +8 -8
  31. package/lib/esm/view/expenseAutomationView/expenseAutomationViewReducer.js +2 -1
  32. package/lib/esm/view/expenseAutomationView/expenseAutomationViewSelector.js +5 -2
  33. package/lib/esm/view/expenseAutomationView/expenseAutomationViewState.js +4 -2
  34. package/lib/esm/view/expenseAutomationView/payload/actualMatchingPayload.js +10 -10
  35. package/lib/esm/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +90 -20
  36. package/lib/esm/view/expenseAutomationView/reducers/jeSchedulesViewReducer.js +2 -1
  37. package/lib/esm/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +206 -61
  38. package/lib/esm/view/expenseAutomationView/selectors/jeSchedulesViewSelector.js +1 -0
  39. package/lib/esm/view/expenseAutomationView/types/actualMatchingViewState.js +3 -0
  40. package/lib/esm/view/expenseAutomationView/types/jeSchedulesViewState.js +3 -0
  41. package/lib/esm/view/financeStatement/financeStatementReducer.js +17 -27
  42. package/lib/esm/view/spendManagement/billPay/billList/billListSelector.js +3 -3
  43. package/lib/index.d.ts +7 -6
  44. package/lib/index.js +52 -48
  45. package/lib/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingEpic.js +3 -5
  46. package/lib/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingMatchEpic.d.ts +1 -1
  47. package/lib/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingMatchEpic.js +3 -2
  48. package/lib/view/expenseAutomationView/epics/actualMatching/matchAndReverseAccrualsEpic.d.ts +2 -1
  49. package/lib/view/expenseAutomationView/epics/actualMatching/matchAndReverseAccrualsEpic.js +77 -22
  50. package/lib/view/expenseAutomationView/epics/actualMatching/refreshActualMatchingList.d.ts +3 -0
  51. package/lib/view/expenseAutomationView/epics/actualMatching/refreshActualMatchingList.js +19 -0
  52. package/lib/view/expenseAutomationView/epics/actualMatching/searchMatchAccrualsEpic.d.ts +3 -2
  53. package/lib/view/expenseAutomationView/epics/actualMatching/searchMatchAccrualsEpic.js +15 -5
  54. package/lib/view/expenseAutomationView/epics/actualMatching/searchMatchTransactionsEpic.js +10 -9
  55. package/lib/view/expenseAutomationView/epics/actualMatching/undoReversalAccrualsEpic.d.ts +2 -1
  56. package/lib/view/expenseAutomationView/epics/actualMatching/undoReversalAccrualsEpic.js +48 -24
  57. package/lib/view/expenseAutomationView/epics/common/fetchAllExpenseAutomationTabsEpic.d.ts +1 -2
  58. package/lib/view/expenseAutomationView/epics/common/fetchAllExpenseAutomationTabsEpic.js +5 -9
  59. package/lib/view/expenseAutomationView/epics/common/refreshExpenseAutomationCurrentTabEpic.d.ts +1 -2
  60. package/lib/view/expenseAutomationView/epics/common/refreshExpenseAutomationCurrentTabEpic.js +1 -9
  61. package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.d.ts +2 -1
  62. package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.js +13 -3
  63. package/lib/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.d.ts +5 -1
  64. package/lib/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.js +33 -15
  65. package/lib/view/expenseAutomationView/epics/jeSchedule/switchJESchedulesTabEpic.d.ts +1 -2
  66. package/lib/view/expenseAutomationView/epics/jeSchedule/switchJESchedulesTabEpic.js +8 -8
  67. package/lib/view/expenseAutomationView/expenseAutomationViewReducer.d.ts +2 -1
  68. package/lib/view/expenseAutomationView/expenseAutomationViewReducer.js +2 -1
  69. package/lib/view/expenseAutomationView/expenseAutomationViewSelector.js +5 -2
  70. package/lib/view/expenseAutomationView/expenseAutomationViewState.d.ts +2 -1
  71. package/lib/view/expenseAutomationView/expenseAutomationViewState.js +6 -3
  72. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  73. package/lib/view/expenseAutomationView/payload/actualMatchingPayload.d.ts +3 -3
  74. package/lib/view/expenseAutomationView/payload/actualMatchingPayload.js +10 -10
  75. package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.d.ts +21 -16
  76. package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +90 -20
  77. package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.d.ts +2 -1
  78. package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.js +2 -1
  79. package/lib/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.d.ts +13 -9
  80. package/lib/view/expenseAutomationView/selectorTypes/expenseAutomationViewSelectorTypes.d.ts +9 -0
  81. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.d.ts +11 -2
  82. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +209 -60
  83. package/lib/view/expenseAutomationView/selectors/jeSchedulesViewSelector.js +1 -0
  84. package/lib/view/expenseAutomationView/types/actualMatchingViewState.d.ts +8 -2
  85. package/lib/view/expenseAutomationView/types/actualMatchingViewState.js +4 -0
  86. package/lib/view/expenseAutomationView/types/jeSchedulesViewState.js +3 -0
  87. package/lib/view/financeStatement/financeStatementReducer.js +17 -27
  88. package/lib/view/spendManagement/billPay/billList/billListSelector.js +2 -2
  89. package/package.json +1 -1
@@ -2,6 +2,10 @@ import { ID } from '../../commonStateTypes/common';
2
2
  import { JEOneTimeAccrualPayload } from '../jeSchedules/jeSchedulesPayload';
3
3
  import { ActualMatchingMatch } from './actualMatchingState';
4
4
  import { ActualMatchingMatchId } from './actualMatchingTypes';
5
+ export interface DisplayCurrencyFallback {
6
+ currencyCode: string;
7
+ currencySymbol: string;
8
+ }
5
9
  export interface ActualMatchingMatchPayload {
6
10
  accrued_amount: number;
7
11
  actual_amount: number;
@@ -12,6 +16,7 @@ export interface ActualMatchingMatchPayload {
12
16
  transaction_type: string;
13
17
  currency_code?: string | null;
14
18
  currency_symbol?: string | null;
19
+ leftovers?: JEOneTimeAccrualPayload[];
15
20
  match_shape?: string | null;
16
21
  match_tier?: string | null;
17
22
  transaction_date?: string | null;
@@ -22,4 +27,5 @@ export interface ActualMatchingListResponsePayload {
22
27
  matches: ActualMatchingMatchPayload[];
23
28
  total: number;
24
29
  }
25
- export declare function toActualMatchingMatch(payload: ActualMatchingMatchPayload): ActualMatchingMatch;
30
+ export declare function withInheritedAccrualCurrency(member: JEOneTimeAccrualPayload, fallbackCurrencyCode: string): JEOneTimeAccrualPayload;
31
+ export declare function toActualMatchingMatch(payload: ActualMatchingMatchPayload, fallbackCurrency?: DisplayCurrencyFallback): ActualMatchingMatch;
@@ -1,30 +1,36 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withInheritedAccrualCurrency = withInheritedAccrualCurrency;
3
4
  exports.toActualMatchingMatch = toActualMatchingMatch;
4
5
  const amount_1 = require("../../commonStateTypes/amount");
6
+ const stringToUnion_1 = require("../../commonStateTypes/stringToUnion");
5
7
  const jeSchedulesPayload_1 = require("../jeSchedules/jeSchedulesPayload");
6
- const USD_CURRENCY_CODE = 'USD';
7
- const USD_CURRENCY_SYMBOL = '$';
8
8
  const MATCH_KINDS = [
9
9
  'auto_reversed',
10
10
  'pending_review',
11
11
  'manually_reversed',
12
12
  ];
13
13
  function toMatchKind(raw) {
14
- if (MATCH_KINDS.includes(raw)) {
15
- return raw;
14
+ return (0, stringToUnion_1.stringToUnion)(raw, MATCH_KINDS);
15
+ }
16
+ function withInheritedAccrualCurrency(member, fallbackCurrencyCode) {
17
+ if (member.currency != null && member.currency !== '') {
18
+ return member;
16
19
  }
17
- console.error(`Unknown ActualMatchingMatchKind: ${raw}`);
18
- return 'auto_reversed';
20
+ return { ...member, currency: fallbackCurrencyCode };
21
+ }
22
+ function toMatchMemberAccrual(member, matchCurrencyCode) {
23
+ return (0, jeSchedulesPayload_1.toJEOneTimeAccrual)(withInheritedAccrualCurrency(member, matchCurrencyCode));
19
24
  }
20
- function toActualMatchingMatch(payload) {
25
+ function toActualMatchingMatch(payload, fallbackCurrency) {
21
26
  const currencyCode = payload.currency_code != null && payload.currency_code !== ''
22
27
  ? payload.currency_code
23
- : USD_CURRENCY_CODE;
28
+ : fallbackCurrency?.currencyCode ?? '';
24
29
  const currencySymbol = payload.currency_symbol != null && payload.currency_symbol !== ''
25
30
  ? payload.currency_symbol
26
- : currencyCode === USD_CURRENCY_CODE
27
- ? USD_CURRENCY_SYMBOL
31
+ : fallbackCurrency != null &&
32
+ currencyCode === fallbackCurrency.currencyCode
33
+ ? fallbackCurrency.currencySymbol
28
34
  : currencyCode;
29
35
  return {
30
36
  matchId: payload.match_id,
@@ -32,12 +38,13 @@ function toActualMatchingMatch(payload) {
32
38
  transactionIntegrationId: payload.transaction_integration_id,
33
39
  transactionDate: payload.transaction_date ?? undefined,
34
40
  vendorId: payload.vendor_id ?? undefined,
35
- vendorName: payload.vendor_name ?? '',
41
+ vendorName: payload.vendor_name ?? undefined,
36
42
  kind: toMatchKind(payload.kind),
37
43
  matchTier: payload.match_tier ?? undefined,
38
44
  matchShape: payload.match_shape ?? undefined,
39
45
  accruedAmount: (0, amount_1.toAmount)(payload.accrued_amount, currencyCode, currencySymbol),
40
46
  actualAmount: (0, amount_1.toAmount)(payload.actual_amount, currencyCode, currencySymbol),
41
- members: payload.members.map(jeSchedulesPayload_1.toJEOneTimeAccrual),
47
+ members: payload.members.map((member) => toMatchMemberAccrual(member, currencyCode)),
48
+ leftovers: (payload.leftovers ?? []).map((member) => toMatchMemberAccrual(member, currencyCode)),
42
49
  };
43
50
  }
@@ -1,8 +1,9 @@
1
- import { ActualMatchingMatchPayload } from './actualMatchingPayload';
1
+ import { ActualMatchingMatchPayload, DisplayCurrencyFallback } from './actualMatchingPayload';
2
2
  import { ActualMatchingState } from './actualMatchingState';
3
3
  export declare const initialState: ActualMatchingState;
4
- export declare const updateActualMatchingMatches: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[matches: ActualMatchingMatchPayload[]], {
4
+ export declare const updateActualMatchingMatches: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[matches: ActualMatchingMatchPayload[], fallbackCurrency?: DisplayCurrencyFallback | undefined], {
5
5
  matches: ActualMatchingMatchPayload[];
6
+ fallbackCurrency: DisplayCurrencyFallback | undefined;
6
7
  }, "actualMatching/updateActualMatchingMatches", never, never>;
7
8
  declare const _default: import("redux").Reducer<ActualMatchingState>;
8
9
  export default _default;
@@ -12,12 +12,12 @@ const actualMatching = (0, toolkit_1.createSlice)({
12
12
  initialState: exports.initialState,
13
13
  reducers: {
14
14
  updateActualMatchingMatches: {
15
- prepare(matches) {
16
- return { payload: { matches } };
15
+ prepare(matches, fallbackCurrency) {
16
+ return { payload: { matches, fallbackCurrency } };
17
17
  },
18
18
  reducer(draft, action) {
19
19
  action.payload.matches.forEach((payload) => {
20
- const match = (0, actualMatchingPayload_1.toActualMatchingMatch)(payload);
20
+ const match = (0, actualMatchingPayload_1.toActualMatchingMatch)(payload, action.payload.fallbackCurrency);
21
21
  draft.byMatchId[match.matchId] = match;
22
22
  });
23
23
  },
@@ -9,13 +9,14 @@ export interface ActualMatchingMatch {
9
9
  kind: ActualMatchingMatchKind;
10
10
  matchId: ActualMatchingMatchId;
11
11
  members: JEOneTimeAccrualState[];
12
- transactionIntegrationId: string;
12
+ transactionIntegrationId: ID;
13
13
  transactionType: string;
14
- vendorName: string;
14
+ leftovers?: JEOneTimeAccrualState[];
15
15
  matchShape?: string;
16
16
  matchTier?: string;
17
17
  transactionDate?: string;
18
18
  vendorId?: ID;
19
+ vendorName?: string;
19
20
  }
20
21
  export interface ActualMatchingState {
21
22
  byMatchId: Record<ActualMatchingMatchId, ActualMatchingMatch>;
@@ -7,6 +7,7 @@ const rootActions_1 = require("../../rootActions");
7
7
  const zeniDayJS_1 = require("../../zeniDayJS");
8
8
  const aiCfoState_1 = require("./aiCfoState");
9
9
  exports.initialAiCfoState = {
10
+ deletedChatSessionIds: [],
10
11
  aiCfoByChatSessionId: {},
11
12
  partialQuestionAnswers: {},
12
13
  syntheticAnswersByChatSessionId: {},
@@ -474,6 +475,11 @@ const toResponseBlockType = (answer, userId) => {
474
475
  createdAt: (0, zeniDayJS_1.date)(timestamp),
475
476
  };
476
477
  };
478
+ // How many deleted session ids to remember. The tombstone only has to outlive
479
+ // an in-flight history request, which is seconds, so this is generous. Capped
480
+ // because `clearSession` would otherwise grow the array for the life of the
481
+ // store and every `setChatHistory` scans it.
482
+ const DELETED_SESSION_MEMORY = 100;
477
483
  const aiCfo = (0, toolkit_1.createSlice)({
478
484
  name: 'aiCfo',
479
485
  initialState: exports.initialAiCfoState,
@@ -495,8 +501,25 @@ const aiCfo = (0, toolkit_1.createSlice)({
495
501
  setSessions(draft, action) {
496
502
  action.payload.forEach((chatSessionPayload) => {
497
503
  const { chat_session_id } = chatSessionPayload;
498
- // This is to prevent overwriting the session if it is already in state
499
- if (draft.aiCfoByChatSessionId[chat_session_id] == null) {
504
+ if (draft.aiCfoByChatSessionId[chat_session_id] != null) {
505
+ // The list is authoritative for a session's metadata, and a session
506
+ // seeded by `setChatHistory` has guessed metadata: no summary, and a
507
+ // `createdAt` taken from one page of a newest-first paginated
508
+ // history, which is later than the session really started.
509
+ // `questionAnswers` is left alone — the list must never wipe a
510
+ // thread that is on screen.
511
+ const entry = draft.aiCfoByChatSessionId[chat_session_id];
512
+ const incoming = toChatSession(chatSessionPayload);
513
+ entry.chatSession = {
514
+ ...incoming,
515
+ // Never downgrade a title already in state: a page of the list can
516
+ // carry a nullish summary, and assigning it wholesale would blank
517
+ // a header that was correct.
518
+ chatSessionSummary: incoming.chatSessionSummary ??
519
+ entry.chatSession.chatSessionSummary,
520
+ };
521
+ }
522
+ else {
500
523
  draft.aiCfoByChatSessionId[chat_session_id] = {
501
524
  chatSession: toChatSession(chatSessionPayload),
502
525
  questionAnswers: [],
@@ -509,8 +532,52 @@ const aiCfo = (0, toolkit_1.createSlice)({
509
532
  setChatHistory(draft, action) {
510
533
  const { chatSessionId, history, isPaginationComplete = false, } = action.payload;
511
534
  if (draft.aiCfoByChatSessionId[chatSessionId] == null) {
512
- console.warn(`session with id ${chatSessionId} not found in setChatHistory`);
513
- return;
535
+ // Sessions created server-side (a routine run) are opened by deep
536
+ // link, so their history can arrive before the sessions list has
537
+ // registered them. Dropping it here loses the messages for good: the
538
+ // response's null page token sets `hasMore: false`, which is the
539
+ // condition the refetch is guarded on. The response is itself proof
540
+ // the session exists, so seed the entry from the messages.
541
+ if (history.length === 0) {
542
+ return;
543
+ }
544
+ // A deleted session. Its history request is never cancelled
545
+ // (`mergeMap`), so seeding would resurrect the conversation into the
546
+ // rail, where clicking it 404s. Only this path is guarded:
547
+ // `clearSession` is optimistic, so if the server still lists the
548
+ // session, `setSessions` re-adding it is correct.
549
+ if (draft.deletedChatSessionIds?.includes(chatSessionId) === true) {
550
+ return;
551
+ }
552
+ // The human's id, taken off a message they actually sent. The API
553
+ // returns newest first, so `history[0]` is usually the agent's reply,
554
+ // and `chatSession.userId` is read as "whose conversation is this".
555
+ const owner = history.find((message) => message.sender === 'user') ?? history[0];
556
+ // Oldest message in the page, not `history[0]`: a session cannot
557
+ // start after its own messages, and the API returns newest first.
558
+ //
559
+ // Compared as instants, not strings. The wire format is an RFC 1123
560
+ // HTTP-date ("Thu, 03 Sep 2026 12:34:56 GMT") because `set_200`
561
+ // bypasses chat's isoformat encoder, and lexically "Mon…" precedes
562
+ // "Sat…" while being the later day. Unparseable values are filtered
563
+ // out, not compared: every comparison against NaN is false, so one
564
+ // used as the reduce's seed would carry through to an Invalid Date.
565
+ const oldest = history
566
+ .map((message) => ({ at: Date.parse(message.created_at), message }))
567
+ .filter(({ at }) => !Number.isNaN(at))
568
+ .reduce((min, entry) => (min == null || entry.at < min.at ? entry : min), undefined)?.message;
569
+ draft.aiCfoByChatSessionId[chatSessionId] = {
570
+ chatSession: {
571
+ chatSessionId,
572
+ userId: owner.user_id,
573
+ chatSessionSummary: undefined,
574
+ // Nothing in the page carried a usable timestamp. "Now" is a
575
+ // guess, but it is a sane one and it keeps the row out of the
576
+ // Invalid-Date behaviour above; the sessions list corrects it.
577
+ createdAt: oldest != null ? (0, zeniDayJS_1.date)(oldest.created_at) : (0, zeniDayJS_1.dateNow)(),
578
+ },
579
+ questionAnswers: [],
580
+ };
514
581
  }
515
582
  const session = draft.aiCfoByChatSessionId[chatSessionId];
516
583
  // Check if we have an existing partial Q&A pair for this session
@@ -790,6 +857,13 @@ const aiCfo = (0, toolkit_1.createSlice)({
790
857
  },
791
858
  clearSession(draft, action) {
792
859
  const sessionId = action.payload;
860
+ draft.deletedChatSessionIds ?? (draft.deletedChatSessionIds = []);
861
+ if (!draft.deletedChatSessionIds.includes(sessionId)) {
862
+ draft.deletedChatSessionIds.push(sessionId);
863
+ if (draft.deletedChatSessionIds.length > DELETED_SESSION_MEMORY) {
864
+ draft.deletedChatSessionIds.splice(0, draft.deletedChatSessionIds.length - DELETED_SESSION_MEMORY);
865
+ }
866
+ }
793
867
  delete draft.aiCfoByChatSessionId[sessionId];
794
868
  delete draft.syntheticAnswersByChatSessionId[sessionId];
795
869
  if (draft.partialQuestionAnswers?.[sessionId] != null) {
@@ -241,4 +241,20 @@ export interface AiCfoState {
241
241
  aiCfoByChatSessionId: Record<ID, ChatSessionWithMessages>;
242
242
  partialQuestionAnswers: Record<ID, AiCfoQuestionWithAnswer | undefined>;
243
243
  syntheticAnswersByChatSessionId: Record<ID, SyntheticAiCfoAnswer[]>;
244
+ /**
245
+ * Sessions this client has deleted. `setChatHistory` seeds an entry for a
246
+ * session it has not seen, so without a tombstone an in-flight history
247
+ * response arriving after the delete would recreate the whole conversation
248
+ * and put the deleted chat back in the rail. `fetchChatHistoryEpic` uses
249
+ * `mergeMap`, so that request is never cancelled. Same race, and the same
250
+ * reasoning, as `deletedScheduleIds` on the view slice. Bounded — it only
251
+ * needs to outlive an in-flight request.
252
+ *
253
+ * Optional because `AiCfoState` is public API of a package four apps
254
+ * consume: making it required turns a bug fix into a compile break for
255
+ * every literal of this type, in this repo and in theirs. The reducer's
256
+ * initial state always sets it, so it is only ever absent on a
257
+ * hand-built literal — which is exactly the case that must keep working.
258
+ */
259
+ deletedChatSessionIds?: ID[];
244
260
  }
@@ -131,6 +131,7 @@ export interface JEOneTimeAccrualClearedPayload {
131
131
  accrual_ids?: string[] | null;
132
132
  consumed_line_ids?: string[] | null;
133
133
  group_sum?: number | null;
134
+ intended_accrual_ids?: string[] | null;
134
135
  match_shape?: string | null;
135
136
  match_tier?: string | null;
136
137
  matched_amount?: number | null;
@@ -183,6 +184,7 @@ export interface JEOneTimeAccrualPayload {
183
184
  posting_date?: string | null;
184
185
  profile_snapshot?: JEOneTimeAccrualProfileSnapshotPayload | null;
185
186
  reversal_policy?: string | null;
187
+ reversal_posting_date?: string | null;
186
188
  reversal_transaction_id?: ID | null;
187
189
  reversal_trigger?: string | null;
188
190
  reversed_by_user?: UserPayload | null;
@@ -472,6 +472,7 @@ function toJEOneTimeAccrualCleared(payload, currencyCode) {
472
472
  ? toOneTimeAccrualAmount(payload.group_sum, currencyCode)
473
473
  : undefined,
474
474
  accrualIds: payload.accrual_ids ?? undefined,
475
+ intendedAccrualIds: payload.intended_accrual_ids ?? undefined,
475
476
  reversedAt: payload.reversed_at != null ? (0, zeniDayJS_1.date)(payload.reversed_at) : undefined,
476
477
  matchedTransactionDate: payload.matched_transaction_date != null
477
478
  ? (0, zeniDayJS_1.date)(payload.matched_transaction_date)
@@ -534,6 +535,9 @@ function toJEOneTimeAccrual(payload) {
534
535
  actualReversalPostDate: payload.actual_reversal_post_date != null
535
536
  ? (0, zeniDayJS_1.date)(payload.actual_reversal_post_date)
536
537
  : undefined,
538
+ reversalPostingDate: payload.reversal_posting_date != null
539
+ ? (0, zeniDayJS_1.date)(payload.reversal_posting_date)
540
+ : undefined,
537
541
  jeDebitAccountId: payload.je_debit_account_id ?? undefined,
538
542
  jeDebitAccountName: payload.je_debit_account_name ?? undefined,
539
543
  jeDebitAccountingClassId: payload.je_debit_accounting_class_id ?? undefined,
@@ -209,6 +209,7 @@ export interface JEOneTimeAccrualCleared {
209
209
  accrualIds?: ID[];
210
210
  consumedLineIds?: string[];
211
211
  groupSum?: Amount;
212
+ intendedAccrualIds?: ID[];
212
213
  matchedAmount?: Amount;
213
214
  matchedLineId?: string;
214
215
  matchedTransactionDate?: ZeniDate;
@@ -265,6 +266,7 @@ export interface JEOneTimeAccrualState {
265
266
  matchSuggestion?: JEOneTimeAccrualMatchSuggestion;
266
267
  postingDate?: ZeniDate;
267
268
  profileSnapshot?: JEOneTimeAccrualProfileSnapshot;
269
+ reversalPostingDate?: ZeniDate;
268
270
  reversalTransactionId?: ID;
269
271
  reversalTrigger?: OneTimeAccrualReversalTriggerType;
270
272
  reversedBy?: JEOneTimeAccrualCreator;
@@ -1,4 +1,4 @@
1
- export declare const toSnackbarMessageSection: (v: string) => "common" | "transactionDetails_updatingPastTransactions" | "transactionDetails_transactionUpdated" | "cockpit_month_end_email_sent" | "cockpit_month_end_email_save" | "cockpit_month_end_email_attachment_upload" | "je_bill_link" | "je_posted" | "audit_score_updated" | "people_invite_people" | "people_invite_multiple_people" | "people_delete_person" | "people_update_person" | "reimbursement_updated" | "reimbursement_deleted" | "reimbursement_cancelled_deleted" | "reimbursement_cancelled" | "reimbursement_approved" | "reimbursement_rejected" | "reimbursement_sent_for_approval" | "account_added" | "business_verification_save" | "business_verification_submit" | "business_verification_from_bills_submit" | "plaid_connection" | "unlink_deposit_account" | "reimbursement_setup" | "reimbursement_approval_create" | "reimbursement_duplicate_approval_create" | "reimbursement_approval_update" | "reimbursement_duplicate_approval_update" | "reimbursement_approval_delete" | "reimbursement_approval_reorder" | "reimbursement_create_mileage" | "reimbursement_update_mileage" | "reimbursement_accept_term" | "reimbursement_accept_employee_term" | "reimbursement_bulk_submit" | "reimbursement_bulk_processed" | "bill_pay_setup" | "zeni_accounts_setup" | "bill_pay_approval_create" | "bill_pay_duplicate_approval_create" | "bill_pay_approval_update" | "bill_pay_duplicate_approval_update" | "bill_pay_approval_delete" | "bill_pay_approval_reorder" | "bill_pay_updated" | "bill_pay_deleted" | "bill_pay_cancelled_deleted" | "bill_pay_cancelled" | "bill_pay_approved" | "real_time_approver_added" | "bill_pay_rejected" | "bill_pay_sent_for_approval" | "bill_pay_accept_term" | "bill_pay_bulk_submit" | "bill_pay_bulk_processed" | "bill_pay_refund" | "bill_pay_retry" | "bill_pay_marked_as_paid" | "zeni_account_accept_term" | "update_vendor" | "data_refresh_update" | "delete_bank_account" | "create_bank_account" | "create_bank_account_ach" | "create_bank_account_wire" | "create_bank_account_international" | "transfer_money" | "update_zeni_account_nickname" | "create_checking_account" | "deposit_check" | "create_vendor" | "onboarding_customer_view" | "onboarding_customer_view_complete" | "onboarding_customer_identity_verification_save" | "onboarding_customer_identity_verification_submit" | "onboarding_customer_business_verification_save" | "onboarding_customer_business_verification_submit" | "retry_bank_account_connection" | "dashboard_invite_sent" | "onboarding_info_saved" | "approve_original_merchant" | "approve_global_merchant" | "reject_global_merchant" | "create_global_merchant" | "fetch_global_merchant_no_recommendation" | "save_vendor_renamed" | "save_vendor_sent_for_review" | "save_vendor_marked_as_employee" | "save_vendor_marked_as_local_contractor" | "charge_card_setup" | "charge_card_accept_term" | "charge_card_receipt_upload" | "charge_card_resend_invite" | "charge_card_revoke_invite" | "card_policy_created" | "card_policy_updated" | "card_policy_deleted" | "charge_card_update_limit" | "charge_cards_update_limit" | "close_charge_card" | "lock_charge_card" | "lock_charge_card_card_user" | "lock_charge_cards" | "unlock_charge_card" | "unlock_charge_card_card_user" | "unlock_charge_cards" | "close_charge_cards" | "revoke_invite_charge_cards" | "charge_card_express_interest" | "create_schedule" | "save_schedule" | "delete_schedule" | "ignore_schedule" | "revert_schedule" | "ignore_accrual" | "revert_accrual" | "save_task_detail" | "fetch_task_detail" | "delete_task" | "archive_task" | "snooze_task" | "unsnooze_task" | "create_tag" | "delete_tag" | "update_charge_card" | "update_charge_card_name" | "update_charge_card_name_card_user" | "issue_charge_card" | "issue_charge_cards" | "notification_settings_saved" | "referral_invite_sent" | "notification_mark_as_read" | "mark_as_complete_schedule" | "cancel_journal_entry" | "settings_accounting_accounts_updated" | "create_card_setup" | "confirm_card_setup" | "add_card_payment_source" | "fetch_payment_sources" | "task_assigned_toast_notification" | "task_due_tomorrow_toast_notification" | "task_deleted_toast_notification" | "task_archived_toast_notification" | "task_overdue_toast_notification" | "task_overdue_toast_notification_creator" | "task_notification_count" | "task_activities_toast_notification" | "task_created_toast_notification" | "task_group_creation_success" | "task_group_deletion_success" | "task_group_update_success" | "primary_funding_account_updated" | "task_time_spent_validation" | "missing_receipts_attachment" | "flux_analysis_unreviewed" | "flux_analysis_reviewed" | "receipt_match" | "receipts_upload" | "receipts_bulk_match" | "billing_address_view" | "express_pay_submit" | "exclude_transaction" | "reconcile" | "save_reconcile_for_later" | "recon_transaction_categorize" | "recon_transaction_match" | "update_debit_card_pin_attempt" | "set_debit_card_pin" | "ai_cfo_create_session_and_submit" | "ai_cfo_chat_session_deleted" | "charge_card_auto_pay_enable" | "charge_card_auto_pay_disable" | "treasury_setup" | "treasury_accept_term" | "treasury_transfer_money" | "treasury_transfer_money_failed" | "auto_transfer_rule_create" | "auto_transfer_rule_update" | "auto_transfer_rule_delete" | "auto_transfer_rule_pause" | "auto_transfer_rule_resume" | "treasury_update_portfolio_allocation" | "send_email_magic_link_to_user" | "complete_profile_done" | "create_transfer_entry" | "reports_resync" | "invalid_phone_number" | "transactions_categorized_updated_failed" | "transaction_categorized_updated_failed" | "transactionsCategorized_transactionsUpdated" | "transactionsCategorized_transactionUpdated" | "transactionCategorized_transactionsUpdated" | "transactionCategorized_transactionUpdated" | "transactionsCategorized_transactionsFailed" | "transactionsCategorized_transactionFailed" | "transactionCategorized_transactionsFailed" | "transactionCategorized_transactionFailed" | "transactionsUpdated_transactionsFailed" | "transactionsUpdated_transactionFailed" | "transactionUpdated_transactionsFailed" | "transactionUpdated_transactionFailed" | "transactionsCategorized_transactionsUpdated_transactionsFailed" | "transactionsCategorized_transactionsUpdated_transactionFailed" | "transactionsCategorized_transactionUpdated_transactionsFailed" | "transactionCategorized_transactionsUpdated_transactionsFailed" | "transactionsCategorized_transactionUpdated_transactionFailed" | "transactionCategorized_transactionsUpdated_transactionFailed" | "transactionCategorized_transactionUpdated_transactionsFailed" | "transactionCategorized_transactionUpdated_transactionFailed" | "accounting_classes_enabled_update" | "accounting_projects_enabled_update" | "accounting_projects_qbo_not_enabled" | "capitalization_threshold_update" | "account_excluded_from_reconciliation" | "account_included_in_reconciliation" | "confirm_statement_mapping" | "statement_parsing_in_progress" | "statement_parsing_complete" | "ai_accountant_trigger_job" | "oauth_consent_approve" | "oauth_consent_invalid_request" | "credit_agent_update_profile" | "credit_agent_save_macro" | "credit_agent_schedule_cron" | "create_sub_task" | "qbo_connection_already_linked" | "qbo_connection_failed" | "accounting_projects_qbo_reconnect_failed" | "product_services_update" | "qbo_realm_id_update" | "invoicing_dunning_case_resolved" | "invoicing_dunning_retry" | "invoicing_dunning_cancel" | "invoicing_dunning_settings_save" | "invoicing_qbo_backfill" | "invoicing_qbo_sync_health_retry" | "invoicing_qbo_settings_save" | "invoicing_branding_save" | "invoicing_discount_archive" | "invoicing_discount_unarchive" | "invoicing_discount_delete" | "invoicing_invoice_reminder_sent" | "invoicing_invoice_regenerated" | "invoicing_invoice_voided" | "invoicing_catalog_item_saved" | "invoicing_customer_saved" | "invoicing_subscription_saved" | "invoicing_coupon_saved" | "invoicing_invoice_saved" | "invoicing_credit_note_saved" | "invoicing_payment_recorded" | "invoicing_subscription_activated" | "invoicing_subscription_cancellation_removed" | "invoicing_subscription_cancelled" | "invoicing_subscription_non_renewing" | "invoicing_subscription_paused" | "invoicing_subscription_reactivated" | "invoicing_subscription_resumed" | "invoicing_subscription_term_end_changed";
1
+ export declare const toSnackbarMessageSection: (v: string) => "common" | "transactionDetails_updatingPastTransactions" | "transactionDetails_transactionUpdated" | "cockpit_month_end_email_sent" | "cockpit_month_end_email_save" | "cockpit_month_end_email_attachment_upload" | "je_bill_link" | "je_posted" | "actual_matching_match_and_reverse" | "actual_matching_undo_reversal" | "audit_score_updated" | "people_invite_people" | "people_invite_multiple_people" | "people_delete_person" | "people_update_person" | "reimbursement_updated" | "reimbursement_deleted" | "reimbursement_cancelled_deleted" | "reimbursement_cancelled" | "reimbursement_approved" | "reimbursement_rejected" | "reimbursement_sent_for_approval" | "account_added" | "business_verification_save" | "business_verification_submit" | "business_verification_from_bills_submit" | "plaid_connection" | "unlink_deposit_account" | "reimbursement_setup" | "reimbursement_approval_create" | "reimbursement_duplicate_approval_create" | "reimbursement_approval_update" | "reimbursement_duplicate_approval_update" | "reimbursement_approval_delete" | "reimbursement_approval_reorder" | "reimbursement_create_mileage" | "reimbursement_update_mileage" | "reimbursement_accept_term" | "reimbursement_accept_employee_term" | "reimbursement_bulk_submit" | "reimbursement_bulk_processed" | "bill_pay_setup" | "zeni_accounts_setup" | "bill_pay_approval_create" | "bill_pay_duplicate_approval_create" | "bill_pay_approval_update" | "bill_pay_duplicate_approval_update" | "bill_pay_approval_delete" | "bill_pay_approval_reorder" | "bill_pay_updated" | "bill_pay_deleted" | "bill_pay_cancelled_deleted" | "bill_pay_cancelled" | "bill_pay_approved" | "real_time_approver_added" | "bill_pay_rejected" | "bill_pay_sent_for_approval" | "bill_pay_accept_term" | "bill_pay_bulk_submit" | "bill_pay_bulk_processed" | "bill_pay_refund" | "bill_pay_retry" | "bill_pay_marked_as_paid" | "zeni_account_accept_term" | "update_vendor" | "data_refresh_update" | "delete_bank_account" | "create_bank_account" | "create_bank_account_ach" | "create_bank_account_wire" | "create_bank_account_international" | "transfer_money" | "update_zeni_account_nickname" | "create_checking_account" | "deposit_check" | "create_vendor" | "onboarding_customer_view" | "onboarding_customer_view_complete" | "onboarding_customer_identity_verification_save" | "onboarding_customer_identity_verification_submit" | "onboarding_customer_business_verification_save" | "onboarding_customer_business_verification_submit" | "retry_bank_account_connection" | "dashboard_invite_sent" | "onboarding_info_saved" | "approve_original_merchant" | "approve_global_merchant" | "reject_global_merchant" | "create_global_merchant" | "fetch_global_merchant_no_recommendation" | "save_vendor_renamed" | "save_vendor_sent_for_review" | "save_vendor_marked_as_employee" | "save_vendor_marked_as_local_contractor" | "charge_card_setup" | "charge_card_accept_term" | "charge_card_receipt_upload" | "charge_card_resend_invite" | "charge_card_revoke_invite" | "card_policy_created" | "card_policy_updated" | "card_policy_deleted" | "charge_card_update_limit" | "charge_cards_update_limit" | "close_charge_card" | "lock_charge_card" | "lock_charge_card_card_user" | "lock_charge_cards" | "unlock_charge_card" | "unlock_charge_card_card_user" | "unlock_charge_cards" | "close_charge_cards" | "revoke_invite_charge_cards" | "charge_card_express_interest" | "create_schedule" | "save_schedule" | "delete_schedule" | "ignore_schedule" | "revert_schedule" | "ignore_accrual" | "revert_accrual" | "save_task_detail" | "fetch_task_detail" | "delete_task" | "archive_task" | "snooze_task" | "unsnooze_task" | "create_tag" | "delete_tag" | "update_charge_card" | "update_charge_card_name" | "update_charge_card_name_card_user" | "issue_charge_card" | "issue_charge_cards" | "notification_settings_saved" | "referral_invite_sent" | "notification_mark_as_read" | "mark_as_complete_schedule" | "cancel_journal_entry" | "settings_accounting_accounts_updated" | "create_card_setup" | "confirm_card_setup" | "add_card_payment_source" | "fetch_payment_sources" | "task_assigned_toast_notification" | "task_due_tomorrow_toast_notification" | "task_deleted_toast_notification" | "task_archived_toast_notification" | "task_overdue_toast_notification" | "task_overdue_toast_notification_creator" | "task_notification_count" | "task_activities_toast_notification" | "task_created_toast_notification" | "task_group_creation_success" | "task_group_deletion_success" | "task_group_update_success" | "primary_funding_account_updated" | "task_time_spent_validation" | "missing_receipts_attachment" | "flux_analysis_unreviewed" | "flux_analysis_reviewed" | "receipt_match" | "receipts_upload" | "receipts_bulk_match" | "billing_address_view" | "express_pay_submit" | "exclude_transaction" | "reconcile" | "save_reconcile_for_later" | "recon_transaction_categorize" | "recon_transaction_match" | "update_debit_card_pin_attempt" | "set_debit_card_pin" | "ai_cfo_create_session_and_submit" | "ai_cfo_chat_session_deleted" | "charge_card_auto_pay_enable" | "charge_card_auto_pay_disable" | "treasury_setup" | "treasury_accept_term" | "treasury_transfer_money" | "treasury_transfer_money_failed" | "auto_transfer_rule_create" | "auto_transfer_rule_update" | "auto_transfer_rule_delete" | "auto_transfer_rule_pause" | "auto_transfer_rule_resume" | "treasury_update_portfolio_allocation" | "send_email_magic_link_to_user" | "complete_profile_done" | "create_transfer_entry" | "reports_resync" | "invalid_phone_number" | "transactions_categorized_updated_failed" | "transaction_categorized_updated_failed" | "transactionsCategorized_transactionsUpdated" | "transactionsCategorized_transactionUpdated" | "transactionCategorized_transactionsUpdated" | "transactionCategorized_transactionUpdated" | "transactionsCategorized_transactionsFailed" | "transactionsCategorized_transactionFailed" | "transactionCategorized_transactionsFailed" | "transactionCategorized_transactionFailed" | "transactionsUpdated_transactionsFailed" | "transactionsUpdated_transactionFailed" | "transactionUpdated_transactionsFailed" | "transactionUpdated_transactionFailed" | "transactionsCategorized_transactionsUpdated_transactionsFailed" | "transactionsCategorized_transactionsUpdated_transactionFailed" | "transactionsCategorized_transactionUpdated_transactionsFailed" | "transactionCategorized_transactionsUpdated_transactionsFailed" | "transactionsCategorized_transactionUpdated_transactionFailed" | "transactionCategorized_transactionsUpdated_transactionFailed" | "transactionCategorized_transactionUpdated_transactionsFailed" | "transactionCategorized_transactionUpdated_transactionFailed" | "accounting_classes_enabled_update" | "accounting_projects_enabled_update" | "accounting_projects_qbo_not_enabled" | "capitalization_threshold_update" | "account_excluded_from_reconciliation" | "account_included_in_reconciliation" | "confirm_statement_mapping" | "statement_parsing_in_progress" | "statement_parsing_complete" | "ai_accountant_trigger_job" | "oauth_consent_approve" | "oauth_consent_invalid_request" | "credit_agent_update_profile" | "credit_agent_save_macro" | "credit_agent_schedule_cron" | "create_sub_task" | "qbo_connection_already_linked" | "qbo_connection_failed" | "accounting_projects_qbo_reconnect_failed" | "product_services_update" | "qbo_realm_id_update" | "invoicing_dunning_case_resolved" | "invoicing_dunning_retry" | "invoicing_dunning_cancel" | "invoicing_dunning_settings_save" | "invoicing_qbo_backfill" | "invoicing_qbo_sync_health_retry" | "invoicing_qbo_settings_save" | "invoicing_branding_save" | "invoicing_discount_archive" | "invoicing_discount_unarchive" | "invoicing_discount_delete" | "invoicing_invoice_reminder_sent" | "invoicing_invoice_regenerated" | "invoicing_invoice_voided" | "invoicing_catalog_item_saved" | "invoicing_customer_saved" | "invoicing_subscription_saved" | "invoicing_coupon_saved" | "invoicing_invoice_saved" | "invoicing_credit_note_saved" | "invoicing_payment_recorded" | "invoicing_subscription_activated" | "invoicing_subscription_cancellation_removed" | "invoicing_subscription_cancelled" | "invoicing_subscription_non_renewing" | "invoicing_subscription_paused" | "invoicing_subscription_reactivated" | "invoicing_subscription_resumed" | "invoicing_subscription_term_end_changed";
2
2
  export type SnackbarMessageSections = ReturnType<typeof toSnackbarMessageSection>;
3
3
  export declare const toSnackbarMessageSectionTexts: (v: string) => "notification" | "failed" | "success";
4
4
  export type SnackbarMessageSectionTexts = ReturnType<typeof toSnackbarMessageSectionTexts>;
@@ -11,6 +11,8 @@ const ALL_SNACKBAR_MESSAGE_SECTIONS = [
11
11
  'cockpit_month_end_email_attachment_upload',
12
12
  'je_bill_link',
13
13
  'je_posted',
14
+ 'actual_matching_match_and_reverse',
15
+ 'actual_matching_undo_reversal',
14
16
  'audit_score_updated',
15
17
  'people_invite_people',
16
18
  'people_invite_multiple_people',
@@ -1,27 +1,32 @@
1
1
  import { toAmount } from '../../commonStateTypes/amount';
2
+ import { stringToUnion } from '../../commonStateTypes/stringToUnion';
2
3
  import { toJEOneTimeAccrual, } from '../jeSchedules/jeSchedulesPayload';
3
- const USD_CURRENCY_CODE = 'USD';
4
- const USD_CURRENCY_SYMBOL = '$';
5
4
  const MATCH_KINDS = [
6
5
  'auto_reversed',
7
6
  'pending_review',
8
7
  'manually_reversed',
9
8
  ];
10
9
  function toMatchKind(raw) {
11
- if (MATCH_KINDS.includes(raw)) {
12
- return raw;
10
+ return stringToUnion(raw, MATCH_KINDS);
11
+ }
12
+ export function withInheritedAccrualCurrency(member, fallbackCurrencyCode) {
13
+ if (member.currency != null && member.currency !== '') {
14
+ return member;
13
15
  }
14
- console.error(`Unknown ActualMatchingMatchKind: ${raw}`);
15
- return 'auto_reversed';
16
+ return { ...member, currency: fallbackCurrencyCode };
17
+ }
18
+ function toMatchMemberAccrual(member, matchCurrencyCode) {
19
+ return toJEOneTimeAccrual(withInheritedAccrualCurrency(member, matchCurrencyCode));
16
20
  }
17
- export function toActualMatchingMatch(payload) {
21
+ export function toActualMatchingMatch(payload, fallbackCurrency) {
18
22
  const currencyCode = payload.currency_code != null && payload.currency_code !== ''
19
23
  ? payload.currency_code
20
- : USD_CURRENCY_CODE;
24
+ : fallbackCurrency?.currencyCode ?? '';
21
25
  const currencySymbol = payload.currency_symbol != null && payload.currency_symbol !== ''
22
26
  ? payload.currency_symbol
23
- : currencyCode === USD_CURRENCY_CODE
24
- ? USD_CURRENCY_SYMBOL
27
+ : fallbackCurrency != null &&
28
+ currencyCode === fallbackCurrency.currencyCode
29
+ ? fallbackCurrency.currencySymbol
25
30
  : currencyCode;
26
31
  return {
27
32
  matchId: payload.match_id,
@@ -29,12 +34,13 @@ export function toActualMatchingMatch(payload) {
29
34
  transactionIntegrationId: payload.transaction_integration_id,
30
35
  transactionDate: payload.transaction_date ?? undefined,
31
36
  vendorId: payload.vendor_id ?? undefined,
32
- vendorName: payload.vendor_name ?? '',
37
+ vendorName: payload.vendor_name ?? undefined,
33
38
  kind: toMatchKind(payload.kind),
34
39
  matchTier: payload.match_tier ?? undefined,
35
40
  matchShape: payload.match_shape ?? undefined,
36
41
  accruedAmount: toAmount(payload.accrued_amount, currencyCode, currencySymbol),
37
42
  actualAmount: toAmount(payload.actual_amount, currencyCode, currencySymbol),
38
- members: payload.members.map(toJEOneTimeAccrual),
43
+ members: payload.members.map((member) => toMatchMemberAccrual(member, currencyCode)),
44
+ leftovers: (payload.leftovers ?? []).map((member) => toMatchMemberAccrual(member, currencyCode)),
39
45
  };
40
46
  }
@@ -9,12 +9,12 @@ const actualMatching = createSlice({
9
9
  initialState,
10
10
  reducers: {
11
11
  updateActualMatchingMatches: {
12
- prepare(matches) {
13
- return { payload: { matches } };
12
+ prepare(matches, fallbackCurrency) {
13
+ return { payload: { matches, fallbackCurrency } };
14
14
  },
15
15
  reducer(draft, action) {
16
16
  action.payload.matches.forEach((payload) => {
17
- const match = toActualMatchingMatch(payload);
17
+ const match = toActualMatchingMatch(payload, action.payload.fallbackCurrency);
18
18
  draft.byMatchId[match.matchId] = match;
19
19
  });
20
20
  },
@@ -1,8 +1,9 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
2
  import { clearAll } from '../../rootActions';
3
- import { date as zeniDate } from '../../zeniDayJS';
3
+ import { dateNow, date as zeniDate } from '../../zeniDayJS';
4
4
  import { ALL_AI_CFO_ANSWER_RESPONSE_TYPES, toAiCfoAnswerResponseType, toAiCfoAnswerResponseTypeStrict, toAiCfoAnswerStateType, toAiCfoVisualizationTypeStrict, toInteractiveFormTypeStrict, toMessageSender, toMessageType, toYFormatScaleStrict, toYFormatTypeStrict, toYFormatUnitStrict, } from './aiCfoState';
5
5
  export const initialAiCfoState = {
6
+ deletedChatSessionIds: [],
6
7
  aiCfoByChatSessionId: {},
7
8
  partialQuestionAnswers: {},
8
9
  syntheticAnswersByChatSessionId: {},
@@ -468,6 +469,11 @@ const toResponseBlockType = (answer, userId) => {
468
469
  createdAt: zeniDate(timestamp),
469
470
  };
470
471
  };
472
+ // How many deleted session ids to remember. The tombstone only has to outlive
473
+ // an in-flight history request, which is seconds, so this is generous. Capped
474
+ // because `clearSession` would otherwise grow the array for the life of the
475
+ // store and every `setChatHistory` scans it.
476
+ const DELETED_SESSION_MEMORY = 100;
471
477
  const aiCfo = createSlice({
472
478
  name: 'aiCfo',
473
479
  initialState: initialAiCfoState,
@@ -489,8 +495,25 @@ const aiCfo = createSlice({
489
495
  setSessions(draft, action) {
490
496
  action.payload.forEach((chatSessionPayload) => {
491
497
  const { chat_session_id } = chatSessionPayload;
492
- // This is to prevent overwriting the session if it is already in state
493
- if (draft.aiCfoByChatSessionId[chat_session_id] == null) {
498
+ if (draft.aiCfoByChatSessionId[chat_session_id] != null) {
499
+ // The list is authoritative for a session's metadata, and a session
500
+ // seeded by `setChatHistory` has guessed metadata: no summary, and a
501
+ // `createdAt` taken from one page of a newest-first paginated
502
+ // history, which is later than the session really started.
503
+ // `questionAnswers` is left alone — the list must never wipe a
504
+ // thread that is on screen.
505
+ const entry = draft.aiCfoByChatSessionId[chat_session_id];
506
+ const incoming = toChatSession(chatSessionPayload);
507
+ entry.chatSession = {
508
+ ...incoming,
509
+ // Never downgrade a title already in state: a page of the list can
510
+ // carry a nullish summary, and assigning it wholesale would blank
511
+ // a header that was correct.
512
+ chatSessionSummary: incoming.chatSessionSummary ??
513
+ entry.chatSession.chatSessionSummary,
514
+ };
515
+ }
516
+ else {
494
517
  draft.aiCfoByChatSessionId[chat_session_id] = {
495
518
  chatSession: toChatSession(chatSessionPayload),
496
519
  questionAnswers: [],
@@ -503,8 +526,52 @@ const aiCfo = createSlice({
503
526
  setChatHistory(draft, action) {
504
527
  const { chatSessionId, history, isPaginationComplete = false, } = action.payload;
505
528
  if (draft.aiCfoByChatSessionId[chatSessionId] == null) {
506
- console.warn(`session with id ${chatSessionId} not found in setChatHistory`);
507
- return;
529
+ // Sessions created server-side (a routine run) are opened by deep
530
+ // link, so their history can arrive before the sessions list has
531
+ // registered them. Dropping it here loses the messages for good: the
532
+ // response's null page token sets `hasMore: false`, which is the
533
+ // condition the refetch is guarded on. The response is itself proof
534
+ // the session exists, so seed the entry from the messages.
535
+ if (history.length === 0) {
536
+ return;
537
+ }
538
+ // A deleted session. Its history request is never cancelled
539
+ // (`mergeMap`), so seeding would resurrect the conversation into the
540
+ // rail, where clicking it 404s. Only this path is guarded:
541
+ // `clearSession` is optimistic, so if the server still lists the
542
+ // session, `setSessions` re-adding it is correct.
543
+ if (draft.deletedChatSessionIds?.includes(chatSessionId) === true) {
544
+ return;
545
+ }
546
+ // The human's id, taken off a message they actually sent. The API
547
+ // returns newest first, so `history[0]` is usually the agent's reply,
548
+ // and `chatSession.userId` is read as "whose conversation is this".
549
+ const owner = history.find((message) => message.sender === 'user') ?? history[0];
550
+ // Oldest message in the page, not `history[0]`: a session cannot
551
+ // start after its own messages, and the API returns newest first.
552
+ //
553
+ // Compared as instants, not strings. The wire format is an RFC 1123
554
+ // HTTP-date ("Thu, 03 Sep 2026 12:34:56 GMT") because `set_200`
555
+ // bypasses chat's isoformat encoder, and lexically "Mon…" precedes
556
+ // "Sat…" while being the later day. Unparseable values are filtered
557
+ // out, not compared: every comparison against NaN is false, so one
558
+ // used as the reduce's seed would carry through to an Invalid Date.
559
+ const oldest = history
560
+ .map((message) => ({ at: Date.parse(message.created_at), message }))
561
+ .filter(({ at }) => !Number.isNaN(at))
562
+ .reduce((min, entry) => (min == null || entry.at < min.at ? entry : min), undefined)?.message;
563
+ draft.aiCfoByChatSessionId[chatSessionId] = {
564
+ chatSession: {
565
+ chatSessionId,
566
+ userId: owner.user_id,
567
+ chatSessionSummary: undefined,
568
+ // Nothing in the page carried a usable timestamp. "Now" is a
569
+ // guess, but it is a sane one and it keeps the row out of the
570
+ // Invalid-Date behaviour above; the sessions list corrects it.
571
+ createdAt: oldest != null ? zeniDate(oldest.created_at) : dateNow(),
572
+ },
573
+ questionAnswers: [],
574
+ };
508
575
  }
509
576
  const session = draft.aiCfoByChatSessionId[chatSessionId];
510
577
  // Check if we have an existing partial Q&A pair for this session
@@ -784,6 +851,13 @@ const aiCfo = createSlice({
784
851
  },
785
852
  clearSession(draft, action) {
786
853
  const sessionId = action.payload;
854
+ draft.deletedChatSessionIds ?? (draft.deletedChatSessionIds = []);
855
+ if (!draft.deletedChatSessionIds.includes(sessionId)) {
856
+ draft.deletedChatSessionIds.push(sessionId);
857
+ if (draft.deletedChatSessionIds.length > DELETED_SESSION_MEMORY) {
858
+ draft.deletedChatSessionIds.splice(0, draft.deletedChatSessionIds.length - DELETED_SESSION_MEMORY);
859
+ }
860
+ }
787
861
  delete draft.aiCfoByChatSessionId[sessionId];
788
862
  delete draft.syntheticAnswersByChatSessionId[sessionId];
789
863
  if (draft.partialQuestionAnswers?.[sessionId] != null) {
@@ -461,6 +461,7 @@ function toJEOneTimeAccrualCleared(payload, currencyCode) {
461
461
  ? toOneTimeAccrualAmount(payload.group_sum, currencyCode)
462
462
  : undefined,
463
463
  accrualIds: payload.accrual_ids ?? undefined,
464
+ intendedAccrualIds: payload.intended_accrual_ids ?? undefined,
464
465
  reversedAt: payload.reversed_at != null ? date(payload.reversed_at) : undefined,
465
466
  matchedTransactionDate: payload.matched_transaction_date != null
466
467
  ? date(payload.matched_transaction_date)
@@ -523,6 +524,9 @@ export function toJEOneTimeAccrual(payload) {
523
524
  actualReversalPostDate: payload.actual_reversal_post_date != null
524
525
  ? date(payload.actual_reversal_post_date)
525
526
  : undefined,
527
+ reversalPostingDate: payload.reversal_posting_date != null
528
+ ? date(payload.reversal_posting_date)
529
+ : undefined,
526
530
  jeDebitAccountId: payload.je_debit_account_id ?? undefined,
527
531
  jeDebitAccountName: payload.je_debit_account_name ?? undefined,
528
532
  jeDebitAccountingClassId: payload.je_debit_accounting_class_id ?? undefined,
@@ -8,6 +8,8 @@ const ALL_SNACKBAR_MESSAGE_SECTIONS = [
8
8
  'cockpit_month_end_email_attachment_upload',
9
9
  'je_bill_link',
10
10
  'je_posted',
11
+ 'actual_matching_match_and_reverse',
12
+ 'actual_matching_undo_reversal',
11
13
  'audit_score_updated',
12
14
  'people_invite_people',
13
15
  'people_invite_multiple_people',