@quotivity/cpq-inventory-core 0.1.0
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.
- package/assets/report/assets/index-CKPi--yh.css +1 -0
- package/assets/report/assets/index-CqUjsqpS.js +9 -0
- package/assets/report/fonts/LexendDeca-VariableFont_wght.ttf +0 -0
- package/assets/report/index.html +14 -0
- package/lib/banner.d.ts +7 -0
- package/lib/banner.js +13 -0
- package/lib/classify/advancedCondition.d.ts +34 -0
- package/lib/classify/advancedCondition.js +112 -0
- package/lib/classify/formula.d.ts +29 -0
- package/lib/classify/formula.js +125 -0
- package/lib/classify/index.d.ts +80 -0
- package/lib/classify/index.js +662 -0
- package/lib/classify/rules.d.ts +31 -0
- package/lib/classify/rules.js +868 -0
- package/lib/config/outbound.d.ts +31 -0
- package/lib/config/outbound.js +35 -0
- package/lib/connection.d.ts +42 -0
- package/lib/connection.js +52 -0
- package/lib/extract/index.d.ts +112 -0
- package/lib/extract/index.js +373 -0
- package/lib/extract/records.d.ts +421 -0
- package/lib/extract/records.js +327 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +51 -0
- package/lib/liveness.d.ts +19 -0
- package/lib/liveness.js +34 -0
- package/lib/mapping.d.ts +14 -0
- package/lib/mapping.js +272 -0
- package/lib/report/assemble.d.ts +16 -0
- package/lib/report/assemble.js +499 -0
- package/lib/report/printDoc.d.ts +13 -0
- package/lib/report/printDoc.js +82 -0
- package/lib/report/shareSummary.d.ts +8 -0
- package/lib/report/shareSummary.js +54 -0
- package/lib/report/types.d.ts +146 -0
- package/lib/report/types.js +2 -0
- package/lib/server.d.ts +32 -0
- package/lib/server.js +185 -0
- package/lib/soql.d.ts +50 -0
- package/lib/soql.js +76 -0
- package/lib/summary.d.ts +14 -0
- package/lib/summary.js +143 -0
- package/lib/types.d.ts +22 -0
- package/lib/types.js +85 -0
- package/package.json +35 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only two outbound requests the tool makes, both user-initiated, both HubSpot form
|
|
3
|
+
* submissions made from the browser. Portal ID and form GUIDs are public and compiled in.
|
|
4
|
+
*/
|
|
5
|
+
export declare const HUBSPOT_PORTAL_ID = "243933280";
|
|
6
|
+
/** Email gate — email capture form. Carries name, email, plugin version, run id, lead source. */
|
|
7
|
+
export declare const HUBSPOT_GATE_FORM_GUID = "78c7c119-3bf5-49d9-8c31-6122cfda26b3";
|
|
8
|
+
/** Close screen — meeting request form. Carries the same plus the rendered report summary. */
|
|
9
|
+
export declare const HUBSPOT_MEETING_FORM_GUID = "7a3e9649-209b-415d-81d0-bf6fe002e105";
|
|
10
|
+
/** Internal name of the contact property on the meeting form that receives the report summary. */
|
|
11
|
+
export declare const HUBSPOT_MEETING_SUMMARY_FIELD = "salesforce_cpq_migration_analysis_result";
|
|
12
|
+
export declare const LEAD_SOURCE = "cpq-inventory-tool";
|
|
13
|
+
export declare const SOURCE_URL = "https://github.com/quotivity/salesforce-cpq-migration-analysis";
|
|
14
|
+
export declare const PRIVACY_POLICY_URL = "https://quotivity.com/privacy";
|
|
15
|
+
export declare const LEARN_MORE_URL = "https://quotivity.com";
|
|
16
|
+
/** HubSpot multi-line text fields hold at most 65,536 characters. */
|
|
17
|
+
export declare const HUBSPOT_MULTILINE_LIMIT = 65536;
|
|
18
|
+
export declare function hubspotFormUrl(formGuid: string): string;
|
|
19
|
+
export interface OutboundConfig {
|
|
20
|
+
portalId: string;
|
|
21
|
+
gateFormGuid: string;
|
|
22
|
+
meetingFormGuid: string;
|
|
23
|
+
meetingSummaryField: string;
|
|
24
|
+
leadSource: string;
|
|
25
|
+
sourceUrl: string;
|
|
26
|
+
privacyPolicyUrl: string;
|
|
27
|
+
learnMoreUrl: string;
|
|
28
|
+
configured: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function outboundConfig(): OutboundConfig;
|
|
31
|
+
//# sourceMappingURL=outbound.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only two outbound requests the tool makes, both user-initiated, both HubSpot form
|
|
3
|
+
* submissions made from the browser. Portal ID and form GUIDs are public and compiled in.
|
|
4
|
+
*/
|
|
5
|
+
export const HUBSPOT_PORTAL_ID = '243933280';
|
|
6
|
+
/** Email gate — email capture form. Carries name, email, plugin version, run id, lead source. */
|
|
7
|
+
export const HUBSPOT_GATE_FORM_GUID = '78c7c119-3bf5-49d9-8c31-6122cfda26b3';
|
|
8
|
+
/** Close screen — meeting request form. Carries the same plus the rendered report summary. */
|
|
9
|
+
export const HUBSPOT_MEETING_FORM_GUID = '7a3e9649-209b-415d-81d0-bf6fe002e105';
|
|
10
|
+
/** Internal name of the contact property on the meeting form that receives the report summary. */
|
|
11
|
+
export const HUBSPOT_MEETING_SUMMARY_FIELD = 'salesforce_cpq_migration_analysis_result';
|
|
12
|
+
export const LEAD_SOURCE = 'cpq-inventory-tool';
|
|
13
|
+
export const SOURCE_URL = 'https://github.com/quotivity/salesforce-cpq-migration-analysis';
|
|
14
|
+
export const PRIVACY_POLICY_URL = 'https://quotivity.com/privacy';
|
|
15
|
+
export const LEARN_MORE_URL = 'https://quotivity.com';
|
|
16
|
+
/** HubSpot multi-line text fields hold at most 65,536 characters. */
|
|
17
|
+
export const HUBSPOT_MULTILINE_LIMIT = 65_536;
|
|
18
|
+
export function hubspotFormUrl(formGuid) {
|
|
19
|
+
return `https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${formGuid}`;
|
|
20
|
+
}
|
|
21
|
+
export function outboundConfig() {
|
|
22
|
+
const configured = ![HUBSPOT_PORTAL_ID, HUBSPOT_GATE_FORM_GUID, HUBSPOT_MEETING_FORM_GUID].some((v) => v.startsWith('REPLACE_WITH_'));
|
|
23
|
+
return {
|
|
24
|
+
portalId: HUBSPOT_PORTAL_ID,
|
|
25
|
+
gateFormGuid: HUBSPOT_GATE_FORM_GUID,
|
|
26
|
+
meetingFormGuid: HUBSPOT_MEETING_FORM_GUID,
|
|
27
|
+
meetingSummaryField: HUBSPOT_MEETING_SUMMARY_FIELD,
|
|
28
|
+
leadSource: LEAD_SOURCE,
|
|
29
|
+
sourceUrl: SOURCE_URL,
|
|
30
|
+
privacyPolicyUrl: PRIVACY_POLICY_URL,
|
|
31
|
+
learnMoreUrl: LEARN_MORE_URL,
|
|
32
|
+
configured,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=outbound.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The connection contract both distributions implement. The plugin adapts the Salesforce CLI's
|
|
3
|
+
* connection; the standalone script implements it over fetch. Core never sees a credential.
|
|
4
|
+
*/
|
|
5
|
+
export interface QueryResult<T> {
|
|
6
|
+
/** Total matching records as reported by the API (exact, independent of what was retrieved). */
|
|
7
|
+
totalSize: number;
|
|
8
|
+
/** Every page of records, unless the SOQL carried a LIMIT. */
|
|
9
|
+
records: T[];
|
|
10
|
+
}
|
|
11
|
+
export interface QueryOptions {
|
|
12
|
+
/** Run against the Tooling API instead of the data API. */
|
|
13
|
+
tooling?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface OrgConnection {
|
|
16
|
+
readonly instanceUrl: string;
|
|
17
|
+
readonly username: string;
|
|
18
|
+
readonly orgId: string;
|
|
19
|
+
/** Alias or display name for the org, used in the report header and the PDF filename. */
|
|
20
|
+
readonly orgName: string;
|
|
21
|
+
query<T extends object = Record<string, unknown>>(soql: string, opts?: QueryOptions): Promise<QueryResult<T>>;
|
|
22
|
+
}
|
|
23
|
+
export declare class ConnectionError extends Error {
|
|
24
|
+
readonly code: string | undefined;
|
|
25
|
+
readonly retryable: boolean;
|
|
26
|
+
constructor(message: string, code: string | undefined, retryable: boolean);
|
|
27
|
+
}
|
|
28
|
+
export declare function isMissingObjectError(err: unknown): boolean;
|
|
29
|
+
export declare function isRetryable(err: unknown): boolean;
|
|
30
|
+
export declare function errorText(err: unknown): string;
|
|
31
|
+
export interface RetryOptions {
|
|
32
|
+
attempts?: number;
|
|
33
|
+
baseDelayMs?: number;
|
|
34
|
+
sleep?: (ms: number) => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Every request is a read, so every request is retryable. Three attempts with backoff; a
|
|
38
|
+
* non-retryable API error (bad object, bad field) fails immediately so the caller can mark the
|
|
39
|
+
* object absent rather than looping.
|
|
40
|
+
*/
|
|
41
|
+
export declare function withRetry<T>(fn: () => Promise<T>, opts?: RetryOptions): Promise<T>;
|
|
42
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class ConnectionError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
retryable;
|
|
4
|
+
constructor(message, code, retryable) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.retryable = retryable;
|
|
8
|
+
this.name = 'ConnectionError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const NON_RETRYABLE = /INVALID_TYPE|INVALID_FIELD|MALFORMED_QUERY|INVALID_QUERY_FILTER_OPERATOR|NO_SUCH_COLUMN|INVALID_LOGIN|INSUFFICIENT_ACCESS|FIELD_NOT_FOUND|sObject type .* is not supported|No such column/i;
|
|
12
|
+
export function isMissingObjectError(err) {
|
|
13
|
+
const text = errorText(err);
|
|
14
|
+
return /INVALID_TYPE|sObject type .* is not supported|Object type .* is not supported/i.test(text);
|
|
15
|
+
}
|
|
16
|
+
export function isRetryable(err) {
|
|
17
|
+
if (err instanceof ConnectionError)
|
|
18
|
+
return err.retryable;
|
|
19
|
+
return !NON_RETRYABLE.test(errorText(err));
|
|
20
|
+
}
|
|
21
|
+
export function errorText(err) {
|
|
22
|
+
if (err instanceof Error) {
|
|
23
|
+
const withCode = err;
|
|
24
|
+
return [withCode.errorCode, withCode.code, err.message].filter(Boolean).join(' ');
|
|
25
|
+
}
|
|
26
|
+
return String(err);
|
|
27
|
+
}
|
|
28
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
+
/**
|
|
30
|
+
* Every request is a read, so every request is retryable. Three attempts with backoff; a
|
|
31
|
+
* non-retryable API error (bad object, bad field) fails immediately so the caller can mark the
|
|
32
|
+
* object absent rather than looping.
|
|
33
|
+
*/
|
|
34
|
+
export async function withRetry(fn, opts = {}) {
|
|
35
|
+
const attempts = opts.attempts ?? 3;
|
|
36
|
+
const base = opts.baseDelayMs ?? 500;
|
|
37
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
38
|
+
let lastErr;
|
|
39
|
+
for (let i = 0; i < attempts; i++) {
|
|
40
|
+
try {
|
|
41
|
+
return await fn();
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
lastErr = err;
|
|
45
|
+
if (!isRetryable(err) || i === attempts - 1)
|
|
46
|
+
throw err;
|
|
47
|
+
await sleep(base * 2 ** i);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw lastErr;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { OrgConnection } from '../connection.js';
|
|
2
|
+
import { type ObjectRead, quote } from '../soql.js';
|
|
3
|
+
import type { BucketId } from '../types.js';
|
|
4
|
+
import * as R from './records.js';
|
|
5
|
+
export interface ExtractOptions {
|
|
6
|
+
/** Dead-configuration window in months. Defaults to 24. */
|
|
7
|
+
windowMonths?: number;
|
|
8
|
+
/** Injectable clock for tests. */
|
|
9
|
+
now?: Date;
|
|
10
|
+
onProgress?: (event: ProgressEvent) => void;
|
|
11
|
+
}
|
|
12
|
+
export type ProgressStep = BucketId | 'seed' | 'classify';
|
|
13
|
+
export interface ProgressEvent {
|
|
14
|
+
step: ProgressStep;
|
|
15
|
+
status: 'start' | 'done' | 'error';
|
|
16
|
+
detail?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface YearCount {
|
|
19
|
+
year: number;
|
|
20
|
+
type: string;
|
|
21
|
+
count: number;
|
|
22
|
+
}
|
|
23
|
+
export interface Extraction {
|
|
24
|
+
windowMonths: number;
|
|
25
|
+
since: string;
|
|
26
|
+
approvalSince: string;
|
|
27
|
+
now: string;
|
|
28
|
+
product2: ObjectRead<R.Product2Rec>;
|
|
29
|
+
pricebook2: ObjectRead<R.Pricebook2Rec>;
|
|
30
|
+
currencyType: ObjectRead<R.CurrencyTypeRec>;
|
|
31
|
+
datedConversionRate: ObjectRead<R.DatedConversionRateRec>;
|
|
32
|
+
productFeature: ObjectRead<R.ProductFeatureRec>;
|
|
33
|
+
productOption: ObjectRead<R.ProductOptionRec>;
|
|
34
|
+
optionConstraint: ObjectRead<R.OptionConstraintRec>;
|
|
35
|
+
configurationRule: ObjectRead<R.ConfigurationRuleRec>;
|
|
36
|
+
configurationAttribute: ObjectRead<R.ConfigurationAttributeRec>;
|
|
37
|
+
productRule: ObjectRead<R.ProductRuleRec>;
|
|
38
|
+
errorCondition: ObjectRead<R.ErrorConditionRec>;
|
|
39
|
+
productAction: ObjectRead<R.ProductActionRec>;
|
|
40
|
+
quoteProcess: ObjectRead<R.QuoteProcessRec>;
|
|
41
|
+
processInput: ObjectRead<R.ProcessInputRec>;
|
|
42
|
+
processInputCondition: ObjectRead<R.ProcessInputConditionRec>;
|
|
43
|
+
priceRule: ObjectRead<R.PriceRuleRec>;
|
|
44
|
+
priceCondition: ObjectRead<R.PriceConditionRec>;
|
|
45
|
+
priceAction: ObjectRead<R.PriceActionRec>;
|
|
46
|
+
summaryVariable: ObjectRead<R.SummaryVariableRec>;
|
|
47
|
+
lookupQuery: ObjectRead<R.LookupQueryRec>;
|
|
48
|
+
blockPrice: ObjectRead<R.BlockPriceRec>;
|
|
49
|
+
dimension: ObjectRead<R.DimensionRec>;
|
|
50
|
+
segmentedLines: ObjectRead<R.SegmentedLineRec>;
|
|
51
|
+
discountSchedule: ObjectRead<R.DiscountScheduleRec>;
|
|
52
|
+
discountTier: ObjectRead<R.DiscountTierRec>;
|
|
53
|
+
contractedPrice: ObjectRead<R.ContractedPriceRec>;
|
|
54
|
+
approvalRule: ObjectRead<R.ApprovalRuleRec>;
|
|
55
|
+
approvalCondition: ObjectRead<R.ApprovalConditionRec>;
|
|
56
|
+
approvalChain: ObjectRead<R.NamedRec>;
|
|
57
|
+
approver: ObjectRead<R.ApproverRec>;
|
|
58
|
+
approvalVariable: ObjectRead<R.NamedRec>;
|
|
59
|
+
trackedField: ObjectRead<R.NamedRec>;
|
|
60
|
+
approvalHistory: ObjectRead<R.ApprovalHistoryRec>;
|
|
61
|
+
processDefinition: ObjectRead<R.ProcessDefinitionRec>;
|
|
62
|
+
processInstance: ObjectRead<R.ProcessInstanceRec>;
|
|
63
|
+
quoteTemplate: ObjectRead<R.NamedRec>;
|
|
64
|
+
templateSection: ObjectRead<R.TemplateSectionRec>;
|
|
65
|
+
templateContent: ObjectRead<R.TemplateContentRec>;
|
|
66
|
+
lineColumn: ObjectRead<R.LineColumnRec>;
|
|
67
|
+
quoteTerm: ObjectRead<R.NamedRec>;
|
|
68
|
+
termCondition: ObjectRead<R.TermConditionRec>;
|
|
69
|
+
quoteLineGroupCount: number;
|
|
70
|
+
quoteLineGroupAlive: number;
|
|
71
|
+
contractActive: number;
|
|
72
|
+
contractAlive: number;
|
|
73
|
+
subscriptionCount: number;
|
|
74
|
+
orderCount: number;
|
|
75
|
+
assetCount: number;
|
|
76
|
+
quotesByYear: YearCount[];
|
|
77
|
+
quoteDateField: string;
|
|
78
|
+
quotesInWindow: number;
|
|
79
|
+
quoteLinesInWindow: number;
|
|
80
|
+
distinctProductsQuoted: number;
|
|
81
|
+
customScript: ObjectRead<R.CustomScriptRec>;
|
|
82
|
+
customAction: ObjectRead<R.CustomActionRec>;
|
|
83
|
+
apexTrigger: ObjectRead<R.ApexTriggerRec>;
|
|
84
|
+
webLink: ObjectRead<R.WebLinkRec>;
|
|
85
|
+
layout: ObjectRead<R.LayoutRec>;
|
|
86
|
+
flexiPage: ObjectRead<R.FlexiPageRec>;
|
|
87
|
+
validationRule: ObjectRead<R.ValidationRuleRec>;
|
|
88
|
+
quoteFields: ObjectRead<R.FieldDefinitionRec>;
|
|
89
|
+
quoteLineFields: ObjectRead<R.FieldDefinitionRec>;
|
|
90
|
+
/** Creator ids that resolve to no User record — the package install signature. */
|
|
91
|
+
seedCreators: Set<string>;
|
|
92
|
+
/** Records per object created by a seed creator (removed from every count). */
|
|
93
|
+
seeded: Record<string, number>;
|
|
94
|
+
/** Objects that could not be read after retries, with the reason. */
|
|
95
|
+
unread: {
|
|
96
|
+
object: string;
|
|
97
|
+
reason: string;
|
|
98
|
+
}[];
|
|
99
|
+
}
|
|
100
|
+
export declare function extractAll(conn: OrgConnection, opts?: ExtractOptions): Promise<Extraction>;
|
|
101
|
+
/** Business date for quote volume: StartDate, then ExpirationDate, then CreatedDate. Prints which. */
|
|
102
|
+
export declare function pickQuoteDateField(conn: OrgConnection): Promise<string>;
|
|
103
|
+
/**
|
|
104
|
+
* For each configuration object, group by CreatedById. A creator that resolves to no User record is
|
|
105
|
+
* the package install user; its records are seed data, reported separately and never given a verdict.
|
|
106
|
+
*/
|
|
107
|
+
export declare function detectSeeds(conn: OrgConnection, ex: Omit<Extraction, 'seedCreators' | 'seeded'>): Promise<{
|
|
108
|
+
seedCreators: Set<string>;
|
|
109
|
+
seeded: Record<string, number>;
|
|
110
|
+
}>;
|
|
111
|
+
export { quote };
|
|
112
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { aggregate, count, inList, quote, readObject, soqlDate } from '../soql.js';
|
|
2
|
+
import * as R from './records.js';
|
|
3
|
+
const monthsAgo = (now, months) => {
|
|
4
|
+
const d = new Date(now);
|
|
5
|
+
d.setUTCMonth(d.getUTCMonth() - months);
|
|
6
|
+
return d;
|
|
7
|
+
};
|
|
8
|
+
/** Objects CPQ seeds through its post-install process. Checked on every configuration object read. */
|
|
9
|
+
const SEED_CHECKED = [
|
|
10
|
+
'Product2',
|
|
11
|
+
'Pricebook2',
|
|
12
|
+
'SBQQ__ProductFeature__c',
|
|
13
|
+
'SBQQ__ProductOption__c',
|
|
14
|
+
'SBQQ__OptionConstraint__c',
|
|
15
|
+
'SBQQ__ConfigurationRule__c',
|
|
16
|
+
'SBQQ__ConfigurationAttribute__c',
|
|
17
|
+
'SBQQ__ProductRule__c',
|
|
18
|
+
'SBQQ__QuoteProcess__c',
|
|
19
|
+
'SBQQ__ProcessInput__c',
|
|
20
|
+
'SBQQ__PriceRule__c',
|
|
21
|
+
'SBQQ__SummaryVariable__c',
|
|
22
|
+
'SBQQ__LookupQuery__c',
|
|
23
|
+
'SBQQ__DiscountSchedule__c',
|
|
24
|
+
'SBQQ__ContractedPrice__c',
|
|
25
|
+
'SBQQ__Dimension__c',
|
|
26
|
+
'SBQQ__BlockPrice__c',
|
|
27
|
+
'sbaa__ApprovalRule__c',
|
|
28
|
+
'sbaa__ApprovalChain__c',
|
|
29
|
+
'sbaa__Approver__c',
|
|
30
|
+
'sbaa__ApprovalVariable__c',
|
|
31
|
+
'sbaa__TrackedField__c',
|
|
32
|
+
'SBQQ__QuoteTemplate__c',
|
|
33
|
+
'SBQQ__TemplateContent__c',
|
|
34
|
+
'SBQQ__LineColumn__c',
|
|
35
|
+
'SBQQ__QuoteTerm__c',
|
|
36
|
+
'SBQQ__CustomScript__c',
|
|
37
|
+
'SBQQ__CustomAction__c',
|
|
38
|
+
];
|
|
39
|
+
export async function extractAll(conn, opts = {}) {
|
|
40
|
+
const now = opts.now ?? new Date();
|
|
41
|
+
const windowMonths = opts.windowMonths ?? 24;
|
|
42
|
+
const since = soqlDate(monthsAgo(now, windowMonths));
|
|
43
|
+
const approvalSince = soqlDate(monthsAgo(now, 12));
|
|
44
|
+
const progress = opts.onProgress ?? (() => { });
|
|
45
|
+
const unread = [];
|
|
46
|
+
const track = (read) => {
|
|
47
|
+
if (read.status === 'unread')
|
|
48
|
+
unread.push({ object: read.object, reason: read.reason ?? 'unknown' });
|
|
49
|
+
return read;
|
|
50
|
+
};
|
|
51
|
+
const step = async (id, fn) => {
|
|
52
|
+
progress({ step: id, status: 'start' });
|
|
53
|
+
try {
|
|
54
|
+
const out = await fn();
|
|
55
|
+
progress({ step: id, status: 'done' });
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
progress({
|
|
60
|
+
step: id,
|
|
61
|
+
status: 'error',
|
|
62
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
63
|
+
});
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const read = (object, fields, o = {}) => readObject(conn, object, fields, o).then(track);
|
|
68
|
+
// ── Catalog ──────────────────────────────────────────────────────────────
|
|
69
|
+
const catalog = await step('catalog', async () => ({
|
|
70
|
+
product2: await read('Product2', R.PRODUCT2_FIELDS, {
|
|
71
|
+
where: 'IsActive = true',
|
|
72
|
+
}),
|
|
73
|
+
pricebook2: await read('Pricebook2', R.PRICEBOOK2_FIELDS),
|
|
74
|
+
currencyType: await read('CurrencyType', R.CURRENCY_FIELDS, {
|
|
75
|
+
optional: true,
|
|
76
|
+
}),
|
|
77
|
+
datedConversionRate: await read('DatedConversionRate', R.DCR_FIELDS, {
|
|
78
|
+
optional: true,
|
|
79
|
+
}),
|
|
80
|
+
}));
|
|
81
|
+
// ── Configuration (bundles + product rules, the first classifier input set) ──
|
|
82
|
+
const configuration = await step('configuration', async () => ({
|
|
83
|
+
productFeature: await read('SBQQ__ProductFeature__c', R.FEATURE_FIELDS),
|
|
84
|
+
productOption: await read('SBQQ__ProductOption__c', R.OPTION_FIELDS),
|
|
85
|
+
optionConstraint: await read('SBQQ__OptionConstraint__c', R.CONSTRAINT_FIELDS),
|
|
86
|
+
configurationRule: await read('SBQQ__ConfigurationRule__c', R.CONFIG_RULE_FIELDS),
|
|
87
|
+
configurationAttribute: await read('SBQQ__ConfigurationAttribute__c', R.ATTRIBUTE_FIELDS),
|
|
88
|
+
productRule: await read('SBQQ__ProductRule__c', R.PRODUCT_RULE_FIELDS, {
|
|
89
|
+
classifierInput: true,
|
|
90
|
+
}),
|
|
91
|
+
errorCondition: await read('SBQQ__ErrorCondition__c', R.ERROR_CONDITION_FIELDS, {
|
|
92
|
+
classifierInput: true,
|
|
93
|
+
orderBy: 'SBQQ__Rule__c, SBQQ__Index__c',
|
|
94
|
+
}),
|
|
95
|
+
productAction: await read('SBQQ__ProductAction__c', R.PRODUCT_ACTION_FIELDS, {
|
|
96
|
+
classifierInput: true,
|
|
97
|
+
}),
|
|
98
|
+
}));
|
|
99
|
+
// ── Discovery & capture ──────────────────────────────────────────────────
|
|
100
|
+
const discovery = await step('discovery', async () => ({
|
|
101
|
+
quoteProcess: await read('SBQQ__QuoteProcess__c', R.QUOTE_PROCESS_FIELDS),
|
|
102
|
+
processInput: await read('SBQQ__ProcessInput__c', R.PROCESS_INPUT_FIELDS),
|
|
103
|
+
processInputCondition: await read('SBQQ__ProcessInputCondition__c', R.PROCESS_INPUT_CONDITION_FIELDS),
|
|
104
|
+
}));
|
|
105
|
+
// ── Price determination (price rules, the second classifier input set) ──
|
|
106
|
+
const price = await step('price', async () => ({
|
|
107
|
+
priceRule: await read('SBQQ__PriceRule__c', R.PRICE_RULE_FIELDS, {
|
|
108
|
+
classifierInput: true,
|
|
109
|
+
}),
|
|
110
|
+
priceCondition: await read('SBQQ__PriceCondition__c', R.PRICE_CONDITION_FIELDS, {
|
|
111
|
+
classifierInput: true,
|
|
112
|
+
orderBy: 'SBQQ__Rule__c, SBQQ__Index__c',
|
|
113
|
+
}),
|
|
114
|
+
priceAction: await read('SBQQ__PriceAction__c', R.PRICE_ACTION_FIELDS, {
|
|
115
|
+
classifierInput: true,
|
|
116
|
+
orderBy: 'SBQQ__Rule__c, SBQQ__Order__c',
|
|
117
|
+
}),
|
|
118
|
+
summaryVariable: await read('SBQQ__SummaryVariable__c', R.SUMMARY_VARIABLE_FIELDS),
|
|
119
|
+
lookupQuery: await read('SBQQ__LookupQuery__c', R.LOOKUP_QUERY_FIELDS),
|
|
120
|
+
blockPrice: await read('SBQQ__BlockPrice__c', R.BLOCK_PRICE_FIELDS),
|
|
121
|
+
dimension: await read('SBQQ__Dimension__c', R.DIMENSION_FIELDS),
|
|
122
|
+
segmentedLines: await read('SBQQ__QuoteLine__c', R.SEGMENTED_LINE_FIELDS, {
|
|
123
|
+
where: `SBQQ__Dimension__c != null AND LastModifiedDate >= ${since}`,
|
|
124
|
+
orderBy: 'SBQQ__SegmentKey__c, SBQQ__SegmentIndex__c',
|
|
125
|
+
}),
|
|
126
|
+
}));
|
|
127
|
+
// ── Discounting ─────────────────────────────────────────────────────────
|
|
128
|
+
const discounting = await step('discounting', async () => ({
|
|
129
|
+
discountSchedule: await read('SBQQ__DiscountSchedule__c', R.DISCOUNT_SCHEDULE_FIELDS),
|
|
130
|
+
discountTier: await read('SBQQ__DiscountTier__c', R.DISCOUNT_TIER_FIELDS, {
|
|
131
|
+
orderBy: 'SBQQ__Schedule__c, SBQQ__Number__c',
|
|
132
|
+
}),
|
|
133
|
+
contractedPrice: await read('SBQQ__ContractedPrice__c', R.CONTRACTED_PRICE_FIELDS),
|
|
134
|
+
}));
|
|
135
|
+
// ── Guardrails (platform validation rules; product rules already read) ──
|
|
136
|
+
const guardrails = await step('guardrails', async () => {
|
|
137
|
+
const validationRule = await read('ValidationRule', R.VALIDATION_RULE_FIELDS, {
|
|
138
|
+
where: 'NamespacePrefix = null',
|
|
139
|
+
tooling: true,
|
|
140
|
+
});
|
|
141
|
+
validationRule.records = validationRule.records.filter((v) => R.isCpqObject(v.EntityDefinition?.QualifiedApiName ?? undefined));
|
|
142
|
+
validationRule.count = validationRule.records.length;
|
|
143
|
+
return { validationRule };
|
|
144
|
+
});
|
|
145
|
+
// ── Approvals: Advanced Approvals (sbaa) and native processes ───────────
|
|
146
|
+
const approvals = await step('approvals', async () => ({
|
|
147
|
+
approvalRule: await read('sbaa__ApprovalRule__c', R.APPROVAL_RULE_FIELDS, {
|
|
148
|
+
optional: true,
|
|
149
|
+
}),
|
|
150
|
+
approvalCondition: await read('sbaa__ApprovalCondition__c', R.APPROVAL_CONDITION_FIELDS, {
|
|
151
|
+
optional: true,
|
|
152
|
+
}),
|
|
153
|
+
approvalChain: await read('sbaa__ApprovalChain__c', R.NAMED_FIELDS, {
|
|
154
|
+
optional: true,
|
|
155
|
+
}),
|
|
156
|
+
approver: await read('sbaa__Approver__c', R.APPROVER_FIELDS, { optional: true }),
|
|
157
|
+
approvalVariable: await read('sbaa__ApprovalVariable__c', R.NAMED_FIELDS, {
|
|
158
|
+
optional: true,
|
|
159
|
+
}),
|
|
160
|
+
trackedField: await read('sbaa__TrackedField__c', R.NAMED_FIELDS, {
|
|
161
|
+
optional: true,
|
|
162
|
+
}),
|
|
163
|
+
approvalHistory: await read('sbaa__Approval__c', R.APPROVAL_HISTORY_FIELDS, {
|
|
164
|
+
optional: true,
|
|
165
|
+
where: `CreatedDate >= ${approvalSince}`,
|
|
166
|
+
}),
|
|
167
|
+
processDefinition: await read('ProcessDefinition', R.PROCESS_DEFINITION_FIELDS, {
|
|
168
|
+
where: "Type = 'Approval' AND TableEnumOrId IN ('SBQQ__Quote__c', 'Opportunity')",
|
|
169
|
+
}),
|
|
170
|
+
processInstance: await read('ProcessInstance', R.PROCESS_INSTANCE_FIELDS, {
|
|
171
|
+
where: `CreatedDate >= ${approvalSince} AND ProcessDefinition.TableEnumOrId IN ('SBQQ__Quote__c', 'Opportunity')`,
|
|
172
|
+
}),
|
|
173
|
+
}));
|
|
174
|
+
// ── Quote output ────────────────────────────────────────────────────────
|
|
175
|
+
const output = await step('output', async () => ({
|
|
176
|
+
quoteTemplate: await read('SBQQ__QuoteTemplate__c', R.NAMED_FIELDS),
|
|
177
|
+
templateSection: await read('SBQQ__TemplateSection__c', R.TEMPLATE_SECTION_FIELDS),
|
|
178
|
+
templateContent: await read('SBQQ__TemplateContent__c', R.TEMPLATE_CONTENT_FIELDS),
|
|
179
|
+
lineColumn: await read('SBQQ__LineColumn__c', R.LINE_COLUMN_FIELDS),
|
|
180
|
+
quoteTerm: await read('SBQQ__QuoteTerm__c', R.NAMED_FIELDS),
|
|
181
|
+
termCondition: await read('SBQQ__TermCondition__c', R.TERM_CONDITION_FIELDS),
|
|
182
|
+
quoteLineGroupCount: await safeCount(conn, 'SBQQ__QuoteLineGroup__c'),
|
|
183
|
+
quoteLineGroupAlive: await safeCount(conn, 'SBQQ__QuoteLineGroup__c', `LastModifiedDate >= ${since}`),
|
|
184
|
+
}));
|
|
185
|
+
// ── Contract lifecycle + volume ─────────────────────────────────────────
|
|
186
|
+
const lifecycle = await step('lifecycle', async () => {
|
|
187
|
+
const quoteDateField = await pickQuoteDateField(conn);
|
|
188
|
+
const dateExpr = quoteDateField === 'CreatedDate' ? 'CreatedDate' : quoteDateField;
|
|
189
|
+
const byYear = await aggregate(conn, `SELECT CALENDAR_YEAR(${dateExpr}) y, SBQQ__Type__c t, COUNT(Id) c FROM SBQQ__Quote__c GROUP BY CALENDAR_YEAR(${dateExpr}), SBQQ__Type__c`);
|
|
190
|
+
const sinceDate = since.slice(0, 10);
|
|
191
|
+
const windowWhere = quoteDateField === 'CreatedDate'
|
|
192
|
+
? `CreatedDate >= ${since}`
|
|
193
|
+
: `${quoteDateField} >= ${sinceDate}`;
|
|
194
|
+
const distinct = await aggregate(conn, `SELECT COUNT_DISTINCT(SBQQ__Product__c) n FROM SBQQ__QuoteLine__c WHERE SBQQ__Quote__r.${windowWhere}`);
|
|
195
|
+
return {
|
|
196
|
+
contractActive: await safeCount(conn, 'Contract', "Status = 'Activated'"),
|
|
197
|
+
contractAlive: await safeCount(conn, 'Contract', `Status = 'Activated' AND LastModifiedDate >= ${since}`),
|
|
198
|
+
subscriptionCount: await safeCount(conn, 'SBQQ__Subscription__c'),
|
|
199
|
+
orderCount: await safeCount(conn, 'Order', 'SBQQ__Quote__c != null'),
|
|
200
|
+
assetCount: await safeCount(conn, 'Asset'),
|
|
201
|
+
quotesByYear: byYear.rows
|
|
202
|
+
.filter((r) => r.y != null)
|
|
203
|
+
.map((r) => ({ year: Number(r.y), type: r.t ?? 'Quote', count: Number(r.c) })),
|
|
204
|
+
quoteDateField,
|
|
205
|
+
quotesInWindow: await safeCount(conn, 'SBQQ__Quote__c', windowWhere),
|
|
206
|
+
quoteLinesInWindow: await safeCount(conn, 'SBQQ__QuoteLine__c', `SBQQ__Quote__r.${windowWhere}`),
|
|
207
|
+
distinctProductsQuoted: Number(distinct.rows[0]?.n ?? 0),
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
// ── Custom code & UI ────────────────────────────────────────────────────
|
|
211
|
+
const code = await step('code', async () => {
|
|
212
|
+
const apexTrigger = await read('ApexTrigger', R.APEX_TRIGGER_FIELDS, {
|
|
213
|
+
where: 'NamespacePrefix = null',
|
|
214
|
+
});
|
|
215
|
+
apexTrigger.records = apexTrigger.records.filter((t) => R.isCpqObject(t.TableEnumOrId));
|
|
216
|
+
apexTrigger.count = apexTrigger.records.length;
|
|
217
|
+
const webLink = await read('WebLink', R.WEBLINK_FIELDS, {
|
|
218
|
+
where: 'NamespacePrefix = null',
|
|
219
|
+
tooling: true,
|
|
220
|
+
});
|
|
221
|
+
webLink.records = webLink.records.filter((w) => R.isCpqObject(w.PageOrSobjectType ?? undefined));
|
|
222
|
+
webLink.count = webLink.records.length;
|
|
223
|
+
const layout = await read('Layout', R.LAYOUT_FIELDS, {
|
|
224
|
+
where: 'NamespacePrefix = null',
|
|
225
|
+
tooling: true,
|
|
226
|
+
});
|
|
227
|
+
layout.records = layout.records.filter((l) => R.isCpqObject(l.TableEnumOrId ?? undefined));
|
|
228
|
+
layout.count = layout.records.length;
|
|
229
|
+
const flexiPage = await read('FlexiPage', R.FLEXIPAGE_FIELDS, {
|
|
230
|
+
where: "NamespacePrefix = null AND Type = 'RecordPage'",
|
|
231
|
+
tooling: true,
|
|
232
|
+
});
|
|
233
|
+
flexiPage.records = flexiPage.records.filter((f) => R.isCpqObject(f.EntityDefinitionId ?? undefined));
|
|
234
|
+
flexiPage.count = flexiPage.records.length;
|
|
235
|
+
return {
|
|
236
|
+
customScript: await read('SBQQ__CustomScript__c', R.CUSTOM_SCRIPT_FIELDS),
|
|
237
|
+
customAction: await read('SBQQ__CustomAction__c', R.CUSTOM_ACTION_FIELDS),
|
|
238
|
+
apexTrigger,
|
|
239
|
+
webLink,
|
|
240
|
+
layout,
|
|
241
|
+
flexiPage,
|
|
242
|
+
quoteFields: await read('FieldDefinition', R.FIELD_DEFINITION_FIELDS, {
|
|
243
|
+
where: "EntityDefinition.QualifiedApiName = 'SBQQ__Quote__c'",
|
|
244
|
+
}),
|
|
245
|
+
quoteLineFields: await read('FieldDefinition', R.FIELD_DEFINITION_FIELDS, {
|
|
246
|
+
where: "EntityDefinition.QualifiedApiName = 'SBQQ__QuoteLine__c'",
|
|
247
|
+
}),
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
const partial = {
|
|
251
|
+
windowMonths,
|
|
252
|
+
since,
|
|
253
|
+
approvalSince,
|
|
254
|
+
now: now.toISOString(),
|
|
255
|
+
...catalog,
|
|
256
|
+
...configuration,
|
|
257
|
+
...discovery,
|
|
258
|
+
...price,
|
|
259
|
+
...discounting,
|
|
260
|
+
...guardrails,
|
|
261
|
+
...approvals,
|
|
262
|
+
...output,
|
|
263
|
+
...lifecycle,
|
|
264
|
+
...code,
|
|
265
|
+
unread,
|
|
266
|
+
};
|
|
267
|
+
// ── Package seed detection, applied to every configuration object ───────
|
|
268
|
+
const { seedCreators, seeded } = await step('seed', () => detectSeeds(conn, partial));
|
|
269
|
+
return { ...partial, seedCreators, seeded };
|
|
270
|
+
}
|
|
271
|
+
async function safeCount(conn, object, where) {
|
|
272
|
+
try {
|
|
273
|
+
return await count(conn, object, where);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** Business date for quote volume: StartDate, then ExpirationDate, then CreatedDate. Prints which. */
|
|
280
|
+
export async function pickQuoteDateField(conn) {
|
|
281
|
+
const total = await safeCount(conn, 'SBQQ__Quote__c');
|
|
282
|
+
if (total === 0)
|
|
283
|
+
return 'CreatedDate';
|
|
284
|
+
for (const field of ['SBQQ__StartDate__c', 'SBQQ__ExpirationDate__c']) {
|
|
285
|
+
const populated = await safeCount(conn, 'SBQQ__Quote__c', `${field} != null`);
|
|
286
|
+
if (populated / total >= 0.5)
|
|
287
|
+
return field;
|
|
288
|
+
}
|
|
289
|
+
return 'CreatedDate';
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* For each configuration object, group by CreatedById. A creator that resolves to no User record is
|
|
293
|
+
* the package install user; its records are seed data, reported separately and never given a verdict.
|
|
294
|
+
*/
|
|
295
|
+
export async function detectSeeds(conn, ex) {
|
|
296
|
+
const groups = new Map();
|
|
297
|
+
const creators = new Set();
|
|
298
|
+
for (const object of SEED_CHECKED) {
|
|
299
|
+
const read = readFor(ex, object);
|
|
300
|
+
if (read?.status !== 'ok' || read.count === 0)
|
|
301
|
+
continue;
|
|
302
|
+
const res = await aggregate(conn, `SELECT CreatedById, COUNT(Id) n FROM ${object} GROUP BY CreatedById`);
|
|
303
|
+
const rows = res.rows
|
|
304
|
+
.filter((r) => r.CreatedById)
|
|
305
|
+
.map((r) => ({ creator: r.CreatedById, n: Number(r.n) }));
|
|
306
|
+
groups.set(object, rows);
|
|
307
|
+
for (const r of rows)
|
|
308
|
+
creators.add(r.creator);
|
|
309
|
+
}
|
|
310
|
+
const seedCreators = new Set();
|
|
311
|
+
if (creators.size) {
|
|
312
|
+
const resolved = new Set();
|
|
313
|
+
const ids = [...creators];
|
|
314
|
+
for (let i = 0; i < ids.length; i += 200) {
|
|
315
|
+
const chunk = ids.slice(i, i + 200);
|
|
316
|
+
const users = await aggregate(conn, `SELECT Id FROM User WHERE Id IN ${inList(chunk)}`);
|
|
317
|
+
if (users.status !== 'ok')
|
|
318
|
+
return { seedCreators, seeded: {} };
|
|
319
|
+
for (const u of users.rows)
|
|
320
|
+
resolved.add(u.Id);
|
|
321
|
+
}
|
|
322
|
+
for (const id of creators)
|
|
323
|
+
if (!resolved.has(id))
|
|
324
|
+
seedCreators.add(id);
|
|
325
|
+
}
|
|
326
|
+
const seeded = {};
|
|
327
|
+
for (const [object, rows] of groups) {
|
|
328
|
+
const n = rows.filter((r) => seedCreators.has(r.creator)).reduce((a, r) => a + r.n, 0);
|
|
329
|
+
if (n > 0)
|
|
330
|
+
seeded[object] = n;
|
|
331
|
+
const read = readFor(ex, object);
|
|
332
|
+
if (read && n > 0) {
|
|
333
|
+
read.records = read.records.filter((rec) => !seedCreators.has(String(rec.CreatedById ?? '')));
|
|
334
|
+
read.count = Math.max(0, read.count - n);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return { seedCreators, seeded };
|
|
338
|
+
}
|
|
339
|
+
function readFor(ex, object) {
|
|
340
|
+
const map = {
|
|
341
|
+
Product2: ex.product2,
|
|
342
|
+
Pricebook2: ex.pricebook2,
|
|
343
|
+
SBQQ__ProductFeature__c: ex.productFeature,
|
|
344
|
+
SBQQ__ProductOption__c: ex.productOption,
|
|
345
|
+
SBQQ__OptionConstraint__c: ex.optionConstraint,
|
|
346
|
+
SBQQ__ConfigurationRule__c: ex.configurationRule,
|
|
347
|
+
SBQQ__ConfigurationAttribute__c: ex.configurationAttribute,
|
|
348
|
+
SBQQ__ProductRule__c: ex.productRule,
|
|
349
|
+
SBQQ__QuoteProcess__c: ex.quoteProcess,
|
|
350
|
+
SBQQ__ProcessInput__c: ex.processInput,
|
|
351
|
+
SBQQ__PriceRule__c: ex.priceRule,
|
|
352
|
+
SBQQ__SummaryVariable__c: ex.summaryVariable,
|
|
353
|
+
SBQQ__LookupQuery__c: ex.lookupQuery,
|
|
354
|
+
SBQQ__DiscountSchedule__c: ex.discountSchedule,
|
|
355
|
+
SBQQ__ContractedPrice__c: ex.contractedPrice,
|
|
356
|
+
SBQQ__Dimension__c: ex.dimension,
|
|
357
|
+
SBQQ__BlockPrice__c: ex.blockPrice,
|
|
358
|
+
sbaa__ApprovalRule__c: ex.approvalRule,
|
|
359
|
+
sbaa__ApprovalChain__c: ex.approvalChain,
|
|
360
|
+
sbaa__Approver__c: ex.approver,
|
|
361
|
+
sbaa__ApprovalVariable__c: ex.approvalVariable,
|
|
362
|
+
sbaa__TrackedField__c: ex.trackedField,
|
|
363
|
+
SBQQ__QuoteTemplate__c: ex.quoteTemplate,
|
|
364
|
+
SBQQ__TemplateContent__c: ex.templateContent,
|
|
365
|
+
SBQQ__LineColumn__c: ex.lineColumn,
|
|
366
|
+
SBQQ__QuoteTerm__c: ex.quoteTerm,
|
|
367
|
+
SBQQ__CustomScript__c: ex.customScript,
|
|
368
|
+
SBQQ__CustomAction__c: ex.customAction,
|
|
369
|
+
};
|
|
370
|
+
return map[object];
|
|
371
|
+
}
|
|
372
|
+
export { quote };
|
|
373
|
+
//# sourceMappingURL=index.js.map
|