@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
@@ -0,0 +1,385 @@
1
+ import { buildVendorChipId, toMccCategoryChipFieldValue, toVendorChipFieldValue, } from './cardPolicyChipIds';
2
+ import { CARD_POLICY_LIMIT_ROW_ID_REQUIRE_RECEIPT, CARD_POLICY_LIMIT_ROW_ID_TRANSACTION, } from './cardPolicyFormDraftTypes';
3
+ const EMPTY_ALLOW_BLOCK = {
4
+ mode: 'allow',
5
+ allowItems: [],
6
+ blockItems: [],
7
+ };
8
+ const EMPTY_FORM_VALUES = {
9
+ vendorPolicy: EMPTY_ALLOW_BLOCK,
10
+ categoryPolicy: EMPTY_ALLOW_BLOCK,
11
+ cardSelections: [],
12
+ limits: [],
13
+ policiesReview: [],
14
+ };
15
+ export const buildEmptyLimitRows = () => [
16
+ { id: CARD_POLICY_LIMIT_ROW_ID_TRANSACTION, isEnabled: false },
17
+ { id: CARD_POLICY_LIMIT_ROW_ID_REQUIRE_RECEIPT, isEnabled: false },
18
+ ];
19
+ /**
20
+ * Build the initial `CardPolicyFormDraft` + companion seed context for
21
+ * a fresh AI CFO card-policy session. Used by the
22
+ * `seedAiCardPolicyFormDraft` reducer.
23
+ *
24
+ * - `auto` (1-step review): chip groups are built eagerly from each
25
+ * `draftPolicies[i]` since there's no live form to derive from.
26
+ * - `guided` (5-step wizard): Steps 1–4 seeded from `draftPolicies[0]`,
27
+ * Step 5 seeded with one identity-only row per `draftPolicies[i]`;
28
+ * chip groups are re-derived on every `applyAiCardPolicyFormDraftUpdate`.
29
+ * - `upload` (3-step): pre-recommendation returns an empty scaffold;
30
+ * post-recommendation, Step 5 review rows + their chip groups are
31
+ * derived from `wizardPlan.step5Review.draftPolicies`.
32
+ */
33
+ export const buildAiCardPolicyFormDraftSeed = ({ initialData, cardsMasterList = [], categorySearchOptions, suggestedAllowMerchants, suggestedBlockMerchants, suggestedAllowCategories, suggestedBlockCategories, }) => {
34
+ const suggestedPoliciesSeed = buildSuggestedPoliciesSeed(initialData);
35
+ const wizardPlan = 'wizardPlan' in initialData ? initialData.wizardPlan : undefined;
36
+ const source = initialData.policyKind === 'upload' ? initialData.source : undefined;
37
+ const context = {
38
+ policyKind: initialData.policyKind,
39
+ cardsMasterList,
40
+ categorySearchOptions: categorySearchOptions ?? [],
41
+ suggestedAllowMerchants,
42
+ suggestedBlockMerchants,
43
+ suggestedAllowCategories,
44
+ suggestedBlockCategories,
45
+ suggestedPoliciesSeed,
46
+ wizardPlan,
47
+ source,
48
+ };
49
+ const draft = buildDraftFromInitialData({
50
+ initialData,
51
+ cardsMasterList,
52
+ categorySearchOptions,
53
+ suggestedAllowMerchants,
54
+ suggestedBlockMerchants,
55
+ suggestedAllowCategories,
56
+ suggestedBlockCategories,
57
+ });
58
+ return { draft, context };
59
+ };
60
+ const buildDraftFromInitialData = ({ initialData, cardsMasterList, categorySearchOptions, suggestedAllowMerchants, suggestedBlockMerchants, suggestedAllowCategories, suggestedBlockCategories, }) => {
61
+ const vendorAllowSeed = merchantsToChips(suggestedAllowMerchants);
62
+ const vendorBlockSeed = merchantsToChips(suggestedBlockMerchants);
63
+ const categoryAllowSeed = categoriesToChips(suggestedAllowCategories);
64
+ const categoryBlockSeed = categoriesToChips(suggestedBlockCategories);
65
+ if (initialData.policyKind === 'auto') {
66
+ const autoDraftPolicies = initialData.wizardPlan?.step5Review?.draftPolicies ?? [];
67
+ return {
68
+ ...EMPTY_FORM_VALUES,
69
+ policyKind: 'auto',
70
+ policiesReview: buildAutoReviewRows(autoDraftPolicies, cardsMasterList, categorySearchOptions),
71
+ };
72
+ }
73
+ if (initialData.policyKind === 'upload') {
74
+ const uploadDraftPolicies = initialData.wizardPlan?.step5Review?.draftPolicies;
75
+ if (uploadDraftPolicies == null) {
76
+ return {
77
+ ...EMPTY_FORM_VALUES,
78
+ policyKind: 'upload',
79
+ cardSelections: cardsMasterList.map((row) => ({
80
+ ...row,
81
+ isChecked: false,
82
+ })),
83
+ limits: buildEmptyLimitRows(),
84
+ };
85
+ }
86
+ return {
87
+ ...EMPTY_FORM_VALUES,
88
+ policyKind: 'upload',
89
+ cardSelections: cardsMasterList.map((row) => ({
90
+ ...row,
91
+ isChecked: false,
92
+ })),
93
+ limits: buildEmptyLimitRows(),
94
+ policiesReview: buildUploadReviewRows(uploadDraftPolicies, cardsMasterList, categorySearchOptions),
95
+ };
96
+ }
97
+ // Guided
98
+ const draftPolicies = initialData.wizardPlan?.step5Review?.draftPolicies ?? [];
99
+ const primary = draftPolicies[0];
100
+ if (primary == null) {
101
+ return {
102
+ policyKind: 'guided',
103
+ vendorPolicy: buildAllowBlockFromSeeds(vendorAllowSeed, vendorBlockSeed),
104
+ categoryPolicy: buildAllowBlockFromSeeds(categoryAllowSeed, categoryBlockSeed),
105
+ cardSelections: cardsMasterList.map((row) => ({
106
+ ...row,
107
+ isChecked: false,
108
+ })),
109
+ limits: buildEmptyLimitRows(),
110
+ policiesReview: [],
111
+ };
112
+ }
113
+ const mccCodeToCategory = buildMccCodeToCategoryMap(categorySearchOptions);
114
+ const vendorMode = inferAllowBlockMode(primary.allowedEntity.merchantNames.length, primary.blockedEntity.merchantNames.length);
115
+ const aiVendorAllow = draftMerchantsToChips(primary.allowedEntity.merchantNames);
116
+ const aiVendorBlock = draftMerchantsToChips(primary.blockedEntity.merchantNames);
117
+ const vendorPolicy = {
118
+ mode: vendorMode,
119
+ allowItems: mergeAiAndSeedChips(aiVendorAllow, vendorAllowSeed),
120
+ blockItems: mergeAiAndSeedChips(aiVendorBlock, vendorBlockSeed),
121
+ };
122
+ const categoryMode = inferAllowBlockMode(primary.allowedEntity.categoryCodes.length, primary.blockedEntity.categoryCodes.length);
123
+ const aiCategoryAllow = draftCategoryCodesToChips(primary.allowedEntity.categoryCodes, mccCodeToCategory);
124
+ const aiCategoryBlock = draftCategoryCodesToChips(primary.blockedEntity.categoryCodes, mccCodeToCategory);
125
+ const categoryPolicy = {
126
+ mode: categoryMode,
127
+ allowItems: mergeAiAndSeedChips(aiCategoryAllow, categoryAllowSeed),
128
+ blockItems: mergeAiAndSeedChips(aiCategoryBlock, categoryBlockSeed),
129
+ };
130
+ const cardSelections = cardsMasterList.map((row) => ({
131
+ ...row,
132
+ isChecked: primary.applyToCards.includes(row.id),
133
+ }));
134
+ const limits = [
135
+ {
136
+ id: CARD_POLICY_LIMIT_ROW_ID_TRANSACTION,
137
+ isEnabled: primary.spendLimits.transaction > 0,
138
+ amount: primary.spendLimits.transaction > 0
139
+ ? primary.spendLimits.transaction
140
+ : undefined,
141
+ },
142
+ {
143
+ id: CARD_POLICY_LIMIT_ROW_ID_REQUIRE_RECEIPT,
144
+ isEnabled: primary.requiredReceiptThreshold > 0,
145
+ amount: primary.requiredReceiptThreshold > 0
146
+ ? primary.requiredReceiptThreshold
147
+ : undefined,
148
+ },
149
+ ];
150
+ return {
151
+ policyKind: 'guided',
152
+ vendorPolicy,
153
+ categoryPolicy,
154
+ cardSelections,
155
+ limits,
156
+ policiesReview: draftPolicies.map((p) => ({
157
+ id: p.id,
158
+ isChecked: true,
159
+ name: p.name,
160
+ chipGroups: [],
161
+ })),
162
+ };
163
+ };
164
+ const buildSuggestedPoliciesSeed = (initialData) => (initialData.wizardPlan?.step5Review?.draftPolicies ?? []).map((draft) => ({
165
+ id: draft.id,
166
+ name: draft.name,
167
+ isChecked: true,
168
+ }));
169
+ const merchantsToChips = (names) => (names ?? []).map((name) => toVendorChipFieldValue(name, false));
170
+ const categoriesToChips = (categories) => (categories ?? []).map((category) => toMccCategoryChipFieldValue(category, false));
171
+ const buildAllowBlockFromSeeds = (allowItems, blockItems) => {
172
+ if (allowItems.length === 0 && blockItems.length === 0) {
173
+ return EMPTY_ALLOW_BLOCK;
174
+ }
175
+ return { mode: 'allow', allowItems, blockItems };
176
+ };
177
+ const buildMccCodeToCategoryMap = (categories) => {
178
+ const map = new Map();
179
+ for (const category of categories ?? []) {
180
+ for (const code of category.mccCodes) {
181
+ if (!map.has(code)) {
182
+ map.set(code, category);
183
+ }
184
+ }
185
+ }
186
+ return map;
187
+ };
188
+ const buildOwnerSummaries = (cardIds, cardsMasterList) => {
189
+ const cardsById = new Map(cardsMasterList.map((card) => [card.id, card]));
190
+ const summaries = new Map();
191
+ for (const cardId of cardIds) {
192
+ const card = cardsById.get(cardId);
193
+ if (card == null) {
194
+ continue;
195
+ }
196
+ const existing = summaries.get(card.owner.userId);
197
+ if (existing != null) {
198
+ existing.cardIds.push(cardId);
199
+ continue;
200
+ }
201
+ summaries.set(card.owner.userId, {
202
+ userId: card.owner.userId,
203
+ displayName: card.owner.displayName,
204
+ cardIds: [cardId],
205
+ });
206
+ }
207
+ return Array.from(summaries.values());
208
+ };
209
+ const buildAutoReviewChipGroupsForDraft = (draft, cardsMasterList, mccCodeToCategory) => {
210
+ const groups = [];
211
+ if (draft.allowedEntity.merchantNames.length > 0) {
212
+ groups.push({
213
+ id: 'allowed-vendors',
214
+ label: 'Allowed Vendors',
215
+ chips: draft.allowedEntity.merchantNames.map((name) => ({
216
+ id: buildVendorChipId(name),
217
+ label: name,
218
+ chipKind: 'positive',
219
+ isSelected: true,
220
+ })),
221
+ });
222
+ }
223
+ if (draft.blockedEntity.merchantNames.length > 0) {
224
+ groups.push({
225
+ id: 'blocked-vendors',
226
+ label: 'Blocked Vendors',
227
+ chips: draft.blockedEntity.merchantNames.map((name) => ({
228
+ id: buildVendorChipId(name),
229
+ label: name,
230
+ chipKind: 'negative',
231
+ isSelected: true,
232
+ })),
233
+ });
234
+ }
235
+ if (draft.allowedEntity.categoryCodes.length > 0) {
236
+ groups.push({
237
+ id: 'allowed-categories',
238
+ label: 'Allowed Categories',
239
+ chips: draft.allowedEntity.categoryCodes.map((code) => {
240
+ const category = mccCodeToCategory.get(code);
241
+ const label = category != null ? `${category.name} (${code})` : code;
242
+ return {
243
+ id: code,
244
+ label,
245
+ chipKind: 'positive',
246
+ isSelected: true,
247
+ optionalCode: code,
248
+ };
249
+ }),
250
+ });
251
+ }
252
+ if (draft.blockedEntity.categoryCodes.length > 0) {
253
+ groups.push({
254
+ id: 'restricted-categories',
255
+ label: 'Restricted Categories',
256
+ chips: draft.blockedEntity.categoryCodes.map((code) => {
257
+ const category = mccCodeToCategory.get(code);
258
+ const label = category != null ? `${category.name} (${code})` : code;
259
+ return {
260
+ id: code,
261
+ label,
262
+ chipKind: 'negative',
263
+ isSelected: true,
264
+ optionalCode: code,
265
+ };
266
+ }),
267
+ });
268
+ }
269
+ if (draft.applyToCards.length > 0) {
270
+ const owners = buildOwnerSummaries(draft.applyToCards, cardsMasterList);
271
+ if (owners.length > 0) {
272
+ groups.push({
273
+ id: 'applied-to-cards',
274
+ label: 'Applied to Cards',
275
+ chips: owners.map((owner) => ({
276
+ id: owner.userId,
277
+ label: owner.displayName,
278
+ chipKind: 'avatar',
279
+ cardIds: owner.cardIds,
280
+ isSelected: true,
281
+ })),
282
+ });
283
+ }
284
+ }
285
+ return groups;
286
+ };
287
+ const buildAutoReviewRows = (draftPolicies, cardsMasterList, categorySearchOptions) => {
288
+ const mccCodeToCategory = buildMccCodeToCategoryMap(categorySearchOptions);
289
+ return draftPolicies.map((draft) => ({
290
+ id: draft.id,
291
+ isChecked: true,
292
+ name: draft.name,
293
+ chipGroups: buildAutoReviewChipGroupsForDraft(draft, cardsMasterList, mccCodeToCategory),
294
+ }));
295
+ };
296
+ /**
297
+ * Upload-only "Limits" chip group carrying the draft's
298
+ * `spend_limits.transaction` and `required_receipt_threshold`. Each
299
+ * chip stashes its numeric value on `optionalCode` so the bulk-submit
300
+ * builder can recover it without re-parsing the label.
301
+ */
302
+ const buildUploadLimitsChipGroup = (draft) => {
303
+ const transactionEnabled = draft.spendLimits.transaction > 0;
304
+ const receiptEnabled = draft.requiredReceiptThreshold > 0;
305
+ return {
306
+ id: 'limits',
307
+ label: 'Limits',
308
+ kind: 'limits',
309
+ chips: [
310
+ {
311
+ id: 'limit-transaction',
312
+ label: 'Transaction Limit',
313
+ helperText: 'Maximum amount per single transaction',
314
+ placeholder: '$0',
315
+ chipKind: 'positive',
316
+ isSelected: transactionEnabled,
317
+ optionalCode: transactionEnabled
318
+ ? String(draft.spendLimits.transaction)
319
+ : undefined,
320
+ },
321
+ {
322
+ id: 'limit-receipt',
323
+ label: 'Require Receipt',
324
+ helperText: 'Require a receipt above this amount',
325
+ placeholder: '$0',
326
+ chipKind: 'positive',
327
+ isSelected: receiptEnabled,
328
+ optionalCode: receiptEnabled
329
+ ? String(draft.requiredReceiptThreshold)
330
+ : undefined,
331
+ },
332
+ ],
333
+ };
334
+ };
335
+ export const buildUploadReviewRows = (draftPolicies, cardsMasterList, categorySearchOptions) => {
336
+ const mccCodeToCategory = buildMccCodeToCategoryMap(categorySearchOptions);
337
+ return draftPolicies.map((draft) => {
338
+ const chipGroups = buildAutoReviewChipGroupsForDraft(draft, cardsMasterList, mccCodeToCategory);
339
+ chipGroups.push(buildUploadLimitsChipGroup(draft));
340
+ return {
341
+ id: draft.id,
342
+ isChecked: true,
343
+ name: draft.name,
344
+ chipGroups,
345
+ };
346
+ });
347
+ };
348
+ /**
349
+ * Infer the AllowBlock mode for a draft policy field. The BE doesn't
350
+ * tell the FE which radio to pre-select; mirrors the rule used by
351
+ * `applyExtractedPolicyTo*FormValues`:
352
+ *
353
+ * - allow side has any value → `"allow"`
354
+ * - block side has any value → `"block"`
355
+ * - both empty → `"allow"` (default)
356
+ * - both populated → `"allow"` (user can flip the radio to reveal block).
357
+ */
358
+ const inferAllowBlockMode = (allowCount, blockCount) => {
359
+ if (allowCount > 0) {
360
+ return 'allow';
361
+ }
362
+ if (blockCount > 0) {
363
+ return 'block';
364
+ }
365
+ return 'allow';
366
+ };
367
+ const draftMerchantsToChips = (names) => names.map((name) => toVendorChipFieldValue(name, true));
368
+ const draftCategoryCodesToChips = (codes, mccCodeToCategory) => codes.map((code) => {
369
+ const category = mccCodeToCategory.get(code);
370
+ if (category != null) {
371
+ return toMccCategoryChipFieldValue(category, true);
372
+ }
373
+ return {
374
+ id: code,
375
+ label: code,
376
+ isSelected: true,
377
+ optionalCode: code,
378
+ optionalCodes: [code],
379
+ };
380
+ });
381
+ const mergeAiAndSeedChips = (aiChips, seedChips) => {
382
+ const seenIds = new Set(aiChips.map((chip) => chip.id));
383
+ const extraSeeds = seedChips.filter((chip) => !seenIds.has(chip.id));
384
+ return [...aiChips, ...extraSeeds];
385
+ };
@@ -0,0 +1,34 @@
1
+ import { buildEmptyLimitRows } from './buildAiCardPolicyFormDraftSeed';
2
+ import { toMccCategoryChipFieldValue, toVendorChipFieldValue, } from './cardPolicyChipIds';
3
+ const EMPTY_ALLOW_BLOCK = {
4
+ mode: 'allow',
5
+ allowItems: [],
6
+ blockItems: [],
7
+ };
8
+ /**
9
+ * Build the initial `CardPolicyCreateFormDraft` for a fresh manual
10
+ * create-page session. Mirrors the AI guided seed minus the wizard
11
+ * plan: every BE-suggested chip lands `isSelected: false` so the user
12
+ * has to opt each suggestion onto the policy explicitly.
13
+ */
14
+ export const buildManualCardPolicyFormDraftSeed = ({ cardsMasterList = [], suggestedAllowMerchants, suggestedBlockMerchants, suggestedAllowCategories, suggestedBlockCategories, }) => {
15
+ const vendorAllow = (suggestedAllowMerchants ?? []).map((name) => toVendorChipFieldValue(name, false));
16
+ const vendorBlock = (suggestedBlockMerchants ?? []).map((name) => toVendorChipFieldValue(name, false));
17
+ const categoryAllow = (suggestedAllowCategories ?? []).map((category) => toMccCategoryChipFieldValue(category, false));
18
+ const categoryBlock = (suggestedBlockCategories ?? []).map((category) => toMccCategoryChipFieldValue(category, false));
19
+ const vendorPolicy = vendorAllow.length === 0 && vendorBlock.length === 0
20
+ ? EMPTY_ALLOW_BLOCK
21
+ : { mode: 'allow', allowItems: vendorAllow, blockItems: vendorBlock };
22
+ const categoryPolicy = categoryAllow.length === 0 && categoryBlock.length === 0
23
+ ? EMPTY_ALLOW_BLOCK
24
+ : { mode: 'allow', allowItems: categoryAllow, blockItems: categoryBlock };
25
+ return {
26
+ name: '',
27
+ description: '',
28
+ mode: 'strict',
29
+ vendorPolicy,
30
+ categoryPolicy,
31
+ cardSelections: cardsMasterList.map((row) => ({ ...row, isChecked: false })),
32
+ limits: buildEmptyLimitRows(),
33
+ };
34
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Centralized chip-id derivation so seeds, search-dropdown rows,
3
+ * saved-template recoveries, and the dedupe logic in
4
+ * `AllowBlockChipPicker.handleAddItem` all key off the **same** id for
5
+ * the same logical entity.
6
+ *
7
+ * The dedupe path is content-blind (`existing.id === option.id`): if
8
+ * the id strings don't match, the picker treats the option as a new
9
+ * chip and appends a duplicate. Every producer of chip values for the
10
+ * card-policy flow MUST funnel through these helpers.
11
+ *
12
+ * - Vendors: `vendor-${name.trim().toLowerCase()}`. Two vendors that
13
+ * share an identical display name collapse into one chip.
14
+ * - MCC categories: `mcc-${name}`. Matches the legacy
15
+ * `toCategoryChipOption` convention so the saved-template loader
16
+ * keeps recovering chips by id.
17
+ */
18
+ export const VENDOR_CHIP_ID_PREFIX = 'vendor-';
19
+ export const MCC_CHIP_ID_PREFIX = 'mcc-';
20
+ export const buildVendorChipId = (name) => `${VENDOR_CHIP_ID_PREFIX}${name.trim().toLowerCase()}`;
21
+ export const buildMccCategoryChipId = (name) => `${MCC_CHIP_ID_PREFIX}${name}`;
22
+ export const toVendorChipFieldValue = (name, isSelected) => ({
23
+ id: buildVendorChipId(name),
24
+ label: name,
25
+ isSelected,
26
+ });
27
+ export const toMccCategoryChipFieldValue = (category, isSelected) => ({
28
+ id: buildMccCategoryChipId(category.name),
29
+ label: category.name,
30
+ isSelected,
31
+ optionalCode: category.mccCodes[0],
32
+ optionalCodes: [...category.mccCodes],
33
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Stable id for the transaction-limit toggle row inside `limits`.
3
+ */
4
+ export const CARD_POLICY_LIMIT_ROW_ID_TRANSACTION = 'transactionLimit';
5
+ /**
6
+ * Stable id for the require-receipt toggle row inside `limits`.
7
+ */
8
+ export const CARD_POLICY_LIMIT_ROW_ID_REQUIRE_RECEIPT = 'requireReceipt';
9
+ /**
10
+ * Adapter so callers can hand the `MccCategory[]` array from
11
+ * `cardPolicyState.mccCategories` (or the suggested seeds) straight to
12
+ * the helpers expecting `MccCategoryLike[]`.
13
+ */
14
+ export const toMccCategoryLike = (category) => ({
15
+ name: category.name,
16
+ mccCodes: category.mccCodes,
17
+ });
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Shared helpers for building the wire-ready
3
+ * `CreateCardPolicyTemplateRequest` payload from either the manual
4
+ * `CardPolicyCreateFormDraft` or the AI CFO `CardPolicyFormDraft`.
5
+ * Both forms emit the same `vendorPolicy` / `categoryPolicy` /
6
+ * `limits` slices, so the mapping primitives are factored out here.
7
+ */
8
+ /**
9
+ * Project the merchant names (chip `label`) selected on the given
10
+ * allow / block field, but only when the field's `mode` matches
11
+ * `forMode`. The other side's chips are preserved in form state for
12
+ * fast mode-flip but never belong on the wire for the current submit.
13
+ */
14
+ export const collectMerchantNames = (field, forMode) => {
15
+ if (field.mode !== forMode) {
16
+ return [];
17
+ }
18
+ const items = forMode === 'allow' ? field.allowItems : field.blockItems;
19
+ return items.filter((item) => item.isSelected).map((item) => item.label);
20
+ };
21
+ /**
22
+ * Project the MCC codes selected on the given allow / block field.
23
+ * Chips with `optionalCodes` emit every code; chips lacking the plural
24
+ * fall back to the singular `optionalCode`. Chips with neither are
25
+ * dropped silently.
26
+ */
27
+ export const collectCategoryCodes = (field, forMode) => {
28
+ if (field.mode !== forMode) {
29
+ return [];
30
+ }
31
+ const items = forMode === 'allow' ? field.allowItems : field.blockItems;
32
+ return items
33
+ .filter((item) => item.isSelected)
34
+ .flatMap((item) => {
35
+ if (item.optionalCodes != null && item.optionalCodes.length > 0) {
36
+ return item.optionalCodes;
37
+ }
38
+ return item.optionalCode != null ? [item.optionalCode] : [];
39
+ });
40
+ };
41
+ /**
42
+ * `null` when the toggle is off (BE treats `null` as "no threshold");
43
+ * otherwise the row's `amount` (or `0` when unset but the toggle is on).
44
+ */
45
+ export const amountFromLimitRow = (limits, rowId) => {
46
+ const row = limits.find((entry) => entry.id === rowId);
47
+ if (row == null || !row.isEnabled) {
48
+ return null;
49
+ }
50
+ return row.amount ?? 0;
51
+ };
52
+ const REVIEW_GROUP_ID_ALLOWED_VENDORS = 'allowed-vendors';
53
+ const REVIEW_GROUP_ID_BLOCKED_VENDORS = 'blocked-vendors';
54
+ const REVIEW_GROUP_ID_ALLOWED_CATEGORIES = 'allowed-categories';
55
+ const REVIEW_GROUP_ID_RESTRICTED_CATEGORIES = 'restricted-categories';
56
+ const REVIEW_GROUP_ID_APPLIED_TO_CARDS = 'applied-to-cards';
57
+ const REVIEW_GROUP_ID_LIMITS = 'limits';
58
+ const REVIEW_LIMIT_CHIP_ID_TRANSACTION = 'limit-transaction';
59
+ const REVIEW_LIMIT_CHIP_ID_RECEIPT = 'limit-receipt';
60
+ const isChipOn = (chip) => chip.isSelected !== false;
61
+ const findGroup = (row, groupId) => row.chipGroups.find((group) => group.id === groupId);
62
+ const projectMerchantNamesFromGroup = (group) => group == null ? [] : group.chips.filter(isChipOn).map((chip) => chip.label);
63
+ const projectCategoryCodesFromGroup = (group) => {
64
+ if (group == null) {
65
+ return [];
66
+ }
67
+ const codes = [];
68
+ for (const chip of group.chips) {
69
+ if (!isChipOn(chip)) {
70
+ continue;
71
+ }
72
+ if (chip.optionalCode != null && chip.optionalCode.length > 0) {
73
+ codes.push(chip.optionalCode);
74
+ }
75
+ }
76
+ return codes;
77
+ };
78
+ const projectApplyToCardsFromGroup = (group) => {
79
+ if (group == null) {
80
+ return [];
81
+ }
82
+ const cardIds = [];
83
+ for (const chip of group.chips) {
84
+ if (!isChipOn(chip)) {
85
+ continue;
86
+ }
87
+ if (chip.cardIds != null && chip.cardIds.length > 0) {
88
+ cardIds.push(...chip.cardIds);
89
+ continue;
90
+ }
91
+ cardIds.push(chip.id);
92
+ }
93
+ return cardIds;
94
+ };
95
+ const amountFromLimitsChip = (chip) => {
96
+ if (chip == null || chip.isSelected !== true) {
97
+ return null;
98
+ }
99
+ if (chip.optionalCode == null || chip.optionalCode.length === 0) {
100
+ return 0;
101
+ }
102
+ const parsed = Number(chip.optionalCode);
103
+ return Number.isFinite(parsed) ? parsed : 0;
104
+ };
105
+ /**
106
+ * Project a single `auto`-mode review row's chip groups into the wire
107
+ * shape. Used by the AI CFO wire builder when `policyKind === "auto"`,
108
+ * where the source slices are empty and the row's `chipGroups` are
109
+ * canonical.
110
+ */
111
+ export const projectAutoReviewRowToCardPolicyRequestParts = (row) => {
112
+ const limitsGroup = findGroup(row, REVIEW_GROUP_ID_LIMITS);
113
+ const transactionChip = limitsGroup?.chips.find((chip) => chip.id === REVIEW_LIMIT_CHIP_ID_TRANSACTION);
114
+ const receiptChip = limitsGroup?.chips.find((chip) => chip.id === REVIEW_LIMIT_CHIP_ID_RECEIPT);
115
+ return {
116
+ allowedEntity: {
117
+ merchantNames: projectMerchantNamesFromGroup(findGroup(row, REVIEW_GROUP_ID_ALLOWED_VENDORS)),
118
+ categoryCodes: projectCategoryCodesFromGroup(findGroup(row, REVIEW_GROUP_ID_ALLOWED_CATEGORIES)),
119
+ },
120
+ blockedEntity: {
121
+ merchantNames: projectMerchantNamesFromGroup(findGroup(row, REVIEW_GROUP_ID_BLOCKED_VENDORS)),
122
+ categoryCodes: projectCategoryCodesFromGroup(findGroup(row, REVIEW_GROUP_ID_RESTRICTED_CATEGORIES)),
123
+ },
124
+ applyToCards: projectApplyToCardsFromGroup(findGroup(row, REVIEW_GROUP_ID_APPLIED_TO_CARDS)),
125
+ spendLimits: { transaction: amountFromLimitsChip(transactionChip) },
126
+ requiredReceiptThreshold: amountFromLimitsChip(receiptChip),
127
+ };
128
+ };
129
+ /**
130
+ * Group-id constants used by both auto and bulk wire builders. Exported
131
+ * so the bulk builder can mirror the contract without re-declaring.
132
+ */
133
+ export const REVIEW_GROUP_IDS = {
134
+ allowedVendors: REVIEW_GROUP_ID_ALLOWED_VENDORS,
135
+ blockedVendors: REVIEW_GROUP_ID_BLOCKED_VENDORS,
136
+ allowedCategories: REVIEW_GROUP_ID_ALLOWED_CATEGORIES,
137
+ restrictedCategories: REVIEW_GROUP_ID_RESTRICTED_CATEGORIES,
138
+ appliedToCards: REVIEW_GROUP_ID_APPLIED_TO_CARDS,
139
+ limits: REVIEW_GROUP_ID_LIMITS,
140
+ };
141
+ export const REVIEW_LIMIT_CHIP_IDS = {
142
+ transaction: REVIEW_LIMIT_CHIP_ID_TRANSACTION,
143
+ receipt: REVIEW_LIMIT_CHIP_ID_RECEIPT,
144
+ };