@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/contact-update.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 * `POST /api/crm/contact-update` — one holder's profile fields on one or\n * more contacts (AGL-2804).\n *\n * Body: `{ hostId, contactIds, set }` under a site, or `{ orgId, hostId?,\n * contactIds, set }` at the organization level, where `set` is\n * `ContactUpdateFields`. Answers `{ ok, results }`: one outcome per contact,\n * in the order the ids were sent.\n *\n * ## Why every facet write comes here\n *\n * A contact's fields live in the facet of the holder that keeps them —\n * `facets.{groupId}.*` — and the Firestore rules cannot tell one field of a\n * facet from another: the holder is a map key a rule cannot address, and a\n * write's affected keys name only `facets`. So the rules let a client update\n * change nothing in a facet beyond letting a holder go, and the console's\n * edits arrive here, where the plan is asked about the fields the request\n * actually carries.\n *\n * ## Who may call it\n *\n * The CRM writer the task routes admit (`authorizeCrmWriter`): under a site,\n * a member whose role writes org data, who holds `data.manage` and whose\n * access reaches the site; at the organization level, an org-wide member\n * with the permission. Each contact must then be one the caller may read —\n * the rules' `canReadScoped()` — and, under a site, one the site's hub lists.\n *\n * ## The plan\n *\n * Every field is the CRM's (AGL-2790), and a plan without the CRM has none\n * of it (AGL-2851), so it edits no contact: not the owner, company, custom\n * values or files, and not the profile a capture writes (name, phone, job\n * title, address), the tags, the notes or the campaign filing either. A\n * request carrying any field is refused to such a plan once the caller is\n * known, and before a contact is read — staff included, because the plan is\n * a fact about the workspace. Erasing a person is another route, open on\n * every plan.\n *\n * ## One holder, by dotted path\n *\n * Under a site the facet is the site's consent group's; at the organization\n * level it is each contact's own primary holder, the facet the record page\n * shows there. Every field is one dotted path — a nested `facets` object\n * would replace every other holder's map — and a text field sent empty is\n * deleted rather than stored blank, so \"no phone\" has one shape.\n *\n * ## Many contacts, one commit\n *\n * The contacts are read together and written in one batch. A batch that\n * fails is retried contact by contact, so the one that could not be written\n * is named and the rest still land. A company's contacts count follows the\n * link: summed per company and settled once the contacts are written, so a\n * company deleted in between cannot fail the save of the people at it — the\n * company page's live aggregate is what corrects a count that did not move.\n */\n\nimport {\n type AglynPostalAddress,\n consentGroupForHost,\n CONTACT_FIELDS_MAX_PER_ORG,\n type ContactCompanyLinkPlan,\n type ContactCustomValue,\n type ContactFieldDefinition,\n campaignMembershipValue,\n contactCampaignFieldPath,\n contactFacetPath,\n contactPrimaryGroup,\n CRM_COLLECTIONS,\n CRM_LEAD_SOURCE_PICKLIST,\n CRM_MEDIA_IDS_FIELD,\n crmReadTokens,\n effectiveCrmLeadSourcePicklist,\n isOrgWideMember,\n judgeCrmLeadSource,\n normalizeAddress,\n normalizeCrmMediaIds,\n normalizeCrmPicklistLabel,\n normalizePhone,\n type PluginApiHandler,\n planContactCompanyLink,\n readContactCompanyLink,\n readContactFacet,\n readCrmCustomInput,\n visibleToTokens,\n} from '@aglyn/aglyn/server'\nimport {\n companyContactsCountFields,\n contactCompanyLinkFields,\n firebaseAdmin,\n resolveOrgMembership,\n restampCrmListFieldsOf,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n CONTACT_UPDATE_FIELDS,\n type ContactUpdateFields,\n type ContactUpdateOutcome,\n type ContactUpdateResponse,\n CRM_CONTACT_UPDATE_MAX,\n} from '../model/contact-update'\nimport {\n CONTACT_NOTES_MAX,\n CONTACT_PHONE_REFUSAL,\n CONTACT_TAGS_MAX,\n normalizeTags,\n readBulkTag,\n typed,\n} from './contact-profile'\nimport { readCrmRouteScope } from './org-caller'\nimport { crmSuiteRefusal } from './suite-gate'\nimport { authorizeCrmWriter, canReach, type Writer } from './task-routes'\n\n/** What the suite gate names for each field a request can carry. */\nconst SUITE_ACTS: Record<keyof ContactUpdateFields, string> = {\n name: \"Editing a contact's profile\",\n phone: \"Editing a contact's profile\",\n jobTitle: \"Editing a contact's profile\",\n leadSource: \"Editing a contact's profile\",\n address: \"Editing a contact's profile\",\n notes: \"Editing a contact's notes\",\n tags: 'Tagging a contact',\n addTag: 'Tagging a contact',\n removeTag: 'Tagging a contact',\n campaignIds: 'Filing a contact under a campaign',\n ownerUid: \"Assigning a contact's owner\",\n companyId: 'Filing a contact under a company',\n companyName: 'Filing a contact under a company',\n custom: \"Editing a contact's custom fields\",\n mediaIds: 'Attaching files to a contact',\n}\n\n/** A contact read at the organization level that no site has captured. */\nconst NO_HOLDER_REFUSAL = 'No site holds this contact yet, so it has no profile to edit.'\n\n/**\n * The act a request is refused for on a plan without the CRM: the first\n * field it carries, in the order the route reads them. Every field is the\n * CRM's, so whichever field a request names is the act its refusal names.\n */\nexport function contactUpdateSuiteAct(fields: ContactUpdateFields): string {\n const field = CONTACT_UPDATE_FIELDS.find((key) => fields[key] !== undefined)\n return field ? SUITE_ACTS[field] : \"Editing a contact's profile\"\n}\n\n/**\n * A body's `set`, read into the fields it names, or the sentence refusing\n * it. A key the route does not save is refused rather than dropped: a save\n * that silently lost a field would read as a save that kept it.\n */\nexport function readContactUpdateFields(\n input: unknown,\n): { ok: true; fields: ContactUpdateFields } | { ok: false; error: string } {\n if (!input || typeof input !== 'object' || Array.isArray(input)) {\n return { ok: false, error: 'Name the fields to save.' }\n }\n const raw = input as Record<string, unknown>\n const unknownKey = Object.keys(raw).find(\n (key) => !(CONTACT_UPDATE_FIELDS as readonly string[]).includes(key),\n )\n if (unknownKey) {\n return { ok: false, error: `\"${unknownKey}\" is not a field a contact's profile saves.` }\n }\n const fields: ContactUpdateFields = {}\n if ('name' in raw) fields.name = typed(raw['name'], 120)\n if ('phone' in raw) {\n const text = typed(raw['phone'], 40)\n const phone = text ? normalizePhone(text) : ''\n if (phone === null) return { ok: false, error: CONTACT_PHONE_REFUSAL }\n fields.phone = phone\n }\n if ('jobTitle' in raw) fields.jobTitle = typed(raw['jobTitle'], 120)\n // Held to the label's shape here; judged against the org's list by the\n // handler, which alone can read it (AGL-3298).\n if ('leadSource' in raw) fields.leadSource = normalizeCrmPicklistLabel(raw['leadSource'])\n if ('address' in raw) {\n const address = raw['address']\n fields.address =\n address && typeof address === 'object' && !Array.isArray(address)\n ? normalizeAddress(address as AglynPostalAddress)\n : null\n }\n if ('notes' in raw) fields.notes = String(raw['notes'] ?? '').slice(0, CONTACT_NOTES_MAX)\n // One write may carry one transform per field, so the tags are replaced,\n // added to or taken from — never two of those at once.\n if (['tags', 'addTag', 'removeTag'].filter((key) => key in raw).length > 1) {\n return { ok: false, error: 'Send the tags, a tag to add or a tag to remove — one of them.' }\n }\n if ('tags' in raw) {\n if (!Array.isArray(raw['tags'])) return { ok: false, error: 'Tags must be a list.' }\n fields.tags = normalizeTags(raw['tags'])\n }\n for (const key of ['addTag', 'removeTag'] as const) {\n if (!(key in raw)) continue\n const tag = readBulkTag(raw[key])\n if (!tag) return { ok: false, error: 'Name the tag.' }\n fields[key] = tag\n }\n if ('campaignIds' in raw) {\n const ids = raw['campaignIds']\n if (!Array.isArray(ids)) return { ok: false, error: 'Campaigns must be a list.' }\n fields.campaignIds = campaignMembershipValue(ids.map((id) => String(id ?? '')))\n }\n if ('ownerUid' in raw) fields.ownerUid = typed(raw['ownerUid'], 128)\n if ('companyId' in raw) {\n const companyId = raw['companyId'] === null ? null : typed(raw['companyId'], 128)\n if (companyId !== null && (!companyId || companyId.includes('/'))) {\n return { ok: false, error: 'The company could not be read.' }\n }\n fields.companyId = companyId\n }\n if ('companyName' in raw) fields.companyName = typed(raw['companyName'], 120)\n if ('custom' in raw) {\n const custom = raw['custom']\n if (!custom || typeof custom !== 'object' || Array.isArray(custom)) {\n return { ok: false, error: 'Custom fields must be values keyed by field key.' }\n }\n fields.custom = custom as Record<string, ContactCustomValue>\n }\n if ('mediaIds' in raw) {\n if (!Array.isArray(raw['mediaIds'])) {\n return { ok: false, error: 'Files must be a list of ids.' }\n }\n fields.mediaIds = normalizeCrmMediaIds(raw['mediaIds'])\n }\n if (!Object.keys(fields).length) return { ok: false, error: 'Name the fields to save.' }\n return { ok: true, fields }\n}\n\n/** The ids a body names, deduplicated — or `null` when any is unreadable or there are too many. */\nfunction readContactIds(value: unknown): string[] | null {\n if (!Array.isArray(value) || !value.length || value.length > CRM_CONTACT_UPDATE_MAX) {\n return null\n }\n const ids = value.map((id) => typed(id, 200))\n if (ids.some((id) => !id || id.includes('/'))) return null\n return [...new Set(ids)]\n}\n\n/**\n * Under a site, whether the caller's access reaches it. The rules could not\n * ask this of a facet write — they cannot see which holder a write is for —\n * and a collaborator on one site must not write another site's notes on a\n * person both of them hold.\n */\nfunction reachesSite(writer: Writer, hostId: string): boolean {\n return (\n writer.staff ||\n isOrgWideMember(writer.member) ||\n Boolean((writer.member?.hostAccess as Record<string, unknown> | undefined)?.[hostId])\n )\n}\n\nconst refused = (contactId: string, error: string): ContactUpdateOutcome => ({\n contactId,\n ok: false,\n error,\n})\n\ninterface ContactWrite {\n contactId: string\n ref: FirebaseFirestore.DocumentReference\n update: Record<string, unknown>\n counts: ContactCompanyLinkPlan['counts']\n}\n\n/**\n * One contact's update for one holder, or the sentence refusing the row.\n * `update` is `null` when the document already says everything asked — a tag\n * the row already carries, a link already made.\n */\nfunction contactPatch(\n contact: Record<string, unknown>,\n groupId: string,\n fields: ContactUpdateFields,\n custom: Record<string, ContactCustomValue> | undefined,\n linkedCompanyName: string,\n):\n | { update: Record<string, unknown> | null; counts: ContactCompanyLinkPlan['counts'] }\n | { refused: string } {\n const path = (field: string) => contactFacetPath(groupId, field)\n /** A text field as stored: the value, or the field's absence. */\n const text = (value: string) => value || FieldValue.delete()\n const update: Record<string, unknown> = {}\n let counts: ContactCompanyLinkPlan['counts'] = []\n\n if (fields.name !== undefined) update[path('name')] = text(fields.name)\n if (fields.phone !== undefined) {\n update[path('phone')] = text(fields.phone)\n // The search echo — see `HostContact.phone`.\n update['phone'] = text(fields.phone)\n }\n if (fields.jobTitle !== undefined) update[path('jobTitle')] = text(fields.jobTitle)\n if (fields.leadSource !== undefined) update[path('leadSource')] = text(fields.leadSource)\n if (fields.address !== undefined) {\n update[path('address')] = fields.address ?? FieldValue.delete()\n }\n if (fields.notes !== undefined) update[path('notes')] = fields.notes\n if (fields.tags !== undefined) update[path('tags')] = fields.tags\n\n const held = (readContactFacet(contact, groupId).tags ?? []).map((tag) =>\n String(tag).toLowerCase(),\n )\n if (fields.addTag && !held.includes(fields.addTag)) {\n // The record page's cap, which a selection must not slip past.\n if (held.length >= CONTACT_TAGS_MAX) {\n return { refused: `already has ${CONTACT_TAGS_MAX} tags` }\n }\n update[path('tags')] = FieldValue.arrayUnion(fields.addTag)\n }\n if (fields.removeTag && held.includes(fields.removeTag)) {\n update[path('tags')] = FieldValue.arrayRemove(fields.removeTag)\n }\n if (fields.campaignIds !== undefined) {\n update[contactCampaignFieldPath(groupId)] = fields.campaignIds\n }\n if (fields.ownerUid !== undefined) update[path('ownerUid')] = text(fields.ownerUid)\n\n if (fields.companyId !== undefined) {\n const plan = planContactCompanyLink(\n readContactCompanyLink(contact, groupId),\n fields.companyId,\n )\n if (plan) {\n Object.assign(update, contactCompanyLinkFields(plan, groupId))\n counts = plan.counts\n if (fields.companyName === undefined) {\n // The label follows the link: the company's own name, or nothing.\n const label = plan.companyId ? linkedCompanyName : ''\n update[path('companyName')] = text(label)\n // The search echo — see `HostContact.companyName`.\n update['companyName'] = text(label)\n }\n }\n }\n if (fields.companyName !== undefined) {\n update[path('companyName')] = text(fields.companyName)\n update['companyName'] = text(fields.companyName)\n }\n for (const [key, value] of Object.entries(custom ?? {})) {\n update[path(`custom.${key}`)] = value\n }\n if (fields.mediaIds !== undefined) update[path(CRM_MEDIA_IDS_FIELD)] = fields.mediaIds\n\n if (!Object.keys(update).length) return { update: null, counts }\n update['updatedAt'] = FieldValue.serverTimestamp()\n return { update, counts }\n}\n\n/**\n * Every write in one batch, and — when the batch is refused — each on its\n * own, so the contact that could not be written is the only one reported.\n * Answers the ids that were not written.\n */\nasync function commitContactWrites(\n firestore: FirebaseFirestore.Firestore,\n writes: readonly ContactWrite[],\n): Promise<Set<string>> {\n const failed = new Set<string>()\n if (!writes.length) return failed\n const batch = firestore.batch()\n for (const write of writes) batch.update(write.ref, write.update)\n let committed = false\n try {\n await batch.commit()\n committed = true\n } catch {\n // A batch names no document; the pass below finds the one it was.\n }\n if (!committed) {\n for (const write of writes) {\n try {\n await write.ref.update(write.update)\n } catch (error) {\n console.error('[crm] contact-update could not write a contact', write.contactId, error)\n failed.add(write.contactId)\n }\n }\n }\n // The owner, tags, company and custom values are what the Contacts list\n // filters by through `facetKeys` (AGL-3321): restamped from each row.\n await restampCrmListFieldsOf(\n writes.filter((write) => !failed.has(write.contactId)).map((write) => write.ref),\n 'contacts',\n )\n return failed\n}\n\n/** Each company's contacts count moved once, by the sum of what the written links moved. */\nasync function settleCompanyCounts(\n companies: FirebaseFirestore.CollectionReference,\n writes: readonly ContactWrite[],\n): Promise<void> {\n const deltas = new Map<string, number>()\n for (const write of writes) {\n for (const count of write.counts) {\n deltas.set(count.companyId, (deltas.get(count.companyId) ?? 0) + count.delta)\n }\n }\n await Promise.all(\n [...deltas]\n .filter(([, delta]) => delta !== 0)\n .map(([companyId, delta]) =>\n companies\n .doc(companyId)\n .update(companyContactsCountFields(delta))\n .catch((error: unknown) => {\n console.error('[crm] company contacts count could not move', companyId, delta, error)\n }),\n ),\n )\n}\n\nexport const crmContactUpdateHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const scope = readCrmRouteScope(body)\n if (!scope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const contactIds = readContactIds(body['contactIds'])\n if (!contactIds) {\n res.status(400).json({ error: `Name between 1 and ${CRM_CONTACT_UPDATE_MAX} contacts.` })\n return\n }\n const read = readContactUpdateFields(body['set'])\n if (read.ok === false) {\n res.status(400).json({ error: read.error })\n return\n }\n const { fields } = read\n\n try {\n const writer = await authorizeCrmWriter(req, scope, { suiteAct: null })\n if (writer.ok === false) {\n res.status(writer.status).json(writer.body)\n return\n }\n if (scope.level === 'site' && !reachesSite(writer, scope.hostId)) {\n res.status(403).json({ error: 'Your access to this organization does not reach this site.' })\n return\n }\n // The plan after the person, and before any contact is read (AGL-2787).\n const suite = crmSuiteRefusal(writer.org, contactUpdateSuiteAct(fields))\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(writer.orgId)\n const siteGroup =\n scope.level === 'site' ? consentGroupForHost(writer.org, scope.hostId) : null\n const siteTokens = siteGroup ? crmReadTokens(siteGroup) : null\n\n let custom: Record<string, ContactCustomValue> | undefined\n if (fields.custom) {\n const definitions = await orgRef\n .collection(CRM_COLLECTIONS.contactFields)\n .limit(CONTACT_FIELDS_MAX_PER_ORG)\n .get()\n const judged = readCrmCustomInput(\n fields.custom,\n definitions.docs.map((doc) => doc.data() as ContactFieldDefinition),\n 'contact',\n )\n if ('errors' in judged) {\n res.status(400).json({\n error: Object.values(judged.errors)[0] ?? 'A custom value could not be saved.',\n })\n return\n }\n custom = judged.values\n }\n\n /*\n * THE ORG'S LEAD SOURCES (AGL-3298), read only when the body names one.\n * Judged per contact below, because a contact keeps the value it\n * already holds even when the list has since deactivated it.\n */\n const leadSources = fields.leadSource\n ? effectiveCrmLeadSourcePicklist(\n (\n await orgRef.collection(CRM_COLLECTIONS.picklists).doc(CRM_LEAD_SOURCE_PICKLIST).get()\n ).data(),\n )\n : null\n\n if (fields.ownerUid) {\n const owner = await resolveOrgMembership(fields.ownerUid, writer.orgId).catch(() => null)\n if (!owner?.member) {\n res.status(400).json({ error: 'The owner is not a member of this organization.' })\n return\n }\n }\n\n // The company a link names has to exist and be visible to the caller and\n // the site, or a person could be filed under a record nobody here can open.\n let linkedCompanyName = ''\n if (fields.companyId) {\n const company = await orgRef\n .collection(CRM_COLLECTIONS.companies)\n .doc(fields.companyId)\n .get()\n const tokens = company.exists ? (company.get('visibleTo') as string[] | undefined) : undefined\n const visible =\n company.exists &&\n canReach(writer, tokens) &&\n (!siteTokens || visibleToTokens(tokens, siteTokens))\n if (!visible) {\n res.status(404).json({ error: 'Unknown company' })\n return\n }\n linkedCompanyName = typed(company.get('name'), 120)\n }\n\n const contacts = orgRef.collection('contacts')\n const snapshots = await firestore.getAll(...contactIds.map((id) => contacts.doc(id)))\n const outcomes = new Map<string, ContactUpdateOutcome>()\n const writes: ContactWrite[] = []\n snapshots.forEach((snapshot, index) => {\n const contactId = contactIds[index]\n if (!snapshot.exists) {\n outcomes.set(contactId, refused(contactId, 'That contact no longer exists.'))\n return\n }\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n const visibleTo = data['visibleTo'] as string[] | undefined\n if (!canReach(writer, visibleTo)) {\n outcomes.set(contactId, refused(contactId, 'That contact is not visible to you.'))\n return\n }\n if (siteTokens && !visibleToTokens(visibleTo, siteTokens)) {\n outcomes.set(contactId, refused(contactId, 'That contact is not visible to this site.'))\n return\n }\n const group = siteGroup ?? contactPrimaryGroup(data, writer.org)\n if (!group.groupId) {\n outcomes.set(contactId, refused(contactId, NO_HOLDER_REFUSAL))\n return\n }\n let rowFields = fields\n if (leadSources && fields.leadSource) {\n const judged = judgeCrmLeadSource(\n leadSources,\n fields.leadSource,\n readContactFacet(data, group.groupId).leadSource,\n )\n if (judged.ok === false) {\n outcomes.set(contactId, refused(contactId, judged.error))\n return\n }\n rowFields = { ...fields, leadSource: judged.value ?? '' }\n }\n const patch = contactPatch(data, group.groupId, rowFields, custom, linkedCompanyName)\n if ('refused' in patch) {\n outcomes.set(contactId, refused(contactId, patch.refused))\n return\n }\n outcomes.set(contactId, { contactId, ok: true })\n if (patch.update) {\n writes.push({ contactId, ref: snapshot.ref, update: patch.update, counts: patch.counts })\n }\n })\n\n const failed = await commitContactWrites(firestore, writes)\n for (const contactId of failed) {\n outcomes.set(contactId, refused(contactId, 'The contact could not be saved.'))\n }\n await settleCompanyCounts(\n orgRef.collection(CRM_COLLECTIONS.companies),\n writes.filter((write) => !failed.has(write.contactId)),\n )\n\n const answer: ContactUpdateResponse = {\n ok: true,\n results: contactIds.map(\n (contactId) => outcomes.get(contactId) ?? refused(contactId, 'The contact could not be saved.'),\n ),\n }\n res.status(200).json(answer)\n } catch (error) {\n console.error('[crm] contact-update failed', error)\n res.status(500).json({ error: 'The contact could not be saved.' })\n }\n}\n"],"names":["consentGroupForHost","CONTACT_FIELDS_MAX_PER_ORG","campaignMembershipValue","contactCampaignFieldPath","contactFacetPath","contactPrimaryGroup","CRM_COLLECTIONS","CRM_LEAD_SOURCE_PICKLIST","CRM_MEDIA_IDS_FIELD","crmReadTokens","effectiveCrmLeadSourcePicklist","isOrgWideMember","judgeCrmLeadSource","normalizeAddress","normalizeCrmMediaIds","normalizeCrmPicklistLabel","normalizePhone","planContactCompanyLink","readContactCompanyLink","readContactFacet","readCrmCustomInput","visibleToTokens","companyContactsCountFields","contactCompanyLinkFields","firebaseAdmin","resolveOrgMembership","restampCrmListFieldsOf","FieldValue","CONTACT_UPDATE_FIELDS","CRM_CONTACT_UPDATE_MAX","CONTACT_NOTES_MAX","CONTACT_PHONE_REFUSAL","CONTACT_TAGS_MAX","normalizeTags","readBulkTag","typed","readCrmRouteScope","crmSuiteRefusal","authorizeCrmWriter","canReach","SUITE_ACTS","name","phone","jobTitle","leadSource","address","notes","tags","addTag","removeTag","campaignIds","ownerUid","companyId","companyName","custom","mediaIds","NO_HOLDER_REFUSAL","contactUpdateSuiteAct","fields","field","find","key","undefined","readContactUpdateFields","input","raw","Array","isArray","ok","error","unknownKey","Object","keys","includes","text","String","slice","filter","length","tag","ids","map","id","readContactIds","value","some","Set","reachesSite","writer","hostId","staff","member","Boolean","hostAccess","refused","contactId","contactPatch","contact","groupId","linkedCompanyName","path","delete","update","counts","held","toLowerCase","arrayUnion","arrayRemove","plan","assign","label","entries","serverTimestamp","commitContactWrites","firestore","writes","failed","batch","write","ref","committed","commit","console","add","has","settleCompanyCounts","companies","deltas","Map","count","set","get","delta","Promise","all","doc","catch","crmContactUpdateHandler","req","res","method","setHeader","status","json","body","scope","contactIds","read","suiteAct","level","suite","org","app","orgRef","collection","orgId","siteGroup","siteTokens","definitions","contactFields","limit","judged","docs","data","values","errors","leadSources","picklists","owner","company","tokens","exists","visible","contacts","snapshots","getAll","outcomes","forEach","snapshot","index","visibleTo","group","rowFields","patch","push","answer","results"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/contact-update.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 * `POST /api/crm/contact-update` — one holder's profile fields on one or\n * more contacts (AGL-2804).\n *\n * Body: `{ hostId, contactIds, set }` under a site, or `{ orgId, hostId?,\n * contactIds, set }` at the organization level, where `set` is\n * `ContactUpdateFields`. Answers `{ ok, results }`: one outcome per contact,\n * in the order the ids were sent.\n *\n * ## Why every facet write comes here\n *\n * A contact's fields live in the facet of the holder that keeps them —\n * `facets.{groupId}.*` — and the Firestore rules cannot tell one field of a\n * facet from another: the holder is a map key a rule cannot address, and a\n * write's affected keys name only `facets`. So the rules let a client update\n * change nothing in a facet beyond letting a holder go, and the console's\n * edits arrive here, where the plan is asked about the fields the request\n * actually carries.\n *\n * ## Who may call it\n *\n * The CRM writer the task routes admit (`authorizeCrmWriter`): under a site,\n * a member whose role writes org data, who holds `data.manage` and whose\n * access reaches the site; at the organization level, an org-wide member\n * with the permission. Each contact must then be one the caller may read —\n * the rules' `canReadScoped()` — and, under a site, one the site's hub lists.\n *\n * ## The plan\n *\n * Every field is the CRM's (AGL-2790), and a plan without the CRM has none\n * of it (AGL-2851), so it edits no contact: not the owner, company, custom\n * values or files, and not the profile a capture writes (name, phone, job\n * title, address), the tags, the notes or the campaign filing either. A\n * request carrying any field is refused to such a plan once the caller is\n * known, and before a contact is read — staff included, because the plan is\n * a fact about the workspace. Erasing a person is another route, open on\n * every plan.\n *\n * ## One holder, by dotted path\n *\n * Under a site the facet is the site's consent group's; at the organization\n * level it is each contact's own primary holder, the facet the record page\n * shows there. Every field is one dotted path — a nested `facets` object\n * would replace every other holder's map — and a text field sent empty is\n * deleted rather than stored blank, so \"no phone\" has one shape.\n *\n * ## Many contacts, one commit\n *\n * The contacts are read together and written in one batch. A batch that\n * fails is retried contact by contact, so the one that could not be written\n * is named and the rest still land. A company's contacts count follows the\n * link: summed per company and settled once the contacts are written, so a\n * company deleted in between cannot fail the save of the people at it — the\n * company page's live aggregate is what corrects a count that did not move.\n */\n\nimport {\n type AglynPostalAddress,\n consentGroupForHost,\n CONTACT_FIELDS_MAX_PER_ORG,\n type ContactCompanyLinkPlan,\n type ContactCustomValue,\n type ContactFieldDefinition,\n campaignMembershipValue,\n contactCampaignFieldPath,\n contactFacetPath,\n CRM_COLLECTIONS,\n CRM_LEAD_SOURCE_PICKLIST,\n CRM_MEDIA_IDS_FIELD,\n crmReadTokens,\n effectiveCrmLeadSourcePicklist,\n isOrgWideMember,\n judgeCrmLeadSource,\n normalizeAddress,\n normalizeCrmMediaIds,\n normalizeCrmPicklistLabel,\n normalizePhone,\n type PluginApiHandler,\n planContactCompanyLink,\n readContactCompanyLink,\n readContactFacet,\n readCrmCustomInput,\n visibleToTokens,\n} from '@aglyn/aglyn/server'\nimport {\n companyContactsCountFields,\n contactCompanyLinkFields,\n firebaseAdmin,\n resolveOrgMembership,\n restampCrmListFieldsOf,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n CONTACT_UPDATE_FIELDS,\n type ContactUpdateFields,\n type ContactUpdateOutcome,\n type ContactUpdateResponse,\n CRM_CONTACT_UPDATE_MAX,\n} from '../model/contact-update'\nimport {\n CONTACT_NOTES_MAX,\n CONTACT_PHONE_REFUSAL,\n CONTACT_TAGS_MAX,\n normalizeTags,\n readBulkTag,\n typed,\n} from './contact-profile'\nimport { readCrmRouteScope } from './org-caller'\nimport { crmSuiteRefusal } from './suite-gate'\nimport { authorizeCrmWriter, canReach, type Writer } from './task-routes'\nimport { contactPrimaryGroup } from '../model/contact-holder'\n\n/** What the suite gate names for each field a request can carry. */\nconst SUITE_ACTS: Record<keyof ContactUpdateFields, string> = {\n name: \"Editing a contact's profile\",\n phone: \"Editing a contact's profile\",\n jobTitle: \"Editing a contact's profile\",\n leadSource: \"Editing a contact's profile\",\n address: \"Editing a contact's profile\",\n notes: \"Editing a contact's notes\",\n tags: 'Tagging a contact',\n addTag: 'Tagging a contact',\n removeTag: 'Tagging a contact',\n campaignIds: 'Filing a contact under a campaign',\n ownerUid: \"Assigning a contact's owner\",\n companyId: 'Filing a contact under a company',\n companyName: 'Filing a contact under a company',\n custom: \"Editing a contact's custom fields\",\n mediaIds: 'Attaching files to a contact',\n}\n\n/** A contact read at the organization level that no site has captured. */\nconst NO_HOLDER_REFUSAL = 'No site holds this contact yet, so it has no profile to edit.'\n\n/**\n * The act a request is refused for on a plan without the CRM: the first\n * field it carries, in the order the route reads them. Every field is the\n * CRM's, so whichever field a request names is the act its refusal names.\n */\nexport function contactUpdateSuiteAct(fields: ContactUpdateFields): string {\n const field = CONTACT_UPDATE_FIELDS.find((key) => fields[key] !== undefined)\n return field ? SUITE_ACTS[field] : \"Editing a contact's profile\"\n}\n\n/**\n * A body's `set`, read into the fields it names, or the sentence refusing\n * it. A key the route does not save is refused rather than dropped: a save\n * that silently lost a field would read as a save that kept it.\n */\nexport function readContactUpdateFields(\n input: unknown,\n): { ok: true; fields: ContactUpdateFields } | { ok: false; error: string } {\n if (!input || typeof input !== 'object' || Array.isArray(input)) {\n return { ok: false, error: 'Name the fields to save.' }\n }\n const raw = input as Record<string, unknown>\n const unknownKey = Object.keys(raw).find(\n (key) => !(CONTACT_UPDATE_FIELDS as readonly string[]).includes(key),\n )\n if (unknownKey) {\n return { ok: false, error: `\"${unknownKey}\" is not a field a contact's profile saves.` }\n }\n const fields: ContactUpdateFields = {}\n if ('name' in raw) fields.name = typed(raw['name'], 120)\n if ('phone' in raw) {\n const text = typed(raw['phone'], 40)\n const phone = text ? normalizePhone(text) : ''\n if (phone === null) return { ok: false, error: CONTACT_PHONE_REFUSAL }\n fields.phone = phone\n }\n if ('jobTitle' in raw) fields.jobTitle = typed(raw['jobTitle'], 120)\n // Held to the label's shape here; judged against the org's list by the\n // handler, which alone can read it (AGL-3298).\n if ('leadSource' in raw) fields.leadSource = normalizeCrmPicklistLabel(raw['leadSource'])\n if ('address' in raw) {\n const address = raw['address']\n fields.address =\n address && typeof address === 'object' && !Array.isArray(address)\n ? normalizeAddress(address as AglynPostalAddress)\n : null\n }\n if ('notes' in raw) fields.notes = String(raw['notes'] ?? '').slice(0, CONTACT_NOTES_MAX)\n // One write may carry one transform per field, so the tags are replaced,\n // added to or taken from — never two of those at once.\n if (['tags', 'addTag', 'removeTag'].filter((key) => key in raw).length > 1) {\n return { ok: false, error: 'Send the tags, a tag to add or a tag to remove — one of them.' }\n }\n if ('tags' in raw) {\n if (!Array.isArray(raw['tags'])) return { ok: false, error: 'Tags must be a list.' }\n fields.tags = normalizeTags(raw['tags'])\n }\n for (const key of ['addTag', 'removeTag'] as const) {\n if (!(key in raw)) continue\n const tag = readBulkTag(raw[key])\n if (!tag) return { ok: false, error: 'Name the tag.' }\n fields[key] = tag\n }\n if ('campaignIds' in raw) {\n const ids = raw['campaignIds']\n if (!Array.isArray(ids)) return { ok: false, error: 'Campaigns must be a list.' }\n fields.campaignIds = campaignMembershipValue(ids.map((id) => String(id ?? '')))\n }\n if ('ownerUid' in raw) fields.ownerUid = typed(raw['ownerUid'], 128)\n if ('companyId' in raw) {\n const companyId = raw['companyId'] === null ? null : typed(raw['companyId'], 128)\n if (companyId !== null && (!companyId || companyId.includes('/'))) {\n return { ok: false, error: 'The company could not be read.' }\n }\n fields.companyId = companyId\n }\n if ('companyName' in raw) fields.companyName = typed(raw['companyName'], 120)\n if ('custom' in raw) {\n const custom = raw['custom']\n if (!custom || typeof custom !== 'object' || Array.isArray(custom)) {\n return { ok: false, error: 'Custom fields must be values keyed by field key.' }\n }\n fields.custom = custom as Record<string, ContactCustomValue>\n }\n if ('mediaIds' in raw) {\n if (!Array.isArray(raw['mediaIds'])) {\n return { ok: false, error: 'Files must be a list of ids.' }\n }\n fields.mediaIds = normalizeCrmMediaIds(raw['mediaIds'])\n }\n if (!Object.keys(fields).length) return { ok: false, error: 'Name the fields to save.' }\n return { ok: true, fields }\n}\n\n/** The ids a body names, deduplicated — or `null` when any is unreadable or there are too many. */\nfunction readContactIds(value: unknown): string[] | null {\n if (!Array.isArray(value) || !value.length || value.length > CRM_CONTACT_UPDATE_MAX) {\n return null\n }\n const ids = value.map((id) => typed(id, 200))\n if (ids.some((id) => !id || id.includes('/'))) return null\n return [...new Set(ids)]\n}\n\n/**\n * Under a site, whether the caller's access reaches it. The rules could not\n * ask this of a facet write — they cannot see which holder a write is for —\n * and a collaborator on one site must not write another site's notes on a\n * person both of them hold.\n */\nfunction reachesSite(writer: Writer, hostId: string): boolean {\n return (\n writer.staff ||\n isOrgWideMember(writer.member) ||\n Boolean((writer.member?.hostAccess as Record<string, unknown> | undefined)?.[hostId])\n )\n}\n\nconst refused = (contactId: string, error: string): ContactUpdateOutcome => ({\n contactId,\n ok: false,\n error,\n})\n\ninterface ContactWrite {\n contactId: string\n ref: FirebaseFirestore.DocumentReference\n update: Record<string, unknown>\n counts: ContactCompanyLinkPlan['counts']\n}\n\n/**\n * One contact's update for one holder, or the sentence refusing the row.\n * `update` is `null` when the document already says everything asked — a tag\n * the row already carries, a link already made.\n */\nfunction contactPatch(\n contact: Record<string, unknown>,\n groupId: string,\n fields: ContactUpdateFields,\n custom: Record<string, ContactCustomValue> | undefined,\n linkedCompanyName: string,\n):\n | { update: Record<string, unknown> | null; counts: ContactCompanyLinkPlan['counts'] }\n | { refused: string } {\n const path = (field: string) => contactFacetPath(groupId, field)\n /** A text field as stored: the value, or the field's absence. */\n const text = (value: string) => value || FieldValue.delete()\n const update: Record<string, unknown> = {}\n let counts: ContactCompanyLinkPlan['counts'] = []\n\n if (fields.name !== undefined) update[path('name')] = text(fields.name)\n if (fields.phone !== undefined) {\n update[path('phone')] = text(fields.phone)\n // The search echo — see `HostContact.phone`.\n update['phone'] = text(fields.phone)\n }\n if (fields.jobTitle !== undefined) update[path('jobTitle')] = text(fields.jobTitle)\n if (fields.leadSource !== undefined) update[path('leadSource')] = text(fields.leadSource)\n if (fields.address !== undefined) {\n update[path('address')] = fields.address ?? FieldValue.delete()\n }\n if (fields.notes !== undefined) update[path('notes')] = fields.notes\n if (fields.tags !== undefined) update[path('tags')] = fields.tags\n\n const held = (readContactFacet(contact, groupId).tags ?? []).map((tag) =>\n String(tag).toLowerCase(),\n )\n if (fields.addTag && !held.includes(fields.addTag)) {\n // The record page's cap, which a selection must not slip past.\n if (held.length >= CONTACT_TAGS_MAX) {\n return { refused: `already has ${CONTACT_TAGS_MAX} tags` }\n }\n update[path('tags')] = FieldValue.arrayUnion(fields.addTag)\n }\n if (fields.removeTag && held.includes(fields.removeTag)) {\n update[path('tags')] = FieldValue.arrayRemove(fields.removeTag)\n }\n if (fields.campaignIds !== undefined) {\n update[contactCampaignFieldPath(groupId)] = fields.campaignIds\n }\n if (fields.ownerUid !== undefined) update[path('ownerUid')] = text(fields.ownerUid)\n\n if (fields.companyId !== undefined) {\n const plan = planContactCompanyLink(\n readContactCompanyLink(contact, groupId),\n fields.companyId,\n )\n if (plan) {\n Object.assign(update, contactCompanyLinkFields(plan, groupId))\n counts = plan.counts\n if (fields.companyName === undefined) {\n // The label follows the link: the company's own name, or nothing.\n const label = plan.companyId ? linkedCompanyName : ''\n update[path('companyName')] = text(label)\n // The search echo — see `HostContact.companyName`.\n update['companyName'] = text(label)\n }\n }\n }\n if (fields.companyName !== undefined) {\n update[path('companyName')] = text(fields.companyName)\n update['companyName'] = text(fields.companyName)\n }\n for (const [key, value] of Object.entries(custom ?? {})) {\n update[path(`custom.${key}`)] = value\n }\n if (fields.mediaIds !== undefined) update[path(CRM_MEDIA_IDS_FIELD)] = fields.mediaIds\n\n if (!Object.keys(update).length) return { update: null, counts }\n update['updatedAt'] = FieldValue.serverTimestamp()\n return { update, counts }\n}\n\n/**\n * Every write in one batch, and — when the batch is refused — each on its\n * own, so the contact that could not be written is the only one reported.\n * Answers the ids that were not written.\n */\nasync function commitContactWrites(\n firestore: FirebaseFirestore.Firestore,\n writes: readonly ContactWrite[],\n): Promise<Set<string>> {\n const failed = new Set<string>()\n if (!writes.length) return failed\n const batch = firestore.batch()\n for (const write of writes) batch.update(write.ref, write.update)\n let committed = false\n try {\n await batch.commit()\n committed = true\n } catch {\n // A batch names no document; the pass below finds the one it was.\n }\n if (!committed) {\n for (const write of writes) {\n try {\n await write.ref.update(write.update)\n } catch (error) {\n console.error('[crm] contact-update could not write a contact', write.contactId, error)\n failed.add(write.contactId)\n }\n }\n }\n // The owner, tags, company and custom values are what the Contacts list\n // filters by through `facetKeys` (AGL-3321): restamped from each row.\n await restampCrmListFieldsOf(\n writes.filter((write) => !failed.has(write.contactId)).map((write) => write.ref),\n 'contacts',\n )\n return failed\n}\n\n/** Each company's contacts count moved once, by the sum of what the written links moved. */\nasync function settleCompanyCounts(\n companies: FirebaseFirestore.CollectionReference,\n writes: readonly ContactWrite[],\n): Promise<void> {\n const deltas = new Map<string, number>()\n for (const write of writes) {\n for (const count of write.counts) {\n deltas.set(count.companyId, (deltas.get(count.companyId) ?? 0) + count.delta)\n }\n }\n await Promise.all(\n [...deltas]\n .filter(([, delta]) => delta !== 0)\n .map(([companyId, delta]) =>\n companies\n .doc(companyId)\n .update(companyContactsCountFields(delta))\n .catch((error: unknown) => {\n console.error('[crm] company contacts count could not move', companyId, delta, error)\n }),\n ),\n )\n}\n\nexport const crmContactUpdateHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const scope = readCrmRouteScope(body)\n if (!scope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const contactIds = readContactIds(body['contactIds'])\n if (!contactIds) {\n res.status(400).json({ error: `Name between 1 and ${CRM_CONTACT_UPDATE_MAX} contacts.` })\n return\n }\n const read = readContactUpdateFields(body['set'])\n if (read.ok === false) {\n res.status(400).json({ error: read.error })\n return\n }\n const { fields } = read\n\n try {\n const writer = await authorizeCrmWriter(req, scope, { suiteAct: null })\n if (writer.ok === false) {\n res.status(writer.status).json(writer.body)\n return\n }\n if (scope.level === 'site' && !reachesSite(writer, scope.hostId)) {\n res.status(403).json({ error: 'Your access to this organization does not reach this site.' })\n return\n }\n // The plan after the person, and before any contact is read (AGL-2787).\n const suite = crmSuiteRefusal(writer.org, contactUpdateSuiteAct(fields))\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(writer.orgId)\n const siteGroup =\n scope.level === 'site' ? consentGroupForHost(writer.org, scope.hostId) : null\n const siteTokens = siteGroup ? crmReadTokens(siteGroup) : null\n\n let custom: Record<string, ContactCustomValue> | undefined\n if (fields.custom) {\n const definitions = await orgRef\n .collection(CRM_COLLECTIONS.contactFields)\n .limit(CONTACT_FIELDS_MAX_PER_ORG)\n .get()\n const judged = readCrmCustomInput(\n fields.custom,\n definitions.docs.map((doc) => doc.data() as ContactFieldDefinition),\n 'contact',\n )\n if ('errors' in judged) {\n res.status(400).json({\n error: Object.values(judged.errors)[0] ?? 'A custom value could not be saved.',\n })\n return\n }\n custom = judged.values\n }\n\n /*\n * THE ORG'S LEAD SOURCES (AGL-3298), read only when the body names one.\n * Judged per contact below, because a contact keeps the value it\n * already holds even when the list has since deactivated it.\n */\n const leadSources = fields.leadSource\n ? effectiveCrmLeadSourcePicklist(\n (\n await orgRef.collection(CRM_COLLECTIONS.picklists).doc(CRM_LEAD_SOURCE_PICKLIST).get()\n ).data(),\n )\n : null\n\n if (fields.ownerUid) {\n const owner = await resolveOrgMembership(fields.ownerUid, writer.orgId).catch(() => null)\n if (!owner?.member) {\n res.status(400).json({ error: 'The owner is not a member of this organization.' })\n return\n }\n }\n\n // The company a link names has to exist and be visible to the caller and\n // the site, or a person could be filed under a record nobody here can open.\n let linkedCompanyName = ''\n if (fields.companyId) {\n const company = await orgRef\n .collection(CRM_COLLECTIONS.companies)\n .doc(fields.companyId)\n .get()\n const tokens = company.exists ? (company.get('visibleTo') as string[] | undefined) : undefined\n const visible =\n company.exists &&\n canReach(writer, tokens) &&\n (!siteTokens || visibleToTokens(tokens, siteTokens))\n if (!visible) {\n res.status(404).json({ error: 'Unknown company' })\n return\n }\n linkedCompanyName = typed(company.get('name'), 120)\n }\n\n const contacts = orgRef.collection('contacts')\n const snapshots = await firestore.getAll(...contactIds.map((id) => contacts.doc(id)))\n const outcomes = new Map<string, ContactUpdateOutcome>()\n const writes: ContactWrite[] = []\n snapshots.forEach((snapshot, index) => {\n const contactId = contactIds[index]\n if (!snapshot.exists) {\n outcomes.set(contactId, refused(contactId, 'That contact no longer exists.'))\n return\n }\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n const visibleTo = data['visibleTo'] as string[] | undefined\n if (!canReach(writer, visibleTo)) {\n outcomes.set(contactId, refused(contactId, 'That contact is not visible to you.'))\n return\n }\n if (siteTokens && !visibleToTokens(visibleTo, siteTokens)) {\n outcomes.set(contactId, refused(contactId, 'That contact is not visible to this site.'))\n return\n }\n const group = siteGroup ?? contactPrimaryGroup(data, writer.org)\n if (!group.groupId) {\n outcomes.set(contactId, refused(contactId, NO_HOLDER_REFUSAL))\n return\n }\n let rowFields = fields\n if (leadSources && fields.leadSource) {\n const judged = judgeCrmLeadSource(\n leadSources,\n fields.leadSource,\n readContactFacet(data, group.groupId).leadSource,\n )\n if (judged.ok === false) {\n outcomes.set(contactId, refused(contactId, judged.error))\n return\n }\n rowFields = { ...fields, leadSource: judged.value ?? '' }\n }\n const patch = contactPatch(data, group.groupId, rowFields, custom, linkedCompanyName)\n if ('refused' in patch) {\n outcomes.set(contactId, refused(contactId, patch.refused))\n return\n }\n outcomes.set(contactId, { contactId, ok: true })\n if (patch.update) {\n writes.push({ contactId, ref: snapshot.ref, update: patch.update, counts: patch.counts })\n }\n })\n\n const failed = await commitContactWrites(firestore, writes)\n for (const contactId of failed) {\n outcomes.set(contactId, refused(contactId, 'The contact could not be saved.'))\n }\n await settleCompanyCounts(\n orgRef.collection(CRM_COLLECTIONS.companies),\n writes.filter((write) => !failed.has(write.contactId)),\n )\n\n const answer: ContactUpdateResponse = {\n ok: true,\n results: contactIds.map(\n (contactId) => outcomes.get(contactId) ?? refused(contactId, 'The contact could not be saved.'),\n ),\n }\n res.status(200).json(answer)\n } catch (error) {\n console.error('[crm] contact-update failed', error)\n res.status(500).json({ error: 'The contact could not be saved.' })\n }\n}\n"],"names":["consentGroupForHost","CONTACT_FIELDS_MAX_PER_ORG","campaignMembershipValue","contactCampaignFieldPath","contactFacetPath","CRM_COLLECTIONS","CRM_LEAD_SOURCE_PICKLIST","CRM_MEDIA_IDS_FIELD","crmReadTokens","effectiveCrmLeadSourcePicklist","isOrgWideMember","judgeCrmLeadSource","normalizeAddress","normalizeCrmMediaIds","normalizeCrmPicklistLabel","normalizePhone","planContactCompanyLink","readContactCompanyLink","readContactFacet","readCrmCustomInput","visibleToTokens","companyContactsCountFields","contactCompanyLinkFields","firebaseAdmin","resolveOrgMembership","restampCrmListFieldsOf","FieldValue","CONTACT_UPDATE_FIELDS","CRM_CONTACT_UPDATE_MAX","CONTACT_NOTES_MAX","CONTACT_PHONE_REFUSAL","CONTACT_TAGS_MAX","normalizeTags","readBulkTag","typed","readCrmRouteScope","crmSuiteRefusal","authorizeCrmWriter","canReach","contactPrimaryGroup","SUITE_ACTS","name","phone","jobTitle","leadSource","address","notes","tags","addTag","removeTag","campaignIds","ownerUid","companyId","companyName","custom","mediaIds","NO_HOLDER_REFUSAL","contactUpdateSuiteAct","fields","field","find","key","undefined","readContactUpdateFields","input","raw","Array","isArray","ok","error","unknownKey","Object","keys","includes","text","String","slice","filter","length","tag","ids","map","id","readContactIds","value","some","Set","reachesSite","writer","hostId","staff","member","Boolean","hostAccess","refused","contactId","contactPatch","contact","groupId","linkedCompanyName","path","delete","update","counts","held","toLowerCase","arrayUnion","arrayRemove","plan","assign","label","entries","serverTimestamp","commitContactWrites","firestore","writes","failed","batch","write","ref","committed","commit","console","add","has","settleCompanyCounts","companies","deltas","Map","count","set","get","delta","Promise","all","doc","catch","crmContactUpdateHandler","req","res","method","setHeader","status","json","body","scope","contactIds","read","suiteAct","level","suite","org","app","orgRef","collection","orgId","siteGroup","siteTokens","definitions","contactFields","limit","judged","docs","data","values","errors","leadSources","picklists","owner","company","tokens","exists","visible","contacts","snapshots","getAll","outcomes","forEach","snapshot","index","visibleTo","group","rowFields","patch","push","answer","results"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsDC,GAED,SAEEA,mBAAmB,EACnBC,0BAA0B,EAI1BC,uBAAuB,EACvBC,wBAAwB,EACxBC,gBAAgB,EAChBC,eAAe,EACfC,wBAAwB,EACxBC,mBAAmB,EACnBC,aAAa,EACbC,8BAA8B,EAC9BC,eAAe,EACfC,kBAAkB,EAClBC,gBAAgB,EAChBC,oBAAoB,EACpBC,yBAAyB,EACzBC,cAAc,EAEdC,sBAAsB,EACtBC,sBAAsB,EACtBC,gBAAgB,EAChBC,kBAAkB,EAClBC,eAAe,QACV,sBAAqB;AAC5B,SACEC,0BAA0B,EAC1BC,wBAAwB,EACxBC,aAAa,EACbC,oBAAoB,EACpBC,sBAAsB,QACjB,2BAA0B;AACjC,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SACEC,qBAAqB,EAIrBC,sBAAsB,QACjB,6BAAyB;AAChC,SACEC,iBAAiB,EACjBC,qBAAqB,EACrBC,gBAAgB,EAChBC,aAAa,EACbC,WAAW,EACXC,KAAK,QACA,uBAAmB;AAC1B,SAASC,iBAAiB,QAAQ,kBAAc;AAChD,SAASC,eAAe,QAAQ,kBAAc;AAC9C,SAASC,kBAAkB,EAAEC,QAAQ,QAAqB,mBAAe;AACzE,SAASC,mBAAmB,QAAQ,6BAAyB;AAE7D,kEAAkE,GAClE,MAAMC,aAAwD;IAC5DC,MAAM;IACNC,OAAO;IACPC,UAAU;IACVC,YAAY;IACZC,SAAS;IACTC,OAAO;IACPC,MAAM;IACNC,QAAQ;IACRC,WAAW;IACXC,aAAa;IACbC,UAAU;IACVC,WAAW;IACXC,aAAa;IACbC,QAAQ;IACRC,UAAU;AACZ;AAEA,wEAAwE,GACxE,MAAMC,oBAAoB;AAE1B;;;;CAIC,GACD,OAAO,SAASC,sBAAsBC,MAA2B;IAC/D,MAAMC,QAAQhC,sBAAsBiC,IAAI,CAAC,CAACC,MAAQH,MAAM,CAACG,IAAI,KAAKC;IAClE,OAAOH,QAAQnB,UAAU,CAACmB,MAAM,GAAG;AACrC;AAEA;;;;CAIC,GACD,OAAO,SAASI,wBACdC,KAAc;QA+B4BC;IA7B1C,IAAI,CAACD,SAAS,OAAOA,UAAU,YAAYE,MAAMC,OAAO,CAACH,QAAQ;QAC/D,OAAO;YAAEI,IAAI;YAAOC,OAAO;QAA2B;IACxD;IACA,MAAMJ,MAAMD;IACZ,MAAMM,aAAaC,OAAOC,IAAI,CAACP,KAAKL,IAAI,CACtC,CAACC,MAAQ,CAAC,AAAClC,sBAA4C8C,QAAQ,CAACZ;IAElE,IAAIS,YAAY;QACd,OAAO;YAAEF,IAAI;YAAOC,OAAO,CAAC,CAAC,EAAEC,WAAW,2CAA2C,CAAC;QAAC;IACzF;IACA,MAAMZ,SAA8B,CAAC;IACrC,IAAI,UAAUO,KAAKP,OAAOjB,IAAI,GAAGP,MAAM+B,GAAG,CAAC,OAAO,EAAE;IACpD,IAAI,WAAWA,KAAK;QAClB,MAAMS,OAAOxC,MAAM+B,GAAG,CAAC,QAAQ,EAAE;QACjC,MAAMvB,QAAQgC,OAAO3D,eAAe2D,QAAQ;QAC5C,IAAIhC,UAAU,MAAM,OAAO;YAAE0B,IAAI;YAAOC,OAAOvC;QAAsB;QACrE4B,OAAOhB,KAAK,GAAGA;IACjB;IACA,IAAI,cAAcuB,KAAKP,OAAOf,QAAQ,GAAGT,MAAM+B,GAAG,CAAC,WAAW,EAAE;IAChE,uEAAuE;IACvE,+CAA+C;IAC/C,IAAI,gBAAgBA,KAAKP,OAAOd,UAAU,GAAG9B,0BAA0BmD,GAAG,CAAC,aAAa;IACxF,IAAI,aAAaA,KAAK;QACpB,MAAMpB,UAAUoB,GAAG,CAAC,UAAU;QAC9BP,OAAOb,OAAO,GACZA,WAAW,OAAOA,YAAY,YAAY,CAACqB,MAAMC,OAAO,CAACtB,WACrDjC,iBAAiBiC,WACjB;IACR;IACA,IAAI,WAAWoB,KAAKP,OAAOZ,KAAK,GAAG6B,QAAOV,aAAAA,GAAG,CAAC,QAAQ,YAAZA,aAAgB,IAAIW,KAAK,CAAC,GAAG/C;IACvE,yEAAyE;IACzE,uDAAuD;IACvD,IAAI;QAAC;QAAQ;QAAU;KAAY,CAACgD,MAAM,CAAC,CAAChB,MAAQA,OAAOI,KAAKa,MAAM,GAAG,GAAG;QAC1E,OAAO;YAAEV,IAAI;YAAOC,OAAO;QAAgE;IAC7F;IACA,IAAI,UAAUJ,KAAK;QACjB,IAAI,CAACC,MAAMC,OAAO,CAACF,GAAG,CAAC,OAAO,GAAG,OAAO;YAAEG,IAAI;YAAOC,OAAO;QAAuB;QACnFX,OAAOX,IAAI,GAAGf,cAAciC,GAAG,CAAC,OAAO;IACzC;IACA,KAAK,MAAMJ,OAAO;QAAC;QAAU;KAAY,CAAW;QAClD,IAAI,CAAEA,CAAAA,OAAOI,GAAE,GAAI;QACnB,MAAMc,MAAM9C,YAAYgC,GAAG,CAACJ,IAAI;QAChC,IAAI,CAACkB,KAAK,OAAO;YAAEX,IAAI;YAAOC,OAAO;QAAgB;QACrDX,MAAM,CAACG,IAAI,GAAGkB;IAChB;IACA,IAAI,iBAAiBd,KAAK;QACxB,MAAMe,MAAMf,GAAG,CAAC,cAAc;QAC9B,IAAI,CAACC,MAAMC,OAAO,CAACa,MAAM,OAAO;YAAEZ,IAAI;YAAOC,OAAO;QAA4B;QAChFX,OAAOR,WAAW,GAAGhD,wBAAwB8E,IAAIC,GAAG,CAAC,CAACC,KAAOP,OAAOO,aAAAA,KAAM;IAC5E;IACA,IAAI,cAAcjB,KAAKP,OAAOP,QAAQ,GAAGjB,MAAM+B,GAAG,CAAC,WAAW,EAAE;IAChE,IAAI,eAAeA,KAAK;QACtB,MAAMb,YAAYa,GAAG,CAAC,YAAY,KAAK,OAAO,OAAO/B,MAAM+B,GAAG,CAAC,YAAY,EAAE;QAC7E,IAAIb,cAAc,QAAS,CAAA,CAACA,aAAaA,UAAUqB,QAAQ,CAAC,IAAG,GAAI;YACjE,OAAO;gBAAEL,IAAI;gBAAOC,OAAO;YAAiC;QAC9D;QACAX,OAAON,SAAS,GAAGA;IACrB;IACA,IAAI,iBAAiBa,KAAKP,OAAOL,WAAW,GAAGnB,MAAM+B,GAAG,CAAC,cAAc,EAAE;IACzE,IAAI,YAAYA,KAAK;QACnB,MAAMX,SAASW,GAAG,CAAC,SAAS;QAC5B,IAAI,CAACX,UAAU,OAAOA,WAAW,YAAYY,MAAMC,OAAO,CAACb,SAAS;YAClE,OAAO;gBAAEc,IAAI;gBAAOC,OAAO;YAAmD;QAChF;QACAX,OAAOJ,MAAM,GAAGA;IAClB;IACA,IAAI,cAAcW,KAAK;QACrB,IAAI,CAACC,MAAMC,OAAO,CAACF,GAAG,CAAC,WAAW,GAAG;YACnC,OAAO;gBAAEG,IAAI;gBAAOC,OAAO;YAA+B;QAC5D;QACAX,OAAOH,QAAQ,GAAG1C,qBAAqBoD,GAAG,CAAC,WAAW;IACxD;IACA,IAAI,CAACM,OAAOC,IAAI,CAACd,QAAQoB,MAAM,EAAE,OAAO;QAAEV,IAAI;QAAOC,OAAO;IAA2B;IACvF,OAAO;QAAED,IAAI;QAAMV;IAAO;AAC5B;AAEA,iGAAiG,GACjG,SAASyB,eAAeC,KAAc;IACpC,IAAI,CAAClB,MAAMC,OAAO,CAACiB,UAAU,CAACA,MAAMN,MAAM,IAAIM,MAAMN,MAAM,GAAGlD,wBAAwB;QACnF,OAAO;IACT;IACA,MAAMoD,MAAMI,MAAMH,GAAG,CAAC,CAACC,KAAOhD,MAAMgD,IAAI;IACxC,IAAIF,IAAIK,IAAI,CAAC,CAACH,KAAO,CAACA,MAAMA,GAAGT,QAAQ,CAAC,OAAO,OAAO;IACtD,OAAO;WAAI,IAAIa,IAAIN;KAAK;AAC1B;AAEA;;;;;CAKC,GACD,SAASO,YAAYC,MAAc,EAAEC,MAAc;QAItCD,2BAAAA;IAHX,OACEA,OAAOE,KAAK,IACZhF,gBAAgB8E,OAAOG,MAAM,KAC7BC,SAASJ,iBAAAA,OAAOG,MAAM,sBAAbH,4BAAAA,eAAeK,UAAU,qBAA1B,AAACL,yBAAmE,CAACC,OAAO;AAExF;AAEA,MAAMK,UAAU,CAACC,WAAmB1B,QAAyC,CAAA;QAC3E0B;QACA3B,IAAI;QACJC;IACF,CAAA;AASA;;;;CAIC,GACD,SAAS2B,aACPC,OAAgC,EAChCC,OAAe,EACfxC,MAA2B,EAC3BJ,MAAsD,EACtD6C,iBAAyB;QAwBXjF;IApBd,MAAMkF,OAAO,CAACzC,QAAkBvD,iBAAiB8F,SAASvC;IAC1D,+DAA+D,GAC/D,MAAMe,OAAO,CAACU,QAAkBA,SAAS1D,WAAW2E,MAAM;IAC1D,MAAMC,SAAkC,CAAC;IACzC,IAAIC,SAA2C,EAAE;IAEjD,IAAI7C,OAAOjB,IAAI,KAAKqB,WAAWwC,MAAM,CAACF,KAAK,QAAQ,GAAG1B,KAAKhB,OAAOjB,IAAI;IACtE,IAAIiB,OAAOhB,KAAK,KAAKoB,WAAW;QAC9BwC,MAAM,CAACF,KAAK,SAAS,GAAG1B,KAAKhB,OAAOhB,KAAK;QACzC,6CAA6C;QAC7C4D,MAAM,CAAC,QAAQ,GAAG5B,KAAKhB,OAAOhB,KAAK;IACrC;IACA,IAAIgB,OAAOf,QAAQ,KAAKmB,WAAWwC,MAAM,CAACF,KAAK,YAAY,GAAG1B,KAAKhB,OAAOf,QAAQ;IAClF,IAAIe,OAAOd,UAAU,KAAKkB,WAAWwC,MAAM,CAACF,KAAK,cAAc,GAAG1B,KAAKhB,OAAOd,UAAU;IACxF,IAAIc,OAAOb,OAAO,KAAKiB,WAAW;YACNJ;QAA1B4C,MAAM,CAACF,KAAK,WAAW,IAAG1C,kBAAAA,OAAOb,OAAO,YAAda,kBAAkBhC,WAAW2E,MAAM;IAC/D;IACA,IAAI3C,OAAOZ,KAAK,KAAKgB,WAAWwC,MAAM,CAACF,KAAK,SAAS,GAAG1C,OAAOZ,KAAK;IACpE,IAAIY,OAAOX,IAAI,KAAKe,WAAWwC,MAAM,CAACF,KAAK,QAAQ,GAAG1C,OAAOX,IAAI;IAEjE,MAAMyD,OAAO,EAACtF,yBAAAA,iBAAiB+E,SAASC,SAASnD,IAAI,YAAvC7B,yBAA2C,EAAE,EAAE+D,GAAG,CAAC,CAACF,MAChEJ,OAAOI,KAAK0B,WAAW;IAEzB,IAAI/C,OAAOV,MAAM,IAAI,CAACwD,KAAK/B,QAAQ,CAACf,OAAOV,MAAM,GAAG;QAClD,+DAA+D;QAC/D,IAAIwD,KAAK1B,MAAM,IAAI/C,kBAAkB;YACnC,OAAO;gBAAE+D,SAAS,CAAC,YAAY,EAAE/D,iBAAiB,KAAK,CAAC;YAAC;QAC3D;QACAuE,MAAM,CAACF,KAAK,QAAQ,GAAG1E,WAAWgF,UAAU,CAAChD,OAAOV,MAAM;IAC5D;IACA,IAAIU,OAAOT,SAAS,IAAIuD,KAAK/B,QAAQ,CAACf,OAAOT,SAAS,GAAG;QACvDqD,MAAM,CAACF,KAAK,QAAQ,GAAG1E,WAAWiF,WAAW,CAACjD,OAAOT,SAAS;IAChE;IACA,IAAIS,OAAOR,WAAW,KAAKY,WAAW;QACpCwC,MAAM,CAACnG,yBAAyB+F,SAAS,GAAGxC,OAAOR,WAAW;IAChE;IACA,IAAIQ,OAAOP,QAAQ,KAAKW,WAAWwC,MAAM,CAACF,KAAK,YAAY,GAAG1B,KAAKhB,OAAOP,QAAQ;IAElF,IAAIO,OAAON,SAAS,KAAKU,WAAW;QAClC,MAAM8C,OAAO5F,uBACXC,uBAAuBgF,SAASC,UAChCxC,OAAON,SAAS;QAElB,IAAIwD,MAAM;YACRrC,OAAOsC,MAAM,CAACP,QAAQhF,yBAAyBsF,MAAMV;YACrDK,SAASK,KAAKL,MAAM;YACpB,IAAI7C,OAAOL,WAAW,KAAKS,WAAW;gBACpC,kEAAkE;gBAClE,MAAMgD,QAAQF,KAAKxD,SAAS,GAAG+C,oBAAoB;gBACnDG,MAAM,CAACF,KAAK,eAAe,GAAG1B,KAAKoC;gBACnC,mDAAmD;gBACnDR,MAAM,CAAC,cAAc,GAAG5B,KAAKoC;YAC/B;QACF;IACF;IACA,IAAIpD,OAAOL,WAAW,KAAKS,WAAW;QACpCwC,MAAM,CAACF,KAAK,eAAe,GAAG1B,KAAKhB,OAAOL,WAAW;QACrDiD,MAAM,CAAC,cAAc,GAAG5B,KAAKhB,OAAOL,WAAW;IACjD;IACA,KAAK,MAAM,CAACQ,KAAKuB,MAAM,IAAIb,OAAOwC,OAAO,CAACzD,iBAAAA,SAAU,CAAC,GAAI;QACvDgD,MAAM,CAACF,KAAK,CAAC,OAAO,EAAEvC,KAAK,EAAE,GAAGuB;IAClC;IACA,IAAI1B,OAAOH,QAAQ,KAAKO,WAAWwC,MAAM,CAACF,KAAK7F,qBAAqB,GAAGmD,OAAOH,QAAQ;IAEtF,IAAI,CAACgB,OAAOC,IAAI,CAAC8B,QAAQxB,MAAM,EAAE,OAAO;QAAEwB,QAAQ;QAAMC;IAAO;IAC/DD,MAAM,CAAC,YAAY,GAAG5E,WAAWsF,eAAe;IAChD,OAAO;QAAEV;QAAQC;IAAO;AAC1B;AAEA;;;;CAIC,GACD,eAAeU,oBACbC,SAAsC,EACtCC,MAA+B;IAE/B,MAAMC,SAAS,IAAI9B;IACnB,IAAI,CAAC6B,OAAOrC,MAAM,EAAE,OAAOsC;IAC3B,MAAMC,QAAQH,UAAUG,KAAK;IAC7B,KAAK,MAAMC,SAASH,OAAQE,MAAMf,MAAM,CAACgB,MAAMC,GAAG,EAAED,MAAMhB,MAAM;IAChE,IAAIkB,YAAY;IAChB,IAAI;QACF,MAAMH,MAAMI,MAAM;QAClBD,YAAY;IACd,EAAE,eAAM;IACN,kEAAkE;IACpE;IACA,IAAI,CAACA,WAAW;QACd,KAAK,MAAMF,SAASH,OAAQ;YAC1B,IAAI;gBACF,MAAMG,MAAMC,GAAG,CAACjB,MAAM,CAACgB,MAAMhB,MAAM;YACrC,EAAE,OAAOjC,OAAO;gBACdqD,QAAQrD,KAAK,CAAC,kDAAkDiD,MAAMvB,SAAS,EAAE1B;gBACjF+C,OAAOO,GAAG,CAACL,MAAMvB,SAAS;YAC5B;QACF;IACF;IACA,wEAAwE;IACxE,sEAAsE;IACtE,MAAMtE,uBACJ0F,OAAOtC,MAAM,CAAC,CAACyC,QAAU,CAACF,OAAOQ,GAAG,CAACN,MAAMvB,SAAS,GAAGd,GAAG,CAAC,CAACqC,QAAUA,MAAMC,GAAG,GAC/E;IAEF,OAAOH;AACT;AAEA,0FAA0F,GAC1F,eAAeS,oBACbC,SAAgD,EAChDX,MAA+B;IAE/B,MAAMY,SAAS,IAAIC;IACnB,KAAK,MAAMV,SAASH,OAAQ;QAC1B,KAAK,MAAMc,SAASX,MAAMf,MAAM,CAAE;gBACHwB;YAA7BA,OAAOG,GAAG,CAACD,MAAM7E,SAAS,EAAE,EAAC2E,cAAAA,OAAOI,GAAG,CAACF,MAAM7E,SAAS,aAA1B2E,cAA+B,KAAKE,MAAMG,KAAK;QAC9E;IACF;IACA,MAAMC,QAAQC,GAAG,CACf;WAAIP;KAAO,CACRlD,MAAM,CAAC,CAAC,GAAGuD,MAAM,GAAKA,UAAU,GAChCnD,GAAG,CAAC,CAAC,CAAC7B,WAAWgF,MAAM,GACtBN,UACGS,GAAG,CAACnF,WACJkD,MAAM,CAACjF,2BAA2B+G,QAClCI,KAAK,CAAC,CAACnE;YACNqD,QAAQrD,KAAK,CAAC,+CAA+CjB,WAAWgF,OAAO/D;QACjF;AAGV;AAEA,OAAO,MAAMoE,0BAA4C,OAAOC,KAAKC;QAMrDD;IALd,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzBD,IAAIE,SAAS,CAAC,SAAS;QACvBF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAE1E,OAAO;QAAqB;QACnD;IACF;IACA,MAAM2E,QAAQN,YAAAA,IAAIM,IAAI,YAARN,YAAY,CAAC;IAC3B,MAAMO,QAAQ9G,kBAAkB6G;IAChC,IAAI,CAACC,OAAO;QACVN,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAE1E,OAAO;QAAiB;QAC/C;IACF;IACA,MAAM6E,aAAa/D,eAAe6D,IAAI,CAAC,aAAa;IACpD,IAAI,CAACE,YAAY;QACfP,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAE1E,OAAO,CAAC,mBAAmB,EAAEzC,uBAAuB,UAAU,CAAC;QAAC;QACvF;IACF;IACA,MAAMuH,OAAOpF,wBAAwBiF,IAAI,CAAC,MAAM;IAChD,IAAIG,KAAK/E,EAAE,KAAK,OAAO;QACrBuE,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAE1E,OAAO8E,KAAK9E,KAAK;QAAC;QACzC;IACF;IACA,MAAM,EAAEX,MAAM,EAAE,GAAGyF;IAEnB,IAAI;QACF,MAAM3D,SAAS,MAAMnD,mBAAmBqG,KAAKO,OAAO;YAAEG,UAAU;QAAK;QACrE,IAAI5D,OAAOpB,EAAE,KAAK,OAAO;YACvBuE,IAAIG,MAAM,CAACtD,OAAOsD,MAAM,EAAEC,IAAI,CAACvD,OAAOwD,IAAI;YAC1C;QACF;QACA,IAAIC,MAAMI,KAAK,KAAK,UAAU,CAAC9D,YAAYC,QAAQyD,MAAMxD,MAAM,GAAG;YAChEkD,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAE1E,OAAO;YAA6D;YAC3F;QACF;QACA,wEAAwE;QACxE,MAAMiF,QAAQlH,gBAAgBoD,OAAO+D,GAAG,EAAE9F,sBAAsBC;QAChE,IAAI4F,OAAO;YACTX,IAAIG,MAAM,CAACQ,MAAMR,MAAM,EAAEC,IAAI,CAACO,MAAMN,IAAI;YACxC;QACF;QAEA,MAAM9B,YAAY3F,cAAciI,GAAG,GAAGtC,SAAS;QAC/C,MAAMuC,SAASvC,UAAUwC,UAAU,CAAC,QAAQnB,GAAG,CAAC/C,OAAOmE,KAAK;QAC5D,MAAMC,YACJX,MAAMI,KAAK,KAAK,SAASrJ,oBAAoBwF,OAAO+D,GAAG,EAAEN,MAAMxD,MAAM,IAAI;QAC3E,MAAMoE,aAAaD,YAAYpJ,cAAcoJ,aAAa;QAE1D,IAAItG;QACJ,IAAII,OAAOJ,MAAM,EAAE;YACjB,MAAMwG,cAAc,MAAML,OACvBC,UAAU,CAACrJ,gBAAgB0J,aAAa,EACxCC,KAAK,CAAC/J,4BACNkI,GAAG;YACN,MAAM8B,SAAS9I,mBACbuC,OAAOJ,MAAM,EACbwG,YAAYI,IAAI,CAACjF,GAAG,CAAC,CAACsD,MAAQA,IAAI4B,IAAI,KACtC;YAEF,IAAI,YAAYF,QAAQ;oBAEb1F;gBADToE,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;oBACnB1E,KAAK,GAAEE,kBAAAA,OAAO6F,MAAM,CAACH,OAAOI,MAAM,CAAC,CAAC,EAAE,YAA/B9F,kBAAmC;gBAC5C;gBACA;YACF;YACAjB,SAAS2G,OAAOG,MAAM;QACxB;QAEA;;;;KAIC,GACD,MAAME,cAAc5G,OAAOd,UAAU,GACjCnC,+BACE,AACE,CAAA,MAAMgJ,OAAOC,UAAU,CAACrJ,gBAAgBkK,SAAS,EAAEhC,GAAG,CAACjI,0BAA0B6H,GAAG,EAAC,EACrFgC,IAAI,MAER;QAEJ,IAAIzG,OAAOP,QAAQ,EAAE;YACnB,MAAMqH,QAAQ,MAAMhJ,qBAAqBkC,OAAOP,QAAQ,EAAEqC,OAAOmE,KAAK,EAAEnB,KAAK,CAAC,IAAM;YACpF,IAAI,EAACgC,yBAAAA,MAAO7E,MAAM,GAAE;gBAClBgD,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAAE1E,OAAO;gBAAkD;gBAChF;YACF;QACF;QAEA,yEAAyE;QACzE,4EAA4E;QAC5E,IAAI8B,oBAAoB;QACxB,IAAIzC,OAAON,SAAS,EAAE;YACpB,MAAMqH,UAAU,MAAMhB,OACnBC,UAAU,CAACrJ,gBAAgByH,SAAS,EACpCS,GAAG,CAAC7E,OAAON,SAAS,EACpB+E,GAAG;YACN,MAAMuC,SAASD,QAAQE,MAAM,GAAIF,QAAQtC,GAAG,CAAC,eAAwCrE;YACrF,MAAM8G,UACJH,QAAQE,MAAM,IACdrI,SAASkD,QAAQkF,WAChB,CAAA,CAACb,cAAczI,gBAAgBsJ,QAAQb,WAAU;YACpD,IAAI,CAACe,SAAS;gBACZjC,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAAE1E,OAAO;gBAAkB;gBAChD;YACF;YACA8B,oBAAoBjE,MAAMuI,QAAQtC,GAAG,CAAC,SAAS;QACjD;QAEA,MAAM0C,WAAWpB,OAAOC,UAAU,CAAC;QACnC,MAAMoB,YAAY,MAAM5D,UAAU6D,MAAM,IAAI7B,WAAWjE,GAAG,CAAC,CAACC,KAAO2F,SAAStC,GAAG,CAACrD;QAChF,MAAM8F,WAAW,IAAIhD;QACrB,MAAMb,SAAyB,EAAE;QACjC2D,UAAUG,OAAO,CAAC,CAACC,UAAUC;gBAMbD;YALd,MAAMnF,YAAYmD,UAAU,CAACiC,MAAM;YACnC,IAAI,CAACD,SAASP,MAAM,EAAE;gBACpBK,SAAS9C,GAAG,CAACnC,WAAWD,QAAQC,WAAW;gBAC3C;YACF;YACA,MAAMoE,QAAQe,iBAAAA,SAASf,IAAI,cAAbe,iBAAmB,CAAC;YAClC,MAAME,YAAYjB,IAAI,CAAC,YAAY;YACnC,IAAI,CAAC7H,SAASkD,QAAQ4F,YAAY;gBAChCJ,SAAS9C,GAAG,CAACnC,WAAWD,QAAQC,WAAW;gBAC3C;YACF;YACA,IAAI8D,cAAc,CAACzI,gBAAgBgK,WAAWvB,aAAa;gBACzDmB,SAAS9C,GAAG,CAACnC,WAAWD,QAAQC,WAAW;gBAC3C;YACF;YACA,MAAMsF,QAAQzB,oBAAAA,YAAarH,oBAAoB4H,MAAM3E,OAAO+D,GAAG;YAC/D,IAAI,CAAC8B,MAAMnF,OAAO,EAAE;gBAClB8E,SAAS9C,GAAG,CAACnC,WAAWD,QAAQC,WAAWvC;gBAC3C;YACF;YACA,IAAI8H,YAAY5H;YAChB,IAAI4G,eAAe5G,OAAOd,UAAU,EAAE;oBAUCqH;gBATrC,MAAMA,SAAStJ,mBACb2J,aACA5G,OAAOd,UAAU,EACjB1B,iBAAiBiJ,MAAMkB,MAAMnF,OAAO,EAAEtD,UAAU;gBAElD,IAAIqH,OAAO7F,EAAE,KAAK,OAAO;oBACvB4G,SAAS9C,GAAG,CAACnC,WAAWD,QAAQC,WAAWkE,OAAO5F,KAAK;oBACvD;gBACF;gBACAiH,YAAY,aAAK5H;oBAAQd,UAAU,GAAEqH,gBAAAA,OAAO7E,KAAK,YAAZ6E,gBAAgB;;YACvD;YACA,MAAMsB,QAAQvF,aAAamE,MAAMkB,MAAMnF,OAAO,EAAEoF,WAAWhI,QAAQ6C;YACnE,IAAI,aAAaoF,OAAO;gBACtBP,SAAS9C,GAAG,CAACnC,WAAWD,QAAQC,WAAWwF,MAAMzF,OAAO;gBACxD;YACF;YACAkF,SAAS9C,GAAG,CAACnC,WAAW;gBAAEA;gBAAW3B,IAAI;YAAK;YAC9C,IAAImH,MAAMjF,MAAM,EAAE;gBAChBa,OAAOqE,IAAI,CAAC;oBAAEzF;oBAAWwB,KAAK2D,SAAS3D,GAAG;oBAAEjB,QAAQiF,MAAMjF,MAAM;oBAAEC,QAAQgF,MAAMhF,MAAM;gBAAC;YACzF;QACF;QAEA,MAAMa,SAAS,MAAMH,oBAAoBC,WAAWC;QACpD,KAAK,MAAMpB,aAAaqB,OAAQ;YAC9B4D,SAAS9C,GAAG,CAACnC,WAAWD,QAAQC,WAAW;QAC7C;QACA,MAAM8B,oBACJ4B,OAAOC,UAAU,CAACrJ,gBAAgByH,SAAS,GAC3CX,OAAOtC,MAAM,CAAC,CAACyC,QAAU,CAACF,OAAOQ,GAAG,CAACN,MAAMvB,SAAS;QAGtD,MAAM0F,SAAgC;YACpCrH,IAAI;YACJsH,SAASxC,WAAWjE,GAAG,CACrB,CAACc;oBAAciF;wBAAAA,gBAAAA,SAAS7C,GAAG,CAACpC,sBAAbiF,gBAA2BlF,QAAQC,WAAW;;QAEjE;QACA4C,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC0C;IACvB,EAAE,OAAOpH,OAAO;QACdqD,QAAQrD,KAAK,CAAC,+BAA+BA;QAC7CsE,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAE1E,OAAO;QAAkC;IAClE;AACF,EAAC"}
|
|
@@ -44,14 +44,37 @@
|
|
|
44
44
|
* returns as a verdict. Both changes are the door's, so the v1 API and the
|
|
45
45
|
* manual add can reach them next.
|
|
46
46
|
*
|
|
47
|
-
* ##
|
|
47
|
+
* ## Every lookup paid once per request, not once per row (AGL-3423)
|
|
48
48
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
49
|
+
* A request has sixty seconds, and a row that reads for itself spends them
|
|
50
|
+
* a round trip at a time: the door alone was a dozen serial reads and three
|
|
51
|
+
* aggregate counts per new person, and a two-hundred-row chunk of an
|
|
52
|
+
* Apollo export ran out of time before it answered. So nothing here is
|
|
53
|
+
* read per row:
|
|
54
|
+
*
|
|
55
|
+
* - The door's own lookups — the site, the consent group, who each address
|
|
56
|
+
* already is, who the site erased, the room in the records band — come
|
|
57
|
+
* from one `prepareContactCaptureBatch` for the whole chunk, which every
|
|
58
|
+
* row's capture is handed. The door still makes every decision.
|
|
59
|
+
* - An owner is named by email and stored by uid, so the org's roster is
|
|
60
|
+
* read once and every row resolves against it.
|
|
61
|
+
* - A company is named by name and stored by id, so the distinct names are
|
|
62
|
+
* looked up together, thirty to an `in` query, and the missing ones are
|
|
63
|
+
* created in one batched write — an Apollo file names a different company
|
|
64
|
+
* on nearly every row, and that was a read, a count and a write each.
|
|
65
|
+
*
|
|
66
|
+
* What is left per row is the row's own writes and what a new person sets
|
|
67
|
+
* off, and those run {@link CONTACT_IMPORT_CONCURRENCY} at a time. A row is
|
|
68
|
+
* started only inside {@link CONTACT_IMPORT_TIME_BUDGET_MS}; the rest are
|
|
69
|
+
* reported `not-reached`, so a slow chunk answers with what it did instead
|
|
70
|
+
* of being cut off at the limit with no answer at all.
|
|
71
|
+
*
|
|
72
|
+
* ## A retry is a merge, never a second person
|
|
73
|
+
*
|
|
74
|
+
* The door dedupes on the address, through the index and then the `email`
|
|
75
|
+
* query, so a row that landed before a request was cut off is found by the
|
|
76
|
+
* next attempt and merged into — the one-person-one-record rule a second
|
|
77
|
+
* form submission meets. A company is found by its `nameLower` the same way.
|
|
55
78
|
*
|
|
56
79
|
* ## Who may call it
|
|
57
80
|
*
|
|
@@ -62,12 +85,25 @@
|
|
|
62
85
|
import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
63
86
|
/** The one sentence every imported contact's timeline opens with. */
|
|
64
87
|
export declare const CONTACT_IMPORT_INTERACTION_SUMMARY = "Imported from CSV";
|
|
88
|
+
/**
|
|
89
|
+
* How many rows are written at once. Enough to overlap the round trips a
|
|
90
|
+
* row still pays; few enough that a chunk never has more writes in flight
|
|
91
|
+
* than one client connection handles comfortably.
|
|
92
|
+
*/
|
|
93
|
+
export declare const CONTACT_IMPORT_CONCURRENCY = 8;
|
|
94
|
+
/**
|
|
95
|
+
* How long after the request arrived a row may still be STARTED. The
|
|
96
|
+
* function is stopped at sixty seconds; this leaves the rows already in
|
|
97
|
+
* flight, and the mail-server check's grace, room to finish before then.
|
|
98
|
+
*/
|
|
99
|
+
export declare const CONTACT_IMPORT_TIME_BUDGET_MS = 40000;
|
|
65
100
|
/**
|
|
66
101
|
* `POST crm/contacts-import` — `{ hostId, rows }` → a {@link ContactImportChunkResult}.
|
|
67
102
|
*
|
|
68
|
-
* Rows are written
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
103
|
+
* Rows are written {@link CONTACT_IMPORT_CONCURRENCY} at a time, which the
|
|
104
|
+
* band allows because it is no longer counted per row: the capture batch
|
|
105
|
+
* counted once, and every create is admitted from its tally — reserved in
|
|
106
|
+
* file order before any row is written, so which rows the band keeps does
|
|
107
|
+
* not depend on which write happened to finish first.
|
|
72
108
|
*/
|
|
73
109
|
export declare const crmContactsImportHandler: PluginApiHandler;
|
|
@@ -44,14 +44,37 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
44
44
|
* returns as a verdict. Both changes are the door's, so the v1 API and the
|
|
45
45
|
* manual add can reach them next.
|
|
46
46
|
*
|
|
47
|
-
* ##
|
|
47
|
+
* ## Every lookup paid once per request, not once per row (AGL-3423)
|
|
48
48
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
49
|
+
* A request has sixty seconds, and a row that reads for itself spends them
|
|
50
|
+
* a round trip at a time: the door alone was a dozen serial reads and three
|
|
51
|
+
* aggregate counts per new person, and a two-hundred-row chunk of an
|
|
52
|
+
* Apollo export ran out of time before it answered. So nothing here is
|
|
53
|
+
* read per row:
|
|
54
|
+
*
|
|
55
|
+
* - The door's own lookups — the site, the consent group, who each address
|
|
56
|
+
* already is, who the site erased, the room in the records band — come
|
|
57
|
+
* from one `prepareContactCaptureBatch` for the whole chunk, which every
|
|
58
|
+
* row's capture is handed. The door still makes every decision.
|
|
59
|
+
* - An owner is named by email and stored by uid, so the org's roster is
|
|
60
|
+
* read once and every row resolves against it.
|
|
61
|
+
* - A company is named by name and stored by id, so the distinct names are
|
|
62
|
+
* looked up together, thirty to an `in` query, and the missing ones are
|
|
63
|
+
* created in one batched write — an Apollo file names a different company
|
|
64
|
+
* on nearly every row, and that was a read, a count and a write each.
|
|
65
|
+
*
|
|
66
|
+
* What is left per row is the row's own writes and what a new person sets
|
|
67
|
+
* off, and those run {@link CONTACT_IMPORT_CONCURRENCY} at a time. A row is
|
|
68
|
+
* started only inside {@link CONTACT_IMPORT_TIME_BUDGET_MS}; the rest are
|
|
69
|
+
* reported `not-reached`, so a slow chunk answers with what it did instead
|
|
70
|
+
* of being cut off at the limit with no answer at all.
|
|
71
|
+
*
|
|
72
|
+
* ## A retry is a merge, never a second person
|
|
73
|
+
*
|
|
74
|
+
* The door dedupes on the address, through the index and then the `email`
|
|
75
|
+
* query, so a row that landed before a request was cut off is found by the
|
|
76
|
+
* next attempt and merged into — the one-person-one-record rule a second
|
|
77
|
+
* form submission meets. A company is found by its `nameLower` the same way.
|
|
55
78
|
*
|
|
56
79
|
* ## Who may call it
|
|
57
80
|
*
|
|
@@ -60,13 +83,25 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
60
83
|
* has one spelling.
|
|
61
84
|
*/ import { CRM_COLLECTIONS, crmNewRecordListFields, nameSearchFields, visibleToTokens } from "@aglyn/aglyn/server";
|
|
62
85
|
import { CONTACT_IMPORT_CHUNK_SIZE, CONTACT_IMPORT_MAX_BODY_BYTES, normalizeContactImportRow } from "../model/crm-import.js";
|
|
63
|
-
import {
|
|
86
|
+
import { firebaseAdmin, prepareContactCaptureBatch } from "@aglyn/tenant-data-admin";
|
|
64
87
|
import { captureHostContact } from "@aglyn/tenant-runtime";
|
|
65
88
|
// The leaf, so a spec that stands a partial barrel in still reaches it.
|
|
66
89
|
import { countUndeliverableEmails, findUndeliverableEmails, IMPORT_MAIL_CHECK_GRACE_MS, settleWithin } from "@aglyn/tenant-data-admin/server/capture-email-check";
|
|
67
90
|
import { FieldValue } from "firebase-admin/firestore";
|
|
68
91
|
import { ownerDirectory, readImportRows, resolveImportContext } from "./import-context.js";
|
|
69
92
|
/** The one sentence every imported contact's timeline opens with. */ export const CONTACT_IMPORT_INTERACTION_SUMMARY = 'Imported from CSV';
|
|
93
|
+
/**
|
|
94
|
+
* How many rows are written at once. Enough to overlap the round trips a
|
|
95
|
+
* row still pays; few enough that a chunk never has more writes in flight
|
|
96
|
+
* than one client connection handles comfortably.
|
|
97
|
+
*/ export const CONTACT_IMPORT_CONCURRENCY = 8;
|
|
98
|
+
/**
|
|
99
|
+
* How long after the request arrived a row may still be STARTED. The
|
|
100
|
+
* function is stopped at sixty seconds; this leaves the rows already in
|
|
101
|
+
* flight, and the mail-server check's grace, room to finish before then.
|
|
102
|
+
*/ export const CONTACT_IMPORT_TIME_BUDGET_MS = 40000;
|
|
103
|
+
/** The most values one Firestore `in` filter may carry. */ const IN_FILTER_LIMIT = 30;
|
|
104
|
+
/** The most writes one batch may carry. */ const BATCH_WRITE_LIMIT = 500;
|
|
70
105
|
/**
|
|
71
106
|
* The holder's live custom-field definitions.
|
|
72
107
|
*
|
|
@@ -80,69 +115,112 @@ import { ownerDirectory, readImportRows, resolveImportContext } from "./import-c
|
|
|
80
115
|
const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.contactFields).limit(200).get();
|
|
81
116
|
return snapshot.docs.map((doc)=>doc.data()).filter((field)=>!!field.key && !field.retiredAt && visibleToTokens(field.visibleTo, readTokens));
|
|
82
117
|
}
|
|
118
|
+
function pagesOf(items, size) {
|
|
119
|
+
const out = [];
|
|
120
|
+
for(let at = 0; at < items.length; at += size)out.push(items.slice(at, at + size));
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
83
123
|
/**
|
|
84
|
-
* The company
|
|
124
|
+
* The company each row names, in this scope, with the missing ones created.
|
|
85
125
|
*
|
|
86
126
|
* Looked up by `nameLower`, the search twin `nameSearchFields` writes on
|
|
87
|
-
* every company,
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
* every company, thirty names to an `in` query and every page at once, and
|
|
128
|
+
* narrowed in memory to the ones the caller's scope can see — two clients
|
|
129
|
+
* of one agency may each have an "Acme" and must each get their own. A
|
|
130
|
+
* name nobody in scope holds is created once for the request, with the
|
|
131
|
+
* same scope stamp every CRM creator uses, so the company lands exactly
|
|
132
|
+
* where a contact captured on this site would; the creates go out in one
|
|
133
|
+
* batched write.
|
|
134
|
+
*
|
|
135
|
+
* THE RECORDS BAND (AGL-2611). A company is a record of the same band the
|
|
136
|
+
* contact behind its row will meet at the door, so the band is walked in
|
|
137
|
+
* file order — the row's company, then the row's contact when the address
|
|
138
|
+
* is new — from the capture batch's one tally. On an org at a full hard
|
|
139
|
+
* band the row gets no company rather than a company it was not allowed to
|
|
140
|
+
* hold, and a later row's company never takes the room an earlier row's
|
|
141
|
+
* contact needed. Nothing is reported here: the contact is refused
|
|
142
|
+
* `audience-band` at its door and the row is listed skipped, which is the
|
|
143
|
+
* one message the operator needs. A plan with an overage rate always
|
|
144
|
+
* admits — a rate is what makes the band meter instead of refuse.
|
|
145
|
+
*
|
|
146
|
+
* Keyed by `nameLower`; a name absent from the map gets no company.
|
|
147
|
+
*/ async function resolveCompanies(context, capture, rows) {
|
|
148
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
149
|
+
const collection = firestore.collection('orgs').doc(context.orgId).collection(CRM_COLLECTIONS.companies);
|
|
150
|
+
const named = new Map();
|
|
151
|
+
for (const { row } of rows){
|
|
152
|
+
if (!row.companyName) continue;
|
|
153
|
+
const fields = nameSearchFields(row.companyName);
|
|
154
|
+
if (!named.has(fields.nameLower)) named.set(fields.nameLower, fields);
|
|
155
|
+
}
|
|
156
|
+
const companies = new Map();
|
|
157
|
+
await Promise.all(pagesOf([
|
|
158
|
+
...named.keys()
|
|
159
|
+
], IN_FILTER_LIMIT).map(async (page)=>{
|
|
160
|
+
const matches = await collection.where('nameLower', 'in', page).get();
|
|
161
|
+
for (const doc of matches.docs){
|
|
162
|
+
var _doc_get, _ref, _doc_get1;
|
|
163
|
+
var _named_get;
|
|
164
|
+
const nameLower = String((_doc_get = doc.get('nameLower')) != null ? _doc_get : '');
|
|
165
|
+
if (!named.has(nameLower) || companies.has(nameLower)) continue;
|
|
166
|
+
if (!visibleToTokens(doc.get('visibleTo'), context.readTokens)) continue;
|
|
167
|
+
// The name as the record spells it, not as this row typed it, so the
|
|
168
|
+
// contact's facet reads the same as the company page.
|
|
169
|
+
companies.set(nameLower, {
|
|
170
|
+
id: doc.id,
|
|
171
|
+
name: String((_ref = (_doc_get1 = doc.get('name')) != null ? _doc_get1 : (_named_get = named.get(nameLower)) == null ? void 0 : _named_get.name) != null ? _ref : '')
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}));
|
|
175
|
+
const creates = [];
|
|
176
|
+
for (const { row } of rows){
|
|
177
|
+
if (row.companyName) {
|
|
178
|
+
const nameLower = nameSearchFields(row.companyName).nameLower;
|
|
179
|
+
if (!companies.has(nameLower) && capture.admit()) {
|
|
180
|
+
const fields = named.get(nameLower);
|
|
181
|
+
const ref = collection.doc();
|
|
182
|
+
creates.push({
|
|
183
|
+
ref,
|
|
184
|
+
record: _extends({}, fields, {
|
|
185
|
+
hostId: context.hostId,
|
|
186
|
+
visibleTo: context.scopeTokens,
|
|
187
|
+
createdByUid: context.uid,
|
|
188
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
189
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
190
|
+
}, crmNewRecordListFields('companies', _extends({}, fields, {
|
|
191
|
+
visibleTo: context.scopeTokens
|
|
192
|
+
})))
|
|
193
|
+
});
|
|
194
|
+
companies.set(nameLower, {
|
|
195
|
+
id: ref.id,
|
|
196
|
+
name: row.companyName
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (capture.peek(row.email) === null && capture.erased(row.email) !== true) {
|
|
201
|
+
capture.reserve(row.email);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const page of pagesOf(creates, BATCH_WRITE_LIMIT)){
|
|
205
|
+
const batch = firestore.batch();
|
|
206
|
+
for (const { ref, record } of page)batch.set(ref, record);
|
|
207
|
+
await batch.commit();
|
|
109
208
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
* at a full hard band the row gets no company rather than a company it
|
|
114
|
-
* was not allowed to hold. Nothing is reported here: the contact is
|
|
115
|
-
* refused `audience-band` a few lines on and the row is listed skipped,
|
|
116
|
-
* which is the one message the operator needs. A plan with an overage
|
|
117
|
-
* rate never reaches this branch — a rate is what makes the band meter
|
|
118
|
-
* instead of refuse.
|
|
119
|
-
*/ const room = await crmRecordsQuotaForOrg(context.org, orgRef);
|
|
120
|
-
if (!room.allowed) return null;
|
|
121
|
-
const created = await companies.add(_extends({}, fields, {
|
|
122
|
-
hostId: context.hostId,
|
|
123
|
-
visibleTo: context.scopeTokens,
|
|
124
|
-
createdByUid: context.uid,
|
|
125
|
-
createdAt: FieldValue.serverTimestamp(),
|
|
126
|
-
updatedAt: FieldValue.serverTimestamp()
|
|
127
|
-
}, crmNewRecordListFields('companies', _extends({}, fields, {
|
|
128
|
-
visibleTo: context.scopeTokens
|
|
129
|
-
}))));
|
|
130
|
-
tally.created += 1;
|
|
131
|
-
const made = {
|
|
132
|
-
id: created.id,
|
|
133
|
-
name
|
|
209
|
+
return {
|
|
210
|
+
companies,
|
|
211
|
+
created: creates.length
|
|
134
212
|
};
|
|
135
|
-
cache.set(fields.nameLower, made);
|
|
136
|
-
return made;
|
|
137
213
|
}
|
|
138
214
|
/**
|
|
139
215
|
* `POST crm/contacts-import` — `{ hostId, rows }` → a {@link ContactImportChunkResult}.
|
|
140
216
|
*
|
|
141
|
-
* Rows are written
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
217
|
+
* Rows are written {@link CONTACT_IMPORT_CONCURRENCY} at a time, which the
|
|
218
|
+
* band allows because it is no longer counted per row: the capture batch
|
|
219
|
+
* counted once, and every create is admitted from its tally — reserved in
|
|
220
|
+
* file order before any row is written, so which rows the band keeps does
|
|
221
|
+
* not depend on which write happened to finish first.
|
|
145
222
|
*/ export const crmContactsImportHandler = async (req, res)=>{
|
|
223
|
+
const startedAt = Date.now();
|
|
146
224
|
if (req.method !== 'POST') {
|
|
147
225
|
res.setHeader('Allow', 'POST');
|
|
148
226
|
return res.status(405).json({
|
|
@@ -203,21 +281,21 @@ import { ownerDirectory, readImportRows, resolveImportContext } from "./import-c
|
|
|
203
281
|
// stamps the records; this is the count the result states.
|
|
204
282
|
const mailServers = findUndeliverableEmails(normalized.map((entry)=>entry.row.email));
|
|
205
283
|
const imported = [];
|
|
206
|
-
const owners = await
|
|
284
|
+
const [owners, capture] = await Promise.all([
|
|
285
|
+
ownerDirectory(context.orgId, normalized.map((entry)=>entry.row)),
|
|
286
|
+
prepareContactCaptureBatch(context.hostId, normalized.map((entry)=>entry.row.email))
|
|
287
|
+
]);
|
|
288
|
+
const { companies, created: companiesCreated } = await resolveCompanies(context, capture, normalized);
|
|
207
289
|
const ownersUnresolved = new Set();
|
|
208
|
-
const companies = new Map();
|
|
209
|
-
const companyTally = {
|
|
210
|
-
created: 0
|
|
211
|
-
};
|
|
212
290
|
let created = 0;
|
|
213
291
|
let merged = 0;
|
|
214
|
-
|
|
292
|
+
const writeRow = async ({ index, row })=>{
|
|
215
293
|
let ownerUid;
|
|
216
294
|
if (row.ownerEmail) {
|
|
217
295
|
ownerUid = owners.get(row.ownerEmail);
|
|
218
296
|
if (!ownerUid) ownersUnresolved.add(row.ownerEmail);
|
|
219
297
|
}
|
|
220
|
-
const company = row.companyName ?
|
|
298
|
+
const company = row.companyName ? companies.get(nameSearchFields(row.companyName).nameLower) : undefined;
|
|
221
299
|
const verdict = await captureHostContact(_extends({
|
|
222
300
|
hostId: context.hostId,
|
|
223
301
|
email: row.email
|
|
@@ -253,7 +331,8 @@ import { ownerDirectory, readImportRows, resolveImportContext } from "./import-c
|
|
|
253
331
|
} : {}),
|
|
254
332
|
// An import is the merchant's own act and files nobody under a
|
|
255
333
|
// campaign; the picker on the profile is where that happens.
|
|
256
|
-
campaignIds: []
|
|
334
|
+
campaignIds: [],
|
|
335
|
+
batch: capture
|
|
257
336
|
}));
|
|
258
337
|
if ('refused' in verdict) {
|
|
259
338
|
skipped.push({
|
|
@@ -261,11 +340,32 @@ import { ownerDirectory, readImportRows, resolveImportContext } from "./import-c
|
|
|
261
340
|
email: row.email,
|
|
262
341
|
reason: verdict.refused === 'band' ? 'audience-band' : verdict.refused === 'invalid-email' ? 'invalid-email' : verdict.refused === 'erased' ? 'erased' : 'write-failed'
|
|
263
342
|
});
|
|
264
|
-
|
|
343
|
+
return;
|
|
265
344
|
}
|
|
266
345
|
imported.push(row.email);
|
|
267
346
|
if (verdict.created) created += 1;
|
|
268
347
|
else merged += 1;
|
|
348
|
+
};
|
|
349
|
+
// A fixed pool drawing from one queue in file order; a row is started
|
|
350
|
+
// only inside the budget, and whatever is left is reported, not dropped.
|
|
351
|
+
let next = 0;
|
|
352
|
+
const worker = async ()=>{
|
|
353
|
+
while(next < normalized.length){
|
|
354
|
+
if (Date.now() - startedAt >= CONTACT_IMPORT_TIME_BUDGET_MS) return;
|
|
355
|
+
const entry = normalized[next];
|
|
356
|
+
next += 1;
|
|
357
|
+
await writeRow(entry);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
await Promise.all(Array.from({
|
|
361
|
+
length: Math.min(CONTACT_IMPORT_CONCURRENCY, normalized.length)
|
|
362
|
+
}, worker));
|
|
363
|
+
for (const { index, row } of normalized.slice(next)){
|
|
364
|
+
skipped.push({
|
|
365
|
+
index,
|
|
366
|
+
email: row.email,
|
|
367
|
+
reason: 'not-reached'
|
|
368
|
+
});
|
|
269
369
|
}
|
|
270
370
|
const noMailServer = countUndeliverableEmails(imported, await settleWithin(mailServers, IMPORT_MAIL_CHECK_GRACE_MS));
|
|
271
371
|
const result = _extends({
|
|
@@ -274,7 +374,7 @@ import { ownerDirectory, readImportRows, resolveImportContext } from "./import-c
|
|
|
274
374
|
merged,
|
|
275
375
|
skipped: skipped.sort((a, b)=>a.index - b.index),
|
|
276
376
|
dropped,
|
|
277
|
-
companiesCreated
|
|
377
|
+
companiesCreated,
|
|
278
378
|
ownersUnresolved: [
|
|
279
379
|
...ownersUnresolved
|
|
280
380
|
]
|