@happyvertical/smrt-sales 0.57.21 → 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.
@@ -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 { OPPORTUNITY_STATUSES as C, SALES_REPRESENTATIVE_STATUSES as D, SALES_ACTIVITY_SUBJECT_KINDS as E, Lead as O, LEAD_STATUSES as S, SALES_ACTIVITY_KINDS as T, PipelineStageCollection as _, projectLeadWorkQueue as a, DEFAULT_PIPELINE_KEY as b, OpportunityConversion as c, SalesRepresentative as d, PipelineDefinitionCollection as f, SalesActivity as g, SalesActivityCollection as h, MAX_LEAD_WORKFLOW_TEXT_LENGTH as i, LeadCollection as l, OpportunityCollection as m, LeadWorkflowService as n, SalesPipelineRecipe as o, PipelineDefinition as p, LeadWorkflowValidationError as r, OpportunityConversionCollection as s, LEAD_HUMAN_ACTIVITY_KINDS as t, SalesRepresentativeCollection as u, PipelineStage as v, PIPELINE_STATUSES as w, DEFAULT_PIPELINE_STAGES as x, Opportunity as y };
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-DaUkdNoG.js.map
2873
+ //# sourceMappingURL=crm-BRICSwf2.js.map