@aglyn/plugins-crm 1.0.0-beta.293 → 1.0.0
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 -14
- package/src/lib/components/activity-list.js +6 -1
- package/src/lib/components/activity-list.js.map +1 -1
- package/src/lib/components/contact-properties-card.js +8 -0
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/deal-board.d.ts +10 -0
- package/src/lib/components/deal-board.js +119 -61
- package/src/lib/components/deal-board.js.map +1 -1
- package/src/lib/components/deal-stage-card.d.ts +11 -7
- package/src/lib/components/deal-stage-card.js +99 -23
- package/src/lib/components/deal-stage-card.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.js +26 -14
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/picklist-values-card.js +7 -4
- package/src/lib/components/picklist-values-card.js.map +1 -1
- package/src/lib/components/reports/activity-card.js +1 -1
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/hooks/use-scroll-edges.d.ts +38 -0
- package/src/lib/hooks/use-scroll-edges.js +75 -0
- package/src/lib/hooks/use-scroll-edges.js.map +1 -0
- package/src/lib/server/api-v1/crm-activities.js +2 -8
- package/src/lib/server/api-v1/crm-activities.js.map +1 -1
- package/src/lib/server/task-reminders-route.d.ts +7 -5
- package/src/lib/server/task-reminders-route.js +82 -15
- package/src/lib/server/task-reminders-route.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/task-reminders-route.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type AglynOrgBilling,\n type AglynOrgMember,\n buildRoute,\n CRM_COLLECTIONS,\n isReleaseFlagOnForOrg,\n notificationMuted,\n parseOrgReleaseFlagOverrides,\n pluginRequestFromWeb,\n resolveBrandingProfile,\n resolveEffectivePlan,\n resolveOrgEntitlements,\n Route,\n} from '@aglyn/aglyn/server'\nimport {\n crmDigestEntitled,\n} from '../model/crm-digest'\nimport { rateLimitedRetryAtMs, sendEmail } from '@aglyn/shared-util-email'\nimport { withoutMailWithheldAccounts } from '@aglyn/tenant-data-admin/server/account-mail'\nimport {\n findUserByUidAcrossPools,\n firebaseAdmin,\n getServerReleaseFlagValues,\n listOrgMembers,\n memberHasOrgPermission,\n meterPlatformEmail,\n notifyUsers,\n} from '@aglyn/tenant-data-admin'\n// From the LEAF, not the barrel (AGL-2407): the cron specs mock\n// `@aglyn/tenant-data-admin` wholesale, and a suppression gate imported\n// through the barrel would be silently replaced by whatever the factory\n// lists. Same reasoning as the digest beside this route.\nimport { filterSuppressedEmails } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport {\n loadSystemEmail,\n renderLoadedSystemEmail,\n systemEmailBrand,\n systemEmailContent,\n type LoadedSystemEmail,\n} from '@aglyn/tenant-data-admin/server/render-system-email'\nimport { isCronAuthorized, isCronDryRun } from '@aglyn/tenant-data-admin/server/cron-auth'\nimport { writeCronBeat } from '@aglyn/aglyn/app-utils/health-report'\nimport { brandSupportLine } from '@aglyn/aglyn/app-utils/brand-support-line'\nimport { platformConsoleOrigin as consoleOrigin } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { digestTimeZone } from './daily-digest-route'\nimport {\n composeCrmTaskReminderBody,\n composeCrmTaskReminderEmailParts,\n composeCrmTaskReminderEmailText,\n crmTaskReminderMergeValues,\n composeCrmTaskReminderSubject,\n CRM_TASK_REMINDER_CEILING,\n type CrmReminderTask,\n crmTaskReminderDue,\n crmTaskReminderLink,\n} from '../model/crm-task-reminders'\n\n/**\n * The mark a scheduled run leaves for `/api/health/crons` (AGL-1955), on the\n * INVOCATION and before the work — the console's rule for every cron. Never\n * throws: reaching the admin app is inside the try, because a monitor that\n * can take down the job it describes is worse than the failure it watches.\n */\nasync function recordCronBeat(jobId: string): Promise<void> {\n try {\n await writeCronBeat(firebaseAdmin.app().firestore(), jobId)\n } catch {\n // Deliberately swallowed: a job that stops stamping reds its own row.\n }\n}\n\n// lockdown-423: exempt — server-internal cron (x-cron-secret), no user caller; it reads an org's CRM to remind its members and writes nothing a locked org could lose.\n\n/**\n * A task's reminder at its own time (AGL-2659): `POST /api/crm/task-reminders`.\n *\n * The digest beside this route says once a morning what is late and what\n * is due today; nothing said \"the 3:00 call is at 3:00\". Every hour this\n * reads, per org, the open tasks whose `remindAtMs` has come and tells\n * each one's assignee ONCE — a console notification per task\n * (`content.taskReminder`, opening the record the task is for) and one\n * email per member per run listing everything of theirs that fell due.\n * The words and the rule live in `crm-task-reminders.ts`; this file is the\n * reads, the sends and the sweep, in the digest's shape wherever the two\n * do the same thing.\n *\n * ## Who is told, and who is not\n *\n * The task's ASSIGNEE, and nobody else — an unassigned task is a deliberate\n * choice in the drawer, as the digest reads it. Only a member who may open\n * the CRM (`data.manage`, resolved through their custom role), only in an\n * org whose plan carries the suite and whose `release_crm` flag is\n * on: the plan gate is the digest's own (`crmDigestEntitled`), so a plan\n * that gets no digest gets no reminders either. The recipient's\n * operational-category mute (`notificationPrefs.content`) governs the WHOLE\n * reminder, mail included — unlike the digest, which has a switch of its\n * own for the schedule it keeps; a reminder is one message about one task.\n *\n * ## Once, and resumable\n *\n * `reminderSentAtMs` on the task is the idempotence: stamped in one batch\n * with the member's sends, so a rerun over the same hour reads the mark and\n * sends nothing, and stamped on every reminder the run HANDLED — sent, or\n * found nobody to send it to — so a reminder nobody can be told about does\n * not come back every hour. A reminder is left unstamped only when the run\n * never reached a verdict: the send-rate governor refused, or the org\n * threw. Orgs are swept in id order behind a cursor, as the digest sweeps\n * them, so the workflow's `done:false` loop carries a platform of any\n * size across invocations.\n *\n * The task query reads the NEWEST reminders that have come due, not the\n * oldest — see `CRM_TASK_REMINDER_CEILING` for why that is what makes a\n * bounded page safe against a window that only ever grows.\n *\n * The email is a `bulk` send under the platform send-rate governor\n * (AGL-2409), metered as PLATFORM mail, for the digest's reasons: a cron\n * fan-out on the shared sending domain, and the platform reminding a\n * member is not the org's sending. A refusal STOPS the run and reports\n * `done: true`; the refused member was never stamped, and the next hour's\n * run finds their reminders at the top of the window again.\n */\n\n/** The beat `/api/health/crons` reads — the `SCHEDULED_JOBS` row's id. */\nexport const CRM_TASK_REMINDERS_JOB_ID = 'crm-task-reminders'\n\n/**\n * Orgs per invocation.\n *\n * More than the digest's twenty-five: an org here costs one task query and,\n * only when something fell due, the roster and the sends. The common hour\n * finds nothing due in most orgs, and a chunk that reads a hundred empty\n * queries finishes well inside `maxDuration = 60`.\n */\nexport const CRM_TASK_REMINDERS_ORG_CHUNK = 100\n\n/**\n * The most members per org whose address is chased through the auth pools\n * when their member document carries none — the digest's bound, for the\n * digest's reason: work, not audience.\n */\nexport const CRM_TASK_REMINDERS_MAX_UID_LOOKUPS = 5\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Snapshot = FirebaseFirestore.QueryDocumentSnapshot\n\n/** What one member's row in the response says. */\ninterface MemberReport {\n tasks: number\n skipped?: string\n notified?: number\n emailed?: boolean\n emailReason?: string\n deferred?: boolean\n}\n\ninterface OrgReport {\n skipped?: string\n /** Reminders that had come due and were not yet handled. */\n due: number\n /** Reminders this run stamped as handled. */\n handled: number\n /** Console notifications sent. */\n notified: number\n /** Emails sent — one per member per run. */\n emailed: number\n members?: Record<string, MemberReport>\n tasksTruncated?: boolean\n /** The send-rate governor refused; the sweep stops here. */\n deferred?: boolean\n}\n\ninterface SweepContext {\n firestore: Firestore\n nowMs: number\n timeZone: string\n dryRun: boolean\n flagValues: Awaited<ReturnType<typeof getServerReleaseFlagValues>>\n origin: string\n}\n\n/** A due reminder with the document it came from, so the stamp has a target. */\ninterface DueTask extends CrmReminderTask {\n ref: FirebaseFirestore.DocumentReference\n}\n\nasync function remindOrg(ctx: SweepContext, orgDoc: Snapshot): Promise<OrgReport> {\n const { firestore, nowMs, timeZone, dryRun } = ctx\n const orgId = orgDoc.id\n const org = (orgDoc.data() ?? {}) as Partial<AglynOrgBilling> & Record<string, unknown>\n const quiet: OrgReport = { due: 0, handled: 0, notified: 0, emailed: 0 }\n\n if (!crmDigestEntitled(resolveOrgEntitlements(org).features)) {\n return { ...quiet, skipped: 'not-entitled' }\n }\n const flagOn = isReleaseFlagOnForOrg(\n 'release_crm',\n ctx.flagValues['release_crm'],\n orgId,\n parseOrgReleaseFlagOverrides(org['releaseFlags']),\n resolveEffectivePlan(org),\n )\n if (!flagOn) return { ...quiet, skipped: 'release-flag' }\n\n /*\n * Every open task whose reminder has come, newest first, on\n * `(status, remindAtMs desc)`. The \"not yet handled\" half is a fact about\n * a field's absence, which a query cannot select on, so it is asked of\n * the page in memory — and the page being the NEWEST five hundred is what\n * keeps the handled ones from crowding out the next one due.\n */\n const page = await orgDoc.ref\n .collection(CRM_COLLECTIONS.tasks)\n .where('status', '==', 'open')\n .where('remindAtMs', '<=', nowMs)\n .orderBy('remindAtMs', 'desc')\n .limit(CRM_TASK_REMINDER_CEILING)\n .get()\n const due: DueTask[] = []\n for (const doc of page.docs) {\n const data = doc.data() as Record<string, unknown>\n if (!crmTaskReminderDue(data as Parameters<typeof crmTaskReminderDue>[0], nowMs)) continue\n due.push({\n ref: doc.ref,\n id: doc.id,\n title: String(data['title'] ?? ''),\n kind: (data['kind'] as CrmReminderTask['kind']) ?? 'todo',\n dueAtMs: typeof data['dueAtMs'] === 'number' ? (data['dueAtMs'] as number) : null,\n remindAtMs: Number(data['remindAtMs']),\n assigneeUid: String(data['assigneeUid'] ?? ''),\n hostId: String(data['hostId'] ?? ''),\n contactId: (data['contactId'] as string) || undefined,\n companyId: (data['companyId'] as string) || undefined,\n dealId: (data['dealId'] as string) || undefined,\n })\n }\n const tasksTruncated = page.docs.length >= CRM_TASK_REMINDER_CEILING\n if (!due.length) {\n return { ...quiet, skipped: 'nothing-due', ...(tasksTruncated ? { tasksTruncated } : {}) }\n }\n\n const report: OrgReport = { due: due.length, handled: 0, notified: 0, emailed: 0, members: {} }\n if (tasksTruncated) report.tasksTruncated = true\n const rows = report.members as Record<string, MemberReport>\n\n /**\n * Stamp a set of reminders as handled, in one batch, unless this is a\n * plan. Everything a member's verdict covers lands together, which is\n * what makes a rerun read the same answer for all of them.\n */\n const settle = async (tasks: DueTask[]): Promise<void> => {\n report.handled += tasks.length\n if (dryRun) return\n const batch = firestore.batch()\n for (const task of tasks) batch.update(task.ref, { reminderSentAtMs: nowMs })\n await batch.commit()\n }\n\n const byAssignee = new Map<string, DueTask[]>()\n const unassigned: DueTask[] = []\n for (const task of due) {\n if (!task.assigneeUid) {\n unassigned.push(task)\n continue\n }\n const mine = byAssignee.get(task.assigneeUid) ?? []\n mine.push(task)\n byAssignee.set(task.assigneeUid, mine)\n }\n // A reminder for nobody is handled by saying so: it would otherwise sit\n // at the top of the window every hour for as long as the task is open.\n if (unassigned.length) {\n rows[''] = { tasks: unassigned.length, skipped: 'unassigned' }\n await settle(unassigned)\n }\n if (!byAssignee.size) return report\n\n const listed = (await listOrgMembers(orgId)).filter(\n (member) => member.orgSuspended !== true,\n )\n // A locked or disabled account is not told (AGL-3418): it cannot sign in\n // to act on it.\n const reachable = new Set(\n await withoutMailWithheldAccounts(listed.map((member) => member.$id)),\n )\n const members = listed.filter((member) => reachable.has(member.$id))\n const memberById = new Map(members.map((member) => [member.$id, member]))\n // Sorted so the order members are told in — and the member a refusal\n // lands on — is the same on every run.\n const uids = [...byAssignee.keys()].sort()\n const userDocs = await firestore.getAll(\n ...uids.map((uid) => firestore.collection('users').doc(uid)),\n )\n const userById = new Map(userDocs.map((doc) => [doc.id, doc]))\n\n // The sites the due tasks name, for the absolute links the mail carries.\n const hostIds = [...new Set(due.map((task) => task.hostId).filter(Boolean))]\n const hostDocs = hostIds.length\n ? await firestore.getAll(...hostIds.map((hostId) => firestore.collection('hosts').doc(hostId)))\n : []\n const hostById = new Map(hostDocs.map((doc) => [doc.id, doc]))\n\n const branding = resolveBrandingProfile(org)\n const orgSlug = String(org['slug'] ?? '')\n const workspaceName = String(org['name'] ?? '').trim() || orgSlug\n const orgHub = orgSlug ? `${ctx.origin}${buildRoute(Route.ORG_HOME, { orgSlug })}/crm` : null\n const hubUrl = (hostId: string): string | null => {\n if (!hostId) return orgHub\n const subdomain = String(hostById.get(hostId)?.get('subdomain') ?? '')\n if (!orgSlug || !subdomain) return null\n return `${ctx.origin}${buildRoute(Route.HOST_DASHBOARD, { orgSlug, host: subdomain })}/crm`\n }\n // The record the task is for, on its own hub — the notification's link\n // past the `/{hostId}/crm` or `/org/crm` prefix the console rewrites.\n const taskUrl = (task: CrmReminderTask): string => {\n const hub = hubUrl(task.hostId)\n if (!hub) return ctx.origin\n const link = crmTaskReminderLink(task.hostId || null, task)\n return `${hub}${link.slice(link.indexOf('/crm') + '/crm'.length)}`\n }\n const settingsUrl = `${ctx.origin}${buildRoute(Route.MANAGE_NOTIFICATION_SETTINGS)}`\n const supportLine = brandSupportLine(branding)\n const brand = systemEmailBrand(org)\n let reminderEmail: Promise<LoadedSystemEmail | null> | undefined\n let poolLookups = 0\n\n for (const uid of uids) {\n const tasks = byAssignee.get(uid) as DueTask[]\n const row: MemberReport = { tasks: tasks.length }\n rows[uid] = row\n const member = memberById.get(uid)\n if (!member) {\n row.skipped = 'not-a-member'\n await settle(tasks)\n continue\n }\n if (!(await memberHasOrgPermission(orgId, member as AglynOrgMember, 'data.manage'))) {\n row.skipped = 'no-data-manage'\n await settle(tasks)\n continue\n }\n const userDoc = userById.get(uid)\n if (notificationMuted(userDoc?.get('notificationPrefs'), 'content.taskReminder')) {\n row.skipped = 'muted'\n await settle(tasks)\n continue\n }\n if (dryRun) {\n row.notified = tasks.length\n row.emailed = true\n await settle(tasks)\n continue\n }\n\n /*\n * THE EMAIL FIRST, for the digest's reason: a refusal from the\n * send-rate governor stops the run with nothing else done for this\n * member — no notification to double next hour, no mark to skip them\n * on — so the order here is the idempotence, not a preference.\n */\n let emailed = false\n let emailReason: string | undefined\n let address = String(member.email ?? '').trim().toLowerCase()\n if (!address.includes('@') && poolLookups < CRM_TASK_REMINDERS_MAX_UID_LOOKUPS) {\n poolLookups += 1\n const pooled = await findUserByUidAcrossPools(uid).catch(() => null)\n address = String(pooled?.record?.email ?? '').trim().toLowerCase()\n }\n const recipients = address.includes('@')\n ? await filterSuppressedEmails([address], firestore)\n : []\n if (!recipients.length) {\n emailReason = address.includes('@') ? 'suppressed' : 'no-recipient'\n } else {\n const reminderInput = {\n tasks,\n timeZone,\n productName: branding.productName,\n workspaceName,\n taskUrl,\n settingsUrl,\n supportLine,\n }\n const subject = composeCrmTaskReminderSubject(tasks)\n // The `crm-task-reminder` system email (AGL-3367), in the org's brand,\n // read once per org and rendered per member.\n reminderEmail ??= loadSystemEmail('crm-task-reminder').catch(() => null)\n const loaded = await reminderEmail\n const rendered = loaded\n ? renderLoadedSystemEmail(\n loaded,\n {\n ...brand.merge,\n 'reminder.subject': subject,\n 'reminder.body': composeCrmTaskReminderEmailText({ ...reminderInput, omitClosing: true }),\n ...crmTaskReminderMergeValues(composeCrmTaskReminderEmailParts(reminderInput)),\n settingsUrl,\n },\n brand.options,\n )\n : null\n const result = await sendEmail({\n to: recipients,\n ...systemEmailContent(rendered, {\n subject,\n text: composeCrmTaskReminderEmailText(reminderInput),\n }),\n fromName: branding.fromName,\n context: 'crm-task-reminder',\n priority: 'bulk',\n })\n if (result.sent) {\n emailed = true\n await meterPlatformEmail().catch(() => undefined)\n } else if (rateLimitedRetryAtMs(result) !== null) {\n row.deferred = true\n report.deferred = true\n return report\n } else {\n emailReason = (result as { reason?: string }).reason ?? 'failed'\n }\n }\n\n // One notification per task: each opens the record it is for.\n for (const task of tasks) {\n await notifyUsers([uid], {\n type: 'content.taskReminder',\n title: 'Task reminder',\n body: composeCrmTaskReminderBody(task, timeZone, workspaceName),\n link: crmTaskReminderLink(task.hostId || null, task),\n orgId,\n ...(task.hostId ? { hostId: task.hostId } : {}),\n })\n }\n await settle(tasks)\n row.notified = tasks.length\n row.emailed = emailed\n if (emailReason) row.emailReason = emailReason\n report.notified += tasks.length\n if (emailed) report.emailed += 1\n }\n return report\n}\n\nexport async function crmTaskRemindersRoute(request: Request): Promise<Response> {\n const { method, body, headers: rawHeaders, query } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'POST' && method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n if (!process.env.CRON_SECRET) {\n return Response.json(\n { error: 'Task reminders are not configured (CRON_SECRET).' },\n { status: 501 },\n )\n }\n if (!isCronAuthorized(headers)) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n // A GET reports the plan and sends nothing; a bodyless POST — what the\n // scheduled workflow sends — is a real run. `isCronDryRun` keys the\n // default on the METHOD for exactly that reason.\n const dryRun = isCronDryRun({ method, body, query })\n // Stamped on the invocation, not on the work (AGL-1955): an hour with\n // nothing due still ran.\n if (method === 'POST') await recordCronBeat(CRM_TASK_REMINDERS_JOB_ID)\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const nowMs = Date.now()\n const ctx: SweepContext = {\n firestore,\n nowMs,\n timeZone: digestTimeZone(),\n dryRun,\n flagValues: await getServerReleaseFlagValues(),\n origin: consoleOrigin(),\n }\n\n // The resumable sweep, in the shape the digest reads orgs (AGL-2220):\n // ordered by id, `limit + 1` to learn whether there is more, the cursor\n // validated as an org id before it becomes a reference.\n const requestedLimit = Number((body as { limit?: unknown } | undefined)?.limit)\n const pageSize =\n Number.isFinite(requestedLimit) && requestedLimit > 0\n ? Math.min(Math.floor(requestedLimit), CRM_TASK_REMINDERS_ORG_CHUNK)\n : CRM_TASK_REMINDERS_ORG_CHUNK\n const rawCursor = (body as { cursor?: unknown } | undefined)?.cursor\n const cursor =\n typeof rawCursor === 'string' && rawCursor.length > 0 && !rawCursor.includes('/')\n ? rawCursor\n : null\n let orgQuery = firestore\n .collection('orgs')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(pageSize + 1)\n if (cursor) orgQuery = orgQuery.startAfter(firestore.collection('orgs').doc(cursor))\n const page = await orgQuery.get()\n const hasMore = page.docs.length > pageSize\n const orgDocs = hasMore ? page.docs.slice(0, pageSize) : page.docs\n\n const orgs: Record<string, OrgReport> = {}\n const failures: Record<string, string> = {}\n let due = 0\n let handled = 0\n let notified = 0\n let emailed = 0\n let deferred = false\n for (const orgDoc of orgDocs) {\n try {\n const report = await remindOrg(ctx, orgDoc)\n orgs[orgDoc.id] = report\n due += report.due\n handled += report.handled\n notified += report.notified\n emailed += report.emailed\n if (report.deferred) {\n deferred = true\n break\n }\n } catch (error) {\n console.error('[crm] task reminders failed for org', orgDoc.id, error)\n failures[orgDoc.id] = (error as Error)?.message ?? 'unknown'\n }\n }\n const failed = Object.keys(failures)\n return Response.json(\n {\n timeZone: ctx.timeZone,\n dryRun,\n swept: orgDocs.length,\n due,\n handled,\n notified,\n emailed,\n orgs,\n ...(deferred ? { deferred: true } : {}),\n ...(failed.length ? { failures } : {}),\n // A deferred run reports `done: true` — see the module comment —\n // so the workflow's loop does not re-POST into a full window.\n nextCursor: deferred || !hasMore ? null : (orgDocs[orgDocs.length - 1]?.id ?? null),\n done: deferred || !hasMore,\n },\n // 207: this chunk finished and some org in it needs a person, which is\n // what fails the workflow (its one alerting channel).\n { status: failed.length ? 207 : 200 },\n )\n } catch (error) {\n console.error('[crm] task reminders run failed', error)\n return Response.json({ error: 'Task reminders run failed' }, { status: 500 })\n }\n}\n"],"names":["buildRoute","CRM_COLLECTIONS","isReleaseFlagOnForOrg","notificationMuted","parseOrgReleaseFlagOverrides","pluginRequestFromWeb","resolveBrandingProfile","resolveEffectivePlan","resolveOrgEntitlements","Route","crmDigestEntitled","rateLimitedRetryAtMs","sendEmail","withoutMailWithheldAccounts","findUserByUidAcrossPools","firebaseAdmin","getServerReleaseFlagValues","listOrgMembers","memberHasOrgPermission","meterPlatformEmail","notifyUsers","filterSuppressedEmails","loadSystemEmail","renderLoadedSystemEmail","systemEmailBrand","systemEmailContent","isCronAuthorized","isCronDryRun","writeCronBeat","brandSupportLine","platformConsoleOrigin","consoleOrigin","digestTimeZone","composeCrmTaskReminderBody","composeCrmTaskReminderEmailParts","composeCrmTaskReminderEmailText","crmTaskReminderMergeValues","composeCrmTaskReminderSubject","CRM_TASK_REMINDER_CEILING","crmTaskReminderDue","crmTaskReminderLink","recordCronBeat","jobId","app","firestore","CRM_TASK_REMINDERS_JOB_ID","CRM_TASK_REMINDERS_ORG_CHUNK","CRM_TASK_REMINDERS_MAX_UID_LOOKUPS","remindOrg","ctx","orgDoc","org","nowMs","timeZone","dryRun","orgId","id","data","quiet","due","handled","notified","emailed","features","skipped","flagOn","flagValues","page","ref","collection","tasks","where","orderBy","limit","get","doc","docs","push","title","String","kind","dueAtMs","remindAtMs","Number","assigneeUid","hostId","contactId","undefined","companyId","dealId","tasksTruncated","length","report","members","rows","settle","batch","task","update","reminderSentAtMs","commit","byAssignee","Map","unassigned","mine","set","size","listed","filter","member","orgSuspended","reachable","Set","map","$id","has","memberById","uids","keys","sort","userDocs","getAll","uid","userById","hostIds","Boolean","hostDocs","hostById","branding","orgSlug","workspaceName","trim","orgHub","origin","ORG_HOME","hubUrl","subdomain","HOST_DASHBOARD","host","taskUrl","hub","link","slice","indexOf","settingsUrl","MANAGE_NOTIFICATION_SETTINGS","supportLine","brand","reminderEmail","poolLookups","row","userDoc","emailReason","address","email","toLowerCase","includes","pooled","catch","record","recipients","reminderInput","productName","subject","loaded","rendered","merge","omitClosing","options","result","to","text","fromName","context","priority","sent","deferred","reason","type","body","crmTaskRemindersRoute","request","method","headers","rawHeaders","query","Response","json","error","status","process","env","CRON_SECRET","orgDocs","Date","now","requestedLimit","pageSize","isFinite","Math","min","floor","rawCursor","cursor","orgQuery","FieldPath","documentId","startAfter","hasMore","orgs","failures","console","message","failed","Object","swept","nextCursor","done"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/task-reminders-route.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type AglynOrgBilling,\n type AglynOrgMember,\n buildRoute,\n CRM_COLLECTIONS,\n isReleaseFlagOnForOrg,\n notificationMuted,\n parseOrgReleaseFlagOverrides,\n pluginRequestFromWeb,\n resolveBrandingProfile,\n resolveEffectivePlan,\n resolveOrgEntitlements,\n Route,\n} from '@aglyn/aglyn/server'\nimport {\n crmDigestEntitled,\n} from '../model/crm-digest'\nimport { rateLimitedRetryAtMs, sendEmail } from '@aglyn/shared-util-email'\nimport { withoutMailWithheldAccounts } from '@aglyn/tenant-data-admin/server/account-mail'\nimport {\n findUserByUidAcrossPools,\n firebaseAdmin,\n getServerReleaseFlagValues,\n listOrgMembers,\n memberHasOrgPermission,\n meterPlatformEmail,\n notifyUsers,\n} from '@aglyn/tenant-data-admin'\n// From the LEAF, not the barrel (AGL-2407): the cron specs mock\n// `@aglyn/tenant-data-admin` wholesale, and a suppression gate imported\n// through the barrel would be silently replaced by whatever the factory\n// lists. Same reasoning as the digest beside this route.\nimport { filterSuppressedEmails } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport {\n loadSystemEmail,\n renderLoadedSystemEmail,\n systemEmailBrand,\n systemEmailContent,\n type LoadedSystemEmail,\n} from '@aglyn/tenant-data-admin/server/render-system-email'\nimport { isCronAuthorized, isCronDryRun } from '@aglyn/tenant-data-admin/server/cron-auth'\nimport { writeCronBeat } from '@aglyn/aglyn/app-utils/health-report'\nimport { brandSupportLine } from '@aglyn/aglyn/app-utils/brand-support-line'\nimport { platformConsoleOrigin as consoleOrigin } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { digestTimeZone } from './daily-digest-route'\nimport {\n composeCrmTaskReminderBody,\n composeCrmTaskReminderEmailParts,\n composeCrmTaskReminderEmailText,\n crmTaskReminderMergeValues,\n composeCrmTaskReminderSubject,\n CRM_TASK_REMINDER_CEILING,\n type CrmReminderTask,\n crmTaskReminderDue,\n crmTaskReminderLink,\n} from '../model/crm-task-reminders'\n\n/**\n * The mark a scheduled run leaves for `/api/health/crons` (AGL-1955), on the\n * INVOCATION and before the work — the console's rule for every cron. Never\n * throws: reaching the admin app is inside the try, because a monitor that\n * can take down the job it describes is worse than the failure it watches.\n */\nasync function recordCronBeat(jobId: string): Promise<void> {\n try {\n await writeCronBeat(firebaseAdmin.app().firestore(), jobId)\n } catch {\n // Deliberately swallowed: a job that stops stamping reds its own row.\n }\n}\n\n// lockdown-423: exempt — server-internal cron (x-cron-secret), no user caller; it reads an org's CRM to remind its members and writes nothing a locked org could lose.\n\n/**\n * A task's reminder at its own time (AGL-2659): `POST /api/crm/task-reminders`.\n *\n * The digest beside this route says once a morning what is late and what\n * is due today; nothing said \"the 3:00 call is at 3:00\". Every hour this\n * finds the orgs with an open task whose `remindAtMs` has come\n * (`orgIdsWithDueReminders`), reads those orgs' tasks and tells each one's\n * assignee ONCE — a console notification per task\n * (`content.taskReminder`, opening the record the task is for) and one\n * email per member per run listing everything of theirs that fell due.\n * The words and the rule live in `crm-task-reminders.ts`; this file is the\n * reads, the sends and the sweep, in the digest's shape wherever the two\n * do the same thing.\n *\n * ## Who is told, and who is not\n *\n * The task's ASSIGNEE, and nobody else — an unassigned task is a deliberate\n * choice in the drawer, as the digest reads it. Only a member who may open\n * the CRM (`data.manage`, resolved through their custom role), only in an\n * org whose plan carries the suite and whose `release_crm` flag is\n * on: the plan gate is the digest's own (`crmDigestEntitled`), so a plan\n * that gets no digest gets no reminders either. The recipient's\n * operational-category mute (`notificationPrefs.content`) governs the WHOLE\n * reminder, mail included — unlike the digest, which has a switch of its\n * own for the schedule it keeps; a reminder is one message about one task.\n *\n * ## Once, and resumable\n *\n * `reminderSentAtMs` on the task is the idempotence: stamped in one batch\n * with the member's sends, so a rerun over the same hour reads the mark and\n * sends nothing, and stamped on every reminder the run HANDLED — sent, or\n * found nobody to send it to — so a reminder nobody can be told about does\n * not come back every hour. A reminder is left unstamped only when the run\n * never reached a verdict: the send-rate governor refused, or the org\n * threw. Orgs are swept in id order behind a cursor, as the digest sweeps\n * them, so the workflow's `done:false` loop carries any number of them\n * across invocations.\n *\n * The task query reads the NEWEST reminders that have come due, not the\n * oldest — see `CRM_TASK_REMINDER_CEILING` for why that is what makes a\n * bounded page safe against a window that only ever grows.\n *\n * The email is a `bulk` send under the platform send-rate governor\n * (AGL-2409), metered as PLATFORM mail, for the digest's reasons: a cron\n * fan-out on the shared sending domain, and the platform reminding a\n * member is not the org's sending. A refusal STOPS the run and reports\n * `done: true`; the refused member was never stamped, and the next hour's\n * run finds their reminders at the top of the window again.\n */\n\n/** The beat `/api/health/crons` reads — the `SCHEDULED_JOBS` row's id. */\nexport const CRM_TASK_REMINDERS_JOB_ID = 'crm-task-reminders'\n\n/**\n * Orgs per invocation.\n *\n * More than the digest's twenty-five: an org here costs one task query and,\n * only when something fell due, the roster and the sends. The common hour\n * finds nothing due in most orgs, and a chunk that reads a hundred empty\n * queries finishes well inside `maxDuration = 60`, which is what the whole\n * platform costs on the night `orgIdsWithDueReminders` cannot answer.\n */\nexport const CRM_TASK_REMINDERS_ORG_CHUNK = 100\n\n/** Tasks per page of the platform-wide due query. */\nconst DUE_TASK_PAGE = 500\n\n/**\n * The most members per org whose address is chased through the auth pools\n * when their member document carries none — the digest's bound, for the\n * digest's reason: work, not audience.\n */\nexport const CRM_TASK_REMINDERS_MAX_UID_LOOKUPS = 5\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Snapshot = FirebaseFirestore.DocumentSnapshot\n\n/** What one member's row in the response says. */\ninterface MemberReport {\n tasks: number\n skipped?: string\n notified?: number\n emailed?: boolean\n emailReason?: string\n deferred?: boolean\n}\n\ninterface OrgReport {\n skipped?: string\n /** Reminders that had come due and were not yet handled. */\n due: number\n /** Reminders this run stamped as handled. */\n handled: number\n /** Console notifications sent. */\n notified: number\n /** Emails sent — one per member per run. */\n emailed: number\n members?: Record<string, MemberReport>\n tasksTruncated?: boolean\n /** The send-rate governor refused; the sweep stops here. */\n deferred?: boolean\n}\n\ninterface SweepContext {\n firestore: Firestore\n nowMs: number\n timeZone: string\n dryRun: boolean\n flagValues: Awaited<ReturnType<typeof getServerReleaseFlagValues>>\n origin: string\n}\n\n/** A due reminder with the document it came from, so the stamp has a target. */\ninterface DueTask extends CrmReminderTask {\n ref: FirebaseFirestore.DocumentReference\n}\n\n/**\n * THE ORGS WITH A REMINDER OWED, found from the tasks (AGL-4121).\n *\n * The sweep used to walk the orgs to find them: every org document read, and\n * every entitled org's tasks asked, every hour, to learn that nothing had come\n * due in almost all of them. Asked of the collection group, an hour with\n * nothing due is one query and no org at all.\n *\n * Every page is read, not the newest, and that is what keeps this honest.\n * `CRM_TASK_REMINDER_CEILING` is safe per org because the newest five hundred\n * of ONE org begin with what fell due since the last run; across the platform\n * one busy org's handled reminders could bury a quiet org's first one. This\n * pays once for the same open tasks the org queries read, projected to what\n * the rule needs, and `remindOrg` then decides from its own window exactly as\n * it always has. An org found here with nothing inside that window reports\n * `nothing-due`, as it did.\n *\n * Needs the COLLECTION_GROUP `(status, remindAtMs desc)` index, and the\n * `crmTasks` entry for it is deployed after the code that first asks: see the\n * caller for what happens until then.\n */\nasync function orgIdsWithDueReminders(firestore: Firestore, nowMs: number): Promise<string[]> {\n const orgIds = new Set<string>()\n let after: FirebaseFirestore.QueryDocumentSnapshot | undefined\n for (;;) {\n let query = firestore\n .collectionGroup(CRM_COLLECTIONS.tasks)\n .where('status', '==', 'open')\n .where('remindAtMs', '<=', nowMs)\n .orderBy('remindAtMs', 'desc')\n .select('status', 'remindAtMs', 'reminderSentAtMs')\n .limit(DUE_TASK_PAGE)\n if (after) query = query.startAfter(after)\n const page = await query.get()\n for (const doc of page.docs) {\n const org = doc.ref.parent.parent\n if (org?.parent.id !== 'orgs') continue\n if (crmTaskReminderDue(doc.data() as Parameters<typeof crmTaskReminderDue>[0], nowMs)) {\n orgIds.add(org.id)\n }\n }\n if (page.docs.length < DUE_TASK_PAGE) return [...orgIds].sort()\n after = page.docs[page.docs.length - 1]\n }\n}\n\nasync function remindOrg(ctx: SweepContext, orgDoc: Snapshot): Promise<OrgReport> {\n const { firestore, nowMs, timeZone, dryRun } = ctx\n const orgId = orgDoc.id\n const org = (orgDoc.data() ?? {}) as Partial<AglynOrgBilling> & Record<string, unknown>\n const quiet: OrgReport = { due: 0, handled: 0, notified: 0, emailed: 0 }\n\n if (!crmDigestEntitled(resolveOrgEntitlements(org).features)) {\n return { ...quiet, skipped: 'not-entitled' }\n }\n const flagOn = isReleaseFlagOnForOrg(\n 'release_crm',\n ctx.flagValues['release_crm'],\n orgId,\n parseOrgReleaseFlagOverrides(org['releaseFlags']),\n resolveEffectivePlan(org),\n )\n if (!flagOn) return { ...quiet, skipped: 'release-flag' }\n\n /*\n * Every open task whose reminder has come, newest first, on\n * `(status, remindAtMs desc)`. The \"not yet handled\" half is a fact about\n * a field's absence, which a query cannot select on, so it is asked of\n * the page in memory — and the page being the NEWEST five hundred is what\n * keeps the handled ones from crowding out the next one due.\n */\n const page = await orgDoc.ref\n .collection(CRM_COLLECTIONS.tasks)\n .where('status', '==', 'open')\n .where('remindAtMs', '<=', nowMs)\n .orderBy('remindAtMs', 'desc')\n .limit(CRM_TASK_REMINDER_CEILING)\n .get()\n const due: DueTask[] = []\n for (const doc of page.docs) {\n const data = doc.data() as Record<string, unknown>\n if (!crmTaskReminderDue(data as Parameters<typeof crmTaskReminderDue>[0], nowMs)) continue\n due.push({\n ref: doc.ref,\n id: doc.id,\n title: String(data['title'] ?? ''),\n kind: (data['kind'] as CrmReminderTask['kind']) ?? 'todo',\n dueAtMs: typeof data['dueAtMs'] === 'number' ? (data['dueAtMs'] as number) : null,\n remindAtMs: Number(data['remindAtMs']),\n assigneeUid: String(data['assigneeUid'] ?? ''),\n hostId: String(data['hostId'] ?? ''),\n contactId: (data['contactId'] as string) || undefined,\n companyId: (data['companyId'] as string) || undefined,\n dealId: (data['dealId'] as string) || undefined,\n })\n }\n const tasksTruncated = page.docs.length >= CRM_TASK_REMINDER_CEILING\n if (!due.length) {\n return { ...quiet, skipped: 'nothing-due', ...(tasksTruncated ? { tasksTruncated } : {}) }\n }\n\n const report: OrgReport = { due: due.length, handled: 0, notified: 0, emailed: 0, members: {} }\n if (tasksTruncated) report.tasksTruncated = true\n const rows = report.members as Record<string, MemberReport>\n\n /**\n * Stamp a set of reminders as handled, in one batch, unless this is a\n * plan. Everything a member's verdict covers lands together, which is\n * what makes a rerun read the same answer for all of them.\n */\n const settle = async (tasks: DueTask[]): Promise<void> => {\n report.handled += tasks.length\n if (dryRun) return\n const batch = firestore.batch()\n for (const task of tasks) batch.update(task.ref, { reminderSentAtMs: nowMs })\n await batch.commit()\n }\n\n const byAssignee = new Map<string, DueTask[]>()\n const unassigned: DueTask[] = []\n for (const task of due) {\n if (!task.assigneeUid) {\n unassigned.push(task)\n continue\n }\n const mine = byAssignee.get(task.assigneeUid) ?? []\n mine.push(task)\n byAssignee.set(task.assigneeUid, mine)\n }\n // A reminder for nobody is handled by saying so: it would otherwise sit\n // at the top of the window every hour for as long as the task is open.\n if (unassigned.length) {\n rows[''] = { tasks: unassigned.length, skipped: 'unassigned' }\n await settle(unassigned)\n }\n if (!byAssignee.size) return report\n\n const listed = (await listOrgMembers(orgId)).filter(\n (member) => member.orgSuspended !== true,\n )\n // A locked or disabled account is not told (AGL-3418): it cannot sign in\n // to act on it.\n const reachable = new Set(\n await withoutMailWithheldAccounts(listed.map((member) => member.$id)),\n )\n const members = listed.filter((member) => reachable.has(member.$id))\n const memberById = new Map(members.map((member) => [member.$id, member]))\n // Sorted so the order members are told in — and the member a refusal\n // lands on — is the same on every run.\n const uids = [...byAssignee.keys()].sort()\n const userDocs = await firestore.getAll(\n ...uids.map((uid) => firestore.collection('users').doc(uid)),\n )\n const userById = new Map(userDocs.map((doc) => [doc.id, doc]))\n\n // The sites the due tasks name, for the absolute links the mail carries.\n const hostIds = [...new Set(due.map((task) => task.hostId).filter(Boolean))]\n const hostDocs = hostIds.length\n ? await firestore.getAll(...hostIds.map((hostId) => firestore.collection('hosts').doc(hostId)))\n : []\n const hostById = new Map(hostDocs.map((doc) => [doc.id, doc]))\n\n const branding = resolveBrandingProfile(org)\n const orgSlug = String(org['slug'] ?? '')\n const workspaceName = String(org['name'] ?? '').trim() || orgSlug\n const orgHub = orgSlug ? `${ctx.origin}${buildRoute(Route.ORG_HOME, { orgSlug })}/crm` : null\n const hubUrl = (hostId: string): string | null => {\n if (!hostId) return orgHub\n const subdomain = String(hostById.get(hostId)?.get('subdomain') ?? '')\n if (!orgSlug || !subdomain) return null\n return `${ctx.origin}${buildRoute(Route.HOST_DASHBOARD, { orgSlug, host: subdomain })}/crm`\n }\n // The record the task is for, on its own hub — the notification's link\n // past the `/{hostId}/crm` or `/org/crm` prefix the console rewrites.\n const taskUrl = (task: CrmReminderTask): string => {\n const hub = hubUrl(task.hostId)\n if (!hub) return ctx.origin\n const link = crmTaskReminderLink(task.hostId || null, task)\n return `${hub}${link.slice(link.indexOf('/crm') + '/crm'.length)}`\n }\n const settingsUrl = `${ctx.origin}${buildRoute(Route.MANAGE_NOTIFICATION_SETTINGS)}`\n const supportLine = brandSupportLine(branding)\n const brand = systemEmailBrand(org)\n let reminderEmail: Promise<LoadedSystemEmail | null> | undefined\n let poolLookups = 0\n\n for (const uid of uids) {\n const tasks = byAssignee.get(uid) as DueTask[]\n const row: MemberReport = { tasks: tasks.length }\n rows[uid] = row\n const member = memberById.get(uid)\n if (!member) {\n row.skipped = 'not-a-member'\n await settle(tasks)\n continue\n }\n if (!(await memberHasOrgPermission(orgId, member as AglynOrgMember, 'data.manage'))) {\n row.skipped = 'no-data-manage'\n await settle(tasks)\n continue\n }\n const userDoc = userById.get(uid)\n if (notificationMuted(userDoc?.get('notificationPrefs'), 'content.taskReminder')) {\n row.skipped = 'muted'\n await settle(tasks)\n continue\n }\n if (dryRun) {\n row.notified = tasks.length\n row.emailed = true\n await settle(tasks)\n continue\n }\n\n /*\n * THE EMAIL FIRST, for the digest's reason: a refusal from the\n * send-rate governor stops the run with nothing else done for this\n * member — no notification to double next hour, no mark to skip them\n * on — so the order here is the idempotence, not a preference.\n */\n let emailed = false\n let emailReason: string | undefined\n let address = String(member.email ?? '').trim().toLowerCase()\n if (!address.includes('@') && poolLookups < CRM_TASK_REMINDERS_MAX_UID_LOOKUPS) {\n poolLookups += 1\n const pooled = await findUserByUidAcrossPools(uid).catch(() => null)\n address = String(pooled?.record?.email ?? '').trim().toLowerCase()\n }\n const recipients = address.includes('@')\n ? await filterSuppressedEmails([address], firestore)\n : []\n if (!recipients.length) {\n emailReason = address.includes('@') ? 'suppressed' : 'no-recipient'\n } else {\n const reminderInput = {\n tasks,\n timeZone,\n productName: branding.productName,\n workspaceName,\n taskUrl,\n settingsUrl,\n supportLine,\n }\n const subject = composeCrmTaskReminderSubject(tasks)\n // The `crm-task-reminder` system email (AGL-3367), in the org's brand,\n // read once per org and rendered per member.\n reminderEmail ??= loadSystemEmail('crm-task-reminder').catch(() => null)\n const loaded = await reminderEmail\n const rendered = loaded\n ? renderLoadedSystemEmail(\n loaded,\n {\n ...brand.merge,\n 'reminder.subject': subject,\n 'reminder.body': composeCrmTaskReminderEmailText({ ...reminderInput, omitClosing: true }),\n ...crmTaskReminderMergeValues(composeCrmTaskReminderEmailParts(reminderInput)),\n settingsUrl,\n },\n brand.options,\n )\n : null\n const result = await sendEmail({\n to: recipients,\n ...systemEmailContent(rendered, {\n subject,\n text: composeCrmTaskReminderEmailText(reminderInput),\n }),\n fromName: branding.fromName,\n context: 'crm-task-reminder',\n priority: 'bulk',\n })\n if (result.sent) {\n emailed = true\n await meterPlatformEmail().catch(() => undefined)\n } else if (rateLimitedRetryAtMs(result) !== null) {\n row.deferred = true\n report.deferred = true\n return report\n } else {\n emailReason = (result as { reason?: string }).reason ?? 'failed'\n }\n }\n\n // One notification per task: each opens the record it is for.\n for (const task of tasks) {\n await notifyUsers([uid], {\n type: 'content.taskReminder',\n title: 'Task reminder',\n body: composeCrmTaskReminderBody(task, timeZone, workspaceName),\n link: crmTaskReminderLink(task.hostId || null, task),\n orgId,\n ...(task.hostId ? { hostId: task.hostId } : {}),\n })\n }\n await settle(tasks)\n row.notified = tasks.length\n row.emailed = emailed\n if (emailReason) row.emailReason = emailReason\n report.notified += tasks.length\n if (emailed) report.emailed += 1\n }\n return report\n}\n\nexport async function crmTaskRemindersRoute(request: Request): Promise<Response> {\n const { method, body, headers: rawHeaders, query } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'POST' && method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n if (!process.env.CRON_SECRET) {\n return Response.json(\n { error: 'Task reminders are not configured (CRON_SECRET).' },\n { status: 501 },\n )\n }\n if (!isCronAuthorized(headers)) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n // A GET reports the plan and sends nothing; a bodyless POST — what the\n // scheduled workflow sends — is a real run. `isCronDryRun` keys the\n // default on the METHOD for exactly that reason.\n const dryRun = isCronDryRun({ method, body, query })\n // Stamped on the invocation, not on the work (AGL-1955): an hour with\n // nothing due still ran.\n if (method === 'POST') await recordCronBeat(CRM_TASK_REMINDERS_JOB_ID)\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const nowMs = Date.now()\n const ctx: SweepContext = {\n firestore,\n nowMs,\n timeZone: digestTimeZone(),\n dryRun,\n flagValues: await getServerReleaseFlagValues(),\n origin: consoleOrigin(),\n }\n\n // The resumable sweep, in the shape the digest reads orgs (AGL-2220):\n // ordered by id, one more than a chunk to learn whether there is more, the\n // cursor validated as an org id before it becomes a reference.\n const requestedLimit = Number((body as { limit?: unknown } | undefined)?.limit)\n const pageSize =\n Number.isFinite(requestedLimit) && requestedLimit > 0\n ? Math.min(Math.floor(requestedLimit), CRM_TASK_REMINDERS_ORG_CHUNK)\n : CRM_TASK_REMINDERS_ORG_CHUNK\n const rawCursor = (body as { cursor?: unknown } | undefined)?.cursor\n const cursor =\n typeof rawCursor === 'string' && rawCursor.length > 0 && !rawCursor.includes('/')\n ? rawCursor\n : null\n /*\n * Only the orgs with something owed. A due query that fails is not an hour\n * with nothing due, so it never answers for one: it falls back to walking\n * every org, which needs no collection-group index and is what makes this\n * safe to ship ahead of the index it uses. Composite indexes deploy in the\n * promotion's tail, and a reminder must not wait on the order of that tail.\n */\n const withDue = await orgIdsWithDueReminders(firestore, nowMs).catch((error: unknown) => {\n console.warn('[crm] task reminders: the due query failed, sweeping every org', error)\n return null\n })\n let orgDocs: Snapshot[]\n let hasMore: boolean\n /** Where the next chunk starts, whichever way this one was chosen. */\n let lastOrgId: string | null\n if (withDue) {\n const ahead = cursor ? withDue.filter((orgId) => orgId > cursor) : withDue\n const chunk = ahead.slice(0, pageSize)\n hasMore = ahead.length > pageSize\n lastOrgId = chunk[chunk.length - 1] ?? null\n // A task can outlive its org by a moment; there is nobody to tell.\n orgDocs = chunk.length\n ? (await firestore.getAll(...chunk.map((orgId) => firestore.collection('orgs').doc(orgId)))).filter(\n (doc) => doc.exists,\n )\n : []\n } else {\n let orgQuery = firestore\n .collection('orgs')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(pageSize + 1)\n if (cursor) orgQuery = orgQuery.startAfter(firestore.collection('orgs').doc(cursor))\n const page = await orgQuery.get()\n hasMore = page.docs.length > pageSize\n orgDocs = hasMore ? page.docs.slice(0, pageSize) : page.docs\n lastOrgId = orgDocs[orgDocs.length - 1]?.id ?? null\n }\n\n const orgs: Record<string, OrgReport> = {}\n const failures: Record<string, string> = {}\n let due = 0\n let handled = 0\n let notified = 0\n let emailed = 0\n let deferred = false\n for (const orgDoc of orgDocs) {\n try {\n const report = await remindOrg(ctx, orgDoc)\n orgs[orgDoc.id] = report\n due += report.due\n handled += report.handled\n notified += report.notified\n emailed += report.emailed\n if (report.deferred) {\n deferred = true\n break\n }\n } catch (error) {\n console.error('[crm] task reminders failed for org', orgDoc.id, error)\n failures[orgDoc.id] = (error as Error)?.message ?? 'unknown'\n }\n }\n const failed = Object.keys(failures)\n return Response.json(\n {\n timeZone: ctx.timeZone,\n dryRun,\n swept: orgDocs.length,\n due,\n handled,\n notified,\n emailed,\n orgs,\n ...(deferred ? { deferred: true } : {}),\n ...(failed.length ? { failures } : {}),\n // A deferred run reports `done: true` — see the module comment —\n // so the workflow's loop does not re-POST into a full window.\n nextCursor: deferred || !hasMore ? null : lastOrgId,\n done: deferred || !hasMore,\n },\n // 207: this chunk finished and some org in it needs a person, which is\n // what fails the workflow (its one alerting channel).\n { status: failed.length ? 207 : 200 },\n )\n } catch (error) {\n console.error('[crm] task reminders run failed', error)\n return Response.json({ error: 'Task reminders run failed' }, { status: 500 })\n }\n}\n"],"names":["buildRoute","CRM_COLLECTIONS","isReleaseFlagOnForOrg","notificationMuted","parseOrgReleaseFlagOverrides","pluginRequestFromWeb","resolveBrandingProfile","resolveEffectivePlan","resolveOrgEntitlements","Route","crmDigestEntitled","rateLimitedRetryAtMs","sendEmail","withoutMailWithheldAccounts","findUserByUidAcrossPools","firebaseAdmin","getServerReleaseFlagValues","listOrgMembers","memberHasOrgPermission","meterPlatformEmail","notifyUsers","filterSuppressedEmails","loadSystemEmail","renderLoadedSystemEmail","systemEmailBrand","systemEmailContent","isCronAuthorized","isCronDryRun","writeCronBeat","brandSupportLine","platformConsoleOrigin","consoleOrigin","digestTimeZone","composeCrmTaskReminderBody","composeCrmTaskReminderEmailParts","composeCrmTaskReminderEmailText","crmTaskReminderMergeValues","composeCrmTaskReminderSubject","CRM_TASK_REMINDER_CEILING","crmTaskReminderDue","crmTaskReminderLink","recordCronBeat","jobId","app","firestore","CRM_TASK_REMINDERS_JOB_ID","CRM_TASK_REMINDERS_ORG_CHUNK","DUE_TASK_PAGE","CRM_TASK_REMINDERS_MAX_UID_LOOKUPS","orgIdsWithDueReminders","nowMs","orgIds","Set","after","query","collectionGroup","tasks","where","orderBy","select","limit","startAfter","page","get","doc","docs","org","ref","parent","id","data","add","length","sort","remindOrg","ctx","orgDoc","timeZone","dryRun","orgId","quiet","due","handled","notified","emailed","features","skipped","flagOn","flagValues","collection","push","title","String","kind","dueAtMs","remindAtMs","Number","assigneeUid","hostId","contactId","undefined","companyId","dealId","tasksTruncated","report","members","rows","settle","batch","task","update","reminderSentAtMs","commit","byAssignee","Map","unassigned","mine","set","size","listed","filter","member","orgSuspended","reachable","map","$id","has","memberById","uids","keys","userDocs","getAll","uid","userById","hostIds","Boolean","hostDocs","hostById","branding","orgSlug","workspaceName","trim","orgHub","origin","ORG_HOME","hubUrl","subdomain","HOST_DASHBOARD","host","taskUrl","hub","link","slice","indexOf","settingsUrl","MANAGE_NOTIFICATION_SETTINGS","supportLine","brand","reminderEmail","poolLookups","row","userDoc","emailReason","address","email","toLowerCase","includes","pooled","catch","record","recipients","reminderInput","productName","subject","loaded","rendered","merge","omitClosing","options","result","to","text","fromName","context","priority","sent","deferred","reason","type","body","crmTaskRemindersRoute","request","method","headers","rawHeaders","Response","json","error","status","process","env","CRON_SECRET","Date","now","requestedLimit","pageSize","isFinite","Math","min","floor","rawCursor","cursor","withDue","console","warn","orgDocs","hasMore","lastOrgId","chunk","ahead","exists","orgQuery","FieldPath","documentId","orgs","failures","message","failed","Object","swept","nextCursor","done"],"mappings":"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"}
|