@aglyn/plugins-crm 1.0.0-beta.217 → 1.0.0-beta.219
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 +14 -13
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/components/companies-bulk-bar.js +12 -4
- package/src/lib/components/companies-bulk-bar.js.map +1 -1
- package/src/lib/components/company-detail-page.js +11 -0
- package/src/lib/components/company-detail-page.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +12 -2
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-import-drawer.d.ts +1 -1
- package/src/lib/components/company-picker.js +10 -3
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/contact-detail-page.d.ts +4 -4
- package/src/lib/components/contact-detail-page.js +50 -53
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -0
- package/src/lib/components/contact-list-columns.js +21 -5
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contact-merge-dialog.js +2 -1
- package/src/lib/components/contact-merge-dialog.js.map +1 -1
- package/src/lib/components/contact-timeline-card.js +4 -3
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
- package/src/lib/components/contacts-bulk-bar.js +24 -51
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +9 -3
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-export-all-button.d.ts +1 -1
- package/src/lib/components/crm-export-all-button.js.map +1 -1
- package/src/lib/components/crm-send-email-dialog.js +9 -6
- package/src/lib/components/crm-send-email-dialog.js.map +1 -1
- package/src/lib/components/csv-import-drawer.js +2 -2
- package/src/lib/components/csv-import-drawer.js.map +1 -1
- package/src/lib/components/deal-detail-page.js +18 -2
- package/src/lib/components/deal-detail-page.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +10 -0
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deals-bulk-bar.js +13 -5
- package/src/lib/components/deals-bulk-bar.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.js +6 -0
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-detail-page.js +25 -5
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +6 -0
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +4 -5
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.js +6 -0
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +28 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +36 -7
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/org-lead-surfaces-note.js +6 -2
- package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
- package/src/lib/components/recipes-card.js +9 -4
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/record-sharing-card.d.ts +57 -0
- package/src/lib/components/record-sharing-card.js +244 -0
- package/src/lib/components/record-sharing-card.js.map +1 -0
- package/src/lib/components/settings-section.js +7 -0
- package/src/lib/components/settings-section.js.map +1 -1
- package/src/lib/components/share-records-dialog.d.ts +28 -0
- package/src/lib/components/share-records-dialog.js +184 -0
- package/src/lib/components/share-records-dialog.js.map +1 -0
- package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
- package/src/lib/components/sharing-rule-drawer.js +374 -0
- package/src/lib/components/sharing-rule-drawer.js.map +1 -0
- package/src/lib/components/sharing-rules-card.d.ts +24 -0
- package/src/lib/components/sharing-rules-card.js +287 -0
- package/src/lib/components/sharing-rules-card.js.map +1 -0
- package/src/lib/components/use-crm-api.d.ts +1 -1
- package/src/lib/components/use-crm-api.js.map +1 -1
- package/src/lib/components/use-site-route-context.d.ts +25 -0
- package/src/lib/components/use-site-route-context.js +58 -0
- package/src/lib/components/use-site-route-context.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +31 -0
- package/src/lib/constants/api-routes.js +27 -1
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +37 -0
- package/src/lib/declarations.console-server.js +103 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/declarations.server.d.ts +6 -0
- package/src/lib/declarations.server.js +33 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-contact-remove.d.ts +35 -0
- package/src/lib/hooks/use-contact-remove.js +77 -0
- package/src/lib/hooks/use-contact-remove.js.map +1 -0
- package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
- package/src/lib/hooks/use-crm-sharing.js +109 -0
- package/src/lib/hooks/use-crm-sharing.js.map +1 -0
- package/src/lib/model/companies-csv.d.ts +1 -1
- package/src/lib/model/companies-csv.js +2 -2
- package/src/lib/model/companies-csv.js.map +1 -1
- package/src/lib/model/contact-holder.d.ts +68 -0
- package/src/lib/model/contact-holder.js +129 -0
- package/src/lib/model/contact-holder.js.map +1 -0
- package/src/lib/model/contact-interaction-links.d.ts +46 -0
- package/src/lib/model/contact-interaction-links.js +65 -0
- package/src/lib/model/contact-interaction-links.js.map +1 -0
- package/src/lib/model/contact-merge.d.ts +136 -0
- package/src/lib/model/contact-merge.js +412 -0
- package/src/lib/model/contact-merge.js.map +1 -0
- package/src/lib/model/contact-record.d.ts +7 -1
- package/src/lib/model/contact-record.js +6 -5
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-remove.d.ts +48 -0
- package/src/lib/model/contact-remove.js +22 -0
- package/src/lib/model/contact-remove.js.map +1 -0
- package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
- package/src/lib/model/contacts-bulk-writes.js +2 -82
- package/src/lib/model/contacts-bulk-writes.js.map +1 -1
- package/src/lib/model/contacts-csv.d.ts +2 -2
- package/src/lib/model/contacts-csv.js +2 -2
- package/src/lib/model/contacts-csv.js.map +1 -1
- package/src/lib/model/crm-csv.d.ts +239 -0
- package/src/lib/model/crm-csv.js +467 -0
- package/src/lib/model/crm-csv.js.map +1 -0
- package/src/lib/model/crm-digest.d.ts +185 -0
- package/src/lib/model/crm-digest.js +272 -0
- package/src/lib/model/crm-digest.js.map +1 -0
- package/src/lib/model/crm-import.d.ts +7 -1
- package/src/lib/model/crm-import.js +2 -1
- package/src/lib/model/crm-import.js.map +1 -1
- package/src/lib/model/crm-record-routes.d.ts +5 -0
- package/src/lib/model/crm-record-routes.js +7 -0
- package/src/lib/model/crm-record-routes.js.map +1 -1
- package/src/lib/model/crm-routes.d.ts +6 -5
- package/src/lib/model/crm-routes.js +7 -7
- package/src/lib/model/crm-routes.js.map +1 -1
- package/src/lib/model/crm-search-sources.d.ts +15 -0
- package/src/lib/model/crm-search-sources.js +162 -0
- package/src/lib/model/crm-search-sources.js.map +1 -0
- package/src/lib/model/crm-sharing-api.d.ts +32 -0
- package/src/lib/model/crm-sharing-api.js +59 -0
- package/src/lib/model/crm-sharing-api.js.map +1 -0
- package/src/lib/model/crm-sharing.d.ts +333 -0
- package/src/lib/model/crm-sharing.js +603 -0
- package/src/lib/model/crm-sharing.js.map +1 -0
- package/src/lib/model/deals-csv.d.ts +1 -1
- package/src/lib/model/deals-csv.js +4 -5
- package/src/lib/model/deals-csv.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +1 -1
- package/src/lib/model/leads-csv.d.ts +1 -1
- package/src/lib/model/leads-csv.js +1 -1
- package/src/lib/model/leads-csv.js.map +1 -1
- package/src/lib/model/tasks-csv.d.ts +1 -1
- package/src/lib/model/tasks-csv.js +1 -1
- package/src/lib/model/tasks-csv.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +75 -0
- package/src/lib/plan-entitlements.js +56 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +3 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
- package/src/lib/server/api-v1/contacts-merge.js +120 -0
- package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
- package/src/lib/server/api-v1/contacts.d.ts +18 -0
- package/src/lib/server/api-v1/contacts.js +1076 -0
- package/src/lib/server/api-v1/contacts.js.map +1 -0
- package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
- package/src/lib/server/api-v1/crm-activities.js +269 -0
- package/src/lib/server/api-v1/crm-activities.js.map +1 -0
- package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
- package/src/lib/server/api-v1/crm-companies.js +391 -0
- package/src/lib/server/api-v1/crm-companies.js.map +1 -0
- package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
- package/src/lib/server/api-v1/crm-deals.js +631 -0
- package/src/lib/server/api-v1/crm-deals.js.map +1 -0
- package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
- package/src/lib/server/api-v1/crm-email-templates.js +336 -0
- package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
- package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
- package/src/lib/server/api-v1/crm-leads.js +832 -0
- package/src/lib/server/api-v1/crm-leads.js.map +1 -0
- package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
- package/src/lib/server/api-v1/crm-pipelines.js +146 -0
- package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
- package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
- package/src/lib/server/api-v1/crm-shared.js +436 -0
- package/src/lib/server/api-v1/crm-shared.js.map +1 -0
- package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
- package/src/lib/server/api-v1/crm-tasks.js +398 -0
- package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
- package/src/lib/server/api-v1/openapi.d.ts +29 -0
- package/src/lib/server/api-v1/openapi.js +985 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/api-v1/usage.d.ts +26 -0
- package/src/lib/server/api-v1/usage.js +73 -0
- package/src/lib/server/api-v1/usage.js.map +1 -0
- package/src/lib/server/consent-group-participant.js +1 -1
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
- package/src/lib/server/contact-lifecycle-floor.js +91 -0
- package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
- package/src/lib/server/contact-remove.d.ts +75 -0
- package/src/lib/server/contact-remove.js +244 -0
- package/src/lib/server/contact-remove.js.map +1 -0
- package/src/lib/server/contact-update.js +2 -1
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.d.ts +47 -11
- package/src/lib/server/contacts-import.js +171 -71
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/contacts-merge.d.ts +1 -1
- package/src/lib/server/contacts-merge.js +3 -2
- package/src/lib/server/contacts-merge.js.map +1 -1
- package/src/lib/server/convert-host-lead.d.ts +189 -0
- package/src/lib/server/convert-host-lead.js +518 -0
- package/src/lib/server/convert-host-lead.js.map +1 -0
- package/src/lib/server/crm-sharing.d.ts +108 -0
- package/src/lib/server/crm-sharing.js +790 -0
- package/src/lib/server/crm-sharing.js.map +1 -0
- package/src/lib/server/daily-digest-route.d.ts +107 -0
- package/src/lib/server/daily-digest-route.js +588 -0
- package/src/lib/server/daily-digest-route.js.map +1 -0
- package/src/lib/server/export-route.d.ts +60 -0
- package/src/lib/server/export-route.js +447 -0
- package/src/lib/server/export-route.js.map +1 -0
- package/src/lib/server/inbound-route.d.ts +74 -0
- package/src/lib/server/inbound-route.js +257 -0
- package/src/lib/server/inbound-route.js.map +1 -0
- package/src/lib/server/lead-convert.d.ts +1 -1
- package/src/lib/server/lead-convert.js +2 -2
- package/src/lib/server/lead-convert.js.map +1 -1
- package/src/lib/server/merge-contacts.d.ts +55 -0
- package/src/lib/server/merge-contacts.js +274 -0
- package/src/lib/server/merge-contacts.js.map +1 -0
- package/src/lib/server/pipeline-record-index.d.ts +36 -0
- package/src/lib/server/pipeline-record-index.js +87 -0
- package/src/lib/server/pipeline-record-index.js.map +1 -0
- package/src/lib/server/record-facts.js +2 -1
- package/src/lib/server/record-facts.js.map +1 -1
- package/src/lib/server/task-reminders-route.d.ts +82 -0
- package/src/lib/server/task-reminders-route.js +497 -0
- package/src/lib/server/task-reminders-route.js.map +1 -0
- package/src/lib/server-deal-stage.js +2 -1
- package/src/lib/server-deal-stage.js.map +1 -1
- package/src/lib/server.d.ts +5 -2
- package/src/lib/server.js +47 -2
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
- package/src/lib/testing/stand-in-site-record-routes.js +55 -0
- package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
- package/src/lib/usage-axes.d.ts +35 -0
- package/src/lib/usage-axes.js +68 -0
- package/src/lib/usage-axes.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +0 -24
- package/src/lib/components/use-emails-hub-path.js +0 -59
- package/src/lib/components/use-emails-hub-path.js.map +0 -1
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright 2026 Aglyn LLC
|
|
6
|
+
*
|
|
7
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
* you may not use this file except in compliance with the License.
|
|
9
|
+
* You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
* See the License for the specific language governing permissions and
|
|
17
|
+
* limitations under the License.
|
|
18
|
+
*/ /**
|
|
19
|
+
* `/v1/tasks` (AGL-2606) — what the team owes a person: a call, an email, a
|
|
20
|
+
* meeting or a to-do, with a due date, pointing at the contact, company or
|
|
21
|
+
* deal it is for. A task may point at nothing — a plain to-do is still a
|
|
22
|
+
* task — which is the one place it differs from an activity.
|
|
23
|
+
*
|
|
24
|
+
* `status` is the only state machine here and it has two states. Marking a
|
|
25
|
+
* task `done` stamps `completedAtMs`; marking it `open` again clears it, so
|
|
26
|
+
* a reopened task never reads as completed on the date it was first closed.
|
|
27
|
+
*
|
|
28
|
+
* `remindAt` (AGL-2659) is the task's reminder: the due time unless the
|
|
29
|
+
* body names a time or `null`, carried along when a moved due date leaves
|
|
30
|
+
* it behind, and cleared by completion while it is still owed — the rule
|
|
31
|
+
* in `crmTaskReminderAfterEdit`, the same one the console's drawer keeps.
|
|
32
|
+
* `reminderSentAt` is read-only: when the hourly runner handled it.
|
|
33
|
+
*/ import { CRM_COLLECTIONS, crmTaskReminderAfterEdit, crmTaskReminderPending, createResourceUid, crmTaskListFields } from "@aglyn/aglyn/server";
|
|
34
|
+
import { apiJson, ApiErrors, crmNextActivityLinksOf, recomputeCrmNextTaskAt, restampCrmListFieldsAt } from "@aglyn/tenant-data-admin";
|
|
35
|
+
import { FieldValue, Timestamp } from "firebase-admin/firestore";
|
|
36
|
+
import { claimWrite, readJsonBody, requireScope } from "@aglyn/tenant-data-admin/server/api-v1-kit";
|
|
37
|
+
import { CRM_TEXT_MAX, CRM_TITLE_MAX, createPayload, crmCollection, crmCreateStamp, crmRefErrors, crmTimes, crmValidationFailed, isoFromMs, listCrm, memberError, parseIsoInstant, readChoice, readCrmSite, readEqualityFilters, readOptionalText, readRefId, refuseUnknownKeys, updatePayload } from "./crm-shared.js";
|
|
38
|
+
const TASK_KINDS = [
|
|
39
|
+
'call',
|
|
40
|
+
'email',
|
|
41
|
+
'meeting',
|
|
42
|
+
'todo'
|
|
43
|
+
];
|
|
44
|
+
const TASK_PRIORITIES = [
|
|
45
|
+
'low',
|
|
46
|
+
'normal',
|
|
47
|
+
'high'
|
|
48
|
+
];
|
|
49
|
+
const TASK_STATUSES = [
|
|
50
|
+
'open',
|
|
51
|
+
'done'
|
|
52
|
+
];
|
|
53
|
+
/** The task object as published. Every writable field appears here. */ function taskView(doc) {
|
|
54
|
+
var _doc_data, _data_title, _data_notes, _data_kind, _data_priority, _data_status, _data_assigneeUid, _data_contactId, _data_companyId, _data_dealId, _data_hostId;
|
|
55
|
+
const data = (_doc_data = doc.data()) != null ? _doc_data : {};
|
|
56
|
+
return _extends({
|
|
57
|
+
id: doc.id,
|
|
58
|
+
object: 'task',
|
|
59
|
+
title: (_data_title = data.title) != null ? _data_title : null,
|
|
60
|
+
notes: (_data_notes = data.notes) != null ? _data_notes : null,
|
|
61
|
+
kind: (_data_kind = data.kind) != null ? _data_kind : 'todo',
|
|
62
|
+
priority: (_data_priority = data.priority) != null ? _data_priority : 'normal',
|
|
63
|
+
status: (_data_status = data.status) != null ? _data_status : 'open',
|
|
64
|
+
dueAt: isoFromMs(data.dueAtMs),
|
|
65
|
+
remindAt: isoFromMs(data.remindAtMs),
|
|
66
|
+
reminderSentAt: isoFromMs(data.reminderSentAtMs),
|
|
67
|
+
completedAt: isoFromMs(data.completedAtMs),
|
|
68
|
+
assigneeUid: (_data_assigneeUid = data.assigneeUid) != null ? _data_assigneeUid : null,
|
|
69
|
+
contactId: (_data_contactId = data.contactId) != null ? _data_contactId : null,
|
|
70
|
+
companyId: (_data_companyId = data.companyId) != null ? _data_companyId : null,
|
|
71
|
+
dealId: (_data_dealId = data.dealId) != null ? _data_dealId : null,
|
|
72
|
+
siteId: (_data_hostId = data.hostId) != null ? _data_hostId : null
|
|
73
|
+
}, crmTimes(data));
|
|
74
|
+
}
|
|
75
|
+
const TASK_WRITABLE = new Set([
|
|
76
|
+
'title',
|
|
77
|
+
'notes',
|
|
78
|
+
'kind',
|
|
79
|
+
'priority',
|
|
80
|
+
'status',
|
|
81
|
+
'dueAt',
|
|
82
|
+
'remindAt',
|
|
83
|
+
'assigneeUid',
|
|
84
|
+
'contactId',
|
|
85
|
+
'companyId',
|
|
86
|
+
'dealId'
|
|
87
|
+
]);
|
|
88
|
+
function readTaskInput(body, { partial }) {
|
|
89
|
+
const errors = {};
|
|
90
|
+
const values = {};
|
|
91
|
+
const allowed = new Set(TASK_WRITABLE);
|
|
92
|
+
if (!partial) allowed.add('consentSiteId');
|
|
93
|
+
refuseUnknownKeys(body, allowed, 'task', errors);
|
|
94
|
+
if (body.title !== undefined || !partial) {
|
|
95
|
+
var _body_title;
|
|
96
|
+
const title = String((_body_title = body.title) != null ? _body_title : '').trim().slice(0, CRM_TITLE_MAX);
|
|
97
|
+
if (title) values.title = title;
|
|
98
|
+
else errors.title = partial ? 'Must not be empty' : 'A title is required';
|
|
99
|
+
}
|
|
100
|
+
const notes = readOptionalText(body, 'notes', CRM_TEXT_MAX, errors);
|
|
101
|
+
if (notes !== undefined) values.notes = notes;
|
|
102
|
+
const kind = readChoice(body, 'kind', TASK_KINDS, errors);
|
|
103
|
+
if (kind) values.kind = kind;
|
|
104
|
+
const priority = readChoice(body, 'priority', TASK_PRIORITIES, errors);
|
|
105
|
+
if (priority) values.priority = priority;
|
|
106
|
+
const status = readChoice(body, 'status', TASK_STATUSES, errors);
|
|
107
|
+
if (status) values.status = status;
|
|
108
|
+
if (body.dueAt !== undefined) {
|
|
109
|
+
if (body.dueAt === null) {
|
|
110
|
+
values.dueAtMs = null;
|
|
111
|
+
} else {
|
|
112
|
+
const ms = parseIsoInstant(body.dueAt);
|
|
113
|
+
if (ms === null) {
|
|
114
|
+
errors.dueAt = 'Must be an ISO 8601 instant, like 2026-09-10T15:00:00Z';
|
|
115
|
+
} else {
|
|
116
|
+
values.dueAtMs = ms;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (body.remindAt !== undefined) {
|
|
121
|
+
if (body.remindAt === null) {
|
|
122
|
+
values.remindAtMs = null;
|
|
123
|
+
} else {
|
|
124
|
+
const ms = parseIsoInstant(body.remindAt);
|
|
125
|
+
if (ms === null) {
|
|
126
|
+
errors.remindAt = 'Must be an ISO 8601 instant, like 2026-09-10T14:00:00Z';
|
|
127
|
+
} else {
|
|
128
|
+
values.remindAtMs = ms;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const assigneeUid = readOptionalText(body, 'assigneeUid', CRM_TITLE_MAX, errors);
|
|
133
|
+
if (assigneeUid !== undefined) values.assigneeUid = assigneeUid;
|
|
134
|
+
for (const field of [
|
|
135
|
+
'contactId',
|
|
136
|
+
'companyId',
|
|
137
|
+
'dealId'
|
|
138
|
+
]){
|
|
139
|
+
const id = readRefId(body, field, errors);
|
|
140
|
+
if (id !== undefined) values[field] = id;
|
|
141
|
+
}
|
|
142
|
+
return Object.keys(errors).length ? {
|
|
143
|
+
errors
|
|
144
|
+
} : {
|
|
145
|
+
values
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function taskRefErrors(ctx, values) {
|
|
149
|
+
var _values_contactId, _values_companyId, _values_dealId;
|
|
150
|
+
const [assignee, refs] = await Promise.all([
|
|
151
|
+
memberError(ctx, 'assigneeUid', values.assigneeUid),
|
|
152
|
+
crmRefErrors(ctx, {
|
|
153
|
+
contactId: (_values_contactId = values.contactId) != null ? _values_contactId : undefined,
|
|
154
|
+
companyId: (_values_companyId = values.companyId) != null ? _values_companyId : undefined,
|
|
155
|
+
dealId: (_values_dealId = values.dealId) != null ? _values_dealId : undefined
|
|
156
|
+
})
|
|
157
|
+
]);
|
|
158
|
+
return _extends({}, assignee, refs);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The records the task names carry `nextTaskAtMs` (AGL-2661), recomputed
|
|
162
|
+
* after every write here from BOTH sides of the change. Its own catch: the
|
|
163
|
+
* task write is done, and a figure that did not move is what the Fields
|
|
164
|
+
* section's recompute is for, not a failed request.
|
|
165
|
+
*/ async function settleNextActivity(ctx, links) {
|
|
166
|
+
try {
|
|
167
|
+
await recomputeCrmNextTaskAt(ctx.firestore, ctx.orgId, links.map((link)=>crmNextActivityLinksOf(link)));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error('[api-v1] next activity could not be recomputed', ctx.orgId, error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** `POST /v1/tasks`. */ async function createTask(request, ctx) {
|
|
173
|
+
const body = await readJsonBody(request);
|
|
174
|
+
const parsed = readTaskInput(body, {
|
|
175
|
+
partial: false
|
|
176
|
+
});
|
|
177
|
+
if ('errors' in parsed) return crmValidationFailed(ctx, 'task', parsed.errors);
|
|
178
|
+
const site = readCrmSite(ctx, 'task', body);
|
|
179
|
+
if ('response' in site) return site.response;
|
|
180
|
+
const refErrors = await taskRefErrors(ctx, parsed.values);
|
|
181
|
+
if (Object.keys(refErrors).length) return crmValidationFailed(ctx, 'task', refErrors);
|
|
182
|
+
const collection = crmCollection(ctx, CRM_COLLECTIONS.tasks);
|
|
183
|
+
const claimed = await claimWrite(ctx, '*', request.headers.get('Idempotency-Key'), 'tasks');
|
|
184
|
+
if ('replay' in claimed) return claimed.replay;
|
|
185
|
+
const { claim } = claimed;
|
|
186
|
+
try {
|
|
187
|
+
var _rest_dueAtMs, _rest_dueAtMs1;
|
|
188
|
+
const _parsed_values = parsed.values, { title, kind, priority, status, remindAtMs } = _parsed_values, rest = _object_without_properties_loose(_parsed_values, [
|
|
189
|
+
"title",
|
|
190
|
+
"kind",
|
|
191
|
+
"priority",
|
|
192
|
+
"status",
|
|
193
|
+
"remindAtMs"
|
|
194
|
+
]);
|
|
195
|
+
const id = createResourceUid();
|
|
196
|
+
const stamp = crmCreateStamp(ctx, site.siteId);
|
|
197
|
+
const record = _extends({
|
|
198
|
+
title,
|
|
199
|
+
kind: kind != null ? kind : 'todo',
|
|
200
|
+
priority: priority != null ? priority : 'normal',
|
|
201
|
+
status: status != null ? status : 'open'
|
|
202
|
+
}, status === 'done' ? {
|
|
203
|
+
completedAtMs: stamp.createdAt.toMillis()
|
|
204
|
+
} : {}, createPayload(_extends({}, rest, {
|
|
205
|
+
// A reminder on a task created done is owed to nobody.
|
|
206
|
+
remindAtMs: status === 'done' ? null : crmTaskReminderAfterEdit({
|
|
207
|
+
dueAtMs: (_rest_dueAtMs = rest.dueAtMs) != null ? _rest_dueAtMs : null,
|
|
208
|
+
remindAtMs,
|
|
209
|
+
previous: null
|
|
210
|
+
})
|
|
211
|
+
})), {
|
|
212
|
+
/*
|
|
213
|
+
* Stored as `null` when the body names none, never left absent: every
|
|
214
|
+
* tasks list orders by `dueAtMs`, and Firestore leaves a document
|
|
215
|
+
* without the ordered field out of the answer (AGL-3321).
|
|
216
|
+
*/ dueAtMs: (_rest_dueAtMs1 = rest.dueAtMs) != null ? _rest_dueAtMs1 : null
|
|
217
|
+
}, stamp);
|
|
218
|
+
// What the console's Tasks list searches by (AGL-3321).
|
|
219
|
+
await collection.doc(id).create(_extends({}, record, crmTaskListFields(record)));
|
|
220
|
+
await settleNextActivity(ctx, [
|
|
221
|
+
rest
|
|
222
|
+
]);
|
|
223
|
+
const view = taskView(await collection.doc(id).get());
|
|
224
|
+
await claim.record(200, view);
|
|
225
|
+
return apiJson(view, {
|
|
226
|
+
status: 201,
|
|
227
|
+
headers: ctx.headers
|
|
228
|
+
});
|
|
229
|
+
} catch (error) {
|
|
230
|
+
await claim.release();
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** `PATCH /v1/tasks/{id}`. */ async function updateTask(request, ctx, ref) {
|
|
235
|
+
const parsed = readTaskInput(await readJsonBody(request), {
|
|
236
|
+
partial: true
|
|
237
|
+
});
|
|
238
|
+
if ('errors' in parsed) return crmValidationFailed(ctx, 'task', parsed.errors);
|
|
239
|
+
const snap = await ref.get();
|
|
240
|
+
if (!snap.exists) {
|
|
241
|
+
return ApiErrors.notFound({
|
|
242
|
+
message: 'No such task',
|
|
243
|
+
headers: ctx.headers
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const refErrors = await taskRefErrors(ctx, parsed.values);
|
|
247
|
+
if (Object.keys(refErrors).length) return crmValidationFailed(ctx, 'task', refErrors);
|
|
248
|
+
const _parsed_values = parsed.values, { status, remindAtMs } = _parsed_values, rest = _object_without_properties_loose(_parsed_values, [
|
|
249
|
+
"status",
|
|
250
|
+
"remindAtMs"
|
|
251
|
+
]);
|
|
252
|
+
const update = updatePayload(rest);
|
|
253
|
+
// A cleared due date is stored `null`, not deleted — see the create.
|
|
254
|
+
if (rest.dueAtMs === null) update.dueAtMs = null;
|
|
255
|
+
// One instant for the write: a task completed at T reads updated at T.
|
|
256
|
+
const now = Timestamp.now();
|
|
257
|
+
const completing = status === 'done' && status !== snap.get('status');
|
|
258
|
+
if (status !== undefined && status !== snap.get('status')) {
|
|
259
|
+
update.status = status;
|
|
260
|
+
update.completedAtMs = status === 'done' ? now.toMillis() : null;
|
|
261
|
+
}
|
|
262
|
+
/*
|
|
263
|
+
* The reminder the edit leaves: what the body said, else the rule over
|
|
264
|
+
* the stored task and the due date this PATCH moves it to. Completion
|
|
265
|
+
* clears one still owed. A reminder that MOVED — to a time or to none —
|
|
266
|
+
* is one not yet sent, so the runner's mark comes off with it; one that
|
|
267
|
+
* stayed keeps its mark.
|
|
268
|
+
*/ const stored = snap.data();
|
|
269
|
+
const previous = {
|
|
270
|
+
dueAtMs: typeof stored.dueAtMs === 'number' ? stored.dueAtMs : null,
|
|
271
|
+
remindAtMs: typeof stored.remindAtMs === 'number' ? stored.remindAtMs : null
|
|
272
|
+
};
|
|
273
|
+
const nextRemindAtMs = completing && crmTaskReminderPending(stored) ? null : crmTaskReminderAfterEdit({
|
|
274
|
+
dueAtMs: rest.dueAtMs === undefined ? previous.dueAtMs : rest.dueAtMs,
|
|
275
|
+
remindAtMs,
|
|
276
|
+
previous
|
|
277
|
+
});
|
|
278
|
+
if (nextRemindAtMs !== previous.remindAtMs) {
|
|
279
|
+
update.remindAtMs = nextRemindAtMs === null ? FieldValue.delete() : nextRemindAtMs;
|
|
280
|
+
update.reminderSentAtMs = FieldValue.delete();
|
|
281
|
+
}
|
|
282
|
+
if (Object.keys(update).length > 0) {
|
|
283
|
+
await ref.update(_extends({}, update, {
|
|
284
|
+
updatedAt: now
|
|
285
|
+
}));
|
|
286
|
+
await settleNextActivity(ctx, [
|
|
287
|
+
snap.data(),
|
|
288
|
+
rest
|
|
289
|
+
]);
|
|
290
|
+
// What the console's Tasks list searches (AGL-3321).
|
|
291
|
+
if ('title' in update) await restampCrmListFieldsAt(ref, 'crmTasks');
|
|
292
|
+
}
|
|
293
|
+
return apiJson(taskView(await ref.get()), {
|
|
294
|
+
headers: ctx.headers
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/** `DELETE /v1/tasks/{id}`. */ async function deleteTask(request, ctx, ref) {
|
|
298
|
+
const claimed = await claimWrite(ctx, '*', request.headers.get('Idempotency-Key'), 'task-deletes');
|
|
299
|
+
if ('replay' in claimed) return claimed.replay;
|
|
300
|
+
const { claim } = claimed;
|
|
301
|
+
try {
|
|
302
|
+
const snap = await ref.get();
|
|
303
|
+
if (!snap.exists) {
|
|
304
|
+
await claim.release();
|
|
305
|
+
return ApiErrors.notFound({
|
|
306
|
+
message: 'No such task',
|
|
307
|
+
headers: ctx.headers
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
// Read before the delete: the records it named are recomputed without it.
|
|
311
|
+
const named = snap.data();
|
|
312
|
+
await ref.delete();
|
|
313
|
+
await settleNextActivity(ctx, [
|
|
314
|
+
named
|
|
315
|
+
]);
|
|
316
|
+
const view = {
|
|
317
|
+
id: ref.id,
|
|
318
|
+
object: 'task',
|
|
319
|
+
deleted: true
|
|
320
|
+
};
|
|
321
|
+
await claim.record(200, view);
|
|
322
|
+
return apiJson(view, {
|
|
323
|
+
headers: ctx.headers
|
|
324
|
+
});
|
|
325
|
+
} catch (error) {
|
|
326
|
+
await claim.release();
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/** `GET /v1/tasks` filters, most selective first. */ async function listTasks(ctx, collection, url) {
|
|
331
|
+
const rawStatus = url.searchParams.get('status');
|
|
332
|
+
if (rawStatus !== null && rawStatus.trim() !== '' && !TASK_STATUSES.includes(rawStatus.trim())) {
|
|
333
|
+
return crmValidationFailed(ctx, 'task filter', {
|
|
334
|
+
status: `Must be one of: ${TASK_STATUSES.join(', ')}`
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const filters = readEqualityFilters(url, [
|
|
338
|
+
'dealId',
|
|
339
|
+
'contactId',
|
|
340
|
+
'companyId',
|
|
341
|
+
'assigneeUid',
|
|
342
|
+
'status'
|
|
343
|
+
]);
|
|
344
|
+
return listCrm(ctx, collection, url, filters, taskView);
|
|
345
|
+
}
|
|
346
|
+
export async function handleTasks(request, ctx, segments, url) {
|
|
347
|
+
const collection = crmCollection(ctx, CRM_COLLECTIONS.tasks);
|
|
348
|
+
const [, taskId] = segments;
|
|
349
|
+
if (!taskId) {
|
|
350
|
+
if (request.method === 'GET') {
|
|
351
|
+
const denied = requireScope(ctx, 'crm:read');
|
|
352
|
+
if (denied) return denied;
|
|
353
|
+
return listTasks(ctx, collection, url);
|
|
354
|
+
}
|
|
355
|
+
if (request.method === 'POST') {
|
|
356
|
+
const denied = requireScope(ctx, 'crm:write');
|
|
357
|
+
if (denied) return denied;
|
|
358
|
+
return createTask(request, ctx);
|
|
359
|
+
}
|
|
360
|
+
return ApiErrors.methodNotAllowed({
|
|
361
|
+
headers: _extends({}, ctx.headers, {
|
|
362
|
+
Allow: 'GET, POST'
|
|
363
|
+
})
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
const ref = collection.doc(taskId);
|
|
367
|
+
if (request.method === 'GET') {
|
|
368
|
+
const denied = requireScope(ctx, 'crm:read');
|
|
369
|
+
if (denied) return denied;
|
|
370
|
+
const snap = await ref.get();
|
|
371
|
+
if (!snap.exists) {
|
|
372
|
+
return ApiErrors.notFound({
|
|
373
|
+
message: 'No such task',
|
|
374
|
+
headers: ctx.headers
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return apiJson(taskView(snap), {
|
|
378
|
+
headers: ctx.headers
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if (request.method === 'PATCH') {
|
|
382
|
+
const denied = requireScope(ctx, 'crm:write');
|
|
383
|
+
if (denied) return denied;
|
|
384
|
+
return updateTask(request, ctx, ref);
|
|
385
|
+
}
|
|
386
|
+
if (request.method === 'DELETE') {
|
|
387
|
+
const denied = requireScope(ctx, 'crm:write');
|
|
388
|
+
if (denied) return denied;
|
|
389
|
+
return deleteTask(request, ctx, ref);
|
|
390
|
+
}
|
|
391
|
+
return ApiErrors.methodNotAllowed({
|
|
392
|
+
headers: _extends({}, ctx.headers, {
|
|
393
|
+
Allow: 'GET, PATCH, DELETE'
|
|
394
|
+
})
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
//# sourceMappingURL=crm-tasks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/server/api-v1/crm-tasks.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/tasks` (AGL-2606) — what the team owes a person: a call, an email, a\n * meeting or a to-do, with a due date, pointing at the contact, company or\n * deal it is for. A task may point at nothing — a plain to-do is still a\n * task — which is the one place it differs from an activity.\n *\n * `status` is the only state machine here and it has two states. Marking a\n * task `done` stamps `completedAtMs`; marking it `open` again clears it, so\n * a reopened task never reads as completed on the date it was first closed.\n *\n * `remindAt` (AGL-2659) is the task's reminder: the due time unless the\n * body names a time or `null`, carried along when a moved due date leaves\n * it behind, and cleared by completion while it is still owed — the rule\n * in `crmTaskReminderAfterEdit`, the same one the console's drawer keeps.\n * `reminderSentAt` is read-only: when the hourly runner handled it.\n */\nimport {\n CRM_COLLECTIONS,\n type CrmTask,\n type CrmTaskKind,\n type CrmTaskPriority,\n crmTaskReminderAfterEdit,\n crmTaskReminderPending,\n type CrmTaskStatus,\n createResourceUid,\n crmTaskListFields,\n} from '@aglyn/aglyn/server'\nimport {\n apiJson,\n ApiErrors,\n crmNextActivityLinksOf,\n recomputeCrmNextTaskAt,\n restampCrmListFieldsAt,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue, Timestamp } from 'firebase-admin/firestore'\nimport {\n type ApiV1Context,\n claimWrite,\n readJsonBody,\n requireScope,\n} from '@aglyn/tenant-data-admin/server/api-v1-kit'\nimport {\n type Clearable,\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 updatePayload,\n} from './crm-shared'\n\nconst TASK_KINDS = ['call', 'email', 'meeting', 'todo'] as const\nconst TASK_PRIORITIES = ['low', 'normal', 'high'] as const\nconst TASK_STATUSES = ['open', 'done'] as const\n\n/** The task object as published. Every writable field appears here. */\nfunction taskView(doc: FirebaseFirestore.DocumentSnapshot) {\n const data = (doc.data() ?? {}) as Partial<CrmTask>\n return {\n id: doc.id,\n object: 'task',\n title: data.title ?? null,\n notes: data.notes ?? null,\n kind: data.kind ?? 'todo',\n priority: data.priority ?? 'normal',\n status: data.status ?? 'open',\n dueAt: isoFromMs(data.dueAtMs),\n remindAt: isoFromMs(data.remindAtMs),\n reminderSentAt: isoFromMs(data.reminderSentAtMs),\n completedAt: isoFromMs(data.completedAtMs),\n assigneeUid: data.assigneeUid ?? null,\n contactId: data.contactId ?? null,\n companyId: data.companyId ?? null,\n dealId: data.dealId ?? null,\n siteId: data.hostId ?? null,\n ...crmTimes(data as FirebaseFirestore.DocumentData),\n }\n}\n\nconst TASK_WRITABLE = new Set([\n 'title',\n 'notes',\n 'kind',\n 'priority',\n 'status',\n 'dueAt',\n 'remindAt',\n 'assigneeUid',\n 'contactId',\n 'companyId',\n 'dealId',\n])\n\ninterface TaskInput {\n title?: string\n notes?: Clearable<string>\n kind?: CrmTaskKind\n priority?: CrmTaskPriority\n status?: CrmTaskStatus\n dueAtMs?: Clearable<number>\n remindAtMs?: Clearable<number>\n assigneeUid?: Clearable<string>\n contactId?: Clearable<string>\n companyId?: Clearable<string>\n dealId?: Clearable<string>\n}\n\nfunction readTaskInput(\n body: Record<string, unknown>,\n { partial }: { partial: boolean },\n): { values: TaskInput } | { errors: Record<string, string> } {\n const errors: Record<string, string> = {}\n const values: TaskInput = {}\n const allowed = new Set(TASK_WRITABLE)\n if (!partial) allowed.add('consentSiteId')\n refuseUnknownKeys(body, allowed, 'task', errors)\n\n if (body.title !== undefined || !partial) {\n const title = String(body.title ?? '')\n .trim()\n .slice(0, CRM_TITLE_MAX)\n if (title) values.title = title\n else errors.title = partial ? 'Must not be empty' : 'A title is required'\n }\n\n const notes = readOptionalText(body, 'notes', CRM_TEXT_MAX, errors)\n if (notes !== undefined) values.notes = notes\n const kind = readChoice(body, 'kind', TASK_KINDS, errors)\n if (kind) values.kind = kind\n const priority = readChoice(body, 'priority', TASK_PRIORITIES, errors)\n if (priority) values.priority = priority\n const status = readChoice(body, 'status', TASK_STATUSES, errors)\n if (status) values.status = status\n\n if (body.dueAt !== undefined) {\n if (body.dueAt === null) {\n values.dueAtMs = null\n } else {\n const ms = parseIsoInstant(body.dueAt)\n if (ms === null) {\n errors.dueAt = 'Must be an ISO 8601 instant, like 2026-09-10T15:00:00Z'\n } else {\n values.dueAtMs = ms\n }\n }\n }\n\n if (body.remindAt !== undefined) {\n if (body.remindAt === null) {\n values.remindAtMs = null\n } else {\n const ms = parseIsoInstant(body.remindAt)\n if (ms === null) {\n errors.remindAt = 'Must be an ISO 8601 instant, like 2026-09-10T14:00:00Z'\n } else {\n values.remindAtMs = ms\n }\n }\n }\n\n const assigneeUid = readOptionalText(body, 'assigneeUid', CRM_TITLE_MAX, errors)\n if (assigneeUid !== undefined) values.assigneeUid = assigneeUid\n for (const field of ['contactId', 'companyId', 'dealId'] as const) {\n const id = readRefId(body, field, errors)\n if (id !== undefined) values[field] = id\n }\n\n return Object.keys(errors).length ? { errors } : { values }\n}\n\nasync function taskRefErrors(\n ctx: ApiV1Context,\n values: TaskInput,\n): Promise<Record<string, string>> {\n const [assignee, refs] = await Promise.all([\n memberError(ctx, 'assigneeUid', values.assigneeUid),\n crmRefErrors(ctx, {\n contactId: values.contactId ?? undefined,\n companyId: values.companyId ?? undefined,\n dealId: values.dealId ?? undefined,\n }),\n ])\n return { ...assignee, ...refs }\n}\n\n/**\n * The records the task names carry `nextTaskAtMs` (AGL-2661), recomputed\n * after every write here from BOTH sides of the change. Its own catch: the\n * task write is done, and a figure that did not move is what the Fields\n * section's recompute is for, not a failed request.\n */\nasync function settleNextActivity(\n ctx: ApiV1Context,\n links: readonly (Record<string, unknown> | null | undefined)[],\n): Promise<void> {\n try {\n await recomputeCrmNextTaskAt(\n ctx.firestore,\n ctx.orgId,\n links.map((link) => crmNextActivityLinksOf(link)),\n )\n } catch (error) {\n console.error('[api-v1] next activity could not be recomputed', ctx.orgId, error)\n }\n}\n\n/** `POST /v1/tasks`. */\nasync function createTask(request: Request, ctx: ApiV1Context): Promise<Response> {\n const body = await readJsonBody(request)\n const parsed = readTaskInput(body, { partial: false })\n if ('errors' in parsed) return crmValidationFailed(ctx, 'task', parsed.errors)\n const site = readCrmSite(ctx, 'task', body)\n if ('response' in site) return site.response\n const refErrors = await taskRefErrors(ctx, parsed.values)\n if (Object.keys(refErrors).length) return crmValidationFailed(ctx, 'task', refErrors)\n\n const collection = crmCollection(ctx, CRM_COLLECTIONS.tasks)\n const claimed = await claimWrite(\n ctx,\n '*',\n request.headers.get('Idempotency-Key'),\n 'tasks',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const { title, kind, priority, status, remindAtMs, ...rest } = parsed.values\n const id = createResourceUid()\n const stamp = crmCreateStamp(ctx, site.siteId)\n const record: Record<string, unknown> = {\n title,\n kind: kind ?? 'todo',\n priority: priority ?? 'normal',\n status: status ?? 'open',\n // A task created done was completed the instant it was created — the\n // same instant its `createdAt` carries.\n ...(status === 'done' ? { completedAtMs: stamp.createdAt.toMillis() } : {}),\n ...createPayload({\n ...rest,\n // A reminder on a task created done is owed to nobody.\n remindAtMs:\n status === 'done'\n ? null\n : crmTaskReminderAfterEdit({\n dueAtMs: rest.dueAtMs ?? null,\n remindAtMs,\n previous: null,\n }),\n }),\n /*\n * Stored as `null` when the body names none, never left absent: every\n * tasks list orders by `dueAtMs`, and Firestore leaves a document\n * without the ordered field out of the answer (AGL-3321).\n */\n dueAtMs: rest.dueAtMs ?? null,\n ...stamp,\n }\n // What the console's Tasks list searches by (AGL-3321).\n await collection.doc(id).create({ ...record, ...crmTaskListFields(record) })\n await settleNextActivity(ctx, [rest as Record<string, unknown>])\n const view = taskView(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/** `PATCH /v1/tasks/{id}`. */\nasync function updateTask(\n request: Request,\n ctx: ApiV1Context,\n ref: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const parsed = readTaskInput(await readJsonBody(request), { partial: true })\n if ('errors' in parsed) return crmValidationFailed(ctx, 'task', parsed.errors)\n const snap = await ref.get()\n if (!snap.exists) {\n return ApiErrors.notFound({ message: 'No such task', headers: ctx.headers })\n }\n const refErrors = await taskRefErrors(ctx, parsed.values)\n if (Object.keys(refErrors).length) return crmValidationFailed(ctx, 'task', refErrors)\n\n const { status, remindAtMs, ...rest } = parsed.values\n const update: Record<string, unknown> = updatePayload(rest)\n // A cleared due date is stored `null`, not deleted — see the create.\n if (rest.dueAtMs === null) update.dueAtMs = null\n // One instant for the write: a task completed at T reads updated at T.\n const now = Timestamp.now()\n const completing = status === 'done' && status !== snap.get('status')\n if (status !== undefined && status !== snap.get('status')) {\n update.status = status\n update.completedAtMs = status === 'done' ? now.toMillis() : null\n }\n /*\n * The reminder the edit leaves: what the body said, else the rule over\n * the stored task and the due date this PATCH moves it to. Completion\n * clears one still owed. A reminder that MOVED — to a time or to none —\n * is one not yet sent, so the runner's mark comes off with it; one that\n * stayed keeps its mark.\n */\n const stored = snap.data() as Partial<CrmTask>\n const previous = {\n dueAtMs: typeof stored.dueAtMs === 'number' ? stored.dueAtMs : null,\n remindAtMs: typeof stored.remindAtMs === 'number' ? stored.remindAtMs : null,\n }\n const nextRemindAtMs =\n completing && crmTaskReminderPending(stored)\n ? null\n : crmTaskReminderAfterEdit({\n dueAtMs: rest.dueAtMs === undefined ? previous.dueAtMs : rest.dueAtMs,\n remindAtMs,\n previous,\n })\n if (nextRemindAtMs !== previous.remindAtMs) {\n update.remindAtMs = nextRemindAtMs === null ? FieldValue.delete() : nextRemindAtMs\n update.reminderSentAtMs = FieldValue.delete()\n }\n if (Object.keys(update).length > 0) {\n await ref.update({ ...update, updatedAt: now })\n await settleNextActivity(ctx, [snap.data(), rest as Record<string, unknown>])\n // What the console's Tasks list searches (AGL-3321).\n if ('title' in update) await restampCrmListFieldsAt(ref, 'crmTasks')\n }\n return apiJson(taskView(await ref.get()), { headers: ctx.headers })\n}\n\n/** `DELETE /v1/tasks/{id}`. */\nasync function deleteTask(\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 'task-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 task', headers: ctx.headers })\n }\n // Read before the delete: the records it named are recomputed without it.\n const named = snap.data()\n await ref.delete()\n await settleNextActivity(ctx, [named])\n const view = { id: ref.id, object: 'task', 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/tasks` filters, most selective first. */\nasync function listTasks(\n ctx: ApiV1Context,\n collection: FirebaseFirestore.CollectionReference,\n url: URL,\n): Promise<Response> {\n const rawStatus = url.searchParams.get('status')\n if (rawStatus !== null && rawStatus.trim() !== '' && !(TASK_STATUSES as readonly string[]).includes(rawStatus.trim())) {\n return crmValidationFailed(ctx, 'task filter', {\n status: `Must be one of: ${TASK_STATUSES.join(', ')}`,\n })\n }\n const filters = readEqualityFilters(url, [\n 'dealId',\n 'contactId',\n 'companyId',\n 'assigneeUid',\n 'status',\n ])\n return listCrm(ctx, collection, url, filters, taskView)\n}\n\nexport async function handleTasks(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const collection = crmCollection(ctx, CRM_COLLECTIONS.tasks)\n const [, taskId] = segments\n\n if (!taskId) {\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'crm:read')\n if (denied) return denied\n return listTasks(ctx, collection, url)\n }\n if (request.method === 'POST') {\n const denied = requireScope(ctx, 'crm:write')\n if (denied) return denied\n return createTask(request, ctx)\n }\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, POST' },\n })\n }\n\n const ref = collection.doc(taskId)\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 task', headers: ctx.headers })\n }\n return apiJson(taskView(snap), { headers: ctx.headers })\n }\n if (request.method === 'PATCH') {\n const denied = requireScope(ctx, 'crm:write')\n if (denied) return denied\n return updateTask(request, ctx, ref)\n }\n if (request.method === 'DELETE') {\n const denied = requireScope(ctx, 'crm:write')\n if (denied) return denied\n return deleteTask(request, ctx, ref)\n }\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, PATCH, DELETE' },\n })\n}\n"],"names":["CRM_COLLECTIONS","crmTaskReminderAfterEdit","crmTaskReminderPending","createResourceUid","crmTaskListFields","apiJson","ApiErrors","crmNextActivityLinksOf","recomputeCrmNextTaskAt","restampCrmListFieldsAt","FieldValue","Timestamp","claimWrite","readJsonBody","requireScope","CRM_TEXT_MAX","CRM_TITLE_MAX","createPayload","crmCollection","crmCreateStamp","crmRefErrors","crmTimes","crmValidationFailed","isoFromMs","listCrm","memberError","parseIsoInstant","readChoice","readCrmSite","readEqualityFilters","readOptionalText","readRefId","refuseUnknownKeys","updatePayload","TASK_KINDS","TASK_PRIORITIES","TASK_STATUSES","taskView","doc","data","id","object","title","notes","kind","priority","status","dueAt","dueAtMs","remindAt","remindAtMs","reminderSentAt","reminderSentAtMs","completedAt","completedAtMs","assigneeUid","contactId","companyId","dealId","siteId","hostId","TASK_WRITABLE","Set","readTaskInput","body","partial","errors","values","allowed","add","undefined","String","trim","slice","ms","field","Object","keys","length","taskRefErrors","ctx","assignee","refs","Promise","all","settleNextActivity","links","firestore","orgId","map","link","error","console","createTask","request","parsed","site","response","refErrors","collection","tasks","claimed","headers","get","replay","claim","rest","stamp","record","createdAt","toMillis","previous","create","view","release","updateTask","ref","snap","exists","notFound","message","update","now","completing","stored","nextRemindAtMs","delete","updatedAt","deleteTask","named","deleted","listTasks","url","rawStatus","searchParams","includes","join","filters","handleTasks","segments","taskId","method","denied","methodNotAllowed","Allow"],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
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 ApiV1ResourceDescription } from '@aglyn/tenant-data-admin/server/api-v1-description';
|
|
18
|
+
/**
|
|
19
|
+
* The CRM's resources as the customer API's OpenAPI document describes them
|
|
20
|
+
* (AGL-2733), keyed by the resource each describes — what the console's
|
|
21
|
+
* builder turns into their paths, tags and schemas, and the MCP tools derive
|
|
22
|
+
* from.
|
|
23
|
+
*
|
|
24
|
+
* Transcribed from the response examples in `apps/docs/api/resources/*.md`,
|
|
25
|
+
* the customer-facing contract, as the platform's own resources are; each
|
|
26
|
+
* write body lists its handler's own writable set. `api-v1-openapi.spec.ts`
|
|
27
|
+
* holds the document to every endpoint the documentation promises.
|
|
28
|
+
*/
|
|
29
|
+
export declare const CRM_API_V1_DESCRIPTIONS: Readonly<Record<string, ApiV1ResourceDescription>>;
|