Clients Layer — Слой клиентов
Версия: 1.0
Дата: 19.04.2026
Статус: Черновик
Обзор
Clients Layer обеспечивает пользовательские интерфейсы для разных типов клиентов согласно разделу "### 6. Clients (Клиенты)" в Архитектура платформы. Реализует принцип: "Веб-сайт и внешние партнёры используют одни и те же API. Одна логика для всех." Включает Website (React/Next.js) для турагентов и B2B интеграции через Partner API из API Layer согласно потоку "Client → API Gateway → Business Service" из Архитектура платформы.
Архитектура клиентов: См. диаграмму vitrip_clients_architecture.jpg — схема веб-приложения и B2B интеграций с единым API Gateway.

UI компоненты: См. диаграмму vitrip_ui_components.jpg — детальная схема React компонентов для поиска отелей, бронирования и конструктора туров.

B2B интеграции: См. диаграмму vitrip_b2b_integrations.jpg — схемы интеграции партнёров через Partner API с различными типами подключений.

Website — Веб-приложение для турагентов
Технологический стек
Фронтенд: React/Next.js согласно технологическому стеку в Архитектура платформы: "Frontend | React/Next.js | SSR, большая экосистема, TypeScript".
// Next.js 14 с App Router
const nextConfig = {
experimental: {
appDir: true,
},
images: {
domains: ['cdn.vitrip.store'],
},
i18n: {
locales: ['ru', 'en', 'uk'],
defaultLocale: 'ru',
},
env: {
API_BASE_URL: process.env.API_BASE_URL || 'https://api.vitrip.store/v1',
API_SANDBOX_URL: 'https://api-sandbox.vitrip.store/v1',
}
}
// TypeScript конфигурация
const tsConfig = {
compilerOptions: {
target: "ES2020",
lib: ["dom", "dom.iterable", "es6"],
allowJs: true,
skipLibCheck: true,
strict: true,
forceConsistentCasingInFileNames: true,
noEmit: true,
esModuleInterop: true,
module: "esnext",
moduleResolution: "node",
resolveJsonModule: true,
isolatedModules: true,
jsx: "preserve",
incremental: true,
plugins: [{ name: "next" }],
paths: {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"],
"@/api/*": ["./src/api/*"]
}
}
}
API Client Integration
Интеграция с REST API: Использует endpoints из API Layer согласно принципу единого API.
// API клиент для интеграции с REST endpoints из overview/layers.md
export class VitripApiClient {
private readonly baseUrl: string;
private readonly apiKey?: string;
constructor(config: ApiConfig) {
this.baseUrl = config.baseUrl;
this.apiKey = config.apiKey;
}
// Интеграция с SearchService из business_services.md через REST API
async searchHotels(params: HotelSearchParams): Promise<HotelSearchResponse> {
// Использует POST /api/v1/hotels/search из overview/layers.md
const response = await this.post('/api/v1/hotels/search', {
location: {
lat: params.location.latitude,
lng: params.location.longitude
},
dates: {
checkIn: params.checkIn.toISOString().split('T')[0],
checkOut: params.checkOut.toISOString().split('T')[0]
},
guests: params.guests,
priceRange: params.priceRange,
amenities: params.amenities,
sortBy: params.sortBy || 'rating_desc',
page: params.page || 1,
pageSize: params.pageSize || 20
});
return this.handleResponse<HotelSearchResponse>(response);
}
// Интеграция с GET /api/v1/hotels/{id} из overview/layers.md
async getHotelDetails(hotelId: string): Promise<HotelDetails> {
const response = await this.get(`/api/v1/hotels/${hotelId}`);
return this.handleResponse<HotelDetails>(response);
}
// Интеграция с BookingService через POST /api/v1/bookings
async createBooking(bookingData: CreateBookingRequest): Promise<BookingResponse> {
const response = await this.post('/api/v1/bookings', bookingData);
return this.handleResponse<BookingResponse>(response);
}
// Интеграция с TourBuilderService через POST /api/v1/tours
async createTour(tourData: CreateTourRequest): Promise<TourResponse> {
const response = await this.post('/api/v1/tours', tourData);
return this.handleResponse<TourResponse>(response);
}
// Генерация PDF программы тура через GET /api/v1/tours/{id}/pdf
async generateTourPdf(tourId: string, language = 'ru'): Promise<Blob> {
const response = await this.get(`/api/v1/tours/${tourId}/pdf?lang=${language}`, {
responseType: 'blob'
});
return response.blob();
}
private async post(endpoint: string, data: any): Promise<Response> {
return fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(this.apiKey && { 'X-API-Key': this.apiKey }),
},
body: JSON.stringify(data),
});
}
private async get(endpoint: string, options: RequestInit = {}): Promise<Response> {
return fetch(`${this.baseUrl}${endpoint}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(this.apiKey && { 'X-API-Key': this.apiKey }),
},
...options,
});
}
}
// TypeScript типы соответствующие API contracts
export interface HotelSearchParams {
location: GeoLocation;
checkIn: Date;
checkOut: Date;
guests: number;
priceRange?: PriceRange;
amenities?: string[];
sortBy?: 'price_asc' | 'price_desc' | 'rating_desc' | 'distance_asc';
page?: number;
pageSize?: number;
}
export interface HotelSearchResponse {
hotels: Hotel[];
totalCount: number;
pagination: PaginationInfo;
filters: AvailableFilters;
}
React Components Architecture
// Архитектура компонентов для турагентского интерфейса
export const AppLayout: React.FC = () => {
return (
<div className="min-h-screen bg-gray-50">
<Header />
<Navigation />
<main className="container mx-auto px-4 py-8">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/search" element={<HotelSearchPage />} />
<Route path="/hotels/:id" element={<HotelDetailsPage />} />
<Route path="/bookings" element={<BookingsPage />} />
<Route path="/tours" element={<TourBuilderPage />} />
<Route path="/clients" element={<ClientsPage />} />
</Routes>
</main>
<Footer />
</div>
);
};
// Компонент поиска отелей
export const HotelSearchForm: React.FC = () => {
const [searchParams, setSearchParams] = useState<HotelSearchParams>();
const [searchResults, setSearchResults] = useState<HotelSearchResponse>();
const [loading, setLoading] = useState(false);
const apiClient = useApiClient();
const handleSearch = async (params: HotelSearchParams) => {
setLoading(true);
try {
// Использует SearchService через REST API
const results = await apiClient.searchHotels(params);
setSearchResults(results);
} catch (error) {
console.error('Search failed:', error);
// Error handling UI
} finally {
setLoading(false);
}
};
return (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-2xl font-bold mb-6">Поиск отелей</h2>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<LocationSelector
onLocationChange={(location) =>
setSearchParams(prev => ({ ...prev, location }))}
/>
<DateRangePicker
onDatesChange={(checkIn, checkOut) =>
setSearchParams(prev => ({ ...prev, checkIn, checkOut }))}
/>
<GuestSelector
onGuestsChange={(guests) =>
setSearchParams(prev => ({ ...prev, guests }))}
/>
<button
onClick={() => searchParams && handleSearch(searchParams)}
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700"
disabled={loading}
>
{loading ? 'Поиск...' : 'Найти отели'}
</button>
</div>
{searchResults && (
<HotelSearchResults
results={searchResults}
onHotelSelect={(hotel) => navigate(`/hotels/${hotel.id}`)}
/>
)}
</div>
);
};
// Компонент бронирования
export const BookingForm: React.FC<{ hotel: Hotel }> = ({ hotel }) => {
const [bookingData, setBookingData] = useState<CreateBookingRequest>();
const [booking, setBooking] = useState<BookingResponse>();
const apiClient = useApiClient();
const handleBooking = async (data: CreateBookingRequest) => {
try {
// Использует BookingService через REST API
const result = await apiClient.createBooking(data);
setBooking(result);
// Уведомления и редирект
showSuccessNotification('Бронирование создано успешно!');
navigate(`/bookings/${result.bookingId}`);
} catch (error) {
console.error('Booking failed:', error);
showErrorNotification('Ошибка при создании бронирования');
}
};
return (
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-xl font-bold mb-4">Забронировать {hotel.name}</h3>
<BookingFormFields
hotel={hotel}
onChange={setBookingData}
/>
<div className="mt-6 flex justify-end space-x-4">
<button
onClick={() => navigate(-1)}
className="px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Отмена
</button>
<button
onClick={() => bookingData && handleBooking(bookingData)}
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
disabled={!bookingData}
>
Подтвердить бронирование
</button>
</div>
</div>
);
};
// Конструктор туров
export const TourBuilder: React.FC = () => {
const [tour, setTour] = useState<TourData>();
const [generatedPdf, setGeneratedPdf] = useState<Blob>();
const apiClient = useApiClient();
const handleSaveTour = async (tourData: CreateTourRequest) => {
try {
// Использует TourBuilderService через REST API
const result = await apiClient.createTour(tourData);
setTour(result.tour);
showSuccessNotification('Программа тура создана!');
} catch (error) {
console.error('Tour creation failed:', error);
}
};
const handleGeneratePdf = async (tourId: string) => {
try {
// Использует PDF generation endpoint
const pdfBlob = await apiClient.generateTourPdf(tourId);
setGeneratedPdf(pdfBlob);
// Скачивание PDF
const url = URL.createObjectURL(pdfBlob);
const link = document.createElement('a');
link.href = url;
link.download = `tour_program_${tourId}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error('PDF generation failed:', error);
}
};
return (
<div className="space-y-6">
<TourDetailsForm onSave={handleSaveTour} />
<TourItineraryBuilder tour={tour} />
<TourHotelsSelector tour={tour} />
{tour && (
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-xl font-bold mb-4">Программа готова!</h3>
<button
onClick={() => handleGeneratePdf(tour.id)}
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700"
>
Скачать PDF программу
</button>
</div>
)}
</div>
);
};
State Management
// Zustand store для глобального состояния
interface AppState {
// Пользователь и аутентификация
user: User | null;
isAuthenticated: boolean;
// Поиск отелей
searchParams: HotelSearchParams | null;
searchResults: HotelSearchResponse | null;
selectedHotel: Hotel | null;
// Бронирования
bookings: Booking[];
currentBooking: Booking | null;
// Туры
tours: Tour[];
currentTour: Tour | null;
// UI состояние
loading: boolean;
notifications: Notification[];
// Actions
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => void;
searchHotels: (params: HotelSearchParams) => Promise<void>;
createBooking: (bookingData: CreateBookingRequest) => Promise<void>;
createTour: (tourData: CreateTourRequest) => Promise<void>;
showNotification: (notification: Notification) => void;
}
export const useAppStore = create<AppState>((set, get) => ({
user: null,
isAuthenticated: false,
searchParams: null,
searchResults: null,
selectedHotel: null,
bookings: [],
currentBooking: null,
tours: [],
currentTour: null,
loading: false,
notifications: [],
login: async (credentials) => {
set({ loading: true });
try {
const response = await authService.login(credentials);
set({
user: response.user,
isAuthenticated: true,
loading: false
});
} catch (error) {
set({ loading: false });
throw error;
}
},
logout: () => {
authService.logout();
set({
user: null,
isAuthenticated: false,
bookings: [],
tours: []
});
},
searchHotels: async (params) => {
set({ loading: true, searchParams: params });
try {
const apiClient = new VitripApiClient({
baseUrl: process.env.API_BASE_URL!,
apiKey: get().user?.apiKey
});
const results = await apiClient.searchHotels(params);
set({ searchResults: results, loading: false });
} catch (error) {
set({ loading: false });
throw error;
}
},
showNotification: (notification) => {
set(state => ({
notifications: [...state.notifications, notification]
}));
}
}));
B2B Partner Integrations
Partner API Integration
Интеграция через Partner API: Использует sandbox/production окружения из API Layer с API ключами согласно разделу "### Partner API" в API Layer.
// SDK для партнёрских интеграций
export class VitripPartnerSDK {
private readonly config: PartnerConfig;
constructor(config: PartnerConfig) {
this.config = config;
}
// Конфигурация для разных окружений из overview/layers.md
static createSandbox(apiKey: string): VitripPartnerSDK {
return new VitripPartnerSDK({
baseUrl: 'https://api-sandbox.vitrip.store/v1',
apiKey: apiKey, // sb_test_1234567890abcdef123456789012
environment: 'sandbox',
timeout: 30000,
retries: 3
});
}
static createProduction(apiKey: string): VitripPartnerSDK {
return new VitripPartnerSDK({
baseUrl: 'https://api.vitrip.store/v1',
apiKey: apiKey, // pk_live_1234567890abcdef123456789012
environment: 'production',
timeout: 15000,
retries: 5
});
}
// Hotel search для B2B партнёров
async searchHotels(request: PartnerSearchRequest): Promise<PartnerSearchResponse> {
const response = await this.makeRequest('POST', '/api/v1/hotels/search', request);
return this.parseResponse<PartnerSearchResponse>(response);
}
// Booking для B2B партнёров
async createBooking(request: PartnerBookingRequest): Promise<PartnerBookingResponse> {
const response = await this.makeRequest('POST', '/api/v1/bookings', request);
return this.parseResponse<PartnerBookingResponse>(response);
}
// Rate limiting согласно overview/layers.md
private async makeRequest(method: string, endpoint: string, data?: any): Promise<Response> {
const url = `${this.config.baseUrl}${endpoint}`;
const headers: HeadersInit = {
'Content-Type': 'application/json',
'X-API-Key': this.config.apiKey,
'User-Agent': `VitripSDK/1.0 (${this.config.environment})`
};
let attempt = 0;
while (attempt <= this.config.retries) {
try {
const response = await fetch(url, {
method,
headers,
body: data ? JSON.stringify(data) : undefined,
timeout: this.config.timeout
});
if (response.status === 429) {
// Rate limit handling
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000;
await this.sleep(delay);
attempt++;
continue;
}
if (!response.ok) {
throw new PartnerAPIError(response.status, await response.text());
}
return response;
} catch (error) {
if (attempt === this.config.retries) {
throw error;
}
attempt++;
await this.sleep(Math.pow(2, attempt) * 1000);
}
}
throw new Error('Max retries exceeded');
}
}
// Типы для партнёрских интеграций
export interface PartnerConfig {
baseUrl: string;
apiKey: string;
environment: 'sandbox' | 'production';
timeout: number;
retries: number;
}
export interface PartnerSearchRequest {
location: GeoLocation;
checkIn: string; // YYYY-MM-DD
checkOut: string; // YYYY-MM-DD
rooms: RoomRequest[];
currency?: string; // USD, EUR, UAH
language?: string; // ru, en, uk
}
export interface PartnerBookingRequest {
hotelId: string;
roomType: string;
checkIn: string;
checkOut: string;
guests: GuestDetails[];
payment: PaymentDetails;
partnerId: string;
externalBookingId?: string;
}
Integration Examples
// Пример интеграции с CRM системой партнёра
export class CRMIntegration {
constructor(
private partnerSDK: VitripPartnerSDK,
private crmConfig: CRMConfig
) {}
async syncBookingsFromCRM(): Promise<void> {
// 1. Получение новых заявок из CRM
const crmBookings = await this.crmConfig.client.getNewBookings();
for (const crmBooking of crmBookings) {
try {
// 2. Поиск отелей в vitrip
const searchResults = await this.partnerSDK.searchHotels({
location: this.convertCRMLocation(crmBooking.destination),
checkIn: crmBooking.checkInDate,
checkOut: crmBooking.checkOutDate,
rooms: this.convertCRMRooms(crmBooking.rooms)
});
if (searchResults.hotels.length === 0) {
await this.crmConfig.client.updateBookingStatus(
crmBooking.id,
'no_availability'
);
continue;
}
// 3. Автоматическое бронирование лучшего варианта
const bestHotel = this.selectBestHotel(searchResults.hotels, crmBooking.preferences);
const bookingResult = await this.partnerSDK.createBooking({
hotelId: bestHotel.id,
roomType: bestHotel.availableRooms[0].type,
checkIn: crmBooking.checkInDate,
checkOut: crmBooking.checkOutDate,
guests: this.convertCRMGuests(crmBooking.guests),
payment: this.convertCRMPayment(crmBooking.payment),
partnerId: this.crmConfig.partnerId,
externalBookingId: crmBooking.id
});
// 4. Обновление статуса в CRM
await this.crmConfig.client.updateBookingStatus(
crmBooking.id,
'confirmed',
bookingResult.confirmationCode
);
} catch (error) {
console.error(`Failed to process CRM booking ${crmBooking.id}:`, error);
await this.crmConfig.client.updateBookingStatus(
crmBooking.id,
'error',
error.message
);
}
}
}
}
// Пример webhook для уведомлений партнёров
export class PartnerWebhookHandler {
constructor(private partnerSDK: VitripPartnerSDK) {}
async handleBookingConfirmation(booking: Booking): Promise<void> {
const partnersToNotify = await this.getPartnersForNotification(booking);
for (const partner of partnersToNotify) {
try {
await this.sendWebhookNotification(partner, {
event: 'booking.confirmed',
bookingId: booking.id,
confirmationCode: booking.confirmationCode,
hotelName: booking.hotel.name,
checkIn: booking.checkIn,
checkOut: booking.checkOut,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error(`Failed to notify partner ${partner.id}:`, error);
// Retry logic или dead letter queue
}
}
}
private async sendWebhookNotification(
partner: Partner,
payload: WebhookPayload
): Promise<void> {
const signature = this.generateSignature(payload, partner.webhookSecret);
await fetch(partner.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Vitrip-Signature': signature,
'X-Vitrip-Event': payload.event
},
body: JSON.stringify(payload)
});
}
}
Развёртывание и инфраструктура
Frontend Deployment
# Next.js приложение в Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: vitrip-frontend
spec:
replicas: 3
selector:
matchLabels:
app: vitrip-frontend
template:
metadata:
labels:
app: vitrip-frontend
spec:
containers:
- name: frontend
image: vitrip/frontend:v1.0
ports:
- containerPort: 3000
env:
- name: API_BASE_URL
value: "https://api.vitrip.store/v1"
- name: API_SANDBOX_URL
value: "https://api-sandbox.vitrip.store/v1"
- name: NODE_ENV
value: "production"
resources:
requests:
memory: "512Mi"
cpu: "300m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
# CDN и статические файлы
apiVersion: v1
kind: Service
metadata:
name: vitrip-frontend-service
spec:
selector:
app: vitrip-frontend
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP
---
# Ingress для доменов
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vitrip-frontend-ingress
annotations:
traefik.ingress.kubernetes.io/router.middlewares: "default-frontend-middleware@kubernetescrd"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- vitrip.store
- www.vitrip.store
secretName: frontend-tls-cert
rules:
- host: vitrip.store
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vitrip-frontend-service
port:
number: 80
- host: www.vitrip.store
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vitrip-frontend-service
port:
number: 80
CDN Configuration
// CDN конфигурация для статических ресурсов
const cdnConfig = {
domain: 'cdn.vitrip.store',
regions: ['eu-central-1', 'eu-west-1', 'us-east-1'],
caching: {
images: {
ttl: '1y',
formats: ['webp', 'avif', 'jpg'],
sizes: [320, 640, 1280, 1920],
},
styles: {
ttl: '1y',
compression: 'gzip',
},
scripts: {
ttl: '1y',
compression: 'brotli',
},
api: {
ttl: '5m', // Кеширование API ответов
varyHeaders: ['X-API-Key', 'Accept-Language'],
}
}
};
// Next.js Image Optimization
const imageConfig = {
domains: ['cdn.vitrip.store', 'images.stuba.com', 'images.hometogo.com'],
formats: ['image/webp', 'image/avif'],
deviceSizes: [320, 420, 640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
dangerouslyAllowSVG: false,
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
};
Мониторинг и аналитика
User Analytics
// Google Analytics 4 + собственная аналитика
export class AnalyticsService {
constructor(
private ga4Id: string,
private internalTracker: InternalTracker
) {}
// Отслеживание поиска отелей
trackHotelSearch(params: HotelSearchParams, resultsCount: number): void {
// GA4 событие
gtag('event', 'search', {
search_term: `${params.location.city} ${params.checkIn} ${params.checkOut}`,
content_category: 'hotel_search',
custom_parameters: {
guests: params.guests,
results_count: resultsCount,
sort_by: params.sortBy
}
});
// Внутренняя аналитика для бизнес-метрик
this.internalTracker.track('hotel_search', {
location: params.location,
dates: { checkIn: params.checkIn, checkOut: params.checkOut },
guests: params.guests,
resultsCount,
timestamp: Date.now()
});
}
// Отслеживание бронирований
trackBookingCreated(booking: Booking): void {
// GA4 конверсия
gtag('event', 'purchase', {
transaction_id: booking.id,
value: booking.totalAmount,
currency: booking.currency,
items: [{
item_id: booking.hotel.id,
item_name: booking.hotel.name,
category: 'hotel_booking',
quantity: 1,
price: booking.totalAmount
}]
});
// Бизнес-метрики
this.internalTracker.track('booking_created', {
bookingId: booking.id,
hotelId: booking.hotel.id,
amount: booking.totalAmount,
currency: booking.currency,
agentId: booking.agentId,
source: booking.source,
timestamp: Date.now()
});
}
}
// Performance monitoring
export class PerformanceMonitor {
private metrics: Map<string, number[]> = new Map();
measureApiCall(endpoint: string, duration: number, success: boolean): void {
const key = `api_${endpoint}_${success ? 'success' : 'error'}`;
const measurements = this.metrics.get(key) || [];
measurements.push(duration);
this.metrics.set(key, measurements);
// Отправка в Prometheus через /metrics endpoint
window.dispatchEvent(new CustomEvent('metric', {
detail: {
name: 'api_request_duration',
value: duration,
labels: { endpoint, success: success.toString() }
}
}));
}
measurePageLoad(page: string, duration: number): void {
// Core Web Vitals
window.dispatchEvent(new CustomEvent('metric', {
detail: {
name: 'page_load_duration',
value: duration,
labels: { page }
}
}));
}
}
Связанная документация
- Общая архитектура: раздел "### 6. Clients (Клиенты)" в Архитектура платформы
- Принцип единого API: "Веб-сайт и внешние партнёры используют одни и те же API" из Архитектура платформы
- Технологический стек: "Frontend | React/Next.js | SSR, большая экосистема, TypeScript" из Архитектура платформы
- REST API интеграция: все endpoints из API Layer используются клиентами
- Partner API: sandbox/production окружения и API ключи из раздела "### Partner API" в API Layer
- Поток пользователей: "Client → API Gateway → Business Service → Cache/Database" из Архитектура платформы
- Business Services: интеграция с Search, Pricing, Booking, TourBuilder через REST API из Business Services