Files
exodevices/medcenterstart/server/src/lead-service.ts
T
Yuriy PanovandClaude Opus 5 0732aa2096 Rename medcenter landings and add the revenue calculator
Rename the two medcenter landings to their audience names:
medcenter -> medcenterphysio, medcenterpersonal -> medcenterstart.

Alongside the rename:
- add a RevenueCalculator section to both landings;
- rework the copy and figures in src/data/content.ts;
- simplify the lead form: drop the "cabinet_state" and "profile"
  selects (along with SelectField and the matching fields in
  shared/lead.ts, lead-mapper.ts and amo-check.ts) and make
  company and email optional;
- add the legacy/new static prototypes for both landings;
- add pnpm-lock.yaml to medcenterstart (package-lock.json is still
  there too).

deploy/apps.conf and deploy/README.md still refer to the old
directory names and need a follow-up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 23:19:46 +06:00

125 lines
4.3 KiB
TypeScript

import type { LeadPayload } from '../../shared/lead.ts'
import { AmoClient, type AmoContact, type AmoFieldEntry } from './amocrm.ts'
import { buildLeadName, mapLead, resolveLeadFieldMap, type LeadFieldMap } from './lead-mapper.ts'
const digitsOnly = (value: string) => value.replace(/\D/g, '')
/**
* Last ten digits — the part that is stable across "+7 (999) 123-45-67",
* "8 999 1234567" and "9991234567". Used to decide whether amoCRM already
* knows a phone number.
*/
const phoneKey = (value: string) => digitsOnly(value).slice(-10)
/**
* amoCRM's full-text search matches digit substrings, but a contact stored as
* "+7 999…" is not found by a query starting with "8 999…". Try every spelling
* a Russian visitor might type.
*/
function phoneQueries(phone: string): string[] {
const digits = digitsOnly(phone)
const queries = new Set<string>([phone.trim(), digits])
if (digits.length >= 10) {
const national = digits.slice(-10)
queries.add(national)
queries.add(`7${national}`)
queries.add(`8${national}`)
}
return [...queries].filter(Boolean)
}
/** Reads the values already stored in a contact's standard multitext field. */
function existingValues(contact: AmoContact, code: 'PHONE' | 'EMAIL'): string[] {
const entry = contact.custom_fields_values?.find((f) => f.field_code === code)
return (entry?.values ?? []).map((v) => String(v.value ?? '')).filter(Boolean)
}
function contactFieldEntries(payload: LeadPayload): AmoFieldEntry[] {
const entries: AmoFieldEntry[] = [
{ field_code: 'PHONE', values: [{ value: payload.phone, enum_code: 'WORK' }] },
]
if (payload.email) {
entries.push({ field_code: 'EMAIL', values: [{ value: payload.email, enum_code: 'WORK' }] })
}
return entries
}
export interface CreatedLead {
leadId: number
contactId: number
contactCreated: boolean
}
export class LeadService {
private fieldMap?: Promise<LeadFieldMap>
constructor(private readonly amo: AmoClient) {}
private getFieldMap(): Promise<LeadFieldMap> {
this.fieldMap ??= this.amo.getLeadFields().then(resolveLeadFieldMap)
return this.fieldMap
}
async submit(payload: LeadPayload): Promise<CreatedLead> {
const fieldMap = await this.getFieldMap()
// 1. Reuse the existing contact when the phone or email is already known.
const existing = await this.amo.findContact([...phoneQueries(payload.phone), payload.email ?? ''])
let contactId: number
let contactCreated = false
if (existing) {
contactId = existing.id
// Add whichever channel amoCRM does not have on file yet.
const missing: AmoFieldEntry[] = []
const knownPhones = existingValues(existing, 'PHONE').map(phoneKey)
if (!knownPhones.includes(phoneKey(payload.phone))) {
missing.push({
field_code: 'PHONE',
values: [
...existingValues(existing, 'PHONE').map((value) => ({ value })),
{ value: payload.phone, enum_code: 'WORK' },
],
})
}
if (payload.email) {
const knownEmails = existingValues(existing, 'EMAIL').map((e) => e.toLowerCase())
if (!knownEmails.includes(payload.email.toLowerCase())) {
missing.push({
field_code: 'EMAIL',
values: [
...existingValues(existing, 'EMAIL').map((value) => ({ value })),
{ value: payload.email, enum_code: 'WORK' },
],
})
}
}
await this.amo.appendContactFields(contactId, missing)
} else {
const created = await this.amo.createContact({ name: payload.name, fields: contactFieldEntries(payload) })
contactId = created.id
contactCreated = true
}
// 2. Lead in the configured pipeline, first stage.
const { fields, note } = mapLead(payload, fieldMap)
const lead = await this.amo.createLead({
name: buildLeadName(payload),
contactId,
fields,
tags: ['заявка с сайта'],
})
// 3. Everything the account has no field for lands in a readable note.
// A failed note must not fail the lead — the lead is the valuable part.
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, contactId, contactCreated }
}
}