feat: add libphonenumber-js for phone number parsing and validation

- Integrated libphonenumber-js to handle phone number parsing and validation in the addContact and registration features.
- Removed deprecated phone validation and normalization functions from utils.
- Updated contact handling logic to ensure valid phone numbers are processed correctly.
This commit is contained in:
vchikalkin 2025-10-12 13:41:48 +03:00
parent b88ac07ba3
commit 6a21f5911e
6 changed files with 37 additions and 27 deletions

View File

@ -25,7 +25,7 @@ description =
start = start =
.description = Запуск бота .description = Запуск бота
addcontact = addcontact =
.description = Добавить контакт пользователя .description = Добавить контакт
sharebot = sharebot =
.description = Поделиться ботом .description = Поделиться ботом
subscribe = subscribe =
@ -36,7 +36,7 @@ help =
.description = Список команд и поддержка .description = Список команд и поддержка
commands-list = commands-list =
📋 Доступные команды: 📋 Доступные команды:
• /addcontact — добавить контакт пользователя • /addcontact — добавить контакт
• /sharebot — поделиться ботом • /sharebot — поделиться ботом
• /subscribe — приобрести Pro доступ • /subscribe — приобрести Pro доступ
• /pro — информация о вашем Pro доступе • /pro — информация о вашем Pro доступе

View File

@ -29,6 +29,7 @@
"dayjs": "catalog:", "dayjs": "catalog:",
"grammy": "^1.38.1", "grammy": "^1.38.1",
"ioredis": "^5.7.0", "ioredis": "^5.7.0",
"libphonenumber-js": "^1.12.24",
"pino": "^9.9.0", "pino": "^9.9.0",
"pino-pretty": "^13.1.1", "pino-pretty": "^13.1.1",
"radashi": "catalog:", "radashi": "catalog:",

View File

@ -5,10 +5,10 @@ import { env } from '@/config/env';
import { KEYBOARD_SHARE_BOT, KEYBOARD_SHARE_PHONE } from '@/config/keyboards'; import { KEYBOARD_SHARE_BOT, KEYBOARD_SHARE_PHONE } from '@/config/keyboards';
import { parseContact } from '@/utils/contact'; import { parseContact } from '@/utils/contact';
import { combine } from '@/utils/messages'; import { combine } from '@/utils/messages';
import { isValidPhoneNumber, normalizePhoneNumber } from '@/utils/phone';
import { type Conversation } from '@grammyjs/conversations'; import { type Conversation } from '@grammyjs/conversations';
import { CustomersService } from '@repo/graphql/api/customers'; import { CustomersService } from '@repo/graphql/api/customers';
import { RegistrationService } from '@repo/graphql/api/registration'; import { RegistrationService } from '@repo/graphql/api/registration';
import parsePhoneNumber from 'libphonenumber-js';
export async function addContact(conversation: Conversation<Context, Context>, ctx: Context) { export async function addContact(conversation: Conversation<Context, Context>, ctx: Context) {
// Все пользователи могут добавлять контакты // Все пользователи могут добавлять контакты
@ -60,23 +60,36 @@ export async function addContact(conversation: Conversation<Context, Context>, c
let phone = ''; let phone = '';
if (firstCtx.message?.contact) { if (firstCtx.message?.contact) {
/**
* Отправлен контакт
*/
const { contact } = firstCtx.message; const { contact } = firstCtx.message;
const parsedContact = parseContact(contact); const parsedContact = parseContact(contact);
const parsedPhone = parsePhoneNumber(contact.phone_number, 'RU');
name = parsedContact.name; name = parsedContact.name;
surname = parsedContact.surname; surname = parsedContact.surname;
phone = normalizePhoneNumber(parsedContact.phone);
if (!parsedPhone?.isValid() || !parsedPhone.number) {
return ctx.reply(await conversation.external(({ t }) => t('msg-invalid-phone')));
}
phone = parsedPhone.number;
} else if (firstCtx.message?.text) { } else if (firstCtx.message?.text) {
const typedPhone = normalizePhoneNumber(firstCtx.message.text); /**
if (!isValidPhoneNumber(typedPhone)) { * Номер в тексте сообщения
*/
const parsedPhone = parsePhoneNumber(firstCtx.message.text, 'RU');
if (!parsedPhone?.isValid() || !parsedPhone.number) {
return ctx.reply(await conversation.external(({ t }) => t('msg-invalid-phone'))); return ctx.reply(await conversation.external(({ t }) => t('msg-invalid-phone')));
} }
// Нельзя добавлять свой собственный номер телефона // Нельзя добавлять свой собственный номер телефона
if (customer.phone && normalizePhoneNumber(customer.phone) === typedPhone) { if (customer.phone && customer.phone === parsedPhone.number) {
return ctx.reply(await conversation.external(({ t }) => t('err-cannot-add-self'))); return ctx.reply(await conversation.external(({ t }) => t('err-cannot-add-self')));
} }
phone = typedPhone; phone = parsedPhone.number;
// Просим ввести имя клиента // Просим ввести имя клиента
await ctx.reply(await conversation.external(({ t }) => t('msg-send-client-name'))); await ctx.reply(await conversation.external(({ t }) => t('msg-send-client-name')));
@ -101,11 +114,6 @@ export async function addContact(conversation: Conversation<Context, Context>, c
return ctx.reply(await conversation.external(({ t }) => t('msg-send-client-contact-or-phone'))); return ctx.reply(await conversation.external(({ t }) => t('msg-send-client-contact-or-phone')));
} }
// Проверяем валидность номера телефона
if (!isValidPhoneNumber(phone)) {
return ctx.reply(await conversation.external(({ t }) => t('msg-invalid-phone')));
}
try { try {
// Проверяем, есть ли клиент с таким номером // Проверяем, есть ли клиент с таким номером
const { customer: existingCustomer } = await registrationService._NOCACHE_GetCustomer({ const { customer: existingCustomer } = await registrationService._NOCACHE_GetCustomer({

View File

@ -2,9 +2,9 @@ import { type Context } from '@/bot/context';
import { logHandle } from '@/bot/helpers/logging'; import { logHandle } from '@/bot/helpers/logging';
import { KEYBOARD_REMOVE, mainMenu } from '@/config/keyboards'; import { KEYBOARD_REMOVE, mainMenu } from '@/config/keyboards';
import { parseContact } from '@/utils/contact'; import { parseContact } from '@/utils/contact';
import { isValidPhoneNumber, normalizePhoneNumber } from '@/utils/phone';
import { RegistrationService } from '@repo/graphql/api/registration'; import { RegistrationService } from '@repo/graphql/api/registration';
import { Composer } from 'grammy'; import { Composer } from 'grammy';
import parsePhoneNumber from 'libphonenumber-js';
const composer = new Composer<Context>(); const composer = new Composer<Context>();
@ -35,13 +35,15 @@ feature.on(':contact', logHandle('contact-registration'), async (ctx) => {
} }
// Нормализация и валидация номера // Нормализация и валидация номера
const phone = normalizePhoneNumber(contact.phone_number); const parsedPhone = parsePhoneNumber(contact.phone_number, 'RU');
if (!isValidPhoneNumber(phone)) { if (!parsedPhone?.isValid() || !parsedPhone?.number) {
return ctx.reply(ctx.t('msg-invalid-phone')); return ctx.reply(ctx.t('msg-invalid-phone'));
} }
try { try {
const { customer } = await registrationService._NOCACHE_GetCustomer({ phone }); const { customer } = await registrationService._NOCACHE_GetCustomer({
phone: parsedPhone.number,
});
if (customer && !customer.telegramId) { if (customer && !customer.telegramId) {
// Пользователь добавлен ранее мастером — обновляем данные // Пользователь добавлен ранее мастером — обновляем данные
@ -57,7 +59,7 @@ feature.on(':contact', logHandle('contact-registration'), async (ctx) => {
// Новый пользователь — создаём и активируем // Новый пользователь — создаём и активируем
const response = await registrationService.createCustomer({ const response = await registrationService.createCustomer({
data: { name, phone, surname, telegramId }, data: { name, phone: parsedPhone.number, surname, telegramId },
}); });
const documentId = response?.createCustomer?.documentId; const documentId = response?.createCustomer?.documentId;

View File

@ -1,9 +0,0 @@
export function isValidPhoneNumber(phone: string) {
return /^\+7\d{10}$/u.test(phone);
}
export function normalizePhoneNumber(phone: string): string {
const digitsOnly = phone.replaceAll(/\D/gu, '');
return `+${digitsOnly}`;
}

8
pnpm-lock.yaml generated
View File

@ -153,6 +153,9 @@ importers:
ioredis: ioredis:
specifier: ^5.7.0 specifier: ^5.7.0
version: 5.7.0 version: 5.7.0
libphonenumber-js:
specifier: ^1.12.24
version: 1.12.24
pino: pino:
specifier: ^9.9.0 specifier: ^9.9.0
version: 9.9.0 version: 9.9.0
@ -6094,6 +6097,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
libphonenumber-js@1.12.24:
resolution: {integrity: sha512-l5IlyL9AONj4voSd7q9xkuQOL4u8Ty44puTic7J88CmdXkxfGsRfoVLXHCxppwehgpb/Chdb80FFehHqjN3ItQ==}
light-my-request@5.14.0: light-my-request@5.14.0:
resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==}
@ -15291,6 +15297,8 @@ snapshots:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
type-check: 0.4.0 type-check: 0.4.0
libphonenumber-js@1.12.24: {}
light-my-request@5.14.0: light-my-request@5.14.0:
dependencies: dependencies:
cookie: 0.7.2 cookie: 0.7.2