@salesforce/plugin-bre-to-cml 1.3.0 → 1.3.2

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 (50) hide show
  1. package/README.md +117 -4
  2. package/lib/commands/cml/convert/surcharge-rules.d.ts +12 -0
  3. package/lib/commands/cml/convert/surcharge-rules.js +79 -5
  4. package/lib/commands/cml/convert/surcharge-rules.js.map +1 -1
  5. package/lib/commands/cml/convert/underwriting-rules.d.ts +41 -2
  6. package/lib/commands/cml/convert/underwriting-rules.js +141 -17
  7. package/lib/commands/cml/convert/underwriting-rules.js.map +1 -1
  8. package/lib/commands/insurance/import/record-updates.d.ts +66 -0
  9. package/lib/commands/insurance/import/record-updates.js +239 -0
  10. package/lib/commands/insurance/import/record-updates.js.map +1 -0
  11. package/lib/shared/cml-operators.d.ts +9 -1
  12. package/lib/shared/cml-operators.js +23 -5
  13. package/lib/shared/cml-operators.js.map +1 -1
  14. package/lib/shared/insurance/confirm-org-changes.d.ts +49 -0
  15. package/lib/shared/insurance/confirm-org-changes.js +43 -0
  16. package/lib/shared/insurance/confirm-org-changes.js.map +1 -0
  17. package/lib/shared/insurance/insurance-cml-merge.d.ts +100 -13
  18. package/lib/shared/insurance/insurance-cml-merge.js +355 -54
  19. package/lib/shared/insurance/insurance-cml-merge.js.map +1 -1
  20. package/lib/shared/insurance/insurance-context-tags.d.ts +124 -0
  21. package/lib/shared/insurance/insurance-context-tags.js +210 -0
  22. package/lib/shared/insurance/insurance-context-tags.js.map +1 -0
  23. package/lib/shared/insurance/insurance-org.d.ts +12 -0
  24. package/lib/shared/insurance/insurance-org.js +38 -2
  25. package/lib/shared/insurance/insurance-org.js.map +1 -1
  26. package/lib/shared/insurance/insurance-rule-convert-command.d.ts +27 -0
  27. package/lib/shared/insurance/insurance-rule-convert-command.js +57 -4
  28. package/lib/shared/insurance/insurance-rule-convert-command.js.map +1 -1
  29. package/lib/shared/insurance/insurance-rule-generator.d.ts +101 -8
  30. package/lib/shared/insurance/insurance-rule-generator.js +293 -42
  31. package/lib/shared/insurance/insurance-rule-generator.js.map +1 -1
  32. package/lib/shared/insurance/models.d.ts +2 -1
  33. package/lib/shared/insurance/planned-change-mapper.d.ts +15 -0
  34. package/lib/shared/insurance/planned-change-mapper.js +51 -0
  35. package/lib/shared/insurance/planned-change-mapper.js.map +1 -0
  36. package/lib/shared/insurance/planned-change.d.ts +83 -0
  37. package/lib/shared/insurance/planned-change.js +74 -0
  38. package/lib/shared/insurance/planned-change.js.map +1 -0
  39. package/lib/shared/insurance/record-update-apply.d.ts +100 -0
  40. package/lib/shared/insurance/record-update-apply.js +303 -0
  41. package/lib/shared/insurance/record-update-apply.js.map +1 -0
  42. package/lib/shared/insurance/record-update-plan.d.ts +104 -0
  43. package/lib/shared/insurance/record-update-plan.js +397 -0
  44. package/lib/shared/insurance/record-update-plan.js.map +1 -0
  45. package/messages/cml.convert.surcharge-rules.md +3 -1
  46. package/messages/cml.convert.underwriting-rules.md +1 -1
  47. package/messages/insurance.import.record-updates.md +176 -0
  48. package/oclif.lock +28 -28
  49. package/oclif.manifest.json +127 -35
  50. package/package.json +16 -8
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Load + validate side of `sf insurance import record-updates`
3
+ * (export/review/import design, work item W-23654540 §3, §8).
4
+ *
5
+ * The `<safeApi>_{Underwriting,Surcharge}Update.json` file is explicitly meant to be reviewed and
6
+ * hand-corrected before it is applied, so everything here treats it as untrusted input: the whole
7
+ * file is rejected on any structural violation, before a single write. Passing the id regex is NOT
8
+ * identity proof — the apply-time Name cross-check (record-update-apply.ts) is the real guard
9
+ * against writing to a valid-but-wrong record.
10
+ */
11
+ import { RecordUpdate, RecordUpdatePlan } from './models.js';
12
+ export declare const RECORD_UPDATE_KINDS: ReadonlyArray<RecordUpdatePlan['kind']>;
13
+ /**
14
+ * Fields each sObject is allowed to set, matching what convert emits. Anything else in the file is
15
+ * a hand-edit gone wrong (or a file from a newer plugin) and is refused rather than written.
16
+ */
17
+ export declare const ALLOWED_FIELDS: Readonly<Record<RecordUpdate['sobject'], readonly string[]>>;
18
+ /**
19
+ * Apply order across sObject types. `UnderwritingRuleGroup` must precede `UnderwritingRule` to
20
+ * preserve the ordering the original live convert path used.
21
+ */
22
+ export declare const SOBJECT_APPLY_ORDER: ReadonlyArray<RecordUpdate['sobject']>;
23
+ /** The one field whose value is itself a JSON document, so it needs structural (not raw) compare. */
24
+ export declare const JSON_BLOB_FIELD = "DynamicRuleDefinition";
25
+ /**
26
+ * Violations that have their own operator-facing message key, because the remediation for them is
27
+ * not the generic "regenerate the file or correct the reported problem".
28
+ */
29
+ export type RecordUpdatePlanErrorCode = 'duplicateRecordId';
30
+ /** Thrown for any structural violation; the command turns it into `error.invalidFile`. */
31
+ export declare class RecordUpdatePlanError extends Error {
32
+ readonly code?: RecordUpdatePlanErrorCode;
33
+ constructor(message: string, code?: RecordUpdatePlanErrorCode);
34
+ }
35
+ export type ParsePlanOptions = {
36
+ /** Non-fatal notices, e.g. a `DynamicRuleDefinition` value that does not parse as JSON. */
37
+ onWarn?: (message: string) => void;
38
+ };
39
+ /**
40
+ * Parses and structurally validates a record-update file. `source` is the file path, used only to
41
+ * make error messages point at the offending file.
42
+ */
43
+ export declare function parseRecordUpdatePlan(raw: string, source: string, options?: ParsePlanOptions): RecordUpdatePlan;
44
+ /** The subset of a `DynamicRuleDefinition` blob that convert actually mutates. */
45
+ type BlobSignature = {
46
+ ruleKey: unknown;
47
+ ruleEngineType: unknown;
48
+ };
49
+ /**
50
+ * Extracts the only parts of the blob convert rewrites. Returns undefined when the value is not
51
+ * parseable JSON, so callers can fall back to a raw compare.
52
+ */
53
+ export declare function dynamicRuleDefinitionSignature(value: string): BlobSignature | undefined;
54
+ /**
55
+ * Renders a `DynamicRuleDefinition` change as a semantic diff of just the fields convert mutates,
56
+ * plus a disclosure of any difference elsewhere in the document.
57
+ *
58
+ * A real blob is several hundred characters whose first 60 are identical before and after, so a
59
+ * truncated raw `old → new` renders byte-identically on both sides and the operator confirms
60
+ * without having seen anything (B1). But {@link isAlreadyCurrent} compares the WHOLE document (B2),
61
+ * so a row can be an `Update` because of a field this diff does not show — and then every pair it
62
+ * does show reads `X → X`, which is the same "confirmed without seeing anything" failure wearing a
63
+ * different hat. The pairs are therefore only rendered when they actually changed, and anything
64
+ * that changed outside them is named.
65
+ *
66
+ * Returns undefined when either side cannot be read structurally, so the caller falls back to the
67
+ * raw diff rather than inventing a value.
68
+ */
69
+ export declare function formatBlobChange(currentValue: string | null | undefined, newValue: string): string | undefined;
70
+ /**
71
+ * The same semantic view of a single blob, for rendering an already-current row.
72
+ *
73
+ * This needs no "other fields" disclosure: the row only exists because {@link isAlreadyCurrent}
74
+ * found the two documents deep-equal, so by construction there is nothing outside these fields to
75
+ * disclose. (The one already-current path that does not prove that — the raw-text fallback for an
76
+ * unparseable blob — never reaches here, because an unparseable value yields no signature.)
77
+ */
78
+ export declare function formatBlobSummary(value: string | null | undefined): string | undefined;
79
+ /** Why {@link dynamicRuleDefinitionApiName} could not produce a name. Callers must not ignore it. */
80
+ export type BlobApiNameFailure = 'unparseable' | 'absent';
81
+ /**
82
+ * Reads the `apiName` out of a `DynamicRuleDefinition` blob, for the apply-time identity check.
83
+ *
84
+ * Returns a discriminated failure rather than `undefined` so the caller cannot silently read "no
85
+ * name" as "no mismatch": an unreadable blob means the guard did not run, which is a different
86
+ * thing from a blob that ran and agreed.
87
+ */
88
+ export declare function dynamicRuleDefinitionApiName(value: string | null | undefined): {
89
+ apiName: string;
90
+ } | {
91
+ failure: BlobApiNameFailure;
92
+ };
93
+ /**
94
+ * Field-type-aware "is the org already in the desired state?" check (§5.3).
95
+ *
96
+ * `DynamicRuleDefinition` is compared as a whole JSON document rather than raw text, so a re-run
97
+ * after the org re-serializes the blob — or after a reviewer reformats it — is correctly
98
+ * recognized as a no-op instead of rewriting it. The comparison covers the *entire* document, not
99
+ * just the fields convert mutates: the apply writes the file's blob verbatim, so a reviewer's
100
+ * hand-correction anywhere in it (a threshold in `ruleCriteria`, say) is a substantive change that
101
+ * must not be silently dropped as "already current". Scalar fields compare raw.
102
+ */
103
+ export declare function isAlreadyCurrent(field: string, currentValue: string | null | undefined, newValue: string): boolean;
104
+ export {};
@@ -0,0 +1,397 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { NO_VALUE, truncateCell } from './planned-change.js';
17
+ import { decodeHtmlEntities } from './insurance-rule-generator.js';
18
+ export const RECORD_UPDATE_KINDS = ['underwriting-update', 'surcharge-update'];
19
+ /**
20
+ * Fields each sObject is allowed to set, matching what convert emits. Anything else in the file is
21
+ * a hand-edit gone wrong (or a file from a newer plugin) and is refused rather than written.
22
+ */
23
+ export const ALLOWED_FIELDS = {
24
+ UnderwritingRuleGroup: ['RuleEngineType'],
25
+ UnderwritingRule: ['DynamicRuleDefinition'],
26
+ ProductSurcharge: ['RuleEngineType'],
27
+ };
28
+ /**
29
+ * Apply order across sObject types. `UnderwritingRuleGroup` must precede `UnderwritingRule` to
30
+ * preserve the ordering the original live convert path used.
31
+ */
32
+ export const SOBJECT_APPLY_ORDER = [
33
+ 'UnderwritingRuleGroup',
34
+ 'UnderwritingRule',
35
+ 'ProductSurcharge',
36
+ ];
37
+ /**
38
+ * Which sObjects each kind may carry. `kind` selects behaviour the sObjects themselves do not
39
+ * imply — most importantly the post-flip RuleKey readback, which the command dispatches purely on
40
+ * `kind`. A file labelled `underwriting-update` that contains ProductSurcharge updates would flip
41
+ * them with that verification silently skipped, losing the one check that catches "imports cleanly
42
+ * but never fires".
43
+ */
44
+ const KIND_SOBJECTS = {
45
+ 'underwriting-update': ['UnderwritingRuleGroup', 'UnderwritingRule'],
46
+ 'surcharge-update': ['ProductSurcharge'],
47
+ };
48
+ const SALESFORCE_ID_PATTERN = /^[a-zA-Z0-9]{15}([a-zA-Z0-9]{3})?$/;
49
+ /** The one field whose value is itself a JSON document, so it needs structural (not raw) compare. */
50
+ export const JSON_BLOB_FIELD = 'DynamicRuleDefinition';
51
+ /** Thrown for any structural violation; the command turns it into `error.invalidFile`. */
52
+ export class RecordUpdatePlanError extends Error {
53
+ code;
54
+ constructor(message, code) {
55
+ super(message);
56
+ this.code = code;
57
+ }
58
+ }
59
+ const fail = (source, detail, code) => {
60
+ throw new RecordUpdatePlanError(`${source}: ${detail}`, code);
61
+ };
62
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
63
+ const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0;
64
+ /**
65
+ * Parses and structurally validates a record-update file. `source` is the file path, used only to
66
+ * make error messages point at the offending file.
67
+ */
68
+ export function parseRecordUpdatePlan(raw, source, options = {}) {
69
+ let parsed;
70
+ try {
71
+ parsed = JSON.parse(raw);
72
+ }
73
+ catch (e) {
74
+ return fail(source, `not valid JSON (${e.message})`);
75
+ }
76
+ if (!isRecord(parsed))
77
+ fail(source, 'expected a top-level JSON object');
78
+ const plan = parsed;
79
+ if (plan.schemaVersion !== 1) {
80
+ fail(source, `unsupported schemaVersion ${JSON.stringify(plan.schemaVersion) ?? '<missing>'} (expected 1)`);
81
+ }
82
+ const kind = plan.kind;
83
+ if (typeof kind !== 'string' || !RECORD_UPDATE_KINDS.includes(kind)) {
84
+ fail(source, `unsupported kind ${JSON.stringify(kind) ?? '<missing>'} (expected one of ${RECORD_UPDATE_KINDS.join(', ')})`);
85
+ }
86
+ if (!isNonEmptyString(plan.cmlApi))
87
+ fail(source, 'missing or non-string cmlApi');
88
+ if (!Array.isArray(plan.updates))
89
+ fail(source, 'updates must be an array');
90
+ const typedKind = kind;
91
+ const updates = plan.updates.map((u, i) => parseUpdate(u, source, i, typedKind, options));
92
+ rejectRepeatedIds(updates, source);
93
+ return {
94
+ schemaVersion: 1,
95
+ kind: typedKind,
96
+ cmlApi: plan.cmlApi,
97
+ generatedAt: typeof plan.generatedAt === 'string' ? plan.generatedAt : '',
98
+ updates,
99
+ };
100
+ }
101
+ /**
102
+ * The 15-character form of an id, which is what identifies the record: the optional 3-character
103
+ * suffix is only a case-safety checksum, so `a0p…001` and `a0p…001AAA` are the same record spelled
104
+ * two ways and must count as a repeat.
105
+ */
106
+ const recordKey = (id) => id.slice(0, 15);
107
+ /**
108
+ * Refuses a file that carries two entries for one record — the record-level twin of the
109
+ * duplicate-field check in {@link parseUpdate}.
110
+ *
111
+ * Two entries for one id are not merged anywhere: the preview renders the record twice, with
112
+ * contradictory operations when the values disagree; the field-level counts report the one record as
113
+ * both an update and an already-current skip while the record-level summary reports neither; and
114
+ * when both values need writing, `applyRecordUpdates` groups by plan entry, so two payloads
115
+ * carrying the same `Id` go into a single `/composite/sobjects` PATCH, which the org rejects. Not
116
+ * scoped per sObject: a Salesforce id names exactly one record of exactly one type, so the same id
117
+ * under two sObjects is a hand-edit gone wrong, and `applied` counts plan entries either way.
118
+ */
119
+ function rejectRepeatedIds(updates, source) {
120
+ const firstSeenAt = new Map();
121
+ updates.forEach((update, index) => {
122
+ const key = recordKey(update.id);
123
+ const first = firstSeenAt.get(key);
124
+ if (first === undefined) {
125
+ firstSeenAt.set(key, index);
126
+ return;
127
+ }
128
+ fail(source, `updates[${index}] (${update.name}) repeats record id ${update.id}, which updates[${first}] (${updates[first].name}) already updates; each record must appear exactly once`, 'duplicateRecordId');
129
+ });
130
+ }
131
+ function parseUpdate(value, source, index, kind, options) {
132
+ const at = `updates[${index}]`;
133
+ if (!isRecord(value))
134
+ return fail(source, `${at} is not an object`);
135
+ const sobject = value.sobject;
136
+ if (typeof sobject !== 'string' || !(sobject in ALLOWED_FIELDS)) {
137
+ fail(source, `${at} has unsupported sobject ${JSON.stringify(sobject) ?? '<missing>'}`);
138
+ }
139
+ const typedSobject = sobject;
140
+ if (!KIND_SOBJECTS[kind].includes(typedSobject)) {
141
+ fail(source, `${at} has sobject ${typedSobject}, which a '${kind}' file cannot contain; allowed: ${KIND_SOBJECTS[kind].join(', ')}`);
142
+ }
143
+ if (typeof value.id !== 'string' || !SALESFORCE_ID_PATTERN.test(value.id)) {
144
+ fail(source, `${at} has a malformed Salesforce id ${JSON.stringify(value.id) ?? '<missing>'}`);
145
+ }
146
+ // `name` is a required verification key, not cosmetic: the apply cross-checks it against the org
147
+ // so an edited id cannot silently retarget a valid-but-wrong record of the same type.
148
+ if (!isNonEmptyString(value.name))
149
+ fail(source, `${at} (${value.id}) is missing a non-empty name`);
150
+ if (!Array.isArray(value.fields) || value.fields.length === 0) {
151
+ fail(source, `${at} (${value.name}) must have at least one field`);
152
+ }
153
+ const fields = value.fields.map((f, fi) => parseField(f, source, `${at}.fields[${fi}]`, typedSobject, value.name, options));
154
+ // Two entries for the same field render two preview rows with two different target values while
155
+ // exactly one write happens (the payload is built with Object.fromEntries, so the last wins).
156
+ // The operator cannot consent to that, so refuse the file rather than pick a winner.
157
+ const duplicate = fields.find((f, i) => fields.findIndex((other) => other.field === f.field) !== i);
158
+ if (duplicate) {
159
+ fail(source, `${at} (${value.name}) sets ${duplicate.field} more than once`);
160
+ }
161
+ return {
162
+ sobject: typedSobject,
163
+ id: value.id,
164
+ name: value.name,
165
+ ...(typeof value.apiName === 'string' ? { apiName: value.apiName } : {}),
166
+ fields,
167
+ ...(typeof value.expectedRuleKey === 'string' ? { expectedRuleKey: value.expectedRuleKey } : {}),
168
+ ...(Array.isArray(value.productCodes) && value.productCodes.every((c) => typeof c === 'string')
169
+ ? { productCodes: value.productCodes }
170
+ : {}),
171
+ };
172
+ }
173
+ function parseField(value, source, at, sobject, recordName, options) {
174
+ if (!isRecord(value))
175
+ return fail(source, `${at} is not an object`);
176
+ const { field, value: fieldValue } = value;
177
+ if (typeof field !== 'string' || !ALLOWED_FIELDS[sobject].includes(field)) {
178
+ fail(source, `${at} sets ${JSON.stringify(field) ?? '<missing>'} on ${sobject}; allowed: ${ALLOWED_FIELDS[sobject].join(', ')}`);
179
+ }
180
+ if (typeof fieldValue !== 'string') {
181
+ fail(source, `${at} (${field}) must have a string value`);
182
+ }
183
+ // An empty value is not "leave it alone": isAlreadyCurrent reports a change, the preview renders
184
+ // an empty right-hand side, and the payload blanks the field in the org. Nothing convert emits
185
+ // is ever empty, so this can only be a hand-edit gone wrong.
186
+ if (!isNonEmptyString(fieldValue)) {
187
+ fail(source, `${at} (${field}) has an empty value; a field to clear must be stated explicitly`);
188
+ }
189
+ // The blob is written verbatim so a reviewer's hand-correction is honored byte-for-byte; an
190
+ // unparseable value is still worth flagging, but the org may accept it, so this is not fatal.
191
+ if (field === JSON_BLOB_FIELD) {
192
+ try {
193
+ JSON.parse(fieldValue);
194
+ }
195
+ catch {
196
+ options.onWarn?.(`${recordName}: ${JSON_BLOB_FIELD} is not valid JSON; it will be written verbatim`);
197
+ }
198
+ }
199
+ return { field: field, value: fieldValue };
200
+ }
201
+ /** Sentinel distinguishing "did not parse" from a document that legitimately parsed to null. */
202
+ const UNPARSEABLE = Symbol('unparseable');
203
+ /**
204
+ * Parses a blob the org stored, tolerating the HTML-entity encoding these fields come back in.
205
+ *
206
+ * The raw parse is attempted first, so a blob that is already clean JSON is never altered — the
207
+ * entity decode only runs on input that is not valid JSON as-is, which is precisely the encoded
208
+ * case (`{&quot;apiName&quot;:…}` cannot parse raw). Convert decodes for the same reason.
209
+ */
210
+ function parseBlob(value) {
211
+ try {
212
+ return JSON.parse(value);
213
+ }
214
+ catch {
215
+ // fall through to the decoded attempt
216
+ }
217
+ try {
218
+ return JSON.parse(decodeHtmlEntities(value));
219
+ }
220
+ catch {
221
+ return UNPARSEABLE;
222
+ }
223
+ }
224
+ /**
225
+ * Extracts the only parts of the blob convert rewrites. Returns undefined when the value is not
226
+ * parseable JSON, so callers can fall back to a raw compare.
227
+ */
228
+ export function dynamicRuleDefinitionSignature(value) {
229
+ const parsed = parseBlob(value);
230
+ if (!isRecord(parsed))
231
+ return undefined;
232
+ const group = parsed.underwritingRuleGroup;
233
+ return {
234
+ ruleKey: parsed.ruleKey,
235
+ ruleEngineType: isRecord(group) ? group.ruleEngineType : undefined,
236
+ };
237
+ }
238
+ /** The mutated fields, paired with the dotted path an operator would recognize them by. */
239
+ const BLOB_SIGNATURE_FIELDS = [
240
+ ['ruleKey', 'ruleKey'],
241
+ ['ruleEngineType', 'underwritingRuleGroup.ruleEngineType'],
242
+ ];
243
+ /** The same two fields as dotted paths, to subtract them from a whole-document difference list. */
244
+ const BLOB_SIGNATURE_PATHS = new Set(BLOB_SIGNATURE_FIELDS.map(([, label]) => label));
245
+ /** How much of the cell the named-path list may take, matching the per-value budget. */
246
+ const OTHER_PATHS_BUDGET = 60;
247
+ /** A single path is truncated well short of the list budget, so one long path cannot crowd out all others. */
248
+ const OTHER_PATH_BUDGET = 40;
249
+ const renderSignatureValue = (value) => {
250
+ if (value == null)
251
+ return NO_VALUE;
252
+ return truncateCell(typeof value === 'string' ? value : JSON.stringify(value));
253
+ };
254
+ /**
255
+ * Names the differing paths that fit the cell budget, then counts the rest.
256
+ *
257
+ * Always names at least one, even if it alone exceeds the budget: a disclosure with no example is
258
+ * barely more actionable than the identical-pairs cell this exists to replace.
259
+ */
260
+ function nameOtherPaths(paths) {
261
+ const named = [];
262
+ let used = 0;
263
+ for (const path of paths) {
264
+ const rendered = truncateCell(path, OTHER_PATH_BUDGET);
265
+ const cost = rendered.length + (named.length > 0 ? ', '.length : 0);
266
+ if (named.length > 0 && used + cost > OTHER_PATHS_BUDGET)
267
+ break;
268
+ named.push(rendered);
269
+ used += cost;
270
+ }
271
+ const overflow = paths.length - named.length;
272
+ return overflow > 0 ? `${named.join(', ')}, +${overflow} more` : named.join(', ');
273
+ }
274
+ /** e.g. `3 other fields differ: status, description, +1 more`. */
275
+ const describeOtherPaths = (paths) => `${paths.length} other ${paths.length === 1 ? 'field differs' : 'fields differ'}: ${nameOtherPaths(paths)}`;
276
+ /**
277
+ * Renders a `DynamicRuleDefinition` change as a semantic diff of just the fields convert mutates,
278
+ * plus a disclosure of any difference elsewhere in the document.
279
+ *
280
+ * A real blob is several hundred characters whose first 60 are identical before and after, so a
281
+ * truncated raw `old → new` renders byte-identically on both sides and the operator confirms
282
+ * without having seen anything (B1). But {@link isAlreadyCurrent} compares the WHOLE document (B2),
283
+ * so a row can be an `Update` because of a field this diff does not show — and then every pair it
284
+ * does show reads `X → X`, which is the same "confirmed without seeing anything" failure wearing a
285
+ * different hat. The pairs are therefore only rendered when they actually changed, and anything
286
+ * that changed outside them is named.
287
+ *
288
+ * Returns undefined when either side cannot be read structurally, so the caller falls back to the
289
+ * raw diff rather than inventing a value.
290
+ */
291
+ export function formatBlobChange(currentValue, newValue) {
292
+ const desired = dynamicRuleDefinitionSignature(newValue);
293
+ if (!desired)
294
+ return undefined;
295
+ // A null current value is a genuinely empty field, not an unreadable one.
296
+ const current = currentValue == null ? undefined : dynamicRuleDefinitionSignature(currentValue);
297
+ if (currentValue != null && !current)
298
+ return undefined;
299
+ const pairs = BLOB_SIGNATURE_FIELDS.map(([key, label]) => `${label}: ${renderSignatureValue(current?.[key])} → ${renderSignatureValue(desired[key])}`).join(', ');
300
+ // Nothing to diff against, and `(none) → value` is already visibly different on both sides: every
301
+ // field in the document is new, so there is no subset worth singling out as "other".
302
+ if (currentValue == null)
303
+ return pairs;
304
+ const other = documentDifferences(parseBlob(currentValue), parseBlob(newValue)).filter((path) => !BLOB_SIGNATURE_PATHS.has(path));
305
+ if (other.length === 0)
306
+ return pairs;
307
+ const signatureChanged = BLOB_SIGNATURE_FIELDS.some(([key]) => documentDifferences(current?.[key], desired[key]).length > 0);
308
+ return signatureChanged
309
+ ? `${pairs}; also ${describeOtherPaths(other)}`
310
+ : `${BLOB_SIGNATURE_FIELDS.map(([, label]) => label).join(' and ')} unchanged; ${describeOtherPaths(other)}`;
311
+ }
312
+ /**
313
+ * The same semantic view of a single blob, for rendering an already-current row.
314
+ *
315
+ * This needs no "other fields" disclosure: the row only exists because {@link isAlreadyCurrent}
316
+ * found the two documents deep-equal, so by construction there is nothing outside these fields to
317
+ * disclose. (The one already-current path that does not prove that — the raw-text fallback for an
318
+ * unparseable blob — never reaches here, because an unparseable value yields no signature.)
319
+ */
320
+ export function formatBlobSummary(value) {
321
+ const signature = value == null ? undefined : dynamicRuleDefinitionSignature(value);
322
+ if (!signature)
323
+ return undefined;
324
+ return BLOB_SIGNATURE_FIELDS.map(([key, label]) => `${label}: ${renderSignatureValue(signature[key])}`).join(', ');
325
+ }
326
+ /**
327
+ * Reads the `apiName` out of a `DynamicRuleDefinition` blob, for the apply-time identity check.
328
+ *
329
+ * Returns a discriminated failure rather than `undefined` so the caller cannot silently read "no
330
+ * name" as "no mismatch": an unreadable blob means the guard did not run, which is a different
331
+ * thing from a blob that ran and agreed.
332
+ */
333
+ export function dynamicRuleDefinitionApiName(value) {
334
+ if (value == null || value === '')
335
+ return { failure: 'absent' };
336
+ const parsed = parseBlob(value);
337
+ if (!isRecord(parsed))
338
+ return { failure: 'unparseable' };
339
+ return typeof parsed.apiName === 'string' ? { apiName: parsed.apiName } : { failure: 'absent' };
340
+ }
341
+ /** Stands in for the path of a difference at the root, where there is no key to name. */
342
+ const ROOT_PATH = '(whole document)';
343
+ /**
344
+ * Every dotted path at which two parsed JSON documents differ: objects compare
345
+ * key-order-insensitively, arrays stay ordered. An empty result is exactly "same document, possibly
346
+ * re-serialized" — the distinction the blob compare needs to draw.
347
+ *
348
+ * Deliberately the single definition of blob difference in this module. {@link isAlreadyCurrent}
349
+ * decides whether to write from it and {@link formatBlobChange} describes the write from it, so the
350
+ * preview cannot claim a document is unchanged that the skip decision is about to rewrite, or the
351
+ * reverse. A second, separately-written comparison would eventually disagree with this one, and a
352
+ * preview that contradicts what gets applied is worse than no preview.
353
+ *
354
+ * An array whose length differs is reported at the array itself rather than per element: the
355
+ * element indices are not stable across an insertion, so naming them would mislead more than the
356
+ * array's own name does.
357
+ */
358
+ function documentDifferences(a, b, path = '') {
359
+ if (a === b)
360
+ return [];
361
+ if (Array.isArray(a) || Array.isArray(b)) {
362
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
363
+ return [path === '' ? ROOT_PATH : path];
364
+ return a.flatMap((item, i) => documentDifferences(item, b[i], `${path}[${i}]`));
365
+ }
366
+ if (!isRecord(a) || !isRecord(b))
367
+ return [path === '' ? ROOT_PATH : path];
368
+ return [...new Set([...Object.keys(a), ...Object.keys(b)])].flatMap((key) => {
369
+ const child = path === '' ? key : `${path}.${key}`;
370
+ if (!Object.hasOwn(a, key) || !Object.hasOwn(b, key))
371
+ return [child];
372
+ return documentDifferences(a[key], b[key], child);
373
+ });
374
+ }
375
+ /**
376
+ * Field-type-aware "is the org already in the desired state?" check (§5.3).
377
+ *
378
+ * `DynamicRuleDefinition` is compared as a whole JSON document rather than raw text, so a re-run
379
+ * after the org re-serializes the blob — or after a reviewer reformats it — is correctly
380
+ * recognized as a no-op instead of rewriting it. The comparison covers the *entire* document, not
381
+ * just the fields convert mutates: the apply writes the file's blob verbatim, so a reviewer's
382
+ * hand-correction anywhere in it (a threshold in `ruleCriteria`, say) is a substantive change that
383
+ * must not be silently dropped as "already current". Scalar fields compare raw.
384
+ */
385
+ export function isAlreadyCurrent(field, currentValue, newValue) {
386
+ if (currentValue == null)
387
+ return false;
388
+ if (field !== JSON_BLOB_FIELD)
389
+ return currentValue === newValue;
390
+ const current = parseBlob(currentValue);
391
+ const desired = parseBlob(newValue);
392
+ // An unparseable side means we cannot reason structurally; fall back to the raw comparison.
393
+ if (current === UNPARSEABLE || desired === UNPARSEABLE)
394
+ return currentValue === newValue;
395
+ return documentDifferences(current, desired).length === 0;
396
+ }
397
+ //# sourceMappingURL=record-update-plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-update-plan.js","sourceRoot":"","sources":["../../../src/shared/insurance/record-update-plan.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,9 @@ Converts BRE-based Product Surcharge dynamic rules to CML eligibility constraint
4
4
 
5
5
  # description
6
6
 
7
- Reads ProductSurcharge records from the org (or a JSON file), parses their RuleDefinition, and merges the generated surcharge rules into the org's existing curated ConstraintModel for the resolved CML API. Each surcharge rule is nested into its leaf product type with a platform-compatible pathed rule key (matching the RuleKey the platform auto-generates), so the rule actually fires for nested products instead of being silently dropped. The command is file-only and never writes to the org. It requires an existing CML model to merge into and outputs a .cml file with the full merged model, a header-only \_Associations.csv file, a \_RuleKeyMapping.json with the ProductSurcharge ID to RuleKey mapping, and a \_SurchargeUpdate.json file enumerating the org-record changes. Review the files, then apply the CML with `sf cml import as-expression-set` and apply the org-record changes enumerated in the \_SurchargeUpdate.json file to the org separately (in that order).
7
+ Reads ProductSurcharge records from the org (or a JSON file), parses their RuleDefinition, and merges the generated surcharge rules into the org's existing curated ConstraintModel for the resolved CML API. Each surcharge rule is nested into its leaf product type with a platform-compatible pathed rule key (matching the RuleKey the platform auto-generates), so the rule actually fires for nested products instead of being silently dropped. The command is file-only and never writes to the org. It requires an existing CML model to merge into and outputs a .cml file with the full merged model, a header-only \_Associations.csv file, a \_RuleKeyMapping.json with the ProductSurcharge ID to RuleKey mapping, and a \_SurchargeUpdate.json file enumerating the org-record changes. Review the files, then apply the CML with `sf cml import as-expression-set` and apply the \_SurchargeUpdate.json file with `sf insurance import record-updates` (in that order — the platform regenerates ProductSurcharge.RuleKey against the active model when RuleEngineType flips).
8
+
9
+ The parent Surcharge's Code field must be set (non-null): the platform derives the leaf segment of the auto-generated ProductSurcharge.RuleKey from Surcharge.Code, so the converter uses it to build a matching rule key. A surcharge whose parent Surcharge has a null Code cannot be flipped to ConstraintEngine (the org raises a save-hook error) and will not convert correctly — populate Surcharge.Code before converting.
8
10
 
9
11
  # examples
10
12
 
@@ -4,7 +4,7 @@ Converts BRE-based Insurance Underwriting dynamic rules to CML eligibility const
4
4
 
5
5
  # description
6
6
 
7
- Reads UnderwritingRule records from the org (or a JSON file), parses their DynamicRuleDefinition, and generates CML constraints that evaluate underwriting eligibility. Each rule becomes a named constraint that returns true/false. The command is file-only and never writes to the org: it outputs a .cml file with the constraint model, an \_Associations.csv file for ExpressionSetConstraintObj records, a \_RuleKeyMapping.json with the UnderwritingRule ID to RuleKey mapping, and a \_UnderwritingUpdate.json file enumerating the org-record changes. Review the files, then apply the CML with `sf cml import as-expression-set` and apply the org-record changes enumerated in the \_UnderwritingUpdate.json file to the org separately.
7
+ Reads UnderwritingRule records from the org (or a JSON file), parses their DynamicRuleDefinition, and generates CML constraints that evaluate underwriting eligibility. Each rule becomes a named constraint that returns true/false. The command is file-only and never writes to the org: it outputs a .cml file with the constraint model, an \_Associations.csv file for ExpressionSetConstraintObj records, a \_RuleKeyMapping.json with the UnderwritingRule ID to RuleKey mapping, and a \_UnderwritingUpdate.json file enumerating the org-record changes. Review the files, then apply the CML with `sf cml import as-expression-set` and apply the \_UnderwritingUpdate.json file with `sf insurance import record-updates`.
8
8
 
9
9
  # examples
10
10