@zeniai/client-epic-state 5.0.83-betaRR3 → 5.0.83-betaRR5

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 (79) hide show
  1. package/lib/entity/aiCfo/aiCfoPayload.d.ts +1 -4
  2. package/lib/entity/aiCfo/aiCfoReducer.js +3 -11
  3. package/lib/entity/aiCfo/aiCfoState.d.ts +0 -19
  4. package/lib/entity/aiCfo/aiCfoState.js +1 -8
  5. package/lib/entity/cardPolicy/policyRecommendationFromUploadEpic.d.ts +3 -2
  6. package/lib/entity/cardPolicy/policyRecommendationFromUploadEpic.js +3 -2
  7. package/lib/epic.d.ts +14 -12
  8. package/lib/epic.js +14 -12
  9. package/lib/esm/entity/aiCfo/aiCfoReducer.js +4 -12
  10. package/lib/esm/entity/aiCfo/aiCfoState.js +0 -7
  11. package/lib/esm/entity/cardPolicy/policyRecommendationFromUploadEpic.js +4 -3
  12. package/lib/esm/epic.js +14 -12
  13. package/lib/esm/index.js +14 -3
  14. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyAiCardPolicyFormDraftUpdate.js +328 -0
  15. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyExtractedPolicyToAiCardPolicyDraft.js +120 -0
  16. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyExtractedPolicyToDraftEpic.js +60 -0
  17. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyExtractedPolicyToManualCardPolicyDraft.js +118 -0
  18. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyUploadPlanToAiCardPolicyDraft.js +33 -0
  19. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/buildAiCardPolicyFormDraftSeed.js +385 -0
  20. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/buildManualCardPolicyFormDraftSeed.js +34 -0
  21. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/cardPolicyChipIds.js +33 -0
  22. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/cardPolicyFormDraftTypes.js +17 -0
  23. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/cardPolicyRequestParts.js +144 -0
  24. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/createCardPolicyReducer.js +137 -1
  25. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/createCardPolicySelector.js +18 -0
  26. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/deriveAiPolicyReviewRowsFromInputs.js +148 -0
  27. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/seedAiCardPolicyFormDraftEpic.js +112 -0
  28. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/toBulkCardPolicyTemplateRequestsFromDraft.js +101 -0
  29. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/toCardPolicyTemplateRequestFromDraft.js +71 -0
  30. package/lib/esm/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/toManualCardPolicyTemplateRequestFromDraft.js +34 -0
  31. package/lib/esm/view/spendManagement/chargeCards/issueChargeCard/applyAiCardCreationFormDraftUpdate.js +0 -1
  32. package/lib/esm/view/spendManagement/chargeCards/issueChargeCard/buildAiCardCreationFormDraftSeed.js +13 -6
  33. package/lib/esm/view/spendManagement/chargeCards/issueChargeCard/deriveAiCardRowsFromTeams.js +1 -3
  34. package/lib/esm/view/spendManagement/chargeCards/issueChargeCard/toIssueChargeCardLocalDataFromDraft.js +0 -5
  35. package/lib/index.d.ts +20 -4
  36. package/lib/index.js +45 -4
  37. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyAiCardPolicyFormDraftUpdate.d.ts +38 -0
  38. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyAiCardPolicyFormDraftUpdate.js +333 -0
  39. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyExtractedPolicyToAiCardPolicyDraft.d.ts +25 -0
  40. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyExtractedPolicyToAiCardPolicyDraft.js +124 -0
  41. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyExtractedPolicyToDraftEpic.d.ts +28 -0
  42. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyExtractedPolicyToDraftEpic.js +64 -0
  43. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyExtractedPolicyToManualCardPolicyDraft.d.ts +17 -0
  44. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyExtractedPolicyToManualCardPolicyDraft.js +122 -0
  45. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyUploadPlanToAiCardPolicyDraft.d.ts +23 -0
  46. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/applyUploadPlanToAiCardPolicyDraft.js +37 -0
  47. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/buildAiCardPolicyFormDraftSeed.d.ts +38 -0
  48. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/buildAiCardPolicyFormDraftSeed.js +391 -0
  49. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/buildManualCardPolicyFormDraftSeed.d.ts +11 -0
  50. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/buildManualCardPolicyFormDraftSeed.js +38 -0
  51. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/cardPolicyChipIds.d.ts +24 -0
  52. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/cardPolicyChipIds.js +40 -0
  53. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/cardPolicyFormDraftTypes.d.ts +253 -0
  54. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/cardPolicyFormDraftTypes.js +21 -0
  55. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/cardPolicyRequestParts.d.ts +66 -0
  56. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/cardPolicyRequestParts.js +151 -0
  57. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/createCardPolicyReducer.d.ts +40 -1
  58. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/createCardPolicyReducer.js +138 -2
  59. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/createCardPolicySelector.d.ts +15 -0
  60. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/createCardPolicySelector.js +20 -0
  61. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/createCardPolicyState.d.ts +28 -0
  62. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/deriveAiPolicyReviewRowsFromInputs.d.ts +30 -0
  63. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/deriveAiPolicyReviewRowsFromInputs.js +152 -0
  64. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/seedAiCardPolicyFormDraftEpic.d.ts +26 -0
  65. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/seedAiCardPolicyFormDraftEpic.js +116 -0
  66. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/toBulkCardPolicyTemplateRequestsFromDraft.d.ts +35 -0
  67. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/toBulkCardPolicyTemplateRequestsFromDraft.js +105 -0
  68. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/toCardPolicyTemplateRequestFromDraft.d.ts +23 -0
  69. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/toCardPolicyTemplateRequestFromDraft.js +75 -0
  70. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/toManualCardPolicyTemplateRequestFromDraft.d.ts +15 -0
  71. package/lib/view/spendManagement/chargeCards/cardPolicy/createCardPolicy/toManualCardPolicyTemplateRequestFromDraft.js +38 -0
  72. package/lib/view/spendManagement/chargeCards/issueChargeCard/aiCardCreationFormDraftTypes.d.ts +3 -6
  73. package/lib/view/spendManagement/chargeCards/issueChargeCard/applyAiCardCreationFormDraftUpdate.js +0 -1
  74. package/lib/view/spendManagement/chargeCards/issueChargeCard/buildAiCardCreationFormDraftSeed.d.ts +6 -3
  75. package/lib/view/spendManagement/chargeCards/issueChargeCard/buildAiCardCreationFormDraftSeed.js +13 -6
  76. package/lib/view/spendManagement/chargeCards/issueChargeCard/deriveAiCardRowsFromTeams.d.ts +1 -4
  77. package/lib/view/spendManagement/chargeCards/issueChargeCard/deriveAiCardRowsFromTeams.js +1 -3
  78. package/lib/view/spendManagement/chargeCards/issueChargeCard/toIssueChargeCardLocalDataFromDraft.js +0 -5
  79. package/package.json +1 -1
@@ -1,4 +1,11 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
+ import { applyAiCardPolicyFormDraftUpdate } from './applyAiCardPolicyFormDraftUpdate';
3
+ import { applyExtractedPolicyToAiCardPolicyDraft as applyExtractedToAiDraftHelper } from './applyExtractedPolicyToAiCardPolicyDraft';
4
+ import { applyExtractedPolicyToManualCardPolicyDraft as applyExtractedToManualDraftHelper } from './applyExtractedPolicyToManualCardPolicyDraft';
5
+ import { applyUploadPlanToAiCardPolicyDraft as applyUploadPlanToAiDraftHelper } from './applyUploadPlanToAiCardPolicyDraft';
6
+ import { buildAiCardPolicyFormDraftSeed } from './buildAiCardPolicyFormDraftSeed';
7
+ import { buildManualCardPolicyFormDraftSeed, } from './buildManualCardPolicyFormDraftSeed';
8
+ import { toMccCategoryLike, } from './cardPolicyFormDraftTypes';
2
9
  export const initialState = {
3
10
  createCardPolicyTemplateRequestState: {
4
11
  fetchState: 'Not-Started',
@@ -36,10 +43,139 @@ const createCardPolicy = createSlice({
36
43
  draft.lastCreateCardPolicyTemplateErrors = action.payload.perItemErrors;
37
44
  }
38
45
  },
46
+ /**
47
+ * Seed the AI CFO card-policy form draft + companion context from
48
+ * the AI's `interactive_form` payload. Dispatched by
49
+ * `seedAiCardPolicyFormDraftEpic` when the AI answer lands.
50
+ */
51
+ seedAiCardPolicyFormDraft: {
52
+ reducer(draft, action) {
53
+ const { draft: nextDraft, context } = buildAiCardPolicyFormDraftSeed({
54
+ initialData: action.payload.initialData,
55
+ cardsMasterList: action.payload.cardsMasterList,
56
+ categorySearchOptions: action.payload.mccCategories.map(toMccCategoryLike),
57
+ suggestedAllowMerchants: action.payload.suggestedAllowMerchants,
58
+ suggestedBlockMerchants: action.payload.suggestedBlockMerchants,
59
+ suggestedAllowCategories: (action.payload.suggestedAllowCategories ?? []).map(toMccCategoryLike),
60
+ suggestedBlockCategories: (action.payload.suggestedBlockCategories ?? []).map(toMccCategoryLike),
61
+ });
62
+ draft.aiCardPolicyFormDraft = nextDraft;
63
+ draft.aiCardPolicyFormDraftContext = context;
64
+ },
65
+ prepare(payload) {
66
+ return { payload };
67
+ },
68
+ },
69
+ /**
70
+ * Replace the AI card-policy form draft from the component's
71
+ * `watchHandler`. Delegates to `applyAiCardPolicyFormDraftUpdate`
72
+ * which:
73
+ * - guided: mirrors review-chip diffs into source slices then
74
+ * re-derives `policiesReview` from those slices,
75
+ * - upload: patches "Applied to Cards" chip group from
76
+ * `cardSelections`,
77
+ * - auto: pass-through.
78
+ */
79
+ updateAiCardPolicyFormDraft(draft, action) {
80
+ draft.aiCardPolicyFormDraft = applyAiCardPolicyFormDraftUpdate({
81
+ prev: draft.aiCardPolicyFormDraft,
82
+ next: action.payload,
83
+ context: draft.aiCardPolicyFormDraftContext,
84
+ });
85
+ },
86
+ /**
87
+ * Apply a fresh `wizardPlan` returned by the upload-mode
88
+ * recommendation API to the AI draft. No-op when the draft isn't
89
+ * in `upload` mode.
90
+ */
91
+ updateAiCardPolicyFormDraftFromUploadPlan: {
92
+ reducer(draft, action) {
93
+ if (draft.aiCardPolicyFormDraft == null ||
94
+ draft.aiCardPolicyFormDraftContext == null) {
95
+ return;
96
+ }
97
+ const { draft: nextDraft, context } = applyUploadPlanToAiDraftHelper({
98
+ current: draft.aiCardPolicyFormDraft,
99
+ context: draft.aiCardPolicyFormDraftContext,
100
+ wizardPlan: action.payload.wizardPlan,
101
+ source: action.payload.source,
102
+ });
103
+ draft.aiCardPolicyFormDraft = nextDraft;
104
+ draft.aiCardPolicyFormDraftContext = context;
105
+ },
106
+ prepare(wizardPlan, source) {
107
+ return { payload: { wizardPlan, source } };
108
+ },
109
+ },
110
+ /**
111
+ * Apply an extracted policy document response to the AI draft.
112
+ * Dispatched by the extract-success bridge epic when the upload
113
+ * was kicked off from inside an AI CFO card-policy form.
114
+ */
115
+ applyExtractedPolicyToAiCardPolicyDraft: {
116
+ reducer(draft, action) {
117
+ if (draft.aiCardPolicyFormDraft == null) {
118
+ return;
119
+ }
120
+ draft.aiCardPolicyFormDraft = applyExtractedToAiDraftHelper({
121
+ current: draft.aiCardPolicyFormDraft,
122
+ extracted: action.payload.extracted,
123
+ vendorSearchOptions: action.payload.vendorSearchOptions,
124
+ categorySearchOptions: action.payload.categorySearchOptions,
125
+ });
126
+ },
127
+ prepare(payload) {
128
+ return { payload };
129
+ },
130
+ },
131
+ clearAiCardPolicyFormDraft(draft) {
132
+ draft.aiCardPolicyFormDraft = undefined;
133
+ draft.aiCardPolicyFormDraftContext = undefined;
134
+ },
135
+ /**
136
+ * Seed the manual `CardPolicyCreatePage` form draft from the BE
137
+ * suggested merchant/category seeds + the cards master list.
138
+ * Dispatched on the create-page mount.
139
+ */
140
+ seedManualCardPolicyFormDraft: {
141
+ reducer(draft, action) {
142
+ draft.manualCardPolicyFormDraft = buildManualCardPolicyFormDraftSeed(action.payload);
143
+ },
144
+ prepare(payload) {
145
+ return { payload };
146
+ },
147
+ },
148
+ updateManualCardPolicyFormDraft(draft, action) {
149
+ draft.manualCardPolicyFormDraft = action.payload;
150
+ },
151
+ /**
152
+ * Apply an extracted policy document response to the manual draft.
153
+ * Dispatched by the extract-success bridge epic when the upload was
154
+ * kicked off from the manual `CardPolicyCreatePage`.
155
+ */
156
+ applyExtractedPolicyToManualCardPolicyDraft: {
157
+ reducer(draft, action) {
158
+ if (draft.manualCardPolicyFormDraft == null) {
159
+ return;
160
+ }
161
+ draft.manualCardPolicyFormDraft = applyExtractedToManualDraftHelper({
162
+ current: draft.manualCardPolicyFormDraft,
163
+ extracted: action.payload.extracted,
164
+ vendorSearchOptions: action.payload.vendorSearchOptions,
165
+ categorySearchOptions: action.payload.categorySearchOptions,
166
+ });
167
+ },
168
+ prepare(payload) {
169
+ return { payload };
170
+ },
171
+ },
172
+ clearManualCardPolicyFormDraft(draft) {
173
+ draft.manualCardPolicyFormDraft = undefined;
174
+ },
39
175
  clearCreateCardPolicy(draft) {
40
176
  Object.assign(draft, initialState);
41
177
  },
42
178
  },
43
179
  });
44
- export const { createCardPolicyTemplates, updateCreateCardPolicyTemplateRequestState, clearCreateCardPolicy, } = createCardPolicy.actions;
180
+ export const { createCardPolicyTemplates, updateCreateCardPolicyTemplateRequestState, seedAiCardPolicyFormDraft, updateAiCardPolicyFormDraft, updateAiCardPolicyFormDraftFromUploadPlan, applyExtractedPolicyToAiCardPolicyDraft, clearAiCardPolicyFormDraft, seedManualCardPolicyFormDraft, updateManualCardPolicyFormDraft, applyExtractedPolicyToManualCardPolicyDraft, clearManualCardPolicyFormDraft, clearCreateCardPolicy, } = createCardPolicy.actions;
45
181
  export default createCardPolicy.reducer;
@@ -37,3 +37,21 @@ export function getLastCreateCardPolicyTemplateErrors(state) {
37
37
  export function getLastCreateCardPolicySourceChatSessionId(state) {
38
38
  return state.lastCreateCardPolicySourceChatSessionId;
39
39
  }
40
+ /**
41
+ * Returns the AI CFO card-policy form draft, or `undefined` before
42
+ * `seedAiCardPolicyFormDraft` runs / after `clearAiCardPolicyFormDraft`.
43
+ * Connected screens use this to hydrate the
44
+ * `CardPolicyInteractiveForm`'s RHF.
45
+ */
46
+ export function getAiCardPolicyFormDraft(state) {
47
+ return state.aiCardPolicyFormDraft;
48
+ }
49
+ /**
50
+ * Returns the manual `CardPolicyCreatePage` form draft, or `undefined`
51
+ * before `seedManualCardPolicyFormDraft` runs / after
52
+ * `clearManualCardPolicyFormDraft`. The connected screen uses this to
53
+ * hydrate `CardPolicyFormSections`.
54
+ */
55
+ export function getManualCardPolicyFormDraft(state) {
56
+ return state.manualCardPolicyFormDraft;
57
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Compose the Step 5 review rows from the live Step 1–4 form state.
3
+ *
4
+ * - One row per `suggestedPoliciesSeed` entry, in the same order.
5
+ * - Per-row `isChecked` preserved from `prevPoliciesReview`; otherwise
6
+ * falls back to the seed value.
7
+ * - Chip groups rebuilt from the live vendor / category / card / limit
8
+ * slices, with per-chip `isSelected` merged from the previous build
9
+ * so user-removed (`+ name`) chips don't snap back when an upstream
10
+ * slice changes.
11
+ */
12
+ export const deriveAiPolicyReviewRowsFromInputs = ({ vendorPolicy, categoryPolicy, cardSelections, limits, suggestedPoliciesSeed, prevPoliciesReview, }) => {
13
+ const prevById = new Map(prevPoliciesReview.map((row) => [row.id, row]));
14
+ const groups = deriveSharedChipGroups({
15
+ vendorPolicy,
16
+ categoryPolicy,
17
+ cardSelections,
18
+ limits,
19
+ });
20
+ return suggestedPoliciesSeed.map((seed) => {
21
+ const previous = prevById.get(seed.id);
22
+ return {
23
+ id: seed.id,
24
+ name: seed.name,
25
+ isChecked: previous?.isChecked ?? seed.isChecked,
26
+ chipGroups: previous != null
27
+ ? mergeChipSelections(groups, previous.chipGroups)
28
+ : groups,
29
+ };
30
+ });
31
+ };
32
+ const mergeChipSelections = (next, prev) => {
33
+ const prevByGroupId = new Map(prev.map((group) => [group.id, group]));
34
+ return next.map((group) => {
35
+ const prevGroup = prevByGroupId.get(group.id);
36
+ if (prevGroup == null) {
37
+ return group;
38
+ }
39
+ const prevSelectedById = new Map(prevGroup.chips.map((chip) => [chip.id, chip.isSelected]));
40
+ return {
41
+ ...group,
42
+ chips: group.chips.map((chip) => {
43
+ const prevSelected = prevSelectedById.get(chip.id);
44
+ return prevSelected === undefined
45
+ ? chip
46
+ : { ...chip, isSelected: prevSelected };
47
+ }),
48
+ };
49
+ });
50
+ };
51
+ const deriveSharedChipGroups = ({ vendorPolicy, categoryPolicy, cardSelections, limits, }) => {
52
+ const groups = [];
53
+ const vendorChips = chipsFromAllowBlock(vendorPolicy);
54
+ if (vendorChips.length > 0) {
55
+ groups.push({
56
+ id: 'vendors',
57
+ label: vendorPolicy.mode === 'allow' ? 'Allowed vendors' : 'Blocked vendors',
58
+ chips: vendorChips,
59
+ });
60
+ }
61
+ const categoryChips = chipsFromAllowBlock(categoryPolicy);
62
+ if (categoryChips.length > 0) {
63
+ groups.push({
64
+ id: 'categories',
65
+ label: categoryPolicy.mode === 'allow'
66
+ ? 'Allowed categories'
67
+ : 'Blocked categories',
68
+ chips: categoryChips,
69
+ });
70
+ }
71
+ const cardChips = cardSelections
72
+ .filter((row) => row.isChecked)
73
+ .map((row) => ({
74
+ id: row.id,
75
+ label: row.owner.displayName,
76
+ chipKind: 'avatar',
77
+ }));
78
+ if (cardChips.length > 0) {
79
+ groups.push({ id: 'cardholders', label: 'Cardholders', chips: cardChips });
80
+ }
81
+ const limitChips = limits.map((row) => ({
82
+ id: limitChipIdForRowId(row.id),
83
+ label: limitDisplayLabelForRowId(row.id),
84
+ helperText: limitHelperTextForRowId(row.id),
85
+ placeholder: '$0',
86
+ chipKind: 'positive',
87
+ isSelected: row.isEnabled,
88
+ optionalCode: row.isEnabled && row.amount != null ? String(row.amount) : undefined,
89
+ }));
90
+ if (limitChips.length > 0) {
91
+ groups.push({
92
+ id: 'limits',
93
+ label: 'Limits',
94
+ kind: 'limits',
95
+ chips: limitChips,
96
+ });
97
+ }
98
+ return groups;
99
+ };
100
+ const chipsFromAllowBlock = (field) => {
101
+ const items = field.mode === 'allow' ? field.allowItems : field.blockItems;
102
+ return items
103
+ .filter((item) => item.isSelected)
104
+ .map((item) => ({
105
+ id: item.id,
106
+ label: item.optionalCode != null
107
+ ? `${item.label} (${item.optionalCode})`
108
+ : item.label,
109
+ chipKind: field.mode === 'allow' ? 'positive' : 'negative',
110
+ }));
111
+ };
112
+ const limitChipIdForRowId = (rowId) => {
113
+ if (rowId === 'transactionLimit') {
114
+ return 'limit-transaction';
115
+ }
116
+ if (rowId === 'requireReceipt') {
117
+ return 'limit-receipt';
118
+ }
119
+ return rowId;
120
+ };
121
+ const limitDisplayLabelForRowId = (rowId) => {
122
+ if (rowId === 'transactionLimit') {
123
+ return 'Transaction Limit';
124
+ }
125
+ if (rowId === 'requireReceipt') {
126
+ return 'Require Receipt';
127
+ }
128
+ return humanizeLimitId(rowId);
129
+ };
130
+ const limitHelperTextForRowId = (rowId) => {
131
+ if (rowId === 'transactionLimit') {
132
+ return 'Maximum amount per single transaction';
133
+ }
134
+ if (rowId === 'requireReceipt') {
135
+ return 'Require a receipt above this amount';
136
+ }
137
+ return undefined;
138
+ };
139
+ const humanizeLimitId = (id) => {
140
+ const spaced = id
141
+ .replace(/([A-Z])/g, ' $1')
142
+ .replace(/[_-]+/g, ' ')
143
+ .trim();
144
+ if (spaced.length === 0) {
145
+ return id;
146
+ }
147
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
148
+ };
@@ -0,0 +1,112 @@
1
+ import { EMPTY, concat, of } from 'rxjs';
2
+ import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
3
+ import { upsertAnswerPayload, upsertOrAddQuestionAnswerPayload, } from '../../../../../entity/aiCfo/aiCfoReducer';
4
+ import { getQuestionAnswerByIdForChatSession } from '../../../../../entity/aiCfo/aiCfoSelector';
5
+ import { fetchCardPolicyMccCategories } from '../../../../../entity/cardPolicy/cardPolicyReducer';
6
+ import { getAllChargeCardsWithUser } from '../../chargeCardList/chargeCardListSelector';
7
+ import { clearAiCardPolicyFormDraft, seedAiCardPolicyFormDraft, updateCreateCardPolicyTemplateRequestState, } from './createCardPolicyReducer';
8
+ const isClosedChargeCardStatusCode = (code) => code === 'closed' || code === 'closed_by_customer';
9
+ const toCardPolicyCardSelectionRow = (card) => {
10
+ const user = card.cardHolderUser;
11
+ const displayName = `${user.firstName ?? ''} ${user.lastName ?? ''}`.trim() ||
12
+ user.email ||
13
+ user.userId;
14
+ return {
15
+ id: card.id,
16
+ isChecked: false,
17
+ cardName: card.cardName,
18
+ last4: card.accountLast4,
19
+ limitAmount: card.limit?.amount ?? 0,
20
+ limitCadence: card.creditLimitFrequency?.code,
21
+ owner: {
22
+ userId: user.userId,
23
+ displayName,
24
+ roleLabel: card.cardHolderRoles?.titleAtCompany,
25
+ },
26
+ utilizationAmount: (card.limit?.amount ?? 0) - (card.available?.amount ?? 0),
27
+ };
28
+ };
29
+ const readCardsMasterList = (state) => {
30
+ const view = getAllChargeCardsWithUser(state);
31
+ return view.cards
32
+ .filter((card) => !isClosedChargeCardStatusCode(card.status?.code))
33
+ .map(toCardPolicyCardSelectionRow);
34
+ };
35
+ const readMccCategoriesAvailable = (state) => state.cardPolicyState.mccCategoriesFetchState === 'Completed';
36
+ const readCardPolicyPayload = (state, chatSessionId, questionAnswerId) => {
37
+ const qa = getQuestionAnswerByIdForChatSession(state.aiCfoState, chatSessionId, questionAnswerId);
38
+ const visualization = qa?.responses.answer?.content.visualization;
39
+ if (visualization?.type !== 'interactive_form') {
40
+ return undefined;
41
+ }
42
+ const data = visualization.data;
43
+ if (!('formType' in data) || data.formType !== 'card_policy') {
44
+ return undefined;
45
+ }
46
+ return data.payload;
47
+ };
48
+ const resolveQuestionAnswerId = (state, action) => {
49
+ if (action.payload.questionAnswerId != null) {
50
+ return action.payload.questionAnswerId;
51
+ }
52
+ const session = state.aiCfoState.aiCfoByChatSessionId[action.payload.chatSessionId];
53
+ return session?.questionAnswers.slice(-1)[0]?.questionAnswerId;
54
+ };
55
+ const isSeedTrigger = (action) => upsertAnswerPayload.match(action) ||
56
+ upsertOrAddQuestionAnswerPayload.match(action);
57
+ const isCreateSuccess = (action) => updateCreateCardPolicyTemplateRequestState.match(action) &&
58
+ action.payload.fetchState === 'Completed';
59
+ const buildSeedAction = (state, payload) => {
60
+ const cardsMasterList = readCardsMasterList(state);
61
+ const mccCategories = state.cardPolicyState.mccCategories;
62
+ return seedAiCardPolicyFormDraft({
63
+ initialData: payload,
64
+ cardsMasterList,
65
+ mccCategories,
66
+ suggestedAllowMerchants: state.cardPolicyState.suggestedAllowMerchants,
67
+ suggestedBlockMerchants: state.cardPolicyState.suggestedBlockMerchants,
68
+ suggestedAllowCategories: state.cardPolicyState.suggestedAllowCategories,
69
+ suggestedBlockCategories: state.cardPolicyState.suggestedBlockCategories,
70
+ });
71
+ };
72
+ /**
73
+ * Listens for AI CFO `interactive_form` answers with
74
+ * `formType === "card_policy"` and seeds the AI card-policy form
75
+ * draft + companion context onto `createCardPolicy` state. Mirrors
76
+ * `seedAiCardCreationFormDraftEpic` for the cards form.
77
+ *
78
+ * Also clears the draft on a successful bulk create so a stale draft
79
+ * doesn't leak into the next AI session.
80
+ *
81
+ * MCC categories drive category chip labels in the seed. When the
82
+ * categories slice isn't loaded yet, the epic kicks off
83
+ * `fetchCardPolicyMccCategories` and waits for `Completed` before
84
+ * dispatching the seed (mirrors `fetchUserListByType` in the cards
85
+ * variant).
86
+ */
87
+ export const seedAiCardPolicyFormDraftEpic = (actions$, state$) => actions$.pipe(mergeMap((action) => {
88
+ if (isCreateSuccess(action)) {
89
+ return of(clearAiCardPolicyFormDraft());
90
+ }
91
+ if (!isSeedTrigger(action)) {
92
+ return EMPTY;
93
+ }
94
+ const state = state$.value;
95
+ const questionAnswerId = resolveQuestionAnswerId(state, action);
96
+ if (questionAnswerId == null) {
97
+ return EMPTY;
98
+ }
99
+ const payload = readCardPolicyPayload(state, action.payload.chatSessionId, questionAnswerId);
100
+ if (payload == null) {
101
+ return EMPTY;
102
+ }
103
+ if (readMccCategoriesAvailable(state)) {
104
+ return of(buildSeedAction(state, payload));
105
+ }
106
+ const seedWhenMccLoaded$ = state$.pipe(map((rs) => readMccCategoriesAvailable(rs)), distinctUntilChanged(), filter((available) => available), take(1), map(() => buildSeedAction(state$.value, payload)));
107
+ const mccFetchState = state.cardPolicyState.mccCategoriesFetchState;
108
+ if (mccFetchState === 'In-Progress') {
109
+ return seedWhenMccLoaded$;
110
+ }
111
+ return concat(of(fetchCardPolicyMccCategories()), seedWhenMccLoaded$);
112
+ }));
@@ -0,0 +1,101 @@
1
+ import { CARD_POLICY_LIMIT_ROW_ID_REQUIRE_RECEIPT, CARD_POLICY_LIMIT_ROW_ID_TRANSACTION, } from './cardPolicyFormDraftTypes';
2
+ import { REVIEW_GROUP_IDS, REVIEW_LIMIT_CHIP_IDS, amountFromLimitRow, collectCategoryCodes, collectMerchantNames, } from './cardPolicyRequestParts';
3
+ const isSelectedChip = (chip) => chip.isSelected !== false;
4
+ const chipsInGroup = (row, groupId) => {
5
+ const group = row.chipGroups.find((g) => g.id === groupId);
6
+ if (group == null) {
7
+ return [];
8
+ }
9
+ return group.chips.filter(isSelectedChip);
10
+ };
11
+ const collectVendorLabelsFromReviewRow = (row, groupId) => chipsInGroup(row, groupId).map((chip) => chip.label);
12
+ const collectCategoryCodesFromReviewRow = (row, groupId) => chipsInGroup(row, groupId).map((chip) => chip.optionalCode ?? chip.id);
13
+ const collectCardIdsFromReviewRow = (row) => chipsInGroup(row, REVIEW_GROUP_IDS.appliedToCards).flatMap((chip) => chip.cardIds ?? []);
14
+ const limitValueFromReviewRow = (row, chipId) => {
15
+ const group = row.chipGroups.find((g) => g.id === REVIEW_GROUP_IDS.limits);
16
+ if (group == null) {
17
+ return null;
18
+ }
19
+ const chip = group.chips.find((c) => c.id === chipId && isSelectedChip(c));
20
+ if (chip == null) {
21
+ return null;
22
+ }
23
+ if (chip.optionalCode != null) {
24
+ const parsed = Number(chip.optionalCode);
25
+ return Number.isFinite(parsed) ? parsed : null;
26
+ }
27
+ const labelDigits = chip.label.match(/\d+(?:\.\d+)?/);
28
+ if (labelDigits == null) {
29
+ return null;
30
+ }
31
+ const parsed = Number(labelDigits[0]);
32
+ return Number.isFinite(parsed) ? parsed : null;
33
+ };
34
+ const buildGuidedEntry = (row, draft, guidedMode, description) => ({
35
+ allowedEntity: {
36
+ categoryCodes: collectCategoryCodes(draft.categoryPolicy, 'allow'),
37
+ merchantNames: collectMerchantNames(draft.vendorPolicy, 'allow'),
38
+ },
39
+ applyToCards: draft.cardSelections
40
+ .filter((card) => card.isChecked)
41
+ .map((card) => card.id),
42
+ blockedEntity: {
43
+ categoryCodes: collectCategoryCodes(draft.categoryPolicy, 'block'),
44
+ merchantNames: collectMerchantNames(draft.vendorPolicy, 'block'),
45
+ },
46
+ description: description.trim(),
47
+ mode: guidedMode,
48
+ name: row.name.trim(),
49
+ requiredReceiptThreshold: amountFromLimitRow(draft.limits, CARD_POLICY_LIMIT_ROW_ID_REQUIRE_RECEIPT),
50
+ spendLimits: {
51
+ transaction: amountFromLimitRow(draft.limits, CARD_POLICY_LIMIT_ROW_ID_TRANSACTION),
52
+ },
53
+ });
54
+ const buildAutoEntry = (row, description) => ({
55
+ allowedEntity: {
56
+ categoryCodes: collectCategoryCodesFromReviewRow(row, REVIEW_GROUP_IDS.allowedCategories),
57
+ merchantNames: collectVendorLabelsFromReviewRow(row, REVIEW_GROUP_IDS.allowedVendors),
58
+ },
59
+ applyToCards: collectCardIdsFromReviewRow(row),
60
+ blockedEntity: {
61
+ categoryCodes: collectCategoryCodesFromReviewRow(row, REVIEW_GROUP_IDS.restrictedCategories),
62
+ merchantNames: collectVendorLabelsFromReviewRow(row, REVIEW_GROUP_IDS.blockedVendors),
63
+ },
64
+ description: description.trim(),
65
+ name: row.name.trim(),
66
+ });
67
+ const buildUploadEntry = (row, draft, description) => ({
68
+ allowedEntity: {
69
+ categoryCodes: collectCategoryCodesFromReviewRow(row, REVIEW_GROUP_IDS.allowedCategories),
70
+ merchantNames: collectVendorLabelsFromReviewRow(row, REVIEW_GROUP_IDS.allowedVendors),
71
+ },
72
+ applyToCards: draft.cardSelections
73
+ .filter((card) => card.isChecked)
74
+ .map((card) => card.id),
75
+ blockedEntity: {
76
+ categoryCodes: collectCategoryCodesFromReviewRow(row, REVIEW_GROUP_IDS.restrictedCategories),
77
+ merchantNames: collectVendorLabelsFromReviewRow(row, REVIEW_GROUP_IDS.blockedVendors),
78
+ },
79
+ description: description.trim(),
80
+ name: row.name.trim(),
81
+ requiredReceiptThreshold: limitValueFromReviewRow(row, REVIEW_LIMIT_CHIP_IDS.receipt),
82
+ spendLimits: {
83
+ transaction: limitValueFromReviewRow(row, REVIEW_LIMIT_CHIP_IDS.transaction),
84
+ },
85
+ });
86
+ /**
87
+ * Build the array of per-row entries for the bulk
88
+ * `createCardPolicyTemplates` action from the AI CFO form draft.
89
+ * Iterates checked review rows and emits one entry per row, with the
90
+ * per-mode key contract enforced internally.
91
+ */
92
+ export const toBulkCardPolicyTemplateRequestsFromDraft = ({ draft, description = '', guidedMode = 'strict', }) => {
93
+ const checkedRows = draft.policiesReview.filter((row) => row.isChecked);
94
+ if (draft.policyKind === 'guided') {
95
+ return checkedRows.map((row) => buildGuidedEntry(row, draft, guidedMode, description));
96
+ }
97
+ if (draft.policyKind === 'upload') {
98
+ return checkedRows.map((row) => buildUploadEntry(row, draft, description));
99
+ }
100
+ return checkedRows.map((row) => buildAutoEntry(row, description));
101
+ };
@@ -0,0 +1,71 @@
1
+ import { CARD_POLICY_LIMIT_ROW_ID_REQUIRE_RECEIPT, CARD_POLICY_LIMIT_ROW_ID_TRANSACTION, } from './cardPolicyFormDraftTypes';
2
+ import { amountFromLimitRow, collectCategoryCodes, collectMerchantNames, projectAutoReviewRowToCardPolicyRequestParts, } from './cardPolicyRequestParts';
3
+ /**
4
+ * Map the AI CFO `CardPolicyFormDraft` to the wire-ready
5
+ * `CreateCardPolicyTemplateRequest`. Branches on `draft.policyKind`:
6
+ *
7
+ * - `auto` / `upload`: active review row's chip groups are canonical;
8
+ * project via `projectAutoReviewRowToCardPolicyRequestParts`. Active
9
+ * row = "first checked, fallback to first".
10
+ * - `guided`: source slices are canonical (the
11
+ * `applyAiCardPolicyFormDraftUpdate` reducer mirrors review-chip
12
+ * edits back into them); read directly from the source slices.
13
+ *
14
+ * Name picking: first checked row of `policiesReview`, fallback to the
15
+ * first row, fallback to empty.
16
+ */
17
+ export const toCardPolicyTemplateRequestFromDraft = ({ draft, description = '', mode = 'strict', }) => {
18
+ const name = pickPolicyName(draft);
19
+ const base = {
20
+ description: description.trim(),
21
+ mode,
22
+ name: name.trim(),
23
+ };
24
+ if (draft.policyKind === 'auto' || draft.policyKind === 'upload') {
25
+ const activeRow = draft.policiesReview.find((row) => row.isChecked) ??
26
+ draft.policiesReview[0];
27
+ if (activeRow == null) {
28
+ return {
29
+ ...base,
30
+ allowedEntity: { categoryCodes: [], merchantNames: [] },
31
+ applyToCards: [],
32
+ blockedEntity: { categoryCodes: [], merchantNames: [] },
33
+ requiredReceiptThreshold: null,
34
+ spendLimits: { transaction: null },
35
+ };
36
+ }
37
+ return {
38
+ ...base,
39
+ ...projectAutoReviewRowToCardPolicyRequestParts(activeRow),
40
+ };
41
+ }
42
+ // Guided
43
+ return {
44
+ ...base,
45
+ allowedEntity: {
46
+ categoryCodes: collectCategoryCodes(draft.categoryPolicy, 'allow'),
47
+ merchantNames: collectMerchantNames(draft.vendorPolicy, 'allow'),
48
+ },
49
+ applyToCards: draft.cardSelections
50
+ .filter((row) => row.isChecked)
51
+ .map((row) => row.id),
52
+ blockedEntity: {
53
+ categoryCodes: collectCategoryCodes(draft.categoryPolicy, 'block'),
54
+ merchantNames: collectMerchantNames(draft.vendorPolicy, 'block'),
55
+ },
56
+ requiredReceiptThreshold: amountFromLimitRow(draft.limits, CARD_POLICY_LIMIT_ROW_ID_REQUIRE_RECEIPT),
57
+ spendLimits: {
58
+ transaction: amountFromLimitRow(draft.limits, CARD_POLICY_LIMIT_ROW_ID_TRANSACTION),
59
+ },
60
+ };
61
+ };
62
+ const pickPolicyName = (draft) => {
63
+ const checked = draft.policiesReview.find((row) => row.isChecked);
64
+ if (checked != null) {
65
+ return checked.name;
66
+ }
67
+ if (draft.policiesReview.length > 0) {
68
+ return draft.policiesReview[0].name;
69
+ }
70
+ return '';
71
+ };
@@ -0,0 +1,34 @@
1
+ import { CARD_POLICY_LIMIT_ROW_ID_REQUIRE_RECEIPT, CARD_POLICY_LIMIT_ROW_ID_TRANSACTION, } from './cardPolicyFormDraftTypes';
2
+ import { amountFromLimitRow, collectCategoryCodes, collectMerchantNames, } from './cardPolicyRequestParts';
3
+ /**
4
+ * Map the manual `CardPolicyCreateFormDraft` onto the wire-ready
5
+ * `CreateCardPolicyTemplateRequest` payload consumed by the
6
+ * `createCardPolicyTemplates` epic.
7
+ *
8
+ * Mirrors the AI guided builder minus the `policiesReview` projection:
9
+ * - `vendorPolicy` / `categoryPolicy` are read directly; `mode` is
10
+ * forwarded from the form.
11
+ * - `applyToCards` is the id list of checked rows in `cardSelections`.
12
+ * - `spendLimits.transaction` / `requiredReceiptThreshold` are `null`
13
+ * when their toggle is off (BE: "not configured").
14
+ */
15
+ export const toManualCardPolicyTemplateRequestFromDraft = (draft) => ({
16
+ allowedEntity: {
17
+ categoryCodes: collectCategoryCodes(draft.categoryPolicy, 'allow'),
18
+ merchantNames: collectMerchantNames(draft.vendorPolicy, 'allow'),
19
+ },
20
+ applyToCards: draft.cardSelections
21
+ .filter((row) => row.isChecked)
22
+ .map((row) => row.id),
23
+ blockedEntity: {
24
+ categoryCodes: collectCategoryCodes(draft.categoryPolicy, 'block'),
25
+ merchantNames: collectMerchantNames(draft.vendorPolicy, 'block'),
26
+ },
27
+ description: draft.description.trim(),
28
+ mode: draft.mode,
29
+ name: draft.name.trim(),
30
+ requiredReceiptThreshold: amountFromLimitRow(draft.limits, CARD_POLICY_LIMIT_ROW_ID_REQUIRE_RECEIPT),
31
+ spendLimits: {
32
+ transaction: amountFromLimitRow(draft.limits, CARD_POLICY_LIMIT_ROW_ID_TRANSACTION),
33
+ },
34
+ });
@@ -24,7 +24,6 @@ export const applyAiCardCreationFormDraftUpdate = ({ next, prev, context, }) =>
24
24
  teamRows: next.teamRows,
25
25
  prevCardRows: next.cardRows,
26
26
  allUsers: context.allUsers,
27
- defaultCardOptionId: context.defaultCardOptionId,
28
27
  suggestedOwnerUserIdByTeamId: context.suggestedOwnerUserIdByTeamId,
29
28
  }),
30
29
  };