@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,391 @@
|
|
|
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/companies` (AGL-2606) — the organizations behind the people.
|
|
20
|
+
*
|
|
21
|
+
* A company is keyed, in practice, by its domain: two contacts at
|
|
22
|
+
* `example.com` work for one company, and the console's auto-association
|
|
23
|
+
* (`companyDomainForEmail`) files them under it by that string. So the
|
|
24
|
+
* domain is normalized exactly as the console stores it, a second company on
|
|
25
|
+
* a domain already held is a `409 company_exists` naming the first — the
|
|
26
|
+
* contact create's own rule for a second row on one email — and `?domain=`
|
|
27
|
+
* is the lookup a sync starts with.
|
|
28
|
+
*/ import { nameSearchFields } from "@aglyn/aglyn/app-utils/name-search";
|
|
29
|
+
import { CRM_COLLECTIONS, CRM_MEDIA_IDS_MAX, createResourceUid, crmNewRecordListFields, normalizeAddress, normalizeCompanyDomain, normalizeCrmMediaIds, normalizePhone } from "@aglyn/aglyn/server";
|
|
30
|
+
import { apiJson, ApiErrors, restampCrmListFieldsAt } from "@aglyn/tenant-data-admin";
|
|
31
|
+
import { Timestamp } from "firebase-admin/firestore";
|
|
32
|
+
import { claimWrite, readJsonBody, requireScope } from "@aglyn/tenant-data-admin/server/api-v1-kit";
|
|
33
|
+
import { CRM_LABEL_MAX, CRM_TEXT_MAX, CRM_TITLE_MAX, createPayload, crmCollection, crmCreateStamp, crmCustomUpdate, crmCustomView, crmRecordsBandRefusal, crmTimes, crmValidationFailed, isoFromMs, listCrm, memberError, readCrmCustomBody, readCrmSite, readEqualityFilters, readOptionalText, refuseUnknownKeys, updatePayload } from "./crm-shared.js";
|
|
34
|
+
/** The company object as published. Every writable field appears here. */ function companyView(doc) {
|
|
35
|
+
var _doc_data, _data_name, _data_domain, _data_website, _data_phone, _data_address, _data_industry, _data_ownerUid, _data_notes, _data_hostId;
|
|
36
|
+
const data = (_doc_data = doc.data()) != null ? _doc_data : {};
|
|
37
|
+
return _extends({
|
|
38
|
+
id: doc.id,
|
|
39
|
+
object: 'company',
|
|
40
|
+
name: (_data_name = data.name) != null ? _data_name : null,
|
|
41
|
+
domain: (_data_domain = data.domain) != null ? _data_domain : null,
|
|
42
|
+
website: (_data_website = data.website) != null ? _data_website : null,
|
|
43
|
+
phone: (_data_phone = data.phone) != null ? _data_phone : null,
|
|
44
|
+
address: (_data_address = data.address) != null ? _data_address : null,
|
|
45
|
+
industry: (_data_industry = data.industry) != null ? _data_industry : null,
|
|
46
|
+
ownerUid: (_data_ownerUid = data.ownerUid) != null ? _data_ownerUid : null,
|
|
47
|
+
notes: (_data_notes = data.notes) != null ? _data_notes : null,
|
|
48
|
+
// The org's company custom fields, keyed by field key (AGL-2661).
|
|
49
|
+
custom: crmCustomView(data),
|
|
50
|
+
// When the earliest open task against the company is due — see
|
|
51
|
+
// `CrmDeal.nextTaskAtMs`. Read-only: the tasks resource maintains it.
|
|
52
|
+
nextTaskAt: isoFromMs(data.nextTaskAtMs),
|
|
53
|
+
mediaIds: normalizeCrmMediaIds(data.mediaIds),
|
|
54
|
+
siteId: (_data_hostId = data.hostId) != null ? _data_hostId : null
|
|
55
|
+
}, crmTimes(data));
|
|
56
|
+
}
|
|
57
|
+
const COMPANY_WRITABLE = new Set([
|
|
58
|
+
'name',
|
|
59
|
+
'domain',
|
|
60
|
+
'website',
|
|
61
|
+
'phone',
|
|
62
|
+
'address',
|
|
63
|
+
'industry',
|
|
64
|
+
'ownerUid',
|
|
65
|
+
'notes',
|
|
66
|
+
'custom',
|
|
67
|
+
'mediaIds'
|
|
68
|
+
]);
|
|
69
|
+
/**
|
|
70
|
+
* The writable half of a company, validated. `partial` separates PATCH from
|
|
71
|
+
* POST as every reader in this API does: a create needs a name, an update
|
|
72
|
+
* may send one field alone. Unknown keys are named, never dropped.
|
|
73
|
+
*
|
|
74
|
+
* `domain` and `phone` go through the console's own normalizers so the API
|
|
75
|
+
* cannot store a value the console would never have written, and a value
|
|
76
|
+
* that does not survive normalization names the field rather than storing
|
|
77
|
+
* the raw string — a raw domain would never match the auto-association, and
|
|
78
|
+
* a raw phone is the unusable half-number `normalizePhone` exists to end.
|
|
79
|
+
*/ function readCompanyInput(body, { partial }) {
|
|
80
|
+
const errors = {};
|
|
81
|
+
const values = {};
|
|
82
|
+
const allowed = new Set(COMPANY_WRITABLE);
|
|
83
|
+
if (!partial) allowed.add('consentSiteId');
|
|
84
|
+
refuseUnknownKeys(body, allowed, 'company', errors);
|
|
85
|
+
if (body.name !== undefined || !partial) {
|
|
86
|
+
var _body_name;
|
|
87
|
+
const name = String((_body_name = body.name) != null ? _body_name : '').trim().slice(0, CRM_TITLE_MAX);
|
|
88
|
+
if (name) values.name = name;
|
|
89
|
+
else errors.name = partial ? 'Must not be empty' : 'A name is required';
|
|
90
|
+
}
|
|
91
|
+
if (body.mediaIds !== undefined) {
|
|
92
|
+
if (!Array.isArray(body.mediaIds)) {
|
|
93
|
+
errors.mediaIds = 'Must be an array of media ids';
|
|
94
|
+
} else if (body.mediaIds.length > CRM_MEDIA_IDS_MAX) {
|
|
95
|
+
errors.mediaIds = `At most ${CRM_MEDIA_IDS_MAX} files may be attached`;
|
|
96
|
+
} else {
|
|
97
|
+
// The SAME normalizer the console card writes through, so the API
|
|
98
|
+
// cannot store a list the console would refuse. An empty array is
|
|
99
|
+
// legal and clears the attachments.
|
|
100
|
+
values.mediaIds = normalizeCrmMediaIds(body.mediaIds);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const domain = readOptionalText(body, 'domain', CRM_TITLE_MAX, errors);
|
|
104
|
+
if (domain) {
|
|
105
|
+
const normalized = normalizeCompanyDomain(domain);
|
|
106
|
+
if (normalized) values.domain = normalized;
|
|
107
|
+
else errors.domain = 'Must be a domain name, like example.com';
|
|
108
|
+
} else if (domain === null) {
|
|
109
|
+
values.domain = null;
|
|
110
|
+
}
|
|
111
|
+
const website = readOptionalText(body, 'website', CRM_TITLE_MAX * 4, errors);
|
|
112
|
+
if (website) {
|
|
113
|
+
// A bare `example.com` is what people paste; stored with the scheme so the
|
|
114
|
+
// console can render it as a link without guessing one.
|
|
115
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(website) ? website : `https://${website}`;
|
|
116
|
+
let parsed;
|
|
117
|
+
try {
|
|
118
|
+
parsed = new URL(withScheme);
|
|
119
|
+
} catch (unused) {
|
|
120
|
+
parsed = null;
|
|
121
|
+
}
|
|
122
|
+
if (parsed && /^https?:$/.test(parsed.protocol) && normalizeCompanyDomain(parsed.hostname)) {
|
|
123
|
+
values.website = parsed.toString();
|
|
124
|
+
} else {
|
|
125
|
+
errors.website = 'Must be a web address, like https://example.com';
|
|
126
|
+
}
|
|
127
|
+
} else if (website === null) {
|
|
128
|
+
values.website = null;
|
|
129
|
+
}
|
|
130
|
+
const phone = readOptionalText(body, 'phone', CRM_LABEL_MAX, errors);
|
|
131
|
+
if (phone) {
|
|
132
|
+
const normalized = normalizePhone(phone);
|
|
133
|
+
if (normalized) values.phone = normalized;
|
|
134
|
+
else errors.phone = 'Must be a phone number with a country code, like +15125550123';
|
|
135
|
+
} else if (phone === null) {
|
|
136
|
+
values.phone = null;
|
|
137
|
+
}
|
|
138
|
+
if (body.address !== undefined) {
|
|
139
|
+
if (body.address === null) {
|
|
140
|
+
values.address = null;
|
|
141
|
+
} else if (typeof body.address !== 'object' || Array.isArray(body.address)) {
|
|
142
|
+
errors.address = 'Must be an address object';
|
|
143
|
+
} else {
|
|
144
|
+
// A blank address normalizes to `null`, which clears — an object of
|
|
145
|
+
// empty strings must not read as "has an address".
|
|
146
|
+
values.address = normalizeAddress(body.address);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const industry = readOptionalText(body, 'industry', CRM_LABEL_MAX, errors);
|
|
150
|
+
if (industry !== undefined) values.industry = industry;
|
|
151
|
+
const ownerUid = readOptionalText(body, 'ownerUid', CRM_LABEL_MAX, errors);
|
|
152
|
+
if (ownerUid !== undefined) values.ownerUid = ownerUid;
|
|
153
|
+
const notes = readOptionalText(body, 'notes', CRM_TEXT_MAX, errors);
|
|
154
|
+
if (notes !== undefined) values.notes = notes;
|
|
155
|
+
// `custom` is shape-checked here and judged against the org's
|
|
156
|
+
// definitions by the writers, which is where the read is paid.
|
|
157
|
+
if (body.custom !== undefined) {
|
|
158
|
+
if (!body.custom || typeof body.custom !== 'object' || Array.isArray(body.custom)) {
|
|
159
|
+
errors.custom = 'Must be an object of field values keyed by field key';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return Object.keys(errors).length ? {
|
|
163
|
+
errors
|
|
164
|
+
} : {
|
|
165
|
+
values
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* `POST /v1/companies`. Claimed first and released on the duplicate, the
|
|
170
|
+
* ordering `createContact` argues for: a refusal that clears when the
|
|
171
|
+
* duplicate is removed must not burn the key the retry will carry.
|
|
172
|
+
*/ async function createCompany(request, ctx) {
|
|
173
|
+
const body = await readJsonBody(request);
|
|
174
|
+
const parsed = readCompanyInput(body, {
|
|
175
|
+
partial: false
|
|
176
|
+
});
|
|
177
|
+
if ('errors' in parsed) return crmValidationFailed(ctx, 'company', parsed.errors);
|
|
178
|
+
const site = readCrmSite(ctx, 'company', body);
|
|
179
|
+
if ('response' in site) return site.response;
|
|
180
|
+
const owner = await memberError(ctx, 'ownerUid', parsed.values.ownerUid);
|
|
181
|
+
if (Object.keys(owner).length) return crmValidationFailed(ctx, 'company', owner);
|
|
182
|
+
// Judged against the org's company definitions, above the claim like
|
|
183
|
+
// every other deterministic 400 (AGL-2661).
|
|
184
|
+
const custom = await readCrmCustomBody(ctx, body, 'company');
|
|
185
|
+
if (custom && 'errors' in custom) return crmValidationFailed(ctx, 'company', custom.errors);
|
|
186
|
+
const customValues = custom && 'values' in custom ? custom.values : {};
|
|
187
|
+
const collection = crmCollection(ctx, CRM_COLLECTIONS.companies);
|
|
188
|
+
const claimed = await claimWrite(ctx, '*', request.headers.get('Idempotency-Key'), 'companies');
|
|
189
|
+
if ('replay' in claimed) return claimed.replay;
|
|
190
|
+
const { claim } = claimed;
|
|
191
|
+
try {
|
|
192
|
+
const _parsed_values = parsed.values, { name, domain } = _parsed_values, rest = _object_without_properties_loose(_parsed_values, [
|
|
193
|
+
"name",
|
|
194
|
+
"domain"
|
|
195
|
+
]);
|
|
196
|
+
if (domain) {
|
|
197
|
+
const existing = await collection.where('domain', '==', domain).limit(1).get();
|
|
198
|
+
if (!existing.empty) {
|
|
199
|
+
await claim.release();
|
|
200
|
+
return ApiErrors.conflict({
|
|
201
|
+
message: `A company with this domain already exists (${existing.docs[0].id}). Update it instead.`,
|
|
202
|
+
code: 'company_exists',
|
|
203
|
+
headers: ctx.headers
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// The records band (AGL-2611), after the duplicate check for the reason
|
|
208
|
+
// the claim ordering gives: a refusal that clears when the band is raised
|
|
209
|
+
// must not burn the key the retry will carry.
|
|
210
|
+
const bandFull = await crmRecordsBandRefusal(ctx);
|
|
211
|
+
if (bandFull) {
|
|
212
|
+
await claim.release();
|
|
213
|
+
return bandFull;
|
|
214
|
+
}
|
|
215
|
+
const id = createResourceUid();
|
|
216
|
+
const record = _extends({}, nameSearchFields(name != null ? name : ''), createPayload(_extends({
|
|
217
|
+
domain
|
|
218
|
+
}, rest)), Object.keys(customValues).length ? {
|
|
219
|
+
custom: createPayload(customValues)
|
|
220
|
+
} : {}, crmCreateStamp(ctx, site.siteId));
|
|
221
|
+
// What the console's Companies list searches and filters by (AGL-3321).
|
|
222
|
+
await collection.doc(id).create(_extends({}, record, crmNewRecordListFields('companies', record)));
|
|
223
|
+
const view = companyView(await collection.doc(id).get());
|
|
224
|
+
// Stored as 200 so a replay is distinguishable from the fresh 201.
|
|
225
|
+
await claim.record(200, view);
|
|
226
|
+
return apiJson(view, {
|
|
227
|
+
status: 201,
|
|
228
|
+
headers: ctx.headers
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
await claim.release();
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** `PATCH /v1/companies/{id}` — no key, for `updateContact`'s reason. */ async function updateCompany(request, ctx, ref) {
|
|
236
|
+
const body = await readJsonBody(request);
|
|
237
|
+
const parsed = readCompanyInput(body, {
|
|
238
|
+
partial: true
|
|
239
|
+
});
|
|
240
|
+
if ('errors' in parsed) return crmValidationFailed(ctx, 'company', parsed.errors);
|
|
241
|
+
const snap = await ref.get();
|
|
242
|
+
if (!snap.exists) {
|
|
243
|
+
return ApiErrors.notFound({
|
|
244
|
+
message: 'No such company',
|
|
245
|
+
headers: ctx.headers
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const owner = await memberError(ctx, 'ownerUid', parsed.values.ownerUid);
|
|
249
|
+
if (Object.keys(owner).length) return crmValidationFailed(ctx, 'company', owner);
|
|
250
|
+
const custom = await readCrmCustomBody(ctx, body, 'company');
|
|
251
|
+
if (custom && 'errors' in custom) return crmValidationFailed(ctx, 'company', custom.errors);
|
|
252
|
+
const _parsed_values = parsed.values, { name, domain } = _parsed_values, rest = _object_without_properties_loose(_parsed_values, [
|
|
253
|
+
"name",
|
|
254
|
+
"domain"
|
|
255
|
+
]);
|
|
256
|
+
if (domain && domain !== snap.get('domain')) {
|
|
257
|
+
const existing = await ref.parent.where('domain', '==', domain).limit(1).get();
|
|
258
|
+
if (!existing.empty && existing.docs[0].id !== ref.id) {
|
|
259
|
+
return ApiErrors.conflict({
|
|
260
|
+
message: `A company with this domain already exists (${existing.docs[0].id}).`,
|
|
261
|
+
code: 'company_exists',
|
|
262
|
+
headers: ctx.headers
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const update = _extends({}, name !== undefined ? nameSearchFields(name) : {}, updatePayload(_extends({
|
|
267
|
+
domain
|
|
268
|
+
}, rest)), custom && 'values' in custom ? crmCustomUpdate(custom.values) : {});
|
|
269
|
+
// An empty body is a no-op answered with the current company.
|
|
270
|
+
if (Object.keys(update).length > 0) {
|
|
271
|
+
await ref.update(_extends({}, update, {
|
|
272
|
+
updatedAt: Timestamp.now()
|
|
273
|
+
}));
|
|
274
|
+
// What the console's Companies list searches (AGL-3321).
|
|
275
|
+
await restampCrmListFieldsAt(ref, 'companies');
|
|
276
|
+
}
|
|
277
|
+
return apiJson(companyView(await ref.get()), {
|
|
278
|
+
headers: ctx.headers
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* `DELETE /v1/companies/{id}` — the company alone. The deals, tasks and
|
|
283
|
+
* activities filed against it keep their `companyId`; they are records of
|
|
284
|
+
* their own, and a delete that cascaded through them would erase a sales
|
|
285
|
+
* history because somebody removed a duplicate account.
|
|
286
|
+
*/ async function deleteCompany(request, ctx, ref) {
|
|
287
|
+
const claimed = await claimWrite(ctx, '*', request.headers.get('Idempotency-Key'), 'company-deletes');
|
|
288
|
+
if ('replay' in claimed) return claimed.replay;
|
|
289
|
+
const { claim } = claimed;
|
|
290
|
+
try {
|
|
291
|
+
const snap = await ref.get();
|
|
292
|
+
if (!snap.exists) {
|
|
293
|
+
await claim.release();
|
|
294
|
+
return ApiErrors.notFound({
|
|
295
|
+
message: 'No such company',
|
|
296
|
+
headers: ctx.headers
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
await ref.delete();
|
|
300
|
+
const view = {
|
|
301
|
+
id: ref.id,
|
|
302
|
+
object: 'company',
|
|
303
|
+
deleted: true
|
|
304
|
+
};
|
|
305
|
+
await claim.record(200, view);
|
|
306
|
+
return apiJson(view, {
|
|
307
|
+
headers: ctx.headers
|
|
308
|
+
});
|
|
309
|
+
} catch (error) {
|
|
310
|
+
await claim.release();
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* `GET /v1/companies` filters. `domain` is normalized exactly as the write
|
|
316
|
+
* stores it, so `?domain=https://www.Example.com/about` finds `example.com`; a
|
|
317
|
+
* value that is not a domain at all is a `400`, not an empty page, for the
|
|
318
|
+
* reason `listContacts` gives about `?email=`.
|
|
319
|
+
*/ async function listCompanies(ctx, collection, url) {
|
|
320
|
+
const rawDomain = url.searchParams.get('domain');
|
|
321
|
+
const filters = readEqualityFilters(url, [
|
|
322
|
+
'ownerUid'
|
|
323
|
+
]);
|
|
324
|
+
if (rawDomain !== null && rawDomain.trim() !== '') {
|
|
325
|
+
const domain = normalizeCompanyDomain(rawDomain);
|
|
326
|
+
if (!domain) {
|
|
327
|
+
return crmValidationFailed(ctx, 'company filter', {
|
|
328
|
+
domain: 'Must be a domain name, like example.com'
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// The domain is unique, so it is the clause that goes to Firestore.
|
|
332
|
+
filters.unshift({
|
|
333
|
+
field: 'domain',
|
|
334
|
+
value: domain
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return listCrm(ctx, collection, url, filters, companyView);
|
|
338
|
+
}
|
|
339
|
+
export async function handleCompanies(request, ctx, segments, url) {
|
|
340
|
+
const collection = crmCollection(ctx, CRM_COLLECTIONS.companies);
|
|
341
|
+
const [, companyId] = segments;
|
|
342
|
+
if (!companyId) {
|
|
343
|
+
if (request.method === 'GET') {
|
|
344
|
+
const denied = requireScope(ctx, 'crm:read');
|
|
345
|
+
if (denied) return denied;
|
|
346
|
+
return listCompanies(ctx, collection, url);
|
|
347
|
+
}
|
|
348
|
+
if (request.method === 'POST') {
|
|
349
|
+
const denied = requireScope(ctx, 'crm:write');
|
|
350
|
+
if (denied) return denied;
|
|
351
|
+
return createCompany(request, ctx);
|
|
352
|
+
}
|
|
353
|
+
return ApiErrors.methodNotAllowed({
|
|
354
|
+
headers: _extends({}, ctx.headers, {
|
|
355
|
+
Allow: 'GET, POST'
|
|
356
|
+
})
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const ref = collection.doc(companyId);
|
|
360
|
+
if (request.method === 'GET') {
|
|
361
|
+
const denied = requireScope(ctx, 'crm:read');
|
|
362
|
+
if (denied) return denied;
|
|
363
|
+
const snap = await ref.get();
|
|
364
|
+
if (!snap.exists) {
|
|
365
|
+
return ApiErrors.notFound({
|
|
366
|
+
message: 'No such company',
|
|
367
|
+
headers: ctx.headers
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return apiJson(companyView(snap), {
|
|
371
|
+
headers: ctx.headers
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (request.method === 'PATCH') {
|
|
375
|
+
const denied = requireScope(ctx, 'crm:write');
|
|
376
|
+
if (denied) return denied;
|
|
377
|
+
return updateCompany(request, ctx, ref);
|
|
378
|
+
}
|
|
379
|
+
if (request.method === 'DELETE') {
|
|
380
|
+
const denied = requireScope(ctx, 'crm:write');
|
|
381
|
+
if (denied) return denied;
|
|
382
|
+
return deleteCompany(request, ctx, ref);
|
|
383
|
+
}
|
|
384
|
+
return ApiErrors.methodNotAllowed({
|
|
385
|
+
headers: _extends({}, ctx.headers, {
|
|
386
|
+
Allow: 'GET, PATCH, DELETE'
|
|
387
|
+
})
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
//# sourceMappingURL=crm-companies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/server/api-v1/crm-companies.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/companies` (AGL-2606) — the organizations behind the people.\n *\n * A company is keyed, in practice, by its domain: two contacts at\n * `example.com` work for one company, and the console's auto-association\n * (`companyDomainForEmail`) files them under it by that string. So the\n * domain is normalized exactly as the console stores it, a second company on\n * a domain already held is a `409 company_exists` naming the first — the\n * contact create's own rule for a second row on one email — and `?domain=`\n * is the lookup a sync starts with.\n */\nimport { nameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n CRM_COLLECTIONS,\n CRM_MEDIA_IDS_MAX,\n createResourceUid,\n crmNewRecordListFields,\n normalizeAddress,\n normalizeCompanyDomain,\n normalizeCrmMediaIds,\n normalizePhone,\n} from '@aglyn/aglyn/server'\nimport { apiJson, ApiErrors, restampCrmListFieldsAt } from '@aglyn/tenant-data-admin'\nimport { 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_LABEL_MAX,\n CRM_TEXT_MAX,\n CRM_TITLE_MAX,\n createPayload,\n crmCollection,\n crmCreateStamp,\n crmCustomUpdate,\n crmCustomView,\n crmRecordsBandRefusal,\n crmTimes,\n crmValidationFailed,\n isoFromMs,\n listCrm,\n memberError,\n readCrmCustomBody,\n readCrmSite,\n readEqualityFilters,\n readOptionalText,\n refuseUnknownKeys,\n updatePayload,\n} from './crm-shared'\n\n/** The company object as published. Every writable field appears here. */\nfunction companyView(doc: FirebaseFirestore.DocumentSnapshot) {\n const data = doc.data() ?? {}\n return {\n id: doc.id,\n object: 'company',\n name: data.name ?? null,\n domain: data.domain ?? null,\n website: data.website ?? null,\n phone: data.phone ?? null,\n address: data.address ?? null,\n industry: data.industry ?? null,\n ownerUid: data.ownerUid ?? null,\n notes: data.notes ?? null,\n // The org's company custom fields, keyed by field key (AGL-2661).\n custom: crmCustomView(data),\n // When the earliest open task against the company is due — see\n // `CrmDeal.nextTaskAtMs`. Read-only: the tasks resource maintains it.\n nextTaskAt: isoFromMs(data.nextTaskAtMs),\n mediaIds: normalizeCrmMediaIds(data.mediaIds),\n siteId: data.hostId ?? null,\n ...crmTimes(data),\n }\n}\n\nconst COMPANY_WRITABLE = new Set([\n 'name',\n 'domain',\n 'website',\n 'phone',\n 'address',\n 'industry',\n 'ownerUid',\n 'notes',\n 'custom',\n 'mediaIds',\n])\n\ninterface CompanyInput {\n name?: string\n domain?: Clearable<string>\n website?: Clearable<string>\n phone?: Clearable<string>\n address?: Clearable<ReturnType<typeof normalizeAddress>>\n industry?: Clearable<string>\n ownerUid?: Clearable<string>\n notes?: Clearable<string>\n /** Org-library files attached to the company (AGL-2662), by media id. */\n mediaIds?: string[]\n}\n\n/**\n * The writable half of a company, validated. `partial` separates PATCH from\n * POST as every reader in this API does: a create needs a name, an update\n * may send one field alone. Unknown keys are named, never dropped.\n *\n * `domain` and `phone` go through the console's own normalizers so the API\n * cannot store a value the console would never have written, and a value\n * that does not survive normalization names the field rather than storing\n * the raw string — a raw domain would never match the auto-association, and\n * a raw phone is the unusable half-number `normalizePhone` exists to end.\n */\nfunction readCompanyInput(\n body: Record<string, unknown>,\n { partial }: { partial: boolean },\n): { values: CompanyInput } | { errors: Record<string, string> } {\n const errors: Record<string, string> = {}\n const values: CompanyInput = {}\n const allowed = new Set(COMPANY_WRITABLE)\n if (!partial) allowed.add('consentSiteId')\n refuseUnknownKeys(body, allowed, 'company', errors)\n\n if (body.name !== undefined || !partial) {\n const name = String(body.name ?? '')\n .trim()\n .slice(0, CRM_TITLE_MAX)\n if (name) values.name = name\n else errors.name = partial ? 'Must not be empty' : 'A name is required'\n }\n\n\n if (body.mediaIds !== undefined) {\n if (!Array.isArray(body.mediaIds)) {\n errors.mediaIds = 'Must be an array of media ids'\n } else if (body.mediaIds.length > CRM_MEDIA_IDS_MAX) {\n errors.mediaIds = `At most ${CRM_MEDIA_IDS_MAX} files may be attached`\n } else {\n // The SAME normalizer the console card writes through, so the API\n // cannot store a list the console would refuse. An empty array is\n // legal and clears the attachments.\n values.mediaIds = normalizeCrmMediaIds(body.mediaIds)\n }\n }\n\n const domain = readOptionalText(body, 'domain', CRM_TITLE_MAX, errors)\n if (domain) {\n const normalized = normalizeCompanyDomain(domain)\n if (normalized) values.domain = normalized\n else errors.domain = 'Must be a domain name, like example.com'\n } else if (domain === null) {\n values.domain = null\n }\n\n const website = readOptionalText(body, 'website', CRM_TITLE_MAX * 4, errors)\n if (website) {\n // A bare `example.com` is what people paste; stored with the scheme so the\n // console can render it as a link without guessing one.\n const withScheme = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(website)\n ? website\n : `https://${website}`\n let parsed: URL | null\n try {\n parsed = new URL(withScheme)\n } catch {\n parsed = null\n }\n if (parsed && /^https?:$/.test(parsed.protocol) && normalizeCompanyDomain(parsed.hostname)) {\n values.website = parsed.toString()\n } else {\n errors.website = 'Must be a web address, like https://example.com'\n }\n } else if (website === null) {\n values.website = null\n }\n\n const phone = readOptionalText(body, 'phone', CRM_LABEL_MAX, errors)\n if (phone) {\n const normalized = normalizePhone(phone)\n if (normalized) values.phone = normalized\n else errors.phone = 'Must be a phone number with a country code, like +15125550123'\n } else if (phone === null) {\n values.phone = null\n }\n\n if (body.address !== undefined) {\n if (body.address === null) {\n values.address = null\n } else if (typeof body.address !== 'object' || Array.isArray(body.address)) {\n errors.address = 'Must be an address object'\n } else {\n // A blank address normalizes to `null`, which clears — an object of\n // empty strings must not read as \"has an address\".\n values.address = normalizeAddress(body.address as never)\n }\n }\n\n const industry = readOptionalText(body, 'industry', CRM_LABEL_MAX, errors)\n if (industry !== undefined) values.industry = industry\n\n const ownerUid = readOptionalText(body, 'ownerUid', CRM_LABEL_MAX, errors)\n if (ownerUid !== undefined) values.ownerUid = ownerUid\n\n const notes = readOptionalText(body, 'notes', CRM_TEXT_MAX, errors)\n if (notes !== undefined) values.notes = notes\n\n // `custom` is shape-checked here and judged against the org's\n // definitions by the writers, which is where the read is paid.\n if (body.custom !== undefined) {\n if (!body.custom || typeof body.custom !== 'object' || Array.isArray(body.custom)) {\n errors.custom = 'Must be an object of field values keyed by field key'\n }\n }\n\n return Object.keys(errors).length ? { errors } : { values }\n}\n\n/**\n * `POST /v1/companies`. Claimed first and released on the duplicate, the\n * ordering `createContact` argues for: a refusal that clears when the\n * duplicate is removed must not burn the key the retry will carry.\n */\nasync function createCompany(\n request: Request,\n ctx: ApiV1Context,\n): Promise<Response> {\n const body = await readJsonBody(request)\n const parsed = readCompanyInput(body, { partial: false })\n if ('errors' in parsed) return crmValidationFailed(ctx, 'company', parsed.errors)\n const site = readCrmSite(ctx, 'company', body)\n if ('response' in site) return site.response\n const owner = await memberError(ctx, 'ownerUid', parsed.values.ownerUid)\n if (Object.keys(owner).length) return crmValidationFailed(ctx, 'company', owner)\n // Judged against the org's company definitions, above the claim like\n // every other deterministic 400 (AGL-2661).\n const custom = await readCrmCustomBody(ctx, body, 'company')\n if (custom && 'errors' in custom) return crmValidationFailed(ctx, 'company', custom.errors)\n const customValues = custom && 'values' in custom ? custom.values : {}\n\n const collection = crmCollection(ctx, CRM_COLLECTIONS.companies)\n const claimed = await claimWrite(\n ctx,\n '*',\n request.headers.get('Idempotency-Key'),\n 'companies',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const { name, domain, ...rest } = parsed.values\n if (domain) {\n const existing = await collection.where('domain', '==', domain).limit(1).get()\n if (!existing.empty) {\n await claim.release()\n return ApiErrors.conflict({\n message: `A company with this domain already exists (${existing.docs[0].id}). Update it instead.`,\n code: 'company_exists',\n headers: ctx.headers,\n })\n }\n }\n // The records band (AGL-2611), after the duplicate check for the reason\n // the claim ordering gives: a refusal that clears when the band is raised\n // must not burn the key the retry will carry.\n const bandFull = await crmRecordsBandRefusal(ctx)\n if (bandFull) {\n await claim.release()\n return bandFull\n }\n const id = createResourceUid()\n const record: Record<string, unknown> = {\n // `nameLower`/`nameTokens` travel with the name: the console's company\n // list searches the collection, not the page it fetched.\n ...nameSearchFields(name ?? ''),\n ...createPayload({ domain, ...rest }),\n // An empty map writes no `custom` key at all; a `null` inside one is\n // an explicit clear a fresh record has no use for.\n ...(Object.keys(customValues).length\n ? { custom: createPayload(customValues) }\n : {}),\n ...crmCreateStamp(ctx, site.siteId),\n }\n // What the console's Companies list searches and filters by (AGL-3321).\n await collection.doc(id).create({ ...record, ...crmNewRecordListFields('companies', record) })\n const view = companyView(await collection.doc(id).get())\n // Stored as 200 so a replay is distinguishable from the fresh 201.\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/companies/{id}` — no key, for `updateContact`'s reason. */\nasync function updateCompany(\n request: Request,\n ctx: ApiV1Context,\n ref: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const body = await readJsonBody(request)\n const parsed = readCompanyInput(body, { partial: true })\n if ('errors' in parsed) return crmValidationFailed(ctx, 'company', parsed.errors)\n const snap = await ref.get()\n if (!snap.exists) {\n return ApiErrors.notFound({ message: 'No such company', headers: ctx.headers })\n }\n const owner = await memberError(ctx, 'ownerUid', parsed.values.ownerUid)\n if (Object.keys(owner).length) return crmValidationFailed(ctx, 'company', owner)\n const custom = await readCrmCustomBody(ctx, body, 'company')\n if (custom && 'errors' in custom) return crmValidationFailed(ctx, 'company', custom.errors)\n\n const { name, domain, ...rest } = parsed.values\n if (domain && domain !== snap.get('domain')) {\n const existing = await ref.parent.where('domain', '==', domain).limit(1).get()\n if (!existing.empty && existing.docs[0].id !== ref.id) {\n return ApiErrors.conflict({\n message: `A company with this domain already exists (${existing.docs[0].id}).`,\n code: 'company_exists',\n headers: ctx.headers,\n })\n }\n }\n const update: Record<string, unknown> = {\n ...(name !== undefined ? nameSearchFields(name) : {}),\n ...updatePayload({ domain, ...rest }),\n ...(custom && 'values' in custom ? crmCustomUpdate(custom.values) : {}),\n }\n // An empty body is a no-op answered with the current company.\n if (Object.keys(update).length > 0) {\n await ref.update({ ...update, updatedAt: Timestamp.now() })\n // What the console's Companies list searches (AGL-3321).\n await restampCrmListFieldsAt(ref, 'companies')\n }\n return apiJson(companyView(await ref.get()), { headers: ctx.headers })\n}\n\n/**\n * `DELETE /v1/companies/{id}` — the company alone. The deals, tasks and\n * activities filed against it keep their `companyId`; they are records of\n * their own, and a delete that cascaded through them would erase a sales\n * history because somebody removed a duplicate account.\n */\nasync function deleteCompany(\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 'company-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 company', headers: ctx.headers })\n }\n await ref.delete()\n const view = { id: ref.id, object: 'company', 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/**\n * `GET /v1/companies` filters. `domain` is normalized exactly as the write\n * stores it, so `?domain=https://www.Example.com/about` finds `example.com`; a\n * value that is not a domain at all is a `400`, not an empty page, for the\n * reason `listContacts` gives about `?email=`.\n */\nasync function listCompanies(\n ctx: ApiV1Context,\n collection: FirebaseFirestore.CollectionReference,\n url: URL,\n): Promise<Response> {\n const rawDomain = url.searchParams.get('domain')\n const filters = readEqualityFilters(url, ['ownerUid'])\n if (rawDomain !== null && rawDomain.trim() !== '') {\n const domain = normalizeCompanyDomain(rawDomain)\n if (!domain) {\n return crmValidationFailed(ctx, 'company filter', {\n domain: 'Must be a domain name, like example.com',\n })\n }\n // The domain is unique, so it is the clause that goes to Firestore.\n filters.unshift({ field: 'domain', value: domain })\n }\n return listCrm(ctx, collection, url, filters, companyView)\n}\n\nexport async function handleCompanies(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const collection = crmCollection(ctx, CRM_COLLECTIONS.companies)\n const [, companyId] = segments\n\n if (!companyId) {\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'crm:read')\n if (denied) return denied\n return listCompanies(ctx, collection, url)\n }\n if (request.method === 'POST') {\n const denied = requireScope(ctx, 'crm:write')\n if (denied) return denied\n return createCompany(request, ctx)\n }\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, POST' },\n })\n }\n\n const ref = collection.doc(companyId)\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 company', headers: ctx.headers })\n }\n return apiJson(companyView(snap), { headers: ctx.headers })\n }\n if (request.method === 'PATCH') {\n const denied = requireScope(ctx, 'crm:write')\n if (denied) return denied\n return updateCompany(request, ctx, ref)\n }\n if (request.method === 'DELETE') {\n const denied = requireScope(ctx, 'crm:write')\n if (denied) return denied\n return deleteCompany(request, ctx, ref)\n }\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, PATCH, DELETE' },\n })\n}\n"],"names":["nameSearchFields","CRM_COLLECTIONS","CRM_MEDIA_IDS_MAX","createResourceUid","crmNewRecordListFields","normalizeAddress","normalizeCompanyDomain","normalizeCrmMediaIds","normalizePhone","apiJson","ApiErrors","restampCrmListFieldsAt","Timestamp","claimWrite","readJsonBody","requireScope","CRM_LABEL_MAX","CRM_TEXT_MAX","CRM_TITLE_MAX","createPayload","crmCollection","crmCreateStamp","crmCustomUpdate","crmCustomView","crmRecordsBandRefusal","crmTimes","crmValidationFailed","isoFromMs","listCrm","memberError","readCrmCustomBody","readCrmSite","readEqualityFilters","readOptionalText","refuseUnknownKeys","updatePayload","companyView","doc","data","id","object","name","domain","website","phone","address","industry","ownerUid","notes","custom","nextTaskAt","nextTaskAtMs","mediaIds","siteId","hostId","COMPANY_WRITABLE","Set","readCompanyInput","body","partial","errors","values","allowed","add","undefined","String","trim","slice","Array","isArray","length","normalized","withScheme","test","parsed","URL","protocol","hostname","toString","Object","keys","createCompany","request","ctx","site","response","owner","customValues","collection","companies","claimed","headers","get","replay","claim","rest","existing","where","limit","empty","release","conflict","message","docs","code","bandFull","record","create","view","status","error","updateCompany","ref","snap","exists","notFound","parent","update","updatedAt","now","deleteCompany","delete","deleted","listCompanies","url","rawDomain","searchParams","filters","unshift","field","value","handleCompanies","segments","companyId","method","denied","methodNotAllowed","Allow"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
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 ApiV1Context } from '@aglyn/tenant-data-admin/server/api-v1-kit';
|
|
18
|
+
export declare function handleDeals(request: Request, ctx: ApiV1Context, segments: string[], url: URL): Promise<Response>;
|