@happyvertical/smrt-sales 0.57.20 → 0.57.22
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/agents/crm.md +23 -0
- package/dist/agreements.js +1 -1
- package/dist/chunks/{__smrt-register__-JXsLTRj7.js → __smrt-register__-bn1C2AUF.js} +2 -2
- package/dist/chunks/{__smrt-register__-JXsLTRj7.js.map → __smrt-register__-bn1C2AUF.js.map} +1 -1
- package/dist/chunks/{crm-DaUkdNoG.js → crm-BRICSwf2.js} +192 -2
- package/dist/chunks/crm-BRICSwf2.js.map +1 -0
- package/dist/commissions.js +1 -1
- package/dist/crm/index.d.ts +3 -1
- package/dist/crm/index.d.ts.map +1 -1
- package/dist/crm/services/LeadConversionService.d.ts +49 -0
- package/dist/crm/services/LeadConversionService.d.ts.map +1 -0
- package/dist/crm/services/LeadWorkflowService.d.ts +11 -0
- package/dist/crm/services/LeadWorkflowService.d.ts.map +1 -1
- package/dist/crm.js +3 -3
- package/dist/index.js +3 -3
- package/dist/manifest.json +1 -1
- package/dist/referrals.js +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/package.json +6 -6
- package/dist/chunks/crm-DaUkdNoG.js.map +0 -1
|
@@ -1842,6 +1842,153 @@ var SalesPipelineRecipe = class extends SmrtRecipe {
|
|
|
1842
1842
|
};
|
|
1843
1843
|
};
|
|
1844
1844
|
//#endregion
|
|
1845
|
+
//#region src/crm/services/LeadConversionService.ts
|
|
1846
|
+
var LeadConversionService = class LeadConversionService {
|
|
1847
|
+
constructor(leads, activities, callerTransaction) {
|
|
1848
|
+
this.leads = leads;
|
|
1849
|
+
this.activities = activities;
|
|
1850
|
+
this.callerTransaction = callerTransaction;
|
|
1851
|
+
}
|
|
1852
|
+
leads;
|
|
1853
|
+
activities;
|
|
1854
|
+
callerTransaction;
|
|
1855
|
+
/** Bind read access to any initialized executor; mutations require caller mode. */
|
|
1856
|
+
static async create(options) {
|
|
1857
|
+
if (!options.db) throw new Error("Lead conversion requires an initialized database");
|
|
1858
|
+
const collectionOptions = {
|
|
1859
|
+
db: options.db,
|
|
1860
|
+
_reuseInitializedDb: true,
|
|
1861
|
+
_deferRuntimeInitialization: true
|
|
1862
|
+
};
|
|
1863
|
+
return new LeadConversionService(await LeadCollection.create(collectionOptions), await SalesActivityCollection.create(collectionOptions), options.transaction === "caller");
|
|
1864
|
+
}
|
|
1865
|
+
/** Read the tenant-local receipt, failing closed for inaccessible Leads. */
|
|
1866
|
+
async getForLead(input) {
|
|
1867
|
+
const scope = await this.scope(input);
|
|
1868
|
+
const id = receiptId(scope);
|
|
1869
|
+
const activity = await this.activities.get({ id }, { cache: false });
|
|
1870
|
+
if (!activity) return null;
|
|
1871
|
+
const envelope = activity.getMetadata();
|
|
1872
|
+
if (activity.tenantId !== scope.tenantId || activity.subjectKind !== "lead" || activity.subjectId !== scope.leadId || activity.activityKind !== "lead_conversion" || envelope.targetKind !== scope.targetKind || typeof envelope.targetId !== "string" || !envelope.targetId || !isObject(envelope.metadata)) throw new Error("Lead conversion receipt is inconsistent");
|
|
1873
|
+
return {
|
|
1874
|
+
...scope,
|
|
1875
|
+
id,
|
|
1876
|
+
targetId: envelope.targetId,
|
|
1877
|
+
actorProfileId: activity.actorProfileId,
|
|
1878
|
+
metadata: envelope.metadata
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
/** Resolve originating Leads for a host target without exposing receipt storage. */
|
|
1882
|
+
async findForTarget(input) {
|
|
1883
|
+
const tenantId = requireTenantId();
|
|
1884
|
+
if (input.tenantId !== tenantId) throw new Error("Lead is unavailable in the active tenant");
|
|
1885
|
+
const targetKind = text(input.targetKind, "targetKind");
|
|
1886
|
+
const targetId = text(input.targetId, "targetId");
|
|
1887
|
+
const candidates = await this.activities.list({
|
|
1888
|
+
where: {
|
|
1889
|
+
tenantId,
|
|
1890
|
+
subjectKind: "lead",
|
|
1891
|
+
activityKind: "lead_conversion"
|
|
1892
|
+
},
|
|
1893
|
+
orderBy: "created_at ASC"
|
|
1894
|
+
});
|
|
1895
|
+
const receipts = [];
|
|
1896
|
+
for (const candidate of candidates) {
|
|
1897
|
+
const metadata = candidate.getMetadata();
|
|
1898
|
+
if (metadata.targetKind !== targetKind || metadata.targetId !== targetId) continue;
|
|
1899
|
+
const receipt = await this.getForLead({
|
|
1900
|
+
tenantId,
|
|
1901
|
+
leadId: candidate.subjectId,
|
|
1902
|
+
targetKind
|
|
1903
|
+
});
|
|
1904
|
+
if (receipt && receipt.id === candidate.id && receipt.targetId === targetId) receipts.push(receipt);
|
|
1905
|
+
}
|
|
1906
|
+
return receipts;
|
|
1907
|
+
}
|
|
1908
|
+
/** Exact retries preserve the original actor and receipt; changed intent fails. */
|
|
1909
|
+
async recordForLead(input) {
|
|
1910
|
+
if (!this.callerTransaction) throw new Error("Lead conversion requires a caller-owned transaction");
|
|
1911
|
+
const targetId = text(input.targetId, "targetId");
|
|
1912
|
+
const actorProfileId = text(input.actorProfileId, "actorProfileId");
|
|
1913
|
+
const metadata = jsonObject(input.metadata ?? {});
|
|
1914
|
+
const existing = await this.getForLead(input);
|
|
1915
|
+
if (existing) {
|
|
1916
|
+
if (existing.targetId !== targetId || stableJson(existing.metadata) !== stableJson(metadata)) throw new Error("Lead conversion already recorded with different intent");
|
|
1917
|
+
return existing;
|
|
1918
|
+
}
|
|
1919
|
+
const scope = await this.scope(input);
|
|
1920
|
+
const id = receiptId(scope);
|
|
1921
|
+
await this.activities.create({
|
|
1922
|
+
id,
|
|
1923
|
+
tenantId: scope.tenantId,
|
|
1924
|
+
subjectKind: "lead",
|
|
1925
|
+
subjectId: scope.leadId,
|
|
1926
|
+
activityKind: "lead_conversion",
|
|
1927
|
+
summary: `Lead converted to ${scope.targetKind}`,
|
|
1928
|
+
actorProfileId,
|
|
1929
|
+
metadata: JSON.stringify({
|
|
1930
|
+
targetKind: scope.targetKind,
|
|
1931
|
+
targetId,
|
|
1932
|
+
metadata
|
|
1933
|
+
})
|
|
1934
|
+
});
|
|
1935
|
+
return {
|
|
1936
|
+
...scope,
|
|
1937
|
+
id,
|
|
1938
|
+
targetId,
|
|
1939
|
+
actorProfileId,
|
|
1940
|
+
metadata
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
async scope(input) {
|
|
1944
|
+
const tenantId = requireTenantId();
|
|
1945
|
+
if (input.tenantId !== tenantId) throw new Error("Lead is unavailable in the active tenant");
|
|
1946
|
+
const leadId = text(input.leadId, "leadId");
|
|
1947
|
+
const targetKind = text(input.targetKind, "targetKind");
|
|
1948
|
+
const lead = await this.leads.get({ id: leadId }, { cache: false });
|
|
1949
|
+
if (!lead || lead.tenantId !== tenantId) throw new Error("Lead is unavailable in the active tenant");
|
|
1950
|
+
return {
|
|
1951
|
+
tenantId,
|
|
1952
|
+
leadId,
|
|
1953
|
+
targetKind
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
function text(value, name) {
|
|
1958
|
+
if (typeof value !== "string" || !value.trim() || value !== value.trim()) throw new Error(`Lead conversion ${name} must be a nonempty trimmed string`);
|
|
1959
|
+
return value;
|
|
1960
|
+
}
|
|
1961
|
+
function isObject(value) {
|
|
1962
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1963
|
+
}
|
|
1964
|
+
function jsonObject(value) {
|
|
1965
|
+
if (!isObject(value)) throw new Error("Lead conversion metadata must be a JSON object");
|
|
1966
|
+
let encoded;
|
|
1967
|
+
try {
|
|
1968
|
+
encoded = JSON.stringify(value, (_key, item) => {
|
|
1969
|
+
if (typeof item === "function" || typeof item === "symbol" || typeof item === "undefined" || typeof item === "number" && !Number.isFinite(item)) throw new Error("Non-JSON metadata");
|
|
1970
|
+
return item;
|
|
1971
|
+
});
|
|
1972
|
+
} catch {
|
|
1973
|
+
throw new Error("Lead conversion metadata must contain only JSON data");
|
|
1974
|
+
}
|
|
1975
|
+
return JSON.parse(encoded);
|
|
1976
|
+
}
|
|
1977
|
+
function stableJson(value) {
|
|
1978
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
1979
|
+
if (isObject(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
1980
|
+
return JSON.stringify(value);
|
|
1981
|
+
}
|
|
1982
|
+
function receiptId(scope) {
|
|
1983
|
+
const hash = createHash("sha256").update(JSON.stringify([
|
|
1984
|
+
"lead-conversion-v1",
|
|
1985
|
+
scope.tenantId,
|
|
1986
|
+
scope.leadId,
|
|
1987
|
+
scope.targetKind
|
|
1988
|
+
])).digest("hex");
|
|
1989
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-5${hash.slice(13, 16)}-a${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
|
|
1990
|
+
}
|
|
1991
|
+
//#endregion
|
|
1845
1992
|
//#region src/crm/services/LeadWorkflowService.ts
|
|
1846
1993
|
var LEAD_HUMAN_ACTIVITY_KINDS = [
|
|
1847
1994
|
"note",
|
|
@@ -2171,6 +2318,49 @@ var LeadWorkflowService = class LeadWorkflowService {
|
|
|
2171
2318
|
};
|
|
2172
2319
|
});
|
|
2173
2320
|
}
|
|
2321
|
+
/** Correct editable contact fields with one actor-attributed audit and row lock. */
|
|
2322
|
+
async updateLeadContact(input) {
|
|
2323
|
+
const actorProfileId = this.requireIdentifier(input.actorProfileId, "actorProfileId");
|
|
2324
|
+
const name = typeof input.name === "string" ? input.name.trim() : "";
|
|
2325
|
+
const contactName = typeof input.contactName === "string" ? input.contactName.trim() : "";
|
|
2326
|
+
const email = typeof input.email === "string" ? input.email.trim().toLowerCase() : "";
|
|
2327
|
+
if (!name) throw this.refusal("name_required", "Lead name is required");
|
|
2328
|
+
if ([
|
|
2329
|
+
name,
|
|
2330
|
+
contactName,
|
|
2331
|
+
email
|
|
2332
|
+
].some((value) => value.length > 1e3)) throw this.refusal("invalid_metadata", "Lead contact text is too long");
|
|
2333
|
+
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(email)) throw this.refusal("invalid_email", "Lead email is invalid");
|
|
2334
|
+
return await this.runMutation(async (deps, tenantId) => {
|
|
2335
|
+
const lead = await this.lockLead(deps, input.leadId, tenantId);
|
|
2336
|
+
if (lead.status === "merged") throw this.refusal("lead_not_actionable", "Merged Lead contact cannot be corrected");
|
|
2337
|
+
if (!email && !lead.phone.trim()) throw this.refusal("contact_required", "Lead requires at least one of email or phone");
|
|
2338
|
+
const before = {
|
|
2339
|
+
name: lead.name,
|
|
2340
|
+
contactName: lead.contactName,
|
|
2341
|
+
email: lead.email
|
|
2342
|
+
};
|
|
2343
|
+
const after = {
|
|
2344
|
+
name,
|
|
2345
|
+
contactName,
|
|
2346
|
+
email
|
|
2347
|
+
};
|
|
2348
|
+
if (before.name === name && before.contactName === contactName && before.email === email) return lead;
|
|
2349
|
+
Object.assign(lead, after);
|
|
2350
|
+
await lead.save();
|
|
2351
|
+
await this.appendAudit(deps.activities, lead, {
|
|
2352
|
+
actorProfileId,
|
|
2353
|
+
activityKind: "contact_update",
|
|
2354
|
+
summary: "Corrected lead contact",
|
|
2355
|
+
metadata: {
|
|
2356
|
+
before,
|
|
2357
|
+
after,
|
|
2358
|
+
occurredAt: this.now(input.now).toISOString()
|
|
2359
|
+
}
|
|
2360
|
+
});
|
|
2361
|
+
return lead;
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2174
2364
|
/** Start a new Lead or reopen a disqualified Lead for active follow-up. */
|
|
2175
2365
|
async startWorking(input) {
|
|
2176
2366
|
const actorProfileId = this.requireIdentifier(input.actorProfileId, "actorProfileId");
|
|
@@ -2678,6 +2868,6 @@ function uuidFromHash(hash) {
|
|
|
2678
2868
|
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
|
|
2679
2869
|
}
|
|
2680
2870
|
//#endregion
|
|
2681
|
-
export {
|
|
2871
|
+
export { LEAD_STATUSES as C, SALES_ACTIVITY_SUBJECT_KINDS as D, SALES_ACTIVITY_KINDS as E, SALES_REPRESENTATIVE_STATUSES as O, DEFAULT_PIPELINE_STAGES as S, PIPELINE_STATUSES as T, SalesActivity as _, projectLeadWorkQueue as a, Opportunity as b, OpportunityConversionCollection as c, SalesRepresentativeCollection as d, SalesRepresentative as f, SalesActivityCollection as g, OpportunityCollection as h, MAX_LEAD_WORKFLOW_TEXT_LENGTH as i, Lead as k, OpportunityConversion as l, PipelineDefinition as m, LeadWorkflowService as n, LeadConversionService as o, PipelineDefinitionCollection as p, LeadWorkflowValidationError as r, SalesPipelineRecipe as s, LEAD_HUMAN_ACTIVITY_KINDS as t, LeadCollection as u, PipelineStageCollection as v, OPPORTUNITY_STATUSES as w, DEFAULT_PIPELINE_KEY as x, PipelineStage as y };
|
|
2682
2872
|
|
|
2683
|
-
//# sourceMappingURL=crm-
|
|
2873
|
+
//# sourceMappingURL=crm-BRICSwf2.js.map
|