@aglyn/plugins-crm 1.0.0-beta.163 → 1.0.0-beta.165
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/package.json +13 -13
- package/src/lib/components/contact-associations-card.js +14 -4
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-field-drawer.js +19 -2
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/components/fields-section.d.ts +2 -1
- package/src/lib/components/fields-section.js +6 -3
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.d.ts +59 -0
- package/src/lib/components/lead-campaigns-card.js +160 -0
- package/src/lib/components/lead-campaigns-card.js.map +1 -0
- package/src/lib/components/lead-detail-page.js +53 -3
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-history-card.js +5 -0
- package/src/lib/components/lead-history-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.d.ts +12 -0
- package/src/lib/components/lead-properties-card.js +63 -5
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.d.ts +6 -0
- package/src/lib/components/leads-bulk-bar.js +34 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +16 -2
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-lead-drawer.d.ts +14 -1
- package/src/lib/components/new-lead-drawer.js +41 -3
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/hooks/use-campaign-filing-log.d.ts +29 -0
- package/src/lib/hooks/use-campaign-filing-log.js +91 -0
- package/src/lib/hooks/use-campaign-filing-log.js.map +1 -0
- package/src/lib/model/campaign-filing-activity.d.ts +82 -0
- package/src/lib/model/campaign-filing-activity.js +88 -0
- package/src/lib/model/campaign-filing-activity.js.map +1 -0
- package/src/lib/server/campaign-filing-activity.d.ts +79 -0
- package/src/lib/server/campaign-filing-activity.js +93 -0
- package/src/lib/server/campaign-filing-activity.js.map +1 -0
- package/src/lib/server/lead-campaign-carry.js +25 -0
- package/src/lib/server/lead-campaign-carry.js.map +1 -1
- package/src/lib/server/lead-create.d.ts +15 -4
- package/src/lib/server/lead-create.js +68 -10
- package/src/lib/server/lead-create.js.map +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { CRM_COLLECTIONS, consentGroupForHost, createResourceUid, crmScopeTokens } from "@aglyn/aglyn";
|
|
19
|
+
import { useFirestore, useUser, useUserName } from "@aglyn/tenant-feature-instance";
|
|
20
|
+
import { collection, doc, setDoc } from "firebase/firestore";
|
|
21
|
+
import { useCallback } from "react";
|
|
22
|
+
import { buildCampaignFilingActivity } from "../model/campaign-filing-activity.js";
|
|
23
|
+
/**
|
|
24
|
+
* FILES A MEMBER'S FILING ON THE RECORD'S TIMELINE (AGL-3274), from the
|
|
25
|
+
* console.
|
|
26
|
+
*
|
|
27
|
+
* The lead's Campaigns card and the Leads list's bulk bar write the
|
|
28
|
+
* membership client-direct — a one-field update the rules already admit —
|
|
29
|
+
* so the entry that records it is written the same way, into the org's
|
|
30
|
+
* activity collection the log dialog writes, with the scope a record made
|
|
31
|
+
* on this site carries (`crmScopeTokens`), the signed-in member as author,
|
|
32
|
+
* and the CRM's own id as the filing plugin.
|
|
33
|
+
*
|
|
34
|
+
* Bookkeeping beside the act: the membership has landed by the time this
|
|
35
|
+
* runs, so a write that fails is logged and the filing stands — the card
|
|
36
|
+
* shows the campaigns from the document, not from the timeline.
|
|
37
|
+
*/ export function useCampaignFilingLog(input) {
|
|
38
|
+
const { orgId, hostId, org } = input;
|
|
39
|
+
const firestore = useFirestore();
|
|
40
|
+
const { data: user } = useUser();
|
|
41
|
+
const authorName = useUserName();
|
|
42
|
+
const uid = user == null ? void 0 : user.uid;
|
|
43
|
+
return useCallback(async (link, change)=>{
|
|
44
|
+
var _change_filed, _change_removed;
|
|
45
|
+
if (!orgId || !hostId || !uid) return;
|
|
46
|
+
const entries = [
|
|
47
|
+
...((_change_filed = change.filed) != null ? _change_filed : []).map((campaign)=>({
|
|
48
|
+
action: 'filed',
|
|
49
|
+
campaign
|
|
50
|
+
})),
|
|
51
|
+
...((_change_removed = change.removed) != null ? _change_removed : []).map((campaign)=>({
|
|
52
|
+
action: 'removed',
|
|
53
|
+
campaign
|
|
54
|
+
}))
|
|
55
|
+
];
|
|
56
|
+
if (!entries.length) return;
|
|
57
|
+
const visibleTo = crmScopeTokens(org, consentGroupForHost(org, hostId));
|
|
58
|
+
const atMs = Date.now();
|
|
59
|
+
const activities = collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.activities);
|
|
60
|
+
for (const entry of entries){
|
|
61
|
+
try {
|
|
62
|
+
// Named by the platform's own id, as every console resource is.
|
|
63
|
+
await setDoc(doc(activities, createResourceUid()), _extends({}, buildCampaignFilingActivity({
|
|
64
|
+
action: entry.action,
|
|
65
|
+
campaign: entry.campaign,
|
|
66
|
+
link,
|
|
67
|
+
hostId,
|
|
68
|
+
visibleTo,
|
|
69
|
+
atMs,
|
|
70
|
+
byUid: uid,
|
|
71
|
+
byName: authorName || null
|
|
72
|
+
}), {
|
|
73
|
+
createdAt: new Date(),
|
|
74
|
+
updatedAt: new Date()
|
|
75
|
+
}));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('[crm] the filing could not be written on the timeline', error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}, [
|
|
81
|
+
firestore,
|
|
82
|
+
orgId,
|
|
83
|
+
hostId,
|
|
84
|
+
org,
|
|
85
|
+
uid,
|
|
86
|
+
authorName
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
export default useCampaignFilingLog;
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=use-campaign-filing-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-campaign-filing-log.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n CRM_COLLECTIONS,\n consentGroupForHost,\n createResourceUid,\n crmScopeTokens,\n type CrmActivityLink,\n} from '@aglyn/aglyn'\nimport { useFirestore, useUser, useUserName } from '@aglyn/tenant-feature-instance'\nimport { collection, doc, setDoc } from 'firebase/firestore'\nimport { useCallback } from 'react'\nimport {\n buildCampaignFilingActivity,\n type CampaignFilingRef,\n} from '../model/campaign-filing-activity'\n\n/** What one surface's save changed, by campaign, with the names it showed. */\nexport interface CampaignFilingChange {\n filed?: readonly CampaignFilingRef[]\n removed?: readonly CampaignFilingRef[]\n}\n\n/**\n * FILES A MEMBER'S FILING ON THE RECORD'S TIMELINE (AGL-3274), from the\n * console.\n *\n * The lead's Campaigns card and the Leads list's bulk bar write the\n * membership client-direct — a one-field update the rules already admit —\n * so the entry that records it is written the same way, into the org's\n * activity collection the log dialog writes, with the scope a record made\n * on this site carries (`crmScopeTokens`), the signed-in member as author,\n * and the CRM's own id as the filing plugin.\n *\n * Bookkeeping beside the act: the membership has landed by the time this\n * runs, so a write that fails is logged and the filing stands — the card\n * shows the campaigns from the document, not from the timeline.\n */\nexport function useCampaignFilingLog(input: {\n orgId: string | null | undefined\n /** The site the entry carries as provenance: the lead's, or the contact's viewing site. */\n hostId: string | null | undefined\n org: Record<string, unknown> | null | undefined\n}) {\n const { orgId, hostId, org } = input\n const firestore = useFirestore()\n const { data: user } = useUser()\n const authorName = useUserName()\n const uid = user?.uid\n\n return useCallback(\n async (link: CrmActivityLink, change: CampaignFilingChange): Promise<void> => {\n if (!orgId || !hostId || !uid) return\n const entries = [\n ...(change.filed ?? []).map((campaign) => ({ action: 'filed' as const, campaign })),\n ...(change.removed ?? []).map((campaign) => ({ action: 'removed' as const, campaign })),\n ]\n if (!entries.length) return\n const visibleTo = crmScopeTokens(org, consentGroupForHost(org, hostId))\n const atMs = Date.now()\n const activities = collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.activities)\n for (const entry of entries) {\n try {\n // Named by the platform's own id, as every console resource is.\n await setDoc(doc(activities, createResourceUid()), {\n ...buildCampaignFilingActivity({\n action: entry.action,\n campaign: entry.campaign,\n link,\n hostId,\n visibleTo,\n atMs,\n byUid: uid,\n byName: authorName || null,\n }),\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n } catch (error) {\n console.error('[crm] the filing could not be written on the timeline', error)\n }\n }\n },\n [firestore, orgId, hostId, org, uid, authorName],\n )\n}\n\nexport default useCampaignFilingLog\n"],"names":["CRM_COLLECTIONS","consentGroupForHost","createResourceUid","crmScopeTokens","useFirestore","useUser","useUserName","collection","doc","setDoc","useCallback","buildCampaignFilingActivity","useCampaignFilingLog","input","orgId","hostId","org","firestore","data","user","authorName","uid","link","change","entries","filed","map","campaign","action","removed","length","visibleTo","atMs","Date","now","activities","entry","byUid","byName","createdAt","updatedAt","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,eAAe,EACfC,mBAAmB,EACnBC,iBAAiB,EACjBC,cAAc,QAET,eAAc;AACrB,SAASC,YAAY,EAAEC,OAAO,EAAEC,WAAW,QAAQ,iCAAgC;AACnF,SAASC,UAAU,EAAEC,GAAG,EAAEC,MAAM,QAAQ,qBAAoB;AAC5D,SAASC,WAAW,QAAQ,QAAO;AACnC,SACEC,2BAA2B,QAEtB,uCAAmC;AAQ1C;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,qBAAqBC,KAKpC;IACC,MAAM,EAAEC,KAAK,EAAEC,MAAM,EAAEC,GAAG,EAAE,GAAGH;IAC/B,MAAMI,YAAYb;IAClB,MAAM,EAAEc,MAAMC,IAAI,EAAE,GAAGd;IACvB,MAAMe,aAAad;IACnB,MAAMe,MAAMF,wBAAAA,KAAME,GAAG;IAErB,OAAOX,YACL,OAAOY,MAAuBC;YAGtBA,eACAA;QAHN,IAAI,CAACT,SAAS,CAACC,UAAU,CAACM,KAAK;QAC/B,MAAMG,UAAU;eACX,EAACD,gBAAAA,OAAOE,KAAK,YAAZF,gBAAgB,EAAE,EAAEG,GAAG,CAAC,CAACC,WAAc,CAAA;oBAAEC,QAAQ;oBAAkBD;gBAAS,CAAA;eAC7E,EAACJ,kBAAAA,OAAOM,OAAO,YAAdN,kBAAkB,EAAE,EAAEG,GAAG,CAAC,CAACC,WAAc,CAAA;oBAAEC,QAAQ;oBAAoBD;gBAAS,CAAA;SACrF;QACD,IAAI,CAACH,QAAQM,MAAM,EAAE;QACrB,MAAMC,YAAY5B,eAAea,KAAKf,oBAAoBe,KAAKD;QAC/D,MAAMiB,OAAOC,KAAKC,GAAG;QACrB,MAAMC,aAAa5B,WAAWU,WAAW,QAAQH,OAAOd,gBAAgBmC,UAAU;QAClF,KAAK,MAAMC,SAASZ,QAAS;YAC3B,IAAI;gBACF,gEAAgE;gBAChE,MAAMf,OAAOD,IAAI2B,YAAYjC,sBAAsB,aAC9CS,4BAA4B;oBAC7BiB,QAAQQ,MAAMR,MAAM;oBACpBD,UAAUS,MAAMT,QAAQ;oBACxBL;oBACAP;oBACAgB;oBACAC;oBACAK,OAAOhB;oBACPiB,QAAQlB,cAAc;gBACxB;oBACAmB,WAAW,IAAIN;oBACfO,WAAW,IAAIP;;YAEnB,EAAE,OAAOQ,OAAO;gBACdC,QAAQD,KAAK,CAAC,yDAAyDA;YACzE;QACF;IACF,GACA;QAACxB;QAAWH;QAAOC;QAAQC;QAAKK;QAAKD;KAAW;AAEpD;AAEA,eAAeR,qBAAoB"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { CrmActivity, CrmActivityLink, ScopeToken } from '@aglyn/aglyn';
|
|
18
|
+
/**
|
|
19
|
+
* WHAT A FILING WRITES ON THE RECORD'S TIMELINE (AGL-3274).
|
|
20
|
+
*
|
|
21
|
+
* A lead or a contact filed under a campaign — from its own page, the bulk
|
|
22
|
+
* bar, the New lead drawer, or by conversion carrying a lead's campaigns
|
|
23
|
+
* onto the contact it became — showed nothing of it in Activity, so the
|
|
24
|
+
* timeline read "email sent" with no line for the act that put the person
|
|
25
|
+
* in the campaign in the first place. Every door now files one entry per
|
|
26
|
+
* campaign changed, and this module is the one place their shape lives:
|
|
27
|
+
* the client hook and the server helper both build the document here, so
|
|
28
|
+
* an entry filed from the page and one filed by the conversion read alike.
|
|
29
|
+
*
|
|
30
|
+
* ## A `note`, not a kind of its own
|
|
31
|
+
*
|
|
32
|
+
* The timeline's kinds are what a PERSON logs — call, email, meeting, note
|
|
33
|
+
* — and a filing is bookkeeping beside them, the way a sequence's "Enrolled
|
|
34
|
+
* in" entry is. A `filing` kind would need its own label, glyph, report
|
|
35
|
+
* column and a place in the log dialog where a member could log one by
|
|
36
|
+
* hand, which is the one thing a filing must never be. So it is a note that
|
|
37
|
+
* names its author (the member, or the conversion), carries the CRM's own
|
|
38
|
+
* id as `sourcePluginId` the way a sequence's entries carry Outreach's, and
|
|
39
|
+
* names the campaign twice: by name in the body, by id in `campaignId`.
|
|
40
|
+
*/
|
|
41
|
+
/** The kind every filing entry is filed as. */
|
|
42
|
+
export declare const CAMPAIGN_FILING_ACTIVITY_KIND: "note";
|
|
43
|
+
/** What the entry says was done: put in the campaign, or taken out of it. */
|
|
44
|
+
export type CampaignFilingAction = 'filed' | 'removed';
|
|
45
|
+
/** A campaign as a filing entry names it: the container's id and its name at the time. */
|
|
46
|
+
export interface CampaignFilingRef {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The author a conversion's carry signs its entries with: nobody in the
|
|
52
|
+
* room typed it, and "A team member" — the timeline's word for an
|
|
53
|
+
* anonymous entry — would credit a person with what the conversion did.
|
|
54
|
+
*/
|
|
55
|
+
export declare const CAMPAIGN_FILING_CARRY_BY_NAME = "Lead conversion";
|
|
56
|
+
/** The one line of the entry, as the timeline shows it. */
|
|
57
|
+
export declare function campaignFilingBody(action: CampaignFilingAction, campaignName: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* The membership a save changed, as the entries it owes: one `filed` per
|
|
60
|
+
* id the save added, one `removed` per id it dropped. Order-insensitive,
|
|
61
|
+
* like `campaignMembershipUnchanged`; a reorder owes nothing.
|
|
62
|
+
*/
|
|
63
|
+
export declare function campaignFilingChanges(before: readonly string[], after: readonly string[]): {
|
|
64
|
+
added: string[];
|
|
65
|
+
removed: string[];
|
|
66
|
+
};
|
|
67
|
+
export interface CampaignFilingActivityInput {
|
|
68
|
+
action: CampaignFilingAction;
|
|
69
|
+
campaign: CampaignFilingRef;
|
|
70
|
+
link: CrmActivityLink;
|
|
71
|
+
/** The site the entry carries as provenance — the lead's site, the contact's viewing site. */
|
|
72
|
+
hostId: string;
|
|
73
|
+
visibleTo: readonly ScopeToken[];
|
|
74
|
+
/** When it happened, epoch ms. */
|
|
75
|
+
atMs: number;
|
|
76
|
+
/** The member who did it, or `''` for the conversion. */
|
|
77
|
+
byUid: string;
|
|
78
|
+
/** The member's name as it read, or the carry's fixed author; absent rather than blank. */
|
|
79
|
+
byName?: string | null;
|
|
80
|
+
}
|
|
81
|
+
/** The document a filing entry is stored as, minus the timestamps the writer stamps. */
|
|
82
|
+
export declare function buildCampaignFilingActivity(input: CampaignFilingActivityInput): CrmActivity;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
18
|
+
/**
|
|
19
|
+
* WHAT A FILING WRITES ON THE RECORD'S TIMELINE (AGL-3274).
|
|
20
|
+
*
|
|
21
|
+
* A lead or a contact filed under a campaign — from its own page, the bulk
|
|
22
|
+
* bar, the New lead drawer, or by conversion carrying a lead's campaigns
|
|
23
|
+
* onto the contact it became — showed nothing of it in Activity, so the
|
|
24
|
+
* timeline read "email sent" with no line for the act that put the person
|
|
25
|
+
* in the campaign in the first place. Every door now files one entry per
|
|
26
|
+
* campaign changed, and this module is the one place their shape lives:
|
|
27
|
+
* the client hook and the server helper both build the document here, so
|
|
28
|
+
* an entry filed from the page and one filed by the conversion read alike.
|
|
29
|
+
*
|
|
30
|
+
* ## A `note`, not a kind of its own
|
|
31
|
+
*
|
|
32
|
+
* The timeline's kinds are what a PERSON logs — call, email, meeting, note
|
|
33
|
+
* — and a filing is bookkeeping beside them, the way a sequence's "Enrolled
|
|
34
|
+
* in" entry is. A `filing` kind would need its own label, glyph, report
|
|
35
|
+
* column and a place in the log dialog where a member could log one by
|
|
36
|
+
* hand, which is the one thing a filing must never be. So it is a note that
|
|
37
|
+
* names its author (the member, or the conversion), carries the CRM's own
|
|
38
|
+
* id as `sourcePluginId` the way a sequence's entries carry Outreach's, and
|
|
39
|
+
* names the campaign twice: by name in the body, by id in `campaignId`.
|
|
40
|
+
*/ /** The kind every filing entry is filed as. */ export const CAMPAIGN_FILING_ACTIVITY_KIND = 'note';
|
|
41
|
+
/**
|
|
42
|
+
* The author a conversion's carry signs its entries with: nobody in the
|
|
43
|
+
* room typed it, and "A team member" — the timeline's word for an
|
|
44
|
+
* anonymous entry — would credit a person with what the conversion did.
|
|
45
|
+
*/ export const CAMPAIGN_FILING_CARRY_BY_NAME = 'Lead conversion';
|
|
46
|
+
/** The one line of the entry, as the timeline shows it. */ export function campaignFilingBody(action, campaignName) {
|
|
47
|
+
const name = campaignName.trim() || 'a campaign';
|
|
48
|
+
return action === 'filed' ? `Filed under ${name}` : `Removed from ${name}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The membership a save changed, as the entries it owes: one `filed` per
|
|
52
|
+
* id the save added, one `removed` per id it dropped. Order-insensitive,
|
|
53
|
+
* like `campaignMembershipUnchanged`; a reorder owes nothing.
|
|
54
|
+
*/ export function campaignFilingChanges(before, after) {
|
|
55
|
+
return {
|
|
56
|
+
added: after.filter((id)=>!before.includes(id)),
|
|
57
|
+
removed: before.filter((id)=>!after.includes(id))
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** The document a filing entry is stored as, minus the timestamps the writer stamps. */ export function buildCampaignFilingActivity(input) {
|
|
61
|
+
var _input_byName;
|
|
62
|
+
const byName = String((_input_byName = input.byName) != null ? _input_byName : '').trim();
|
|
63
|
+
return _extends({
|
|
64
|
+
kind: CAMPAIGN_FILING_ACTIVITY_KIND,
|
|
65
|
+
body: campaignFilingBody(input.action, input.campaign.name),
|
|
66
|
+
atMs: input.atMs,
|
|
67
|
+
byUid: input.byUid
|
|
68
|
+
}, byName ? {
|
|
69
|
+
byName
|
|
70
|
+
} : {}, input.link.contactId ? {
|
|
71
|
+
contactId: input.link.contactId
|
|
72
|
+
} : {}, input.link.companyId ? {
|
|
73
|
+
companyId: input.link.companyId
|
|
74
|
+
} : {}, input.link.dealId ? {
|
|
75
|
+
dealId: input.link.dealId
|
|
76
|
+
} : {}, input.link.leadId ? {
|
|
77
|
+
leadId: input.link.leadId
|
|
78
|
+
} : {}, {
|
|
79
|
+
hostId: input.hostId,
|
|
80
|
+
visibleTo: [
|
|
81
|
+
...input.visibleTo
|
|
82
|
+
],
|
|
83
|
+
sourcePluginId: BUNDLE_ID,
|
|
84
|
+
campaignId: input.campaign.id
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=campaign-filing-activity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/campaign-filing-activity.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { CrmActivity, CrmActivityLink, ScopeToken } from '@aglyn/aglyn'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * WHAT A FILING WRITES ON THE RECORD'S TIMELINE (AGL-3274).\n *\n * A lead or a contact filed under a campaign — from its own page, the bulk\n * bar, the New lead drawer, or by conversion carrying a lead's campaigns\n * onto the contact it became — showed nothing of it in Activity, so the\n * timeline read \"email sent\" with no line for the act that put the person\n * in the campaign in the first place. Every door now files one entry per\n * campaign changed, and this module is the one place their shape lives:\n * the client hook and the server helper both build the document here, so\n * an entry filed from the page and one filed by the conversion read alike.\n *\n * ## A `note`, not a kind of its own\n *\n * The timeline's kinds are what a PERSON logs — call, email, meeting, note\n * — and a filing is bookkeeping beside them, the way a sequence's \"Enrolled\n * in\" entry is. A `filing` kind would need its own label, glyph, report\n * column and a place in the log dialog where a member could log one by\n * hand, which is the one thing a filing must never be. So it is a note that\n * names its author (the member, or the conversion), carries the CRM's own\n * id as `sourcePluginId` the way a sequence's entries carry Outreach's, and\n * names the campaign twice: by name in the body, by id in `campaignId`.\n */\n\n/** The kind every filing entry is filed as. */\nexport const CAMPAIGN_FILING_ACTIVITY_KIND = 'note' as const\n\n/** What the entry says was done: put in the campaign, or taken out of it. */\nexport type CampaignFilingAction = 'filed' | 'removed'\n\n/** A campaign as a filing entry names it: the container's id and its name at the time. */\nexport interface CampaignFilingRef {\n id: string\n name: string\n}\n\n/**\n * The author a conversion's carry signs its entries with: nobody in the\n * room typed it, and \"A team member\" — the timeline's word for an\n * anonymous entry — would credit a person with what the conversion did.\n */\nexport const CAMPAIGN_FILING_CARRY_BY_NAME = 'Lead conversion'\n\n/** The one line of the entry, as the timeline shows it. */\nexport function campaignFilingBody(action: CampaignFilingAction, campaignName: string): string {\n const name = campaignName.trim() || 'a campaign'\n return action === 'filed' ? `Filed under ${name}` : `Removed from ${name}`\n}\n\n/**\n * The membership a save changed, as the entries it owes: one `filed` per\n * id the save added, one `removed` per id it dropped. Order-insensitive,\n * like `campaignMembershipUnchanged`; a reorder owes nothing.\n */\nexport function campaignFilingChanges(\n before: readonly string[],\n after: readonly string[],\n): { added: string[]; removed: string[] } {\n return {\n added: after.filter((id) => !before.includes(id)),\n removed: before.filter((id) => !after.includes(id)),\n }\n}\n\nexport interface CampaignFilingActivityInput {\n action: CampaignFilingAction\n campaign: CampaignFilingRef\n link: CrmActivityLink\n /** The site the entry carries as provenance — the lead's site, the contact's viewing site. */\n hostId: string\n visibleTo: readonly ScopeToken[]\n /** When it happened, epoch ms. */\n atMs: number\n /** The member who did it, or `''` for the conversion. */\n byUid: string\n /** The member's name as it read, or the carry's fixed author; absent rather than blank. */\n byName?: string | null\n}\n\n/** The document a filing entry is stored as, minus the timestamps the writer stamps. */\nexport function buildCampaignFilingActivity(input: CampaignFilingActivityInput): CrmActivity {\n const byName = String(input.byName ?? '').trim()\n return {\n kind: CAMPAIGN_FILING_ACTIVITY_KIND,\n body: campaignFilingBody(input.action, input.campaign.name),\n atMs: input.atMs,\n byUid: input.byUid,\n ...(byName ? { byName } : {}),\n ...(input.link.contactId ? { contactId: input.link.contactId } : {}),\n ...(input.link.companyId ? { companyId: input.link.companyId } : {}),\n ...(input.link.dealId ? { dealId: input.link.dealId } : {}),\n ...(input.link.leadId ? { leadId: input.link.leadId } : {}),\n hostId: input.hostId,\n visibleTo: [...input.visibleTo],\n sourcePluginId: BUNDLE_ID,\n campaignId: input.campaign.id,\n }\n}\n"],"names":["BUNDLE_ID","CAMPAIGN_FILING_ACTIVITY_KIND","CAMPAIGN_FILING_CARRY_BY_NAME","campaignFilingBody","action","campaignName","name","trim","campaignFilingChanges","before","after","added","filter","id","includes","removed","buildCampaignFilingActivity","input","byName","String","kind","body","campaign","atMs","byUid","link","contactId","companyId","dealId","leadId","hostId","visibleTo","sourcePluginId","campaignId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,SAAS,QAAQ,gCAA4B;AAEtD;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,6CAA6C,GAC7C,OAAO,MAAMC,gCAAgC,OAAe;AAW5D;;;;CAIC,GACD,OAAO,MAAMC,gCAAgC,kBAAiB;AAE9D,yDAAyD,GACzD,OAAO,SAASC,mBAAmBC,MAA4B,EAAEC,YAAoB;IACnF,MAAMC,OAAOD,aAAaE,IAAI,MAAM;IACpC,OAAOH,WAAW,UAAU,CAAC,YAAY,EAAEE,MAAM,GAAG,CAAC,aAAa,EAAEA,MAAM;AAC5E;AAEA;;;;CAIC,GACD,OAAO,SAASE,sBACdC,MAAyB,EACzBC,KAAwB;IAExB,OAAO;QACLC,OAAOD,MAAME,MAAM,CAAC,CAACC,KAAO,CAACJ,OAAOK,QAAQ,CAACD;QAC7CE,SAASN,OAAOG,MAAM,CAAC,CAACC,KAAO,CAACH,MAAMI,QAAQ,CAACD;IACjD;AACF;AAiBA,sFAAsF,GACtF,OAAO,SAASG,4BAA4BC,KAAkC;QACtDA;IAAtB,MAAMC,SAASC,QAAOF,gBAAAA,MAAMC,MAAM,YAAZD,gBAAgB,IAAIV,IAAI;IAC9C,OAAO;QACLa,MAAMnB;QACNoB,MAAMlB,mBAAmBc,MAAMb,MAAM,EAAEa,MAAMK,QAAQ,CAAChB,IAAI;QAC1DiB,MAAMN,MAAMM,IAAI;QAChBC,OAAOP,MAAMO,KAAK;OACdN,SAAS;QAAEA;IAAO,IAAI,CAAC,GACvBD,MAAMQ,IAAI,CAACC,SAAS,GAAG;QAAEA,WAAWT,MAAMQ,IAAI,CAACC,SAAS;IAAC,IAAI,CAAC,GAC9DT,MAAMQ,IAAI,CAACE,SAAS,GAAG;QAAEA,WAAWV,MAAMQ,IAAI,CAACE,SAAS;IAAC,IAAI,CAAC,GAC9DV,MAAMQ,IAAI,CAACG,MAAM,GAAG;QAAEA,QAAQX,MAAMQ,IAAI,CAACG,MAAM;IAAC,IAAI,CAAC,GACrDX,MAAMQ,IAAI,CAACI,MAAM,GAAG;QAAEA,QAAQZ,MAAMQ,IAAI,CAACI,MAAM;IAAC,IAAI,CAAC;QACzDC,QAAQb,MAAMa,MAAM;QACpBC,WAAW;eAAId,MAAMc,SAAS;SAAC;QAC/BC,gBAAgBhC;QAChBiC,YAAYhB,MAAMK,QAAQ,CAACT,EAAE;;AAEjC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type CrmActivityLink } from '@aglyn/aglyn/app-utils/crm';
|
|
18
|
+
import { type CampaignFilingAction, type CampaignFilingRef } from '../model/campaign-filing-activity';
|
|
19
|
+
/**
|
|
20
|
+
* FILES A FILING ON THE RECORD'S TIMELINE (AGL-3274), from the server.
|
|
21
|
+
*
|
|
22
|
+
* The two doors that file a lead under a campaign on the server — the
|
|
23
|
+
* `leads-create` route behind the New lead drawer, and the conversion's
|
|
24
|
+
* carry of a lead's campaigns onto the contact — write the entry through
|
|
25
|
+
* this, with the scope a record made on the site carries and the CRM's
|
|
26
|
+
* own id as the filing plugin, exactly as the console's hook writes one.
|
|
27
|
+
*
|
|
28
|
+
* ## Once, when the caller says so
|
|
29
|
+
*
|
|
30
|
+
* A member's act is an act each time, and the route's entries are named
|
|
31
|
+
* by `createResourceUid()`, the id every console resource carries. The
|
|
32
|
+
* conversion carry runs from a seam every converting
|
|
33
|
+
* door reaches, and a door that runs twice must not file twice — so a
|
|
34
|
+
* caller that hands in a `dedupeKey` gets a keyed `create()`, and the
|
|
35
|
+
* second run finds its own entry and writes nothing. The key is hashed
|
|
36
|
+
* under the CRM's id the way the timeline seam hashes a plugin's, so no
|
|
37
|
+
* caller's raw key becomes a document id.
|
|
38
|
+
*
|
|
39
|
+
* ## Never throws
|
|
40
|
+
*
|
|
41
|
+
* The membership has landed by the time this runs; an entry that could
|
|
42
|
+
* not be filed is logged, and the record's page shows the campaigns from
|
|
43
|
+
* the document either way. A record at its activity ceiling is refused
|
|
44
|
+
* before anything is written, in the same breath.
|
|
45
|
+
*/
|
|
46
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
47
|
+
export interface CampaignFilingActivitiesInput {
|
|
48
|
+
orgId: string;
|
|
49
|
+
/**
|
|
50
|
+
* The organization document, when the caller already holds it — the
|
|
51
|
+
* scope tokens read `defaultResourceScope` and the consent groups off
|
|
52
|
+
* it. Read here otherwise.
|
|
53
|
+
*/
|
|
54
|
+
org?: Record<string, unknown> | null;
|
|
55
|
+
/** The site the entry carries as provenance. */
|
|
56
|
+
hostId: string;
|
|
57
|
+
link: CrmActivityLink;
|
|
58
|
+
action: CampaignFilingAction;
|
|
59
|
+
campaigns: readonly CampaignFilingRef[];
|
|
60
|
+
atMs: number;
|
|
61
|
+
/** The member who did it, or `''` for the conversion. */
|
|
62
|
+
byUid: string;
|
|
63
|
+
byName?: string | null;
|
|
64
|
+
/**
|
|
65
|
+
* A key that files each campaign's entry once across runs — the carry's
|
|
66
|
+
* — or nothing for a member's act, which is new each time.
|
|
67
|
+
*/
|
|
68
|
+
dedupeKey?: string | null;
|
|
69
|
+
}
|
|
70
|
+
/** The document id a keyed entry files under: the CRM's namespace, never a raw key. */
|
|
71
|
+
export declare function campaignFilingActivityId(key: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* The names of a site's campaigns, by id, for the entries that name them.
|
|
74
|
+
* A container that is gone answers its id, so the entry still says which.
|
|
75
|
+
*/
|
|
76
|
+
export declare function siteCampaignRefs(firestore: Firestore, hostId: string, campaignIds: readonly string[]): Promise<CampaignFilingRef[]>;
|
|
77
|
+
/** Files one entry per campaign; answers how many were written. */
|
|
78
|
+
export declare function fileCampaignFilingActivities(firestore: Firestore, input: CampaignFilingActivitiesInput): Promise<number>;
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
|
|
18
|
+
import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
|
|
19
|
+
import { CRM_COLLECTIONS, crmActivityLogHasRoom, crmScopeTokens } from "@aglyn/aglyn/app-utils/crm";
|
|
20
|
+
import { countCrmActivitiesForRecord } from "@aglyn/tenant-data-admin/server/crm-records";
|
|
21
|
+
import { createHash } from "crypto";
|
|
22
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
23
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
24
|
+
import { buildCampaignFilingActivity } from "../model/campaign-filing-activity.js";
|
|
25
|
+
/** The document id a keyed entry files under: the CRM's namespace, never a raw key. */ export function campaignFilingActivityId(key) {
|
|
26
|
+
return `plg_${createHash('sha256').update(`${BUNDLE_ID}:${key}`).digest('hex').slice(0, 28)}`;
|
|
27
|
+
}
|
|
28
|
+
/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */ const ALREADY_EXISTS = 6;
|
|
29
|
+
/**
|
|
30
|
+
* The names of a site's campaigns, by id, for the entries that name them.
|
|
31
|
+
* A container that is gone answers its id, so the entry still says which.
|
|
32
|
+
*/ export async function siteCampaignRefs(firestore, hostId, campaignIds) {
|
|
33
|
+
if (!campaignIds.length) return [];
|
|
34
|
+
const containers = firestore.collection('hosts').doc(hostId).collection('emailCampaigns');
|
|
35
|
+
const found = await firestore.getAll(...campaignIds.map((id)=>containers.doc(id)));
|
|
36
|
+
return campaignIds.map((id, index)=>{
|
|
37
|
+
var _ref;
|
|
38
|
+
var _found_index;
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
name: String((_ref = (_found_index = found[index]) == null ? void 0 : _found_index.get('name')) != null ? _ref : '').trim() || id
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Files one entry per campaign; answers how many were written. */ export async function fileCampaignFilingActivities(firestore, input) {
|
|
46
|
+
if (!input.campaigns.length) return 0;
|
|
47
|
+
try {
|
|
48
|
+
var _ref, _input_org;
|
|
49
|
+
const orgRef = firestore.collection('orgs').doc(input.orgId);
|
|
50
|
+
const org = (_ref = (_input_org = input.org) != null ? _input_org : (await orgRef.get()).data()) != null ? _ref : null;
|
|
51
|
+
const visibleTo = crmScopeTokens(org, consentGroupForHost(org, input.hostId));
|
|
52
|
+
if (!crmActivityLogHasRoom(await countCrmActivitiesForRecord(orgRef, input.link))) {
|
|
53
|
+
console.warn('[crm] the filing was not written on the timeline: the record’s log is full', input.link);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
const activities = orgRef.collection(CRM_COLLECTIONS.activities);
|
|
57
|
+
let written = 0;
|
|
58
|
+
for (const campaign of input.campaigns){
|
|
59
|
+
var _input_byName;
|
|
60
|
+
const entry = _extends({}, buildCampaignFilingActivity({
|
|
61
|
+
action: input.action,
|
|
62
|
+
campaign,
|
|
63
|
+
link: input.link,
|
|
64
|
+
hostId: input.hostId,
|
|
65
|
+
visibleTo,
|
|
66
|
+
atMs: input.atMs,
|
|
67
|
+
byUid: input.byUid,
|
|
68
|
+
byName: (_input_byName = input.byName) != null ? _input_byName : null
|
|
69
|
+
}), {
|
|
70
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
71
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
72
|
+
});
|
|
73
|
+
if (input.dedupeKey) {
|
|
74
|
+
try {
|
|
75
|
+
await activities.doc(campaignFilingActivityId(`${input.dedupeKey}:${campaign.id}`)).create(entry);
|
|
76
|
+
written += 1;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if ((error == null ? void 0 : error.code) !== ALREADY_EXISTS) throw error;
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
// A member's act, new each time, named by the platform's own id.
|
|
82
|
+
await activities.doc(createResourceUid()).create(entry);
|
|
83
|
+
written += 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return written;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('[crm] the filing could not be written on the timeline', input.link, error);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=campaign-filing-activity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/campaign-filing-activity.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport {\n CRM_COLLECTIONS,\n crmActivityLogHasRoom,\n crmScopeTokens,\n type CrmActivityLink,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { countCrmActivitiesForRecord } from '@aglyn/tenant-data-admin/server/crm-records'\nimport { createHash } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n buildCampaignFilingActivity,\n type CampaignFilingAction,\n type CampaignFilingRef,\n} from '../model/campaign-filing-activity'\n\n/**\n * FILES A FILING ON THE RECORD'S TIMELINE (AGL-3274), from the server.\n *\n * The two doors that file a lead under a campaign on the server — the\n * `leads-create` route behind the New lead drawer, and the conversion's\n * carry of a lead's campaigns onto the contact — write the entry through\n * this, with the scope a record made on the site carries and the CRM's\n * own id as the filing plugin, exactly as the console's hook writes one.\n *\n * ## Once, when the caller says so\n *\n * A member's act is an act each time, and the route's entries are named\n * by `createResourceUid()`, the id every console resource carries. The\n * conversion carry runs from a seam every converting\n * door reaches, and a door that runs twice must not file twice — so a\n * caller that hands in a `dedupeKey` gets a keyed `create()`, and the\n * second run finds its own entry and writes nothing. The key is hashed\n * under the CRM's id the way the timeline seam hashes a plugin's, so no\n * caller's raw key becomes a document id.\n *\n * ## Never throws\n *\n * The membership has landed by the time this runs; an entry that could\n * not be filed is logged, and the record's page shows the campaigns from\n * the document either way. A record at its activity ceiling is refused\n * before anything is written, in the same breath.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CampaignFilingActivitiesInput {\n orgId: string\n /**\n * The organization document, when the caller already holds it — the\n * scope tokens read `defaultResourceScope` and the consent groups off\n * it. Read here otherwise.\n */\n org?: Record<string, unknown> | null\n /** The site the entry carries as provenance. */\n hostId: string\n link: CrmActivityLink\n action: CampaignFilingAction\n campaigns: readonly CampaignFilingRef[]\n atMs: number\n /** The member who did it, or `''` for the conversion. */\n byUid: string\n byName?: string | null\n /**\n * A key that files each campaign's entry once across runs — the carry's\n * — or nothing for a member's act, which is new each time.\n */\n dedupeKey?: string | null\n}\n\n/** The document id a keyed entry files under: the CRM's namespace, never a raw key. */\nexport function campaignFilingActivityId(key: string): string {\n return `plg_${createHash('sha256').update(`${BUNDLE_ID}:${key}`).digest('hex').slice(0, 28)}`\n}\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\n/**\n * The names of a site's campaigns, by id, for the entries that name them.\n * A container that is gone answers its id, so the entry still says which.\n */\nexport async function siteCampaignRefs(\n firestore: Firestore,\n hostId: string,\n campaignIds: readonly string[],\n): Promise<CampaignFilingRef[]> {\n if (!campaignIds.length) return []\n const containers = firestore.collection('hosts').doc(hostId).collection('emailCampaigns')\n const found = await firestore.getAll(...campaignIds.map((id) => containers.doc(id)))\n return campaignIds.map((id, index) => ({\n id,\n name: String(found[index]?.get('name') ?? '').trim() || id,\n }))\n}\n\n/** Files one entry per campaign; answers how many were written. */\nexport async function fileCampaignFilingActivities(\n firestore: Firestore,\n input: CampaignFilingActivitiesInput,\n): Promise<number> {\n if (!input.campaigns.length) return 0\n try {\n const orgRef = firestore.collection('orgs').doc(input.orgId)\n const org =\n input.org ?? ((await orgRef.get()).data() as Record<string, unknown> | undefined) ?? null\n const visibleTo = crmScopeTokens(org, consentGroupForHost(org, input.hostId))\n if (!crmActivityLogHasRoom(await countCrmActivitiesForRecord(orgRef, input.link))) {\n console.warn('[crm] the filing was not written on the timeline: the record’s log is full', input.link)\n return 0\n }\n const activities = orgRef.collection(CRM_COLLECTIONS.activities)\n let written = 0\n for (const campaign of input.campaigns) {\n const entry = {\n ...buildCampaignFilingActivity({\n action: input.action,\n campaign,\n link: input.link,\n hostId: input.hostId,\n visibleTo,\n atMs: input.atMs,\n byUid: input.byUid,\n byName: input.byName ?? null,\n }),\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n }\n if (input.dedupeKey) {\n try {\n await activities.doc(campaignFilingActivityId(`${input.dedupeKey}:${campaign.id}`)).create(entry)\n written += 1\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n }\n } else {\n // A member's act, new each time, named by the platform's own id.\n await activities.doc(createResourceUid()).create(entry)\n written += 1\n }\n }\n return written\n } catch (error) {\n console.error('[crm] the filing could not be written on the timeline', input.link, error)\n return 0\n }\n}\n"],"names":["consentGroupForHost","createResourceUid","CRM_COLLECTIONS","crmActivityLogHasRoom","crmScopeTokens","countCrmActivitiesForRecord","createHash","FieldValue","BUNDLE_ID","buildCampaignFilingActivity","campaignFilingActivityId","key","update","digest","slice","ALREADY_EXISTS","siteCampaignRefs","firestore","hostId","campaignIds","length","containers","collection","doc","found","getAll","map","id","index","name","String","get","trim","fileCampaignFilingActivities","input","campaigns","orgRef","orgId","org","data","visibleTo","link","console","warn","activities","written","campaign","entry","action","atMs","byUid","byName","createdAt","serverTimestamp","updatedAt","dedupeKey","create","error","code"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,wCAAuC;AAC3E,SAASC,iBAAiB,QAAQ,6CAA4C;AAC9E,SACEC,eAAe,EACfC,qBAAqB,EACrBC,cAAc,QAET,6BAA4B;AACnC,SAASC,2BAA2B,QAAQ,8CAA6C;AACzF,SAASC,UAAU,QAAQ,SAAQ;AACnC,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SACEC,2BAA2B,QAGtB,uCAAmC;AAwD1C,qFAAqF,GACrF,OAAO,SAASC,yBAAyBC,GAAW;IAClD,OAAO,CAAC,IAAI,EAAEL,WAAW,UAAUM,MAAM,CAAC,GAAGJ,UAAU,CAAC,EAAEG,KAAK,EAAEE,MAAM,CAAC,OAAOC,KAAK,CAAC,GAAG,KAAK;AAC/F;AAEA,qFAAqF,GACrF,MAAMC,iBAAiB;AAEvB;;;CAGC,GACD,OAAO,eAAeC,iBACpBC,SAAoB,EACpBC,MAAc,EACdC,WAA8B;IAE9B,IAAI,CAACA,YAAYC,MAAM,EAAE,OAAO,EAAE;IAClC,MAAMC,aAAaJ,UAAUK,UAAU,CAAC,SAASC,GAAG,CAACL,QAAQI,UAAU,CAAC;IACxE,MAAME,QAAQ,MAAMP,UAAUQ,MAAM,IAAIN,YAAYO,GAAG,CAAC,CAACC,KAAON,WAAWE,GAAG,CAACI;IAC/E,OAAOR,YAAYO,GAAG,CAAC,CAACC,IAAIC;;YAEbJ;eAFwB;YACrCG;YACAE,MAAMC,gBAAON,eAAAA,KAAK,CAACI,MAAM,qBAAZJ,aAAcO,GAAG,CAAC,0BAAW,IAAIC,IAAI,MAAML;QAC1D;;AACF;AAEA,iEAAiE,GACjE,OAAO,eAAeM,6BACpBhB,SAAoB,EACpBiB,KAAoC;IAEpC,IAAI,CAACA,MAAMC,SAAS,CAACf,MAAM,EAAE,OAAO;IACpC,IAAI;YAGAc,MAAAA;QAFF,MAAME,SAASnB,UAAUK,UAAU,CAAC,QAAQC,GAAG,CAACW,MAAMG,KAAK;QAC3D,MAAMC,OACJJ,QAAAA,aAAAA,MAAMI,GAAG,YAATJ,aAAc,AAAC,CAAA,MAAME,OAAOL,GAAG,EAAC,EAAGQ,IAAI,cAAvCL,OAAqF;QACvF,MAAMM,YAAYpC,eAAekC,KAAKtC,oBAAoBsC,KAAKJ,MAAMhB,MAAM;QAC3E,IAAI,CAACf,sBAAsB,MAAME,4BAA4B+B,QAAQF,MAAMO,IAAI,IAAI;YACjFC,QAAQC,IAAI,CAAC,8EAA8ET,MAAMO,IAAI;YACrG,OAAO;QACT;QACA,MAAMG,aAAaR,OAAOd,UAAU,CAACpB,gBAAgB0C,UAAU;QAC/D,IAAIC,UAAU;QACd,KAAK,MAAMC,YAAYZ,MAAMC,SAAS,CAAE;gBAU1BD;YATZ,MAAMa,QAAQ,aACTtC,4BAA4B;gBAC7BuC,QAAQd,MAAMc,MAAM;gBACpBF;gBACAL,MAAMP,MAAMO,IAAI;gBAChBvB,QAAQgB,MAAMhB,MAAM;gBACpBsB;gBACAS,MAAMf,MAAMe,IAAI;gBAChBC,OAAOhB,MAAMgB,KAAK;gBAClBC,MAAM,GAAEjB,gBAAAA,MAAMiB,MAAM,YAAZjB,gBAAgB;YAC1B;gBACAkB,WAAW7C,WAAW8C,eAAe;gBACrCC,WAAW/C,WAAW8C,eAAe;;YAEvC,IAAInB,MAAMqB,SAAS,EAAE;gBACnB,IAAI;oBACF,MAAMX,WAAWrB,GAAG,CAACb,yBAAyB,GAAGwB,MAAMqB,SAAS,CAAC,CAAC,EAAET,SAASnB,EAAE,EAAE,GAAG6B,MAAM,CAACT;oBAC3FF,WAAW;gBACb,EAAE,OAAOY,OAAO;oBACd,IAAI,CAACA,yBAAD,AAACA,MAA8BC,IAAI,MAAK3C,gBAAgB,MAAM0C;gBACpE;YACF,OAAO;gBACL,iEAAiE;gBACjE,MAAMb,WAAWrB,GAAG,CAACtB,qBAAqBuD,MAAM,CAACT;gBACjDF,WAAW;YACb;QACF;QACA,OAAOA;IACT,EAAE,OAAOY,OAAO;QACdf,QAAQe,KAAK,CAAC,yDAAyDvB,MAAMO,IAAI,EAAEgB;QACnF,OAAO;IACT;AACF"}
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
18
18
|
import { consentGroupForSite } from "@aglyn/tenant-data-admin/server/organizations";
|
|
19
19
|
import { FieldValue } from "firebase-admin/firestore";
|
|
20
|
+
import { CAMPAIGN_FILING_CARRY_BY_NAME } from "../model/campaign-filing-activity.js";
|
|
21
|
+
import { fileCampaignFilingActivities, siteCampaignRefs } from "./campaign-filing-activity.js";
|
|
20
22
|
/**
|
|
21
23
|
* WHAT A LEAD'S CAMPAIGNS BECOME WHEN IT CONVERTS (AGL-3254).
|
|
22
24
|
*
|
|
@@ -48,6 +50,29 @@ import { FieldValue } from "firebase-admin/firestore";
|
|
|
48
50
|
[contactCampaignFieldPath(group.groupId)]: FieldValue.arrayUnion(...campaignIds),
|
|
49
51
|
updatedAt: FieldValue.serverTimestamp()
|
|
50
52
|
});
|
|
53
|
+
/*
|
|
54
|
+
* The carry on the contact's Activity (AGL-3274): one "Filed under"
|
|
55
|
+
* per campaign, by the conversion rather than a member, keyed once per
|
|
56
|
+
* lead and contact so a door that converts twice files once. After the
|
|
57
|
+
* membership has landed, and never failing the carry that is done.
|
|
58
|
+
*/ await fileCampaignFilingActivities(firestore, {
|
|
59
|
+
orgId: request.orgId,
|
|
60
|
+
hostId: request.hostId,
|
|
61
|
+
link: {
|
|
62
|
+
contactId: request.contactId
|
|
63
|
+
},
|
|
64
|
+
action: 'filed',
|
|
65
|
+
// Named where the containers answer; by id where a read fails, so a
|
|
66
|
+
// name lookup can never unmake a carry that has landed.
|
|
67
|
+
campaigns: await siteCampaignRefs(firestore, request.hostId, campaignIds).catch(()=>campaignIds.map((id)=>({
|
|
68
|
+
id,
|
|
69
|
+
name: id
|
|
70
|
+
}))),
|
|
71
|
+
atMs: Date.now(),
|
|
72
|
+
byUid: '',
|
|
73
|
+
byName: CAMPAIGN_FILING_CARRY_BY_NAME,
|
|
74
|
+
dedupeKey: `carry:${request.leadId}:${request.contactId}`
|
|
75
|
+
});
|
|
51
76
|
return {
|
|
52
77
|
campaigns: campaignIds.length
|
|
53
78
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/lead-campaign-carry.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n contactCampaignFieldPath,\n readCampaignIds,\n} from '@aglyn/aglyn/app-utils/campaign-membership'\nimport type {\n PluginLeadConversionReport,\n PluginLeadConversionRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { consentGroupForSite } from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/**\n * WHAT A LEAD'S CAMPAIGNS BECOME WHEN IT CONVERTS (AGL-3254).\n *\n * A lead is filed under a site's campaigns at the top of its own document;\n * a contact holds the same membership inside the site's consent-group\n * facet, because a contact is shared across the org and a campaign is one\n * site's. When the lead becomes a contact, the campaigns go with it — added\n * to what the contact already carries there with `arrayUnion`, never in\n * place of it — so the campaign's page goes on listing the person after\n * they became a contact, and a lead a sequence enrolled under a campaign\n * does not leave the campaign by converting.\n *\n * The CRM's own module, because a lead and a contact are the CRM's records\n * (docs/PACKAGES.md rule 3): it runs as the CRM's share of the platform's\n * lead-conversion seam, which every door that converts a lead — the\n * dialog, the REST API, a sign-up, a purchase — reaches through\n * `handOffLeadRecords`. Never throws: the conversion has happened, and a\n * membership that could not be carried is one the contact's page can pick\n * by hand.\n */\nexport async function carryLeadCampaignsToContact(\n firestore: FirebaseFirestore.Firestore,\n request: Pick<\n PluginLeadConversionRequest,\n 'orgId' | 'hostId' | 'leadId' | 'contactId'\n >,\n): Promise<PluginLeadConversionReport> {\n try {\n const lead = await firestore\n .collection('hosts')\n .doc(request.hostId)\n .collection('leads')\n .doc(request.leadId)\n .get()\n const campaignIds = readCampaignIds(\n lead.exists ? (lead.data() as Record<string, unknown>) : null,\n )\n if (!campaignIds.length) return { campaigns: 0 }\n const group = await consentGroupForSite(request.hostId)\n await firestore\n .collection('orgs')\n .doc(request.orgId)\n .collection('contacts')\n .doc(request.contactId)\n .update({\n [contactCampaignFieldPath(group.groupId)]: FieldValue.arrayUnion(\n ...campaignIds,\n ),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return { campaigns: campaignIds.length }\n } catch (error) {\n console.error(\n '[crm] the lead’s campaigns could not be carried to the contact',\n request.hostId,\n request.leadId,\n error,\n )\n return { campaigns: null }\n }\n}\n\n/**\n * The carry as the lead-conversion seam calls it: on the Admin SDK's own\n * Firestore. The SDK is resolved HERE, not in `declarations.server.ts`, for\n * two reasons that point the same way. That file must stay light — it runs\n * at the boot of every server process — so it defers this module and must\n * not import the data layer itself. And a library the plugin imports\n * statically in thirty files cannot also be lazy-loaded in one:\n * `enforce-module-boundaries` refuses every static import of a library the\n * project lazy-loads anywhere. This module is the deferred one; the SDK\n * comes with it, the way `capture-contact` brings its own.\n */\nexport function carryLeadCampaignsOnConversion(\n request: Pick<\n PluginLeadConversionRequest,\n 'orgId' | 'hostId' | 'leadId' | 'contactId'\n >,\n): Promise<PluginLeadConversionReport> {\n return carryLeadCampaignsToContact(firebaseAdmin.app().firestore(), request)\n}\n"],"names":["contactCampaignFieldPath","readCampaignIds","firebaseAdmin","consentGroupForSite","FieldValue","carryLeadCampaignsToContact","firestore","request","lead","collection","doc","hostId","leadId","get","campaignIds","exists","data","length","campaigns","group","orgId","contactId","update","groupId","arrayUnion","updatedAt","serverTimestamp","error","console","carryLeadCampaignsOnConversion","app"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,wBAAwB,EACxBC,eAAe,QACV,6CAA4C;AAKnD,SAASC,aAAa,QAAQ,iDAAgD;AAC9E,SAASC,mBAAmB,QAAQ,gDAA+C;AACnF,SAASC,UAAU,QAAQ,2BAA0B;
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/lead-campaign-carry.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n contactCampaignFieldPath,\n readCampaignIds,\n} from '@aglyn/aglyn/app-utils/campaign-membership'\nimport type {\n PluginLeadConversionReport,\n PluginLeadConversionRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { consentGroupForSite } from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { CAMPAIGN_FILING_CARRY_BY_NAME } from '../model/campaign-filing-activity'\nimport { fileCampaignFilingActivities, siteCampaignRefs } from './campaign-filing-activity'\n\n/**\n * WHAT A LEAD'S CAMPAIGNS BECOME WHEN IT CONVERTS (AGL-3254).\n *\n * A lead is filed under a site's campaigns at the top of its own document;\n * a contact holds the same membership inside the site's consent-group\n * facet, because a contact is shared across the org and a campaign is one\n * site's. When the lead becomes a contact, the campaigns go with it — added\n * to what the contact already carries there with `arrayUnion`, never in\n * place of it — so the campaign's page goes on listing the person after\n * they became a contact, and a lead a sequence enrolled under a campaign\n * does not leave the campaign by converting.\n *\n * The CRM's own module, because a lead and a contact are the CRM's records\n * (docs/PACKAGES.md rule 3): it runs as the CRM's share of the platform's\n * lead-conversion seam, which every door that converts a lead — the\n * dialog, the REST API, a sign-up, a purchase — reaches through\n * `handOffLeadRecords`. Never throws: the conversion has happened, and a\n * membership that could not be carried is one the contact's page can pick\n * by hand.\n */\nexport async function carryLeadCampaignsToContact(\n firestore: FirebaseFirestore.Firestore,\n request: Pick<\n PluginLeadConversionRequest,\n 'orgId' | 'hostId' | 'leadId' | 'contactId'\n >,\n): Promise<PluginLeadConversionReport> {\n try {\n const lead = await firestore\n .collection('hosts')\n .doc(request.hostId)\n .collection('leads')\n .doc(request.leadId)\n .get()\n const campaignIds = readCampaignIds(\n lead.exists ? (lead.data() as Record<string, unknown>) : null,\n )\n if (!campaignIds.length) return { campaigns: 0 }\n const group = await consentGroupForSite(request.hostId)\n await firestore\n .collection('orgs')\n .doc(request.orgId)\n .collection('contacts')\n .doc(request.contactId)\n .update({\n [contactCampaignFieldPath(group.groupId)]: FieldValue.arrayUnion(\n ...campaignIds,\n ),\n updatedAt: FieldValue.serverTimestamp(),\n })\n /*\n * The carry on the contact's Activity (AGL-3274): one \"Filed under\"\n * per campaign, by the conversion rather than a member, keyed once per\n * lead and contact so a door that converts twice files once. After the\n * membership has landed, and never failing the carry that is done.\n */\n await fileCampaignFilingActivities(firestore, {\n orgId: request.orgId,\n hostId: request.hostId,\n link: { contactId: request.contactId },\n action: 'filed',\n // Named where the containers answer; by id where a read fails, so a\n // name lookup can never unmake a carry that has landed.\n campaigns: await siteCampaignRefs(firestore, request.hostId, campaignIds).catch(() =>\n campaignIds.map((id) => ({ id, name: id })),\n ),\n atMs: Date.now(),\n byUid: '',\n byName: CAMPAIGN_FILING_CARRY_BY_NAME,\n dedupeKey: `carry:${request.leadId}:${request.contactId}`,\n })\n return { campaigns: campaignIds.length }\n } catch (error) {\n console.error(\n '[crm] the lead’s campaigns could not be carried to the contact',\n request.hostId,\n request.leadId,\n error,\n )\n return { campaigns: null }\n }\n}\n\n/**\n * The carry as the lead-conversion seam calls it: on the Admin SDK's own\n * Firestore. The SDK is resolved HERE, not in `declarations.server.ts`, for\n * two reasons that point the same way. That file must stay light — it runs\n * at the boot of every server process — so it defers this module and must\n * not import the data layer itself. And a library the plugin imports\n * statically in thirty files cannot also be lazy-loaded in one:\n * `enforce-module-boundaries` refuses every static import of a library the\n * project lazy-loads anywhere. This module is the deferred one; the SDK\n * comes with it, the way `capture-contact` brings its own.\n */\nexport function carryLeadCampaignsOnConversion(\n request: Pick<\n PluginLeadConversionRequest,\n 'orgId' | 'hostId' | 'leadId' | 'contactId'\n >,\n): Promise<PluginLeadConversionReport> {\n return carryLeadCampaignsToContact(firebaseAdmin.app().firestore(), request)\n}\n"],"names":["contactCampaignFieldPath","readCampaignIds","firebaseAdmin","consentGroupForSite","FieldValue","CAMPAIGN_FILING_CARRY_BY_NAME","fileCampaignFilingActivities","siteCampaignRefs","carryLeadCampaignsToContact","firestore","request","lead","collection","doc","hostId","leadId","get","campaignIds","exists","data","length","campaigns","group","orgId","contactId","update","groupId","arrayUnion","updatedAt","serverTimestamp","link","action","catch","map","id","name","atMs","Date","now","byUid","byName","dedupeKey","error","console","carryLeadCampaignsOnConversion","app"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,wBAAwB,EACxBC,eAAe,QACV,6CAA4C;AAKnD,SAASC,aAAa,QAAQ,iDAAgD;AAC9E,SAASC,mBAAmB,QAAQ,gDAA+C;AACnF,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,6BAA6B,QAAQ,uCAAmC;AACjF,SAASC,4BAA4B,EAAEC,gBAAgB,QAAQ,gCAA4B;AAE3F;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,eAAeC,4BACpBC,SAAsC,EACtCC,OAGC;IAED,IAAI;QACF,MAAMC,OAAO,MAAMF,UAChBG,UAAU,CAAC,SACXC,GAAG,CAACH,QAAQI,MAAM,EAClBF,UAAU,CAAC,SACXC,GAAG,CAACH,QAAQK,MAAM,EAClBC,GAAG;QACN,MAAMC,cAAchB,gBAClBU,KAAKO,MAAM,GAAIP,KAAKQ,IAAI,KAAiC;QAE3D,IAAI,CAACF,YAAYG,MAAM,EAAE,OAAO;YAAEC,WAAW;QAAE;QAC/C,MAAMC,QAAQ,MAAMnB,oBAAoBO,QAAQI,MAAM;QACtD,MAAML,UACHG,UAAU,CAAC,QACXC,GAAG,CAACH,QAAQa,KAAK,EACjBX,UAAU,CAAC,YACXC,GAAG,CAACH,QAAQc,SAAS,EACrBC,MAAM,CAAC;YACN,CAACzB,yBAAyBsB,MAAMI,OAAO,EAAE,EAAEtB,WAAWuB,UAAU,IAC3DV;YAELW,WAAWxB,WAAWyB,eAAe;QACvC;QACF;;;;;KAKC,GACD,MAAMvB,6BAA6BG,WAAW;YAC5Cc,OAAOb,QAAQa,KAAK;YACpBT,QAAQJ,QAAQI,MAAM;YACtBgB,MAAM;gBAAEN,WAAWd,QAAQc,SAAS;YAAC;YACrCO,QAAQ;YACR,oEAAoE;YACpE,wDAAwD;YACxDV,WAAW,MAAMd,iBAAiBE,WAAWC,QAAQI,MAAM,EAAEG,aAAae,KAAK,CAAC,IAC9Ef,YAAYgB,GAAG,CAAC,CAACC,KAAQ,CAAA;wBAAEA;wBAAIC,MAAMD;oBAAG,CAAA;YAE1CE,MAAMC,KAAKC,GAAG;YACdC,OAAO;YACPC,QAAQnC;YACRoC,WAAW,CAAC,MAAM,EAAE/B,QAAQK,MAAM,CAAC,CAAC,EAAEL,QAAQc,SAAS,EAAE;QAC3D;QACA,OAAO;YAAEH,WAAWJ,YAAYG,MAAM;QAAC;IACzC,EAAE,OAAOsB,OAAO;QACdC,QAAQD,KAAK,CACX,kEACAhC,QAAQI,MAAM,EACdJ,QAAQK,MAAM,EACd2B;QAEF,OAAO;YAAErB,WAAW;QAAK;IAC3B;AACF;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASuB,+BACdlC,OAGC;IAED,OAAOF,4BAA4BN,cAAc2C,GAAG,GAAGpC,SAAS,IAAIC;AACtE"}
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
* because a lead entered by hand is suite work: the capture doors that fill
|
|
46
46
|
* a Free workspace's leads do not come through here.
|
|
47
47
|
*/
|
|
48
|
-
import { type CrmLeadProfilePatch, type CrmLeadStatus, type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
48
|
+
import { type CrmCustomValue, type CrmLeadProfilePatch, type CrmLeadStatus, type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
49
|
+
import type { CampaignFilingRef } from '../model/campaign-filing-activity';
|
|
49
50
|
/** The surface a hand-entered lead names, beside `signup`, `booking`, `form:{id}` and `import`. */
|
|
50
51
|
export declare const LEAD_MANUAL_SOURCE = "manual";
|
|
51
52
|
/** What the New lead drawer posts. */
|
|
@@ -65,17 +66,27 @@ export interface LeadCreateRequest {
|
|
|
65
66
|
notes?: string;
|
|
66
67
|
/** The site's campaigns to file the lead under (AGL-3254), by container id. */
|
|
67
68
|
campaignIds?: string[];
|
|
69
|
+
/**
|
|
70
|
+
* The org's custom LEAD fields (AGL-3272), keyed by each definition's
|
|
71
|
+
* `key`. Judged against the definitions whose `object` is `lead`, so a
|
|
72
|
+
* contact field of the same name cannot be written through this door.
|
|
73
|
+
*/
|
|
74
|
+
custom?: Record<string, CrmCustomValue>;
|
|
68
75
|
}
|
|
69
76
|
/** The sentence a campaign that is not the site's is refused with, under the field. */
|
|
70
77
|
export declare const LEAD_CAMPAIGN_REFUSAL = "One of the campaigns picked isn't a campaign of this site any more. Pick it again.";
|
|
71
78
|
/**
|
|
72
|
-
* The
|
|
79
|
+
* The campaigns a request may file a lead under: the site's own live
|
|
73
80
|
* containers, and nothing else. A request can claim any id, and a lead
|
|
74
81
|
* filed under another site's campaign — or one the console soft-deleted —
|
|
75
|
-
* would sit on a page nobody at this site can open.
|
|
82
|
+
* would sit on a page nobody at this site can open. Answered with the
|
|
83
|
+
* names as they stand, for the timeline entries the filing writes
|
|
84
|
+
* (AGL-3274).
|
|
76
85
|
*
|
|
77
|
-
* @returns the clean ids, or `null` when one
|
|
86
|
+
* @returns the clean ids with their names, or `null` when one is not the site's.
|
|
78
87
|
*/
|
|
88
|
+
export declare function siteCampaigns(hostRef: FirebaseFirestore.DocumentReference, raw: unknown): Promise<CampaignFilingRef[] | null>;
|
|
89
|
+
/** The ids alone — see {@link siteCampaigns}. */
|
|
79
90
|
export declare function siteCampaignIds(hostRef: FirebaseFirestore.DocumentReference, raw: unknown): Promise<string[] | null>;
|
|
80
91
|
/** What the route answers on success. */
|
|
81
92
|
export interface LeadCreateResponse {
|