Files
protekauto-cms/src/lib/yandex-delivery-service.ts

976 lines
34 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
// Группируем ограничения по времени
const timeGroups: Record<string, number[]> = {};
schedule.restrictions.forEach(restriction => {
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')}`;
const timeRange = `${timeFrom}-${timeTo}`;
if (!timeGroups[timeRange]) {
timeGroups[timeRange] = [];
}
timeGroups[timeRange].push(...restriction.days);
});
// Форматируем каждую группу времени
return Object.entries(timeGroups).map(([timeRange, days]) => {
// Убираем дубликаты и сортируем дни
const uniqueDays = [...new Set(days)].sort((a, b) => a - b);
// Группируем последовательные дни в диапазоны
const dayRanges: string[] = [];
let rangeStart = uniqueDays[0];
let rangeEnd = uniqueDays[0];
for (let i = 1; i < uniqueDays.length; i++) {
const currentDay = uniqueDays[i];
const prevDay = uniqueDays[i - 1];
// Если дни идут подряд, расширяем диапазон
if (currentDay === prevDay + 1) {
rangeEnd = currentDay;
} else {
// Завершаем текущий диапазон и начинаем новый
if (rangeStart === rangeEnd) {
dayRanges.push(dayNames[rangeStart - 1]);
} else {
dayRanges.push(`${dayNames[rangeStart - 1]}-${dayNames[rangeEnd - 1]}`);
}
rangeStart = currentDay;
rangeEnd = currentDay;
}
}
// Добавляем последний диапазон
if (rangeStart === rangeEnd) {
dayRanges.push(dayNames[rangeStart - 1]);
} else {
dayRanges.push(`${dayNames[rangeStart - 1]}-${dayNames[rangeEnd - 1]}`);
}
return `${dayRanges.join(', ')}: ${timeRange}`;
}).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 [];
}
};