@aglyn/plugins-crm 1.0.0-beta.293 → 1.0.0-beta.296

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.
@@ -0,0 +1,75 @@
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 { useCallback, useEffect, useState } from "react";
18
+ /**
19
+ * Which way a sideways-scrolling box has more to show, and a way to page
20
+ * it — the facts a pair of scroll arrows needs, the way MUI's Tabs draws
21
+ * them. A scrollbar alone says nothing on a trackpad Mac, where it stays
22
+ * hidden until the reader already knows to scroll.
23
+ *
24
+ * Re-measured on scroll and whenever the box or its content changes size,
25
+ * so a column collapsing, a deal arriving or the window narrowing moves the
26
+ * arrows with it. A page is most of a view rather than all of it, so the
27
+ * column at the edge stays in sight as a landmark.
28
+ */ export function useScrollEdges() {
29
+ const [node, setNode] = useState(null);
30
+ const [edges, setEdges] = useState({
31
+ start: false,
32
+ end: false
33
+ });
34
+ useEffect(()=>{
35
+ if (!node) return;
36
+ const measure = ()=>{
37
+ const max = node.scrollWidth - node.clientWidth;
38
+ const start = node.scrollLeft > 1;
39
+ const end = node.scrollLeft < max - 1;
40
+ setEdges((previous)=>previous.start === start && previous.end === end ? previous : {
41
+ start,
42
+ end
43
+ });
44
+ };
45
+ measure();
46
+ node.addEventListener('scroll', measure, {
47
+ passive: true
48
+ });
49
+ const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(measure);
50
+ observer == null ? void 0 : observer.observe(node);
51
+ if (node.firstElementChild) observer == null ? void 0 : observer.observe(node.firstElementChild);
52
+ return ()=>{
53
+ node.removeEventListener('scroll', measure);
54
+ observer == null ? void 0 : observer.disconnect();
55
+ };
56
+ }, [
57
+ node
58
+ ]);
59
+ const page = useCallback((direction)=>{
60
+ node == null ? void 0 : node.scrollBy({
61
+ left: direction * node.clientWidth * 0.8,
62
+ behavior: 'smooth'
63
+ });
64
+ }, [
65
+ node
66
+ ]);
67
+ return {
68
+ ref: setNode,
69
+ start: edges.start,
70
+ end: edges.end,
71
+ page
72
+ };
73
+ }
74
+
75
+ //# sourceMappingURL=use-scroll-edges.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-scroll-edges.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\n'use client'\n\nimport { useCallback, useEffect, useState } from 'react'\n\nexport interface ScrollEdges {\n /** Callback ref for the box that scrolls sideways. */\n ref: (node: HTMLElement | null) => void\n /** Whether there is more to the left of what is in view. */\n start: boolean\n /** Whether there is more to the right of what is in view. */\n end: boolean\n /** Scrolls most of a view's width toward the start (`-1`) or the end (`1`). */\n page: (direction: -1 | 1) => void\n}\n\n/**\n * Which way a sideways-scrolling box has more to show, and a way to page\n * it — the facts a pair of scroll arrows needs, the way MUI's Tabs draws\n * them. A scrollbar alone says nothing on a trackpad Mac, where it stays\n * hidden until the reader already knows to scroll.\n *\n * Re-measured on scroll and whenever the box or its content changes size,\n * so a column collapsing, a deal arriving or the window narrowing moves the\n * arrows with it. A page is most of a view rather than all of it, so the\n * column at the edge stays in sight as a landmark.\n */\nexport function useScrollEdges(): ScrollEdges {\n const [node, setNode] = useState<HTMLElement | null>(null)\n const [edges, setEdges] = useState({ start: false, end: false })\n\n useEffect(() => {\n if (!node) return\n const measure = () => {\n const max = node.scrollWidth - node.clientWidth\n const start = node.scrollLeft > 1\n const end = node.scrollLeft < max - 1\n setEdges((previous) =>\n previous.start === start && previous.end === end ? previous : { start, end },\n )\n }\n measure()\n node.addEventListener('scroll', measure, { passive: true })\n const observer =\n typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(measure)\n observer?.observe(node)\n if (node.firstElementChild) observer?.observe(node.firstElementChild)\n return () => {\n node.removeEventListener('scroll', measure)\n observer?.disconnect()\n }\n }, [node])\n\n const page = useCallback(\n (direction: -1 | 1) => {\n node?.scrollBy({ left: direction * node.clientWidth * 0.8, behavior: 'smooth' })\n },\n [node],\n )\n\n return { ref: setNode, start: edges.start, end: edges.end, page }\n}\n"],"names":["useCallback","useEffect","useState","useScrollEdges","node","setNode","edges","setEdges","start","end","measure","max","scrollWidth","clientWidth","scrollLeft","previous","addEventListener","passive","observer","ResizeObserver","observe","firstElementChild","removeEventListener","disconnect","page","direction","scrollBy","left","behavior","ref"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;AAEA,SAASA,WAAW,EAAEC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAaxD;;;;;;;;;;CAUC,GACD,OAAO,SAASC;IACd,MAAM,CAACC,MAAMC,QAAQ,GAAGH,SAA6B;IACrD,MAAM,CAACI,OAAOC,SAAS,GAAGL,SAAS;QAAEM,OAAO;QAAOC,KAAK;IAAM;IAE9DR,UAAU;QACR,IAAI,CAACG,MAAM;QACX,MAAMM,UAAU;YACd,MAAMC,MAAMP,KAAKQ,WAAW,GAAGR,KAAKS,WAAW;YAC/C,MAAML,QAAQJ,KAAKU,UAAU,GAAG;YAChC,MAAML,MAAML,KAAKU,UAAU,GAAGH,MAAM;YACpCJ,SAAS,CAACQ,WACRA,SAASP,KAAK,KAAKA,SAASO,SAASN,GAAG,KAAKA,MAAMM,WAAW;oBAAEP;oBAAOC;gBAAI;QAE/E;QACAC;QACAN,KAAKY,gBAAgB,CAAC,UAAUN,SAAS;YAAEO,SAAS;QAAK;QACzD,MAAMC,WACJ,OAAOC,mBAAmB,cAAc,OAAO,IAAIA,eAAeT;QACpEQ,4BAAAA,SAAUE,OAAO,CAAChB;QAClB,IAAIA,KAAKiB,iBAAiB,EAAEH,4BAAAA,SAAUE,OAAO,CAAChB,KAAKiB,iBAAiB;QACpE,OAAO;YACLjB,KAAKkB,mBAAmB,CAAC,UAAUZ;YACnCQ,4BAAAA,SAAUK,UAAU;QACtB;IACF,GAAG;QAACnB;KAAK;IAET,MAAMoB,OAAOxB,YACX,CAACyB;QACCrB,wBAAAA,KAAMsB,QAAQ,CAAC;YAAEC,MAAMF,YAAYrB,KAAKS,WAAW,GAAG;YAAKe,UAAU;QAAS;IAChF,GACA;QAACxB;KAAK;IAGR,OAAO;QAAEyB,KAAKxB;QAASG,OAAOF,MAAME,KAAK;QAAEC,KAAKH,MAAMG,GAAG;QAAEe;IAAK;AAClE"}
@@ -25,17 +25,11 @@ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_wi
25
25
  * rather than what somebody later wished had been. It also has to hang off
26
26
  * something — a contact, a company or a deal — because an activity attached
27
27
  * to nothing is a sentence nobody can find from anywhere.
28
- */ import { CRM_COLLECTIONS, CRM_ACTIVITY_DIRECTIONS, CRM_ACTIVITY_LOG_FULL_MESSAGE, createResourceUid, crmActivityDirection, crmActivityLogHasRoom } from "@aglyn/aglyn/server";
28
+ */ import { CRM_COLLECTIONS, CRM_ACTIVITY_DIRECTIONS, CRM_ACTIVITY_KINDS, CRM_ACTIVITY_LOG_FULL_MESSAGE, createResourceUid, crmActivityDirection, crmActivityLogHasRoom } from "@aglyn/aglyn/server";
29
29
  import { apiJson, ApiErrors, countCrmActivitiesForRecord } from "@aglyn/tenant-data-admin";
30
30
  import { claimWrite, readJsonBody, requireScope } from "@aglyn/tenant-data-admin/server/api-v1-kit";
31
31
  import { CRM_LABEL_MAX, CRM_TEXT_MAX, CRM_TITLE_MAX, createPayload, crmCollection, crmCreateStamp, crmRefErrors, crmTimes, crmValidationFailed, isoFromMs, listCrm, memberError, parseIsoInstant, readChoice, readCrmSite, readEqualityFilters, readOptionalText, readRefId, refuseUnknownKeys } from "./crm-shared.js";
32
- const ACTIVITY_KINDS = [
33
- 'call',
34
- 'email',
35
- 'meeting',
36
- 'note',
37
- 'other'
38
- ];
32
+ /** The kinds the API accepts: the CRM's own list, so a kind the console logs is one the API takes. */ const ACTIVITY_KINDS = CRM_ACTIVITY_KINDS;
39
33
  /** The activity object as published. */ function activityView(doc) {
40
34
  var _doc_data, _data_kind, _data_body, _data_byUid, _data_contactId, _data_companyId, _data_dealId, _data_outcome, _data_hostId;
41
35
  const data = (_doc_data = doc.data()) != null ? _doc_data : {};
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/server/api-v1/crm-activities.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\n/**\n * `/v1/activities` (AGL-2606) — one thing that happened, logged by a person\n * or by an integration: a call made, a meeting held, a note taken.\n *\n * An activity is a LOG ENTRY, and a log is written once. There is no\n * `PATCH`: an entry that turns out to be wrong is deleted and logged again,\n * which keeps every row's `at` and `byUid` the record of what was logged\n * rather than what somebody later wished had been. It also has to hang off\n * something — a contact, a company or a deal — because an activity attached\n * to nothing is a sentence nobody can find from anywhere.\n */\nimport {\n CRM_COLLECTIONS,\n type CrmActivity,\n type CrmActivityKind,\n CRM_ACTIVITY_DIRECTIONS,\n CRM_ACTIVITY_LOG_FULL_MESSAGE,\n type CrmActivityDirection,\n createResourceUid,\n crmActivityDirection,\n crmActivityLogHasRoom,\n} from '@aglyn/aglyn/server'\nimport {\n apiJson,\n ApiErrors,\n countCrmActivitiesForRecord,\n} from '@aglyn/tenant-data-admin'\nimport {\n type ApiV1Context,\n claimWrite,\n readJsonBody,\n requireScope,\n} from '@aglyn/tenant-data-admin/server/api-v1-kit'\nimport {\n CRM_LABEL_MAX,\n CRM_TEXT_MAX,\n CRM_TITLE_MAX,\n createPayload,\n crmCollection,\n crmCreateStamp,\n crmRefErrors,\n crmTimes,\n crmValidationFailed,\n isoFromMs,\n listCrm,\n memberError,\n parseIsoInstant,\n readChoice,\n readCrmSite,\n readEqualityFilters,\n readOptionalText,\n readRefId,\n refuseUnknownKeys,\n} from './crm-shared'\n\nconst ACTIVITY_KINDS = ['call', 'email', 'meeting', 'note', 'other'] as const\n\n/** The activity object as published. */\nfunction activityView(doc: FirebaseFirestore.DocumentSnapshot) {\n const data = (doc.data() ?? {}) as Partial<CrmActivity>\n return {\n id: doc.id,\n object: 'activity',\n kind: data.kind ?? 'note',\n body: data.body ?? null,\n at: isoFromMs(data.atMs),\n byUid: data.byUid ?? null,\n contactId: data.contactId ?? null,\n companyId: data.companyId ?? null,\n dealId: data.dealId ?? null,\n outcome: data.outcome ?? null,\n durationMinutes:\n typeof data.durationMinutes === 'number' ? data.durationMinutes : null,\n direction: crmActivityDirection(data.kind, data.direction),\n siteId: data.hostId ?? null,\n ...crmTimes(data as FirebaseFirestore.DocumentData),\n }\n}\n\nconst ACTIVITY_WRITABLE = new Set([\n 'kind',\n 'body',\n 'at',\n 'byUid',\n 'contactId',\n 'companyId',\n 'dealId',\n 'outcome',\n 'durationMinutes',\n 'direction',\n 'consentSiteId',\n])\n\ninterface ActivityInput {\n kind?: CrmActivityKind\n body: string\n atMs?: number\n byUid?: string\n contactId?: string\n companyId?: string\n dealId?: string\n outcome?: string\n durationMinutes?: number\n direction?: CrmActivityDirection\n}\n\nfunction readActivityInput(\n body: Record<string, unknown>,\n): { values: ActivityInput } | { errors: Record<string, string> } {\n const errors: Record<string, string> = {}\n refuseUnknownKeys(body, ACTIVITY_WRITABLE, 'activity', errors)\n\n const text = String(body.body ?? '')\n .trim()\n .slice(0, CRM_TEXT_MAX)\n if (!text) errors.body = 'A body is required'\n const values: ActivityInput = { body: text }\n\n const kind = readChoice(body, 'kind', ACTIVITY_KINDS, errors)\n if (kind) values.kind = kind\n\n if (body.at !== undefined) {\n const ms = parseIsoInstant(body.at)\n if (ms === null) errors.at = 'Must be an ISO 8601 instant, like 2026-09-04T16:30:00Z'\n else values.atMs = ms\n }\n\n const byUid = readOptionalText(body, 'byUid', CRM_TITLE_MAX, errors)\n if (byUid) values.byUid = byUid\n for (const field of ['contactId', 'companyId', 'dealId'] as const) {\n const id = readRefId(body, field, errors)\n if (id) values[field] = id\n }\n if (!values.contactId && !values.companyId && !values.dealId && !errors.contactId) {\n errors.contactId = 'Name a contactId, companyId or dealId — an activity is logged against something'\n }\n\n const outcome = readOptionalText(body, 'outcome', CRM_LABEL_MAX, errors)\n if (outcome) values.outcome = outcome\n\n if (body.durationMinutes !== undefined && body.durationMinutes !== null) {\n if (\n typeof body.durationMinutes !== 'number' ||\n !Number.isInteger(body.durationMinutes) ||\n body.durationMinutes < 0\n ) {\n errors.durationMinutes = 'Must be a whole number of minutes, 0 or more'\n } else {\n values.durationMinutes = body.durationMinutes\n }\n }\n\n // Which way a call or an email went (AGL-3517); no other kind takes one.\n if (body.direction !== undefined && body.direction !== null) {\n const kind = values.kind ?? 'note'\n const direction = crmActivityDirection(kind, body.direction)\n const allowed = CRM_ACTIVITY_DIRECTIONS[kind] ?? []\n if (direction) values.direction = direction\n else {\n errors.direction = allowed.length\n ? `Must be one of: ${allowed.join(', ')}`\n : 'Only a call or an email takes a direction'\n }\n }\n\n return Object.keys(errors).length ? { errors } : { values }\n}\n\n/**\n * `POST /v1/activities`. `at` defaults to now and `byUid` to `'api'`: an\n * integration logging a call as it ends is the common case, and the one\n * logging it on behalf of a member names them.\n */\nasync function createActivity(request: Request, ctx: ApiV1Context): Promise<Response> {\n const body = await readJsonBody(request)\n const parsed = readActivityInput(body)\n if ('errors' in parsed) return crmValidationFailed(ctx, 'activity', parsed.errors)\n const site = readCrmSite(ctx, 'activity', body)\n if ('response' in site) return site.response\n const [by, refs] = await Promise.all([\n memberError(ctx, 'byUid', parsed.values.byUid),\n crmRefErrors(ctx, {\n contactId: parsed.values.contactId,\n companyId: parsed.values.companyId,\n dealId: parsed.values.dealId,\n }),\n ])\n const refErrors = { ...by, ...refs }\n if (Object.keys(refErrors).length) {\n return crmValidationFailed(ctx, 'activity', refErrors)\n }\n\n const collection = crmCollection(ctx, CRM_COLLECTIONS.activities)\n const claimed = await claimWrite(\n ctx,\n '*',\n request.headers.get('Idempotency-Key'),\n 'activities',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const { kind, atMs, byUid, ...rest } = parsed.values\n // The per-record ceiling (AGL-2611): one aggregate on the record the\n // activity is filed under. A platform bound rather than a plan one, so\n // it is a conflict with the record's state, not a plan refusal.\n const logged = await countCrmActivitiesForRecord(\n ctx.firestore.collection('orgs').doc(ctx.orgId),\n parsed.values,\n )\n if (!crmActivityLogHasRoom(logged)) {\n await claim.release()\n return ApiErrors.conflict({\n message: CRM_ACTIVITY_LOG_FULL_MESSAGE,\n code: 'activity_log_full',\n headers: ctx.headers,\n })\n }\n const id = createResourceUid()\n const stamp = crmCreateStamp(ctx, site.siteId)\n await collection.doc(id).create({\n kind: kind ?? 'note',\n // The instant the record was created, from the same clock as its\n // `createdAt`, so an activity logged as it happened reads as one moment.\n atMs: atMs ?? stamp.createdAt.toMillis(),\n byUid: byUid ?? 'api',\n ...createPayload(rest),\n ...stamp,\n })\n const view = activityView(await collection.doc(id).get())\n await claim.record(200, view)\n return apiJson(view, { status: 201, headers: ctx.headers })\n } catch (error) {\n await claim.release()\n throw error\n }\n}\n\n/** `DELETE /v1/activities/{id}`. */\nasync function deleteActivity(\n request: Request,\n ctx: ApiV1Context,\n ref: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const claimed = await claimWrite(\n ctx,\n '*',\n request.headers.get('Idempotency-Key'),\n 'activity-deletes',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n try {\n const snap = await ref.get()\n if (!snap.exists) {\n await claim.release()\n return ApiErrors.notFound({ message: 'No such activity', headers: ctx.headers })\n }\n await ref.delete()\n const view = { id: ref.id, object: 'activity', deleted: true }\n await claim.record(200, view)\n return apiJson(view, { headers: ctx.headers })\n } catch (error) {\n await claim.release()\n throw error\n }\n}\n\n/** `GET /v1/activities` filters, most selective first. */\nasync function listActivities(\n ctx: ApiV1Context,\n collection: FirebaseFirestore.CollectionReference,\n url: URL,\n): Promise<Response> {\n const rawKind = url.searchParams.get('kind')\n if (rawKind !== null && rawKind.trim() !== '' && !(ACTIVITY_KINDS as readonly string[]).includes(rawKind.trim())) {\n return crmValidationFailed(ctx, 'activity filter', {\n kind: `Must be one of: ${ACTIVITY_KINDS.join(', ')}`,\n })\n }\n const filters = readEqualityFilters(url, ['dealId', 'contactId', 'companyId', 'kind'])\n return listCrm(ctx, collection, url, filters, activityView)\n}\n\nexport async function handleActivities(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const collection = crmCollection(ctx, CRM_COLLECTIONS.activities)\n const [, activityId] = segments\n\n if (!activityId) {\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'crm:read')\n if (denied) return denied\n return listActivities(ctx, collection, url)\n }\n if (request.method === 'POST') {\n const denied = requireScope(ctx, 'crm:write')\n if (denied) return denied\n return createActivity(request, ctx)\n }\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, POST' },\n })\n }\n\n const ref = collection.doc(activityId)\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'crm:read')\n if (denied) return denied\n const snap = await ref.get()\n if (!snap.exists) {\n return ApiErrors.notFound({ message: 'No such activity', headers: ctx.headers })\n }\n return apiJson(activityView(snap), { headers: ctx.headers })\n }\n if (request.method === 'DELETE') {\n const denied = requireScope(ctx, 'crm:write')\n if (denied) return denied\n return deleteActivity(request, ctx, ref)\n }\n // No PATCH — see the module header. The `Allow` header says so rather\n // than a 404 that reads as \"no such activity\".\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, DELETE' },\n })\n}\n"],"names":["CRM_COLLECTIONS","CRM_ACTIVITY_DIRECTIONS","CRM_ACTIVITY_LOG_FULL_MESSAGE","createResourceUid","crmActivityDirection","crmActivityLogHasRoom","apiJson","ApiErrors","countCrmActivitiesForRecord","claimWrite","readJsonBody","requireScope","CRM_LABEL_MAX","CRM_TEXT_MAX","CRM_TITLE_MAX","createPayload","crmCollection","crmCreateStamp","crmRefErrors","crmTimes","crmValidationFailed","isoFromMs","listCrm","memberError","parseIsoInstant","readChoice","readCrmSite","readEqualityFilters","readOptionalText","readRefId","refuseUnknownKeys","ACTIVITY_KINDS","activityView","doc","data","id","object","kind","body","at","atMs","byUid","contactId","companyId","dealId","outcome","durationMinutes","direction","siteId","hostId","ACTIVITY_WRITABLE","Set","readActivityInput","errors","text","String","trim","slice","values","undefined","ms","field","Number","isInteger","allowed","length","join","Object","keys","createActivity","request","ctx","parsed","site","response","by","refs","Promise","all","refErrors","collection","activities","claimed","headers","get","replay","claim","rest","logged","firestore","orgId","release","conflict","message","code","stamp","create","createdAt","toMillis","view","record","status","error","deleteActivity","ref","snap","exists","notFound","delete","deleted","listActivities","url","rawKind","searchParams","includes","filters","handleActivities","segments","activityId","method","denied","methodNotAllowed","Allow"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/server/api-v1/crm-activities.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\n/**\n * `/v1/activities` (AGL-2606) — one thing that happened, logged by a person\n * or by an integration: a call made, a meeting held, a note taken.\n *\n * An activity is a LOG ENTRY, and a log is written once. There is no\n * `PATCH`: an entry that turns out to be wrong is deleted and logged again,\n * which keeps every row's `at` and `byUid` the record of what was logged\n * rather than what somebody later wished had been. It also has to hang off\n * something — a contact, a company or a deal — because an activity attached\n * to nothing is a sentence nobody can find from anywhere.\n */\nimport {\n CRM_COLLECTIONS,\n type CrmActivity,\n type CrmActivityKind,\n CRM_ACTIVITY_DIRECTIONS,\n CRM_ACTIVITY_KINDS,\n CRM_ACTIVITY_LOG_FULL_MESSAGE,\n type CrmActivityDirection,\n createResourceUid,\n crmActivityDirection,\n crmActivityLogHasRoom,\n} from '@aglyn/aglyn/server'\nimport {\n apiJson,\n ApiErrors,\n countCrmActivitiesForRecord,\n} from '@aglyn/tenant-data-admin'\nimport {\n type ApiV1Context,\n claimWrite,\n readJsonBody,\n requireScope,\n} from '@aglyn/tenant-data-admin/server/api-v1-kit'\nimport {\n CRM_LABEL_MAX,\n CRM_TEXT_MAX,\n CRM_TITLE_MAX,\n createPayload,\n crmCollection,\n crmCreateStamp,\n crmRefErrors,\n crmTimes,\n crmValidationFailed,\n isoFromMs,\n listCrm,\n memberError,\n parseIsoInstant,\n readChoice,\n readCrmSite,\n readEqualityFilters,\n readOptionalText,\n readRefId,\n refuseUnknownKeys,\n} from './crm-shared'\n\n/** The kinds the API accepts: the CRM's own list, so a kind the console logs is one the API takes. */\nconst ACTIVITY_KINDS = CRM_ACTIVITY_KINDS\n\n/** The activity object as published. */\nfunction activityView(doc: FirebaseFirestore.DocumentSnapshot) {\n const data = (doc.data() ?? {}) as Partial<CrmActivity>\n return {\n id: doc.id,\n object: 'activity',\n kind: data.kind ?? 'note',\n body: data.body ?? null,\n at: isoFromMs(data.atMs),\n byUid: data.byUid ?? null,\n contactId: data.contactId ?? null,\n companyId: data.companyId ?? null,\n dealId: data.dealId ?? null,\n outcome: data.outcome ?? null,\n durationMinutes:\n typeof data.durationMinutes === 'number' ? data.durationMinutes : null,\n direction: crmActivityDirection(data.kind, data.direction),\n siteId: data.hostId ?? null,\n ...crmTimes(data as FirebaseFirestore.DocumentData),\n }\n}\n\nconst ACTIVITY_WRITABLE = new Set([\n 'kind',\n 'body',\n 'at',\n 'byUid',\n 'contactId',\n 'companyId',\n 'dealId',\n 'outcome',\n 'durationMinutes',\n 'direction',\n 'consentSiteId',\n])\n\ninterface ActivityInput {\n kind?: CrmActivityKind\n body: string\n atMs?: number\n byUid?: string\n contactId?: string\n companyId?: string\n dealId?: string\n outcome?: string\n durationMinutes?: number\n direction?: CrmActivityDirection\n}\n\nfunction readActivityInput(\n body: Record<string, unknown>,\n): { values: ActivityInput } | { errors: Record<string, string> } {\n const errors: Record<string, string> = {}\n refuseUnknownKeys(body, ACTIVITY_WRITABLE, 'activity', errors)\n\n const text = String(body.body ?? '')\n .trim()\n .slice(0, CRM_TEXT_MAX)\n if (!text) errors.body = 'A body is required'\n const values: ActivityInput = { body: text }\n\n const kind = readChoice(body, 'kind', ACTIVITY_KINDS, errors)\n if (kind) values.kind = kind\n\n if (body.at !== undefined) {\n const ms = parseIsoInstant(body.at)\n if (ms === null) errors.at = 'Must be an ISO 8601 instant, like 2026-09-04T16:30:00Z'\n else values.atMs = ms\n }\n\n const byUid = readOptionalText(body, 'byUid', CRM_TITLE_MAX, errors)\n if (byUid) values.byUid = byUid\n for (const field of ['contactId', 'companyId', 'dealId'] as const) {\n const id = readRefId(body, field, errors)\n if (id) values[field] = id\n }\n if (!values.contactId && !values.companyId && !values.dealId && !errors.contactId) {\n errors.contactId = 'Name a contactId, companyId or dealId — an activity is logged against something'\n }\n\n const outcome = readOptionalText(body, 'outcome', CRM_LABEL_MAX, errors)\n if (outcome) values.outcome = outcome\n\n if (body.durationMinutes !== undefined && body.durationMinutes !== null) {\n if (\n typeof body.durationMinutes !== 'number' ||\n !Number.isInteger(body.durationMinutes) ||\n body.durationMinutes < 0\n ) {\n errors.durationMinutes = 'Must be a whole number of minutes, 0 or more'\n } else {\n values.durationMinutes = body.durationMinutes\n }\n }\n\n // Which way a call or an email went (AGL-3517); no other kind takes one.\n if (body.direction !== undefined && body.direction !== null) {\n const kind = values.kind ?? 'note'\n const direction = crmActivityDirection(kind, body.direction)\n const allowed = CRM_ACTIVITY_DIRECTIONS[kind] ?? []\n if (direction) values.direction = direction\n else {\n errors.direction = allowed.length\n ? `Must be one of: ${allowed.join(', ')}`\n : 'Only a call or an email takes a direction'\n }\n }\n\n return Object.keys(errors).length ? { errors } : { values }\n}\n\n/**\n * `POST /v1/activities`. `at` defaults to now and `byUid` to `'api'`: an\n * integration logging a call as it ends is the common case, and the one\n * logging it on behalf of a member names them.\n */\nasync function createActivity(request: Request, ctx: ApiV1Context): Promise<Response> {\n const body = await readJsonBody(request)\n const parsed = readActivityInput(body)\n if ('errors' in parsed) return crmValidationFailed(ctx, 'activity', parsed.errors)\n const site = readCrmSite(ctx, 'activity', body)\n if ('response' in site) return site.response\n const [by, refs] = await Promise.all([\n memberError(ctx, 'byUid', parsed.values.byUid),\n crmRefErrors(ctx, {\n contactId: parsed.values.contactId,\n companyId: parsed.values.companyId,\n dealId: parsed.values.dealId,\n }),\n ])\n const refErrors = { ...by, ...refs }\n if (Object.keys(refErrors).length) {\n return crmValidationFailed(ctx, 'activity', refErrors)\n }\n\n const collection = crmCollection(ctx, CRM_COLLECTIONS.activities)\n const claimed = await claimWrite(\n ctx,\n '*',\n request.headers.get('Idempotency-Key'),\n 'activities',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const { kind, atMs, byUid, ...rest } = parsed.values\n // The per-record ceiling (AGL-2611): one aggregate on the record the\n // activity is filed under. A platform bound rather than a plan one, so\n // it is a conflict with the record's state, not a plan refusal.\n const logged = await countCrmActivitiesForRecord(\n ctx.firestore.collection('orgs').doc(ctx.orgId),\n parsed.values,\n )\n if (!crmActivityLogHasRoom(logged)) {\n await claim.release()\n return ApiErrors.conflict({\n message: CRM_ACTIVITY_LOG_FULL_MESSAGE,\n code: 'activity_log_full',\n headers: ctx.headers,\n })\n }\n const id = createResourceUid()\n const stamp = crmCreateStamp(ctx, site.siteId)\n await collection.doc(id).create({\n kind: kind ?? 'note',\n // The instant the record was created, from the same clock as its\n // `createdAt`, so an activity logged as it happened reads as one moment.\n atMs: atMs ?? stamp.createdAt.toMillis(),\n byUid: byUid ?? 'api',\n ...createPayload(rest),\n ...stamp,\n })\n const view = activityView(await collection.doc(id).get())\n await claim.record(200, view)\n return apiJson(view, { status: 201, headers: ctx.headers })\n } catch (error) {\n await claim.release()\n throw error\n }\n}\n\n/** `DELETE /v1/activities/{id}`. */\nasync function deleteActivity(\n request: Request,\n ctx: ApiV1Context,\n ref: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const claimed = await claimWrite(\n ctx,\n '*',\n request.headers.get('Idempotency-Key'),\n 'activity-deletes',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n try {\n const snap = await ref.get()\n if (!snap.exists) {\n await claim.release()\n return ApiErrors.notFound({ message: 'No such activity', headers: ctx.headers })\n }\n await ref.delete()\n const view = { id: ref.id, object: 'activity', deleted: true }\n await claim.record(200, view)\n return apiJson(view, { headers: ctx.headers })\n } catch (error) {\n await claim.release()\n throw error\n }\n}\n\n/** `GET /v1/activities` filters, most selective first. */\nasync function listActivities(\n ctx: ApiV1Context,\n collection: FirebaseFirestore.CollectionReference,\n url: URL,\n): Promise<Response> {\n const rawKind = url.searchParams.get('kind')\n if (rawKind !== null && rawKind.trim() !== '' && !(ACTIVITY_KINDS as readonly string[]).includes(rawKind.trim())) {\n return crmValidationFailed(ctx, 'activity filter', {\n kind: `Must be one of: ${ACTIVITY_KINDS.join(', ')}`,\n })\n }\n const filters = readEqualityFilters(url, ['dealId', 'contactId', 'companyId', 'kind'])\n return listCrm(ctx, collection, url, filters, activityView)\n}\n\nexport async function handleActivities(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const collection = crmCollection(ctx, CRM_COLLECTIONS.activities)\n const [, activityId] = segments\n\n if (!activityId) {\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'crm:read')\n if (denied) return denied\n return listActivities(ctx, collection, url)\n }\n if (request.method === 'POST') {\n const denied = requireScope(ctx, 'crm:write')\n if (denied) return denied\n return createActivity(request, ctx)\n }\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, POST' },\n })\n }\n\n const ref = collection.doc(activityId)\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'crm:read')\n if (denied) return denied\n const snap = await ref.get()\n if (!snap.exists) {\n return ApiErrors.notFound({ message: 'No such activity', headers: ctx.headers })\n }\n return apiJson(activityView(snap), { headers: ctx.headers })\n }\n if (request.method === 'DELETE') {\n const denied = requireScope(ctx, 'crm:write')\n if (denied) return denied\n return deleteActivity(request, ctx, ref)\n }\n // No PATCH — see the module header. The `Allow` header says so rather\n // than a 404 that reads as \"no such activity\".\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, DELETE' },\n })\n}\n"],"names":["CRM_COLLECTIONS","CRM_ACTIVITY_DIRECTIONS","CRM_ACTIVITY_KINDS","CRM_ACTIVITY_LOG_FULL_MESSAGE","createResourceUid","crmActivityDirection","crmActivityLogHasRoom","apiJson","ApiErrors","countCrmActivitiesForRecord","claimWrite","readJsonBody","requireScope","CRM_LABEL_MAX","CRM_TEXT_MAX","CRM_TITLE_MAX","createPayload","crmCollection","crmCreateStamp","crmRefErrors","crmTimes","crmValidationFailed","isoFromMs","listCrm","memberError","parseIsoInstant","readChoice","readCrmSite","readEqualityFilters","readOptionalText","readRefId","refuseUnknownKeys","ACTIVITY_KINDS","activityView","doc","data","id","object","kind","body","at","atMs","byUid","contactId","companyId","dealId","outcome","durationMinutes","direction","siteId","hostId","ACTIVITY_WRITABLE","Set","readActivityInput","errors","text","String","trim","slice","values","undefined","ms","field","Number","isInteger","allowed","length","join","Object","keys","createActivity","request","ctx","parsed","site","response","by","refs","Promise","all","refErrors","collection","activities","claimed","headers","get","replay","claim","rest","logged","firestore","orgId","release","conflict","message","code","stamp","create","createdAt","toMillis","view","record","status","error","deleteActivity","ref","snap","exists","notFound","delete","deleted","listActivities","url","rawKind","searchParams","includes","filters","handleActivities","segments","activityId","method","denied","methodNotAllowed","Allow"],"mappings":"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"}