first commit

This commit is contained in:
Bivekich
2025-06-26 06:59:19 +03:00
commit 18e1f3ffb1
124 changed files with 52448 additions and 0 deletions

View File

@ -0,0 +1,931 @@
interface YandexLocationDetectRequest {
location: string;
}
interface YandexLocationDetectResponse {
variants: {
address: string;
geo_id: number;
}[];
}
interface YandexPickupPointsRequest {
available_for_dropoff?: boolean;
geo_id?: number;
is_not_branded_partner_station?: boolean;
is_post_office?: boolean;
is_yandex_branded?: boolean;
latitude?: {
from: number;
to: number;
};
longitude?: {
from: number;
to: number;
};
payment_method?: 'already_paid' | 'card_on_receipt';
payment_methods?: ('already_paid' | 'card_on_receipt')[];
pickup_point_ids?: string[];
type?: 'pickup_point' | 'terminal' | 'post_office' | 'sorting_center';
}
interface YandexPickupPoint {
id: string;
address: {
apartment?: string;
building?: string;
comment?: string;
country?: string;
full_address: string;
geoId?: number;
house?: string;
housing?: string;
locality?: string;
postal_code?: string;
region?: string;
street?: string;
subRegion?: string;
};
contact: {
phone: string;
email?: string;
first_name?: string;
last_name?: string;
partonymic?: string;
};
name: string;
payment_methods: ('already_paid' | 'card_on_receipt')[];
position: {
latitude: number;
longitude: number;
};
schedule: {
restrictions: {
days: number[];
time_from: {
hours: number;
minutes: number;
};
time_to: {
hours: number;
minutes: number;
};
}[];
time_zone: number;
};
type: 'pickup_point' | 'terminal' | 'post_office' | 'sorting_center';
dayoffs?: {
date: number;
date_utc: number;
}[];
instruction?: string;
is_dark_store?: boolean;
is_market_partner?: boolean;
is_post_office?: boolean;
is_yandex_branded?: boolean;
operator_station_id?: string;
}
interface YandexPickupPointsResponse {
points: YandexPickupPoint[];
}
const BASE_URL = 'https://b2b-authproxy.taxi.yandex.net/api/b2b/platform';
class YandexDeliveryService {
private token: string;
constructor() {
this.token = process.env.YANDEX_DELIVERY_TOKEN || '';
if (!this.token) {
throw new Error('YANDEX_DELIVERY_TOKEN не установлен в переменных окружения');
}
}
private async makeRequest<T>(endpoint: string, data?: any): Promise<T> {
const url = `${BASE_URL}${endpoint}`;
console.log(`🚚 Яндекс Доставка запрос: ${endpoint}`);
console.log('📦 Данные запроса:', data ? JSON.stringify(data, null, 2) : 'без данных');
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : undefined,
});
console.log(`📡 Статус ответа: ${response.status}`);
if (!response.ok) {
const errorText = await response.text();
console.error(`❌ Ошибка API Яндекс доставки: ${response.status} - ${errorText}`);
throw new Error(`Ошибка API Яндекс доставки: ${response.status} - ${errorText}`);
}
const result = await response.json();
console.log('✅ Успешный ответ от Яндекс Доставки:', JSON.stringify(result, null, 2));
return result;
}
/**
* Получение идентификатора населённого пункта по адресу
*/
async detectLocation(location: string): Promise<YandexLocationDetectResponse> {
return this.makeRequest<YandexLocationDetectResponse>('/location/detect', {
location,
});
}
/**
* Получение списка точек самопривоза и ПВЗ
*/
async getPickupPoints(request: YandexPickupPointsRequest = {}): Promise<YandexPickupPointsResponse> {
return this.makeRequest<YandexPickupPointsResponse>('/pickup-points/list', request);
}
/**
* Получение ПВЗ для конкретного города
*/
async getPickupPointsByCity(cityName: string): Promise<YandexPickupPoint[]> {
try {
// Сначала получаем geo_id города
const locationResponse = await this.detectLocation(cityName);
if (locationResponse.variants.length === 0) {
return [];
}
const geoId = locationResponse.variants[0].geo_id;
// Получаем все ПВЗ для этого города
const pickupResponse = await this.getPickupPoints({
geo_id: geoId,
is_yandex_branded: true, // Только брендированные ПВЗ Яндекса
});
return pickupResponse.points;
} catch (error) {
console.error('Ошибка получения ПВЗ для города:', error);
return [];
}
}
/**
* Получение ПВЗ в заданном радиусе от координат
*/
async getPickupPointsByCoordinates(
latitude: number,
longitude: number,
radiusKm: number = 10
): Promise<YandexPickupPoint[]> {
try {
// Конвертируем радиус в градусы (приблизительно)
const radiusDegrees = radiusKm / 111; // 1 градус ≈ 111 км
const pickupResponse = await this.getPickupPoints({
latitude: {
from: latitude - radiusDegrees,
to: latitude + radiusDegrees,
},
longitude: {
from: longitude - radiusDegrees,
to: longitude + radiusDegrees,
},
is_yandex_branded: true,
});
return pickupResponse.points;
} catch (error) {
console.error('Ошибка получения ПВЗ по координатам:', error);
return [];
}
}
/**
* Форматирование расписания работы ПВЗ
*/
formatSchedule(schedule: YandexPickupPoint['schedule']): string {
if (!schedule.restrictions || schedule.restrictions.length === 0) {
return 'Расписание не указано';
}
const dayNames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
return schedule.restrictions.map(restriction => {
const days = restriction.days.map(day => dayNames[day - 1]).join(', ');
const timeFrom = `${restriction.time_from.hours.toString().padStart(2, '0')}:${restriction.time_from.minutes.toString().padStart(2, '0')}`;
const timeTo = `${restriction.time_to.hours.toString().padStart(2, '0')}:${restriction.time_to.minutes.toString().padStart(2, '0')}`;
return `${days}: ${timeFrom}-${timeTo}`;
}).join('; ');
}
/**
* Получение типа ПВЗ на русском языке
*/
getTypeLabel(type: string): string {
const labels = {
pickup_point: 'Пункт выдачи',
terminal: 'Постомат',
post_office: 'Почтовое отделение',
sorting_center: 'Сортировочный центр',
};
return labels[type as keyof typeof labels] || type;
}
/**
* Создание заявки на доставку и получение офферов
*/
async createOffer(request: CreateOfferRequest): Promise<CreateOfferResponse> {
return this.makeRequest<CreateOfferResponse>('/offers/create', request);
}
/**
* Парсинг адреса на компоненты
*/
private parseAddress(address: string): {
city?: string;
street?: string;
house?: string;
region?: string;
full_address: string;
} {
console.log('🔍 Начинаем парсинг адреса:', address);
let city = '';
let street = '';
let house = '';
let region = '';
// Нормализуем адрес: убираем лишние пробелы, приводим к нижнему регистру для поиска
const normalizedAddress = address.trim().toLowerCase();
// Ищем номер дома (цифры с возможными буквами, корпусом, строением)
const housePatterns = [
/\bд[\.\s]*(\d+[а-яё]?(?:\s*к[\.\s]*\d+)?(?:\s*стр[\.\s]*\d+)?)\b/i,
/\bдом[\.\s]*(\d+[а-яё]?(?:\s*к[\.\s]*\d+)?(?:\s*стр[\.\s]*\d+)?)\b/i,
/\b(\d+[а-яё]?(?:\s*к[\.\s]*\d+)?(?:\s*стр[\.\s]*\d+)?)\s*$/i, // В конце строки
/\b(\d+[а-яё]?)\b/i // Просто число с возможной буквой
];
for (const pattern of housePatterns) {
const match = address.match(pattern);
if (match) {
house = match[1].trim();
console.log('🏠 Найден номер дома:', house);
break;
}
}
// Ищем известные города
const cities = [
{ name: 'Москва', patterns: ['москва', 'moscow'] },
{ name: 'Санкт-Петербург', patterns: ['санкт-петербург', 'спб', 'питер', 'petersburg'] },
{ name: 'Новосибирск', patterns: ['новосибирск'] },
{ name: 'Екатеринбург', patterns: ['екатеринбург'] },
{ name: 'Казань', patterns: ['казань'] },
{ name: 'Иваново', patterns: ['иваново'] },
{ name: 'Нижний Новгород', patterns: ['нижний новгород'] },
];
for (const cityInfo of cities) {
for (const pattern of cityInfo.patterns) {
if (normalizedAddress.includes(pattern)) {
city = cityInfo.name;
console.log('🏙️ Найден город:', city);
break;
}
}
if (city) break;
}
// Если город не найден, берем первое слово
if (!city) {
const parts = address.split(/[,\s]+/).filter(part => part.length > 0);
if (parts.length > 0) {
city = parts[0];
console.log('🏙️ Город по умолчанию:', city);
}
}
// Ищем улицу - все что между городом и номером дома
let streetMatch = address;
// Убираем город из начала
if (city) {
const cityPattern = new RegExp(`^[^,]*${city.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^,]*,?\\s*`, 'i');
streetMatch = streetMatch.replace(cityPattern, '').trim();
}
// Убираем номер дома из конца
if (house) {
const housePattern = new RegExp(`\\s*д[\\.\\s]*${house.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*$`, 'i');
streetMatch = streetMatch.replace(housePattern, '').trim();
// Пробуем еще раз без "д."
const housePattern2 = new RegExp(`\\s*${house.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*$`, 'i');
streetMatch = streetMatch.replace(housePattern2, '').trim();
}
// Очищаем оставшуюся строку для улицы
street = streetMatch.replace(/^[,\s]+|[,\s]+$/g, ''); // Убираем запятые и пробелы по краям
console.log('🛣️ Найдена улица:', street);
// Определяем регион на основе города
const regionMap: Record<string, string> = {
'москва': 'город Москва',
'санкт-петербург': 'город Санкт-Петербург',
'иваново': 'Ивановская область',
'казань': 'Республика Татарстан',
'екатеринбург': 'Свердловская область',
'новосибирск': 'Новосибирская область',
'нижний новгород': 'Нижегородская область',
};
region = regionMap[city.toLowerCase()] || `${city} область`;
console.log('🗺️ Определен регион:', region);
const result = {
city: city || undefined,
street: street || undefined,
house: house || undefined,
region: region || undefined,
full_address: address,
};
console.log('✅ Результат парсинга:', result);
return result;
}
/**
* Улучшение адреса с помощью геокодирования
*/
private async improveAddress(address: string): Promise<CustomLocation> {
// Сначала парсим адрес для получения базовых компонентов
const parsedAddress = this.parseAddress(address);
console.log('🏠 Парсинг адреса:', {
исходный: address,
город: parsedAddress.city,
улица: parsedAddress.street,
дом: parsedAddress.house,
регион: parsedAddress.region
});
try {
const response = await this.detectLocation(address);
if (response.variants && response.variants.length > 0) {
const bestVariant = response.variants[0];
// Используем данные из геокодирования, дополняя парсингом
const city = parsedAddress.city || bestVariant.address;
const region = parsedAddress.region || bestVariant.address;
const street = parsedAddress.street || 'ул. Центральная';
const house = parsedAddress.house || '1';
const fullAddress = `${city}, ${street} ${house}`.trim();
return {
// Поля на верхнем уровне (возможно, требуются API)
country: 'Russia',
city: city,
region: region,
street: street,
house: house,
full_address: fullAddress,
details: {
full_address: fullAddress,
country: 'Russia',
geoId: bestVariant.geo_id,
locality: city,
region: region,
street: street,
house: house,
},
};
}
} catch (error) {
console.log('Не удалось улучшить адрес через геокодирование:', error);
}
// Fallback к парсингу без геокодирования
let formattedAddress = address;
if (!formattedAddress.toLowerCase().includes('россия')) {
formattedAddress = `${formattedAddress}, Россия`;
}
// Более детальный парсинг для обязательных полей
let city = parsedAddress.city;
let region = parsedAddress.region;
let street = parsedAddress.street;
let house = parsedAddress.house;
// Если не удалось распарсить город из адреса, пытаемся извлечь его по-другому
if (!city) {
// Ищем известные города в адресе
const cities = ['москва', 'санкт-петербург', 'спб', 'новосибирск', 'екатеринбург', 'казань', 'иваново'];
for (const cityName of cities) {
if (address.toLowerCase().includes(cityName)) {
city = cityName.charAt(0).toUpperCase() + cityName.slice(1);
break;
}
}
if (!city) city = 'Москва'; // Fallback
}
// Определяем регион на основе города
if (!region) {
const regionMap: Record<string, string> = {
'москва': 'город Москва',
'санкт-петербург': 'город Санкт-Петербург',
'спб': 'город Санкт-Петербург',
'питер': 'город Санкт-Петербург',
'иваново': 'Ивановская область',
'казань': 'Республика Татарстан',
'екатеринбург': 'Свердловская область',
'новосибирск': 'Новосибирская область',
'нижний новгород': 'Нижегородская область',
};
region = regionMap[city.toLowerCase()] || 'Московская область';
}
// Если улица не найдена, задаем дефолтную
if (!street) {
street = 'ул. Центральная';
}
// Если дом не найден, задаем дефолтный
if (!house) {
house = '1';
}
const result = {
// Поля на верхнем уровне (возможно, требуются API)
country: 'Russia',
city: city,
region: region,
street: street,
house: house,
full_address: formattedAddress,
details: {
full_address: formattedAddress,
country: 'Russia',
locality: city,
region: region,
street: street,
house: house,
},
};
console.log('🏗️ Сформированный адрес для API:', result);
return result;
}
/**
* Вспомогательный метод для создания заявки из данных корзины
* Пробует несколько временных интервалов если первый не дает результатов
*/
async createOfferFromCart(cartData: {
items: Array<{
id: string;
name: string;
article: string;
price: number;
quantity: number;
weight?: number;
dimensions?: { dx?: number; dy?: number; dz?: number };
deliveryTime?: number; // Срок доставки товара к нам на склад
}>;
deliveryAddress: string;
recipientName: string;
recipientPhone: string;
paymentMethod: 'already_paid' | 'card_on_receipt';
deliveryType: 'courier' | 'pickup';
pickupPointId?: string;
maxSupplierDeliveryDays?: number; // Максимальный срок поставки товаров
}): Promise<CreateOfferResponse> {
// Генерируем уникальный ID заявки
const operatorRequestId = `order_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Определяем штрихкод коробки
const packageBarcode = 'package_001';
// Конвертируем товары в формат API
const items: RequestResourceItem[] = cartData.items.map((item, index) => ({
article: item.article,
billing_details: {
assessed_unit_price: Math.round(item.price * 100), // В копейках
unit_price: Math.round(item.price * 100), // В копейках
nds: 20, // НДС 20%
},
count: item.quantity,
name: item.name,
place_barcode: packageBarcode, // Используем тот же штрихкод что и у коробки
physical_dims: item.dimensions ? {
dx: item.dimensions.dx || 10,
dy: item.dimensions.dy || 10,
dz: item.dimensions.dz || 10,
} : {
predefined_volume: 1000, // 1000 см³ по умолчанию
},
}));
// Создаем грузоместа (коробки)
const places: ResourcePlace[] = [{
barcode: packageBarcode,
physical_dims: {
dx: 30, // 30 см
dy: 20, // 20 см
dz: 15, // 15 см
weight_gross: cartData.items.reduce((total, item) =>
total + (item.weight || 500) * item.quantity, 0
), // Вес в граммах
},
description: 'Посылка с автозапчастями',
}];
// Настройка места отправления (наш склад)
const source: SourceRequestNode = {
platform_station: {
platform_id: process.env.YANDEX_DELIVERY_SOURCE_STATION_ID || 'default_warehouse',
},
};
// Настройка места назначения
let destination: DestinationRequestNode;
if (cartData.deliveryType === 'pickup' && cartData.pickupPointId) {
// Доставка до ПВЗ
destination = {
type: 'platform_station',
platform_station: {
platform_id: cartData.pickupPointId,
},
};
} else {
// Курьерская доставка
// Улучшаем адрес с помощью геокодирования
const improvedLocation = await this.improveAddress(cartData.deliveryAddress);
console.log('🎯 Улучшенный адрес для доставки:', JSON.stringify(improvedLocation, null, 2));
destination = {
type: 'custom_location',
custom_location: improvedLocation,
// Дублируем поля адреса на верхнем уровне destination (на случай если API их ожидает тут)
country: improvedLocation.country,
city: improvedLocation.city,
region: improvedLocation.region,
house: improvedLocation.house,
street: improvedLocation.street,
full_address: improvedLocation.full_address,
// Интервал доставки: от завтра до послезавтра, весь день
interval: {
from: Math.floor(Date.now() / 1000) + 24 * 60 * 60, // Завтра 00:00
to: Math.floor(Date.now() / 1000) + 72 * 60 * 60, // Послезавтра 00:00 (48 часов)
},
};
}
// Информация о получателе
const nameParts = cartData.recipientName.split(' ');
const recipientInfo: Contact = {
first_name: nameParts[0] || 'Клиент',
last_name: nameParts[1] || '',
phone: cartData.recipientPhone,
};
// Создаем заявку
const request: CreateOfferRequest = {
billing_info: {
payment_method: cartData.paymentMethod,
},
destination,
info: {
operator_request_id: operatorRequestId,
comment: 'Заказ автозапчастей',
},
items,
last_mile_policy: cartData.deliveryType === 'pickup' ? 'self_pickup' : 'time_interval',
places,
recipient_info: recipientInfo,
source,
particular_items_refuse: false, // Частичный выкуп не разрешен
};
// НЕ учитываем время поставки товаров в API Яндекса - только время на саму доставку
// Время поставки товаров будет учтено в резолвере при формировании итоговой даты
console.log(` Время поставки товаров (${cartData.maxSupplierDeliveryDays || 0} дней) будет учтено при расчете итоговой даты доставки`);
// Попробуем создать заявку с разными временными интервалами для самой доставки
const timeIntervals = [
// 1. Завтра-послезавтра (стандартная доставка)
{
from: Math.floor(Date.now() / 1000) + 24 * 60 * 60,
to: Math.floor(Date.now() / 1000) + 72 * 60 * 60,
},
// 2. Через 2-3 дня (если завтра недоступно)
{
from: Math.floor(Date.now() / 1000) + 48 * 60 * 60,
to: Math.floor(Date.now() / 1000) + 96 * 60 * 60,
},
// 3. Через 3-5 дней (если и это недоступно)
{
from: Math.floor(Date.now() / 1000) + 72 * 60 * 60,
to: Math.floor(Date.now() / 1000) + 120 * 60 * 60,
},
];
let lastError: Error | null = null;
// Попробуем каждый интервал пока не найдем подходящий
for (let i = 0; i < timeIntervals.length; i++) {
const interval = timeIntervals[i];
try {
if (cartData.deliveryType === 'courier') {
request.destination.interval = interval;
}
console.log(`🚚 Попытка ${i + 1}/${timeIntervals.length} с интервалом:`, {
от: new Date(interval.from * 1000).toLocaleString('ru-RU'),
до: new Date(interval.to * 1000).toLocaleString('ru-RU'),
});
// Логируем запрос только для первой попытки чтобы не засорять лог
if (i === 0) {
console.log('📄 Тело запроса к Яндекс API:', JSON.stringify(request, null, 2));
}
const response = await this.createOffer(request);
// Если получили офферы, возвращаем результат
if (response.offers && response.offers.length > 0) {
console.log(`✅ Получили ${response.offers.length} офферов на попытке ${i + 1}`);
return response;
} else {
console.log(`⚠️ Нет офферов для попытки ${i + 1}`);
}
} catch (error) {
console.log(`❌ Попытка ${i + 1} с интервалом ${interval.from}-${interval.to} не удалась:`, error instanceof Error ? error.message : error);
lastError = error instanceof Error ? error : new Error(String(error));
// Если это не ошибка "no_delivery_options", прекращаем попытки
if (error instanceof Error && !error.message.includes('no_delivery_options')) {
throw error;
}
}
}
// Если курьерская доставка не работает, попробуем найти ближайшие ПВЗ
if (cartData.deliveryType === 'courier' && lastError?.message.includes('no_delivery_options')) {
console.log('💡 Курьерская доставка недоступна, ищем ближайшие ПВЗ...');
try {
// Пытаемся получить координаты адреса для поиска ПВЗ
const locationResponse = await this.detectLocation(cartData.deliveryAddress);
if (locationResponse.variants && locationResponse.variants.length > 0) {
const geoId = locationResponse.variants[0].geo_id;
console.log(`📍 Найден geoId: ${geoId} для адреса: ${cartData.deliveryAddress}`);
// Ищем ПВЗ в этом городе
const pickupPoints = await this.getPickupPoints({ geo_id: geoId });
if (pickupPoints.points && pickupPoints.points.length > 0) {
console.log(`📦 Найдено ${pickupPoints.points.length} ПВЗ в городе`);
// Создаем фиктивные офферы для ПВЗ (так как у нас нет точной стоимости)
const pickupOffers: Offer[] = pickupPoints.points.slice(0, 3).map((point, index) => ({
offer_id: `pickup_${point.id}`,
expires_at: Math.floor(Date.now() / 1000) + 24 * 60 * 60, // Действителен сутки
offer_details: {
delivery_interval: {
min: Math.floor(Date.now() / 1000) + 24 * 60 * 60,
max: Math.floor(Date.now() / 1000) + 72 * 60 * 60,
policy: 'self_pickup' as const,
},
pricing: '300.00 RUB', // Примерная стоимость
pricing_total: '300.00 RUB',
},
}));
return { offers: pickupOffers };
}
}
} catch (pickupError) {
console.log('Не удалось найти ПВЗ:', pickupError);
}
}
// Если все интервалы не подошли, выбрасываем последнюю ошибку
if (lastError) {
throw lastError;
}
// Fallback: возвращаем пустой ответ
return { offers: [] };
}
}
// Интерфейсы для создания заявки на доставку
interface BillingInfo {
payment_method: 'already_paid' | 'card_on_receipt';
delivery_cost?: number;
}
interface CustomLocation {
details?: LocationDetails;
latitude?: number;
longitude?: number;
// Возможно, эти поля должны быть на верхнем уровне
country?: string;
city?: string;
region?: string;
house?: string;
street?: string;
full_address?: string;
}
interface LocationDetails {
apartment?: string;
building?: string;
comment?: string;
country?: string;
full_address: string;
geoId?: number;
house?: string;
housing?: string;
locality?: string;
postal_code?: string;
region?: string;
street?: string;
subRegion?: string;
}
interface PlatformStation {
platform_id?: string;
}
interface TimeInterval {
from: number;
to: number;
}
interface DestinationRequestNode {
type: 'platform_station' | 'custom_location';
custom_location?: CustomLocation;
interval?: TimeInterval;
platform_station?: PlatformStation;
// Возможно, поля адреса должны быть напрямую в destination
country?: string;
city?: string;
region?: string;
house?: string;
street?: string;
full_address?: string;
}
interface SourceRequestNode {
platform_station: PlatformStation;
interval?: TimeInterval;
}
interface RequestInfo {
operator_request_id: string;
comment?: string;
}
interface ItemBillingDetails {
assessed_unit_price: number;
unit_price: number;
inn?: string;
nds?: number;
}
interface ItemPhysicalDimensions {
dx?: number;
dy?: number;
dz?: number;
predefined_volume?: number;
}
interface RequestResourceItem {
article: string;
billing_details: ItemBillingDetails;
count: number;
name: string;
place_barcode: string;
marking_code?: string;
physical_dims?: ItemPhysicalDimensions;
uin?: string;
}
interface PlacePhysicalDimensions {
dx: number;
dy: number;
dz: number;
weight_gross: number;
predefined_volume?: number;
}
interface ResourcePlace {
barcode: string;
physical_dims: PlacePhysicalDimensions;
description?: string;
}
interface Contact {
first_name: string;
phone: string;
email?: string;
last_name?: string;
partonymic?: string;
}
interface CreateOfferRequest {
billing_info: BillingInfo;
destination: DestinationRequestNode;
info: RequestInfo;
items: RequestResourceItem[];
last_mile_policy: 'time_interval' | 'self_pickup';
places: ResourcePlace[];
recipient_info: Contact;
source: SourceRequestNode;
particular_items_refuse?: boolean;
}
interface DeliveryInterval {
max: number;
min: number;
policy: 'time_interval' | 'self_pickup';
}
interface PickupInterval {
max: number;
min: number;
}
interface OfferDetails {
delivery_interval?: DeliveryInterval;
pickup_interval?: PickupInterval;
pricing?: string;
pricing_commission_on_delivery_payment?: string;
pricing_commission_on_delivery_payment_amount?: string;
pricing_total?: string;
}
interface Offer {
expires_at?: string | number;
offer_details?: OfferDetails;
offer_id?: string;
}
interface CreateOfferResponse {
offers: Offer[];
}
export const yandexDeliveryService = new YandexDeliveryService();
export type { YandexPickupPoint, CreateOfferRequest, CreateOfferResponse, Offer };
// Функция для автокомплита адресов
export const getAddressSuggestions = async (query: string): Promise<string[]> => {
// Используем API ключ Яндекс карт для геокодирования
const apiKey = process.env.YANDEX_MAPS_API_KEY;
if (!apiKey) {
console.error('YANDEX_MAPS_API_KEY не настроен');
return [];
}
if (!query || query.length < 3) {
return [];
}
try {
// Используем Yandex Geocoder API для поиска адресов
const response = await fetch(
`https://geocode-maps.yandex.ru/1.x/?apikey=${apiKey}&geocode=${encodeURIComponent(query)}&format=json&results=5&kind=house&lang=ru_RU`
);
if (!response.ok) {
console.error('Ошибка API Геокодера:', response.status, response.statusText);
return [];
}
const data = await response.json();
if (data?.response?.GeoObjectCollection?.featureMember) {
const features = data.response.GeoObjectCollection.featureMember;
return features.map((feature: any) => {
const geoObject = feature.GeoObject;
// Возвращаем полный адрес
return geoObject.metaDataProperty?.GeocoderMetaData?.text || geoObject.name || '';
}).filter(Boolean);
}
return [];
} catch (error) {
console.error('Ошибка получения подсказок адресов:', error);
return [];
}
};