leads: new phone number and an amoCRM lead on every call-button click

The published number becomes +7 927 789-60-71 across all four landings —
`contacts` in each `src/data/content.ts`, plus the fallback messages in the
lead endpoints and `src/lib/lead.ts` that spell it out when amoCRM is down.
The `legacy/` reference pages keep the old number: they are the visual
originals, not something that ships.

Tapping the phone button now also creates a lead. It cannot reuse the form
path: a click carries no name and no number, so `callClickSchema` in
`shared/lead.ts` validates the tracking data alone, `LeadService.submitCallClick`
creates a contactless lead tagged `клик по телефону`, and `AmoClient.createLead`
takes an optional `contactId` for it. The mapper's field/note split moved into
`collect()` so both lead kinds share it.

The browser fires this with `sendBeacon` (falling back to `fetch keepalive`),
because the same click hands the page to `tel:` and a plain fetch would be
cut off mid-flight. Duplicates are held down from both ends: one lead per
browser session on the client, four per IP per 30 minutes on the server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Yuriy Panov
2026-09-09 00:58:15 +06:00
co-authored by Claude Opus 5
parent e7385e8898
commit e13854e918
39 changed files with 1059 additions and 106 deletions
+34 -2
View File
@@ -1,6 +1,13 @@
import type { LeadPayload } from '../../shared/lead.ts'
import type { CallClickPayload, LeadPayload } from '../../shared/lead.ts'
import { AmoClient, type AmoContact, type AmoFieldEntry } from './amocrm.ts'
import { buildLeadName, mapLead, resolveLeadFieldMap, type LeadFieldMap } from './lead-mapper.ts'
import {
buildCallClickName,
buildLeadName,
mapCallClick,
mapLead,
resolveLeadFieldMap,
type LeadFieldMap,
} from './lead-mapper.ts'
const digitsOnly = (value: string) => value.replace(/\D/g, '')
@@ -121,4 +128,29 @@ export class LeadService {
return { leadId: lead.id, contactId, contactCreated }
}
/**
* A visitor tapped the phone button. There is no contact to reuse or create —
* only the click itself — so the lead stands alone, and its tag and note are
* what tell a manager to expect an incoming call from this landing.
*/
async submitCallClick(payload: CallClickPayload): Promise<{ leadId: number }> {
const fieldMap = await this.getFieldMap()
const { fields, note } = mapCallClick(payload, fieldMap)
const lead = await this.amo.createLead({
name: buildCallClickName(),
fields,
tags: ['клик по телефону'],
})
// As above: a failed note must not fail the lead.
if (note) {
await this.amo.addLeadNote(lead.id, note).catch((error: unknown) => {
console.warn('[amo] lead %d created but the note failed:', lead.id, error)
})
}
return { leadId: lead.id }
}
}