Skip to main content
Glama

RS.ge Waybill MCP Server

soap-client.tsโ€ข11 kB
/** * RS.ge SOAP API Client * * Handles all communication with RS.ge Waybill SOAP service * * ๐ŸŽ“ TEACHING: SOAP Client Architecture * ===================================== * This client: * 1. Builds SOAP XML requests * 2. Sends HTTP POST requests * 3. Parses SOAP XML responses * 4. Handles errors and retries * 5. Validates responses * * Key Features: * - โœ… Proper XML parsing (no phantom elements!) * - โœ… Automatic retries with exponential backoff * - โœ… Comprehensive logging * - โœ… Type-safe responses * - โœ… Error handling */ import axios, { AxiosInstance, AxiosError } from 'axios'; import { buildSoapRequest, buildWaybillXml, parseSoapResponse } from './xml-parser.js'; import { getLogger } from '../utils/logger.js'; import { SoapApiError, AuthenticationError, retryWithBackoff } from '../utils/error-handler.js'; import { DateRange } from '../utils/date-range.js'; import type { ApiConfig } from '../config/config.schema.js'; import type { Waybill, GetWaybillsResponse, GetWaybillResponse, SaveWaybillResponse, SendWaybillResponse, CloseWaybillResponse, ConfirmWaybillResponse, RejectWaybillResponse, GetErrorCodesResponse, GetAkcizCodesResponse, GetWaybillTypesResponse, GetNameFromTinResponse, GetServiceUsersResponse, } from '../types/waybill.types.js'; import { normalizeToArray, normalizeWaybill } from '../types/waybill.types.js'; /** * RS.ge SOAP Client */ export class RsWaybillSoapClient { private client: AxiosInstance; private config: ApiConfig; private credentials: { user: string; password: string }; private logger = getLogger(); constructor( config: ApiConfig, credentials: { user: string; password: string } ) { this.config = config; this.credentials = credentials; this.client = axios.create({ baseURL: config.baseUrl, timeout: config.timeout, headers: { 'Content-Type': 'text/xml; charset=utf-8', }, }); // Log configuration this.logger.info('SOAP client initialized', { baseUrl: config.baseUrl, timeout: config.timeout, retries: config.retries, }); } /** * Call SOAP method with automatic retries */ private async callSoap<T>( method: string, params: Record<string, any>, namespace?: string ): Promise<T> { return retryWithBackoff( async () => { const soapRequest = buildSoapRequest(method, params, namespace); this.logger.debug(`SOAP request: ${method}`, { params }); try { const response = await this.client.post('', soapRequest, { headers: { SOAPAction: `http://tempuri.org/${method}`, }, }); this.logger.debug(`SOAP response: ${method}`, { status: response.status, dataLength: response.data?.length, }); const parsed = parseSoapResponse<T>(response.data); return parsed; } catch (error) { if (axios.isAxiosError(error)) { throw this.handleAxiosError(error, method); } throw error; } }, { maxRetries: this.config.retries, initialDelay: this.config.retryDelay, shouldRetry: (error) => { // Retry on network errors and 5xx server errors if (axios.isAxiosError(error)) { return !error.response || error.response.status >= 500; } return false; }, } ); } /** * Handle Axios errors */ private handleAxiosError(error: AxiosError, method: string): Error { if (error.response) { const status = error.response.status; const data = error.response.data; this.logger.error(`SOAP error: ${method}`, { status, data: typeof data === 'string' ? data.substring(0, 500) : data, }); if (status === 401 || status === 403) { return new AuthenticationError( 'Invalid RS.ge credentials. Please check your service user and password.' ); } if (status >= 500) { return new SoapApiError( `RS.ge API server error (${status}). The service may be temporarily unavailable.` ); } return new SoapApiError( `RS.ge API error (${status}): ${error.message}` ); } if (error.request) { this.logger.error(`Network error: ${method}`, { message: error.message, code: error.code, }); return new SoapApiError( `Cannot connect to RS.ge API at ${this.config.baseUrl}. ` + `Please check your internet connection and try again.` ); } return new SoapApiError(`Request failed: ${error.message}`); } /** * Get waybills with date filtering * * โš ๏ธ CRITICAL REQUIREMENTS: * 1. Uses get_waybills operation (NOT get_waybills_v1) * 2. Uses create_date_s/e parameters in ISO datetime format (YYYY-MM-DDTHH:MM:SS) * 3. End date is automatically set to next day at 00:00:00 to include the entire day * 4. Includes seller_un_id extracted from credentials * * @param startDate Start date (YYYY-MM-DD) - REQUIRED * @param endDate End date (YYYY-MM-DD) - REQUIRED * @param buyerTin Optional buyer TIN filter */ async getWaybillsV1( startDate?: string, endDate?: string, buyerTin?: string ): Promise<Waybill[]> { // Validate date range if (!startDate || !endDate) { throw new SoapApiError( 'Start date and end date are required. ' + 'RS.ge API requires date ranges to be specified.' ); } // Create and validate date range const dateRange = DateRange.create(startDate, endDate); const apiDates = dateRange.toApiFormat(); // Extract seller_un_id from credentials (e.g., "4053098841:405309884" -> "405309884") const sellerUnId = this.credentials.user.includes(':') ? this.credentials.user.split(':')[1] : ''; this.logger.info(`Querying waybills: ${dateRange.toString()}`); const response = await this.callSoap<GetWaybillsResponse>('get_waybills', { su: this.credentials.user, sp: this.credentials.password, seller_un_id: sellerUnId, create_date_s: apiDates.start, create_date_e: apiDates.end, buyer_tin: buyerTin || '', }); const waybills = normalizeToArray(response.WAYBILL_LIST?.WAYBILL).map(normalizeWaybill); this.logger.info(`Received ${waybills.length} waybills from API`); return waybills; } /** * Get single waybill by ID */ async getWaybill(waybillId: string): Promise<Waybill | null> { const response = await this.callSoap<GetWaybillResponse>('get_waybill', { su: this.credentials.user, sp: this.credentials.password, waybill_id: waybillId, }); return response.WAYBILL ? normalizeWaybill(response.WAYBILL) : null; } /** * Save (create or update) waybill * * @param waybill Waybill data * @returns Created/updated waybill ID */ async saveWaybill(waybill: Waybill): Promise<string> { const waybillXml = buildWaybillXml(waybill); const response = await this.callSoap<SaveWaybillResponse>('save_waybill', { su: this.credentials.user, sp: this.credentials.password, waybill: waybillXml, }); if (!response.SAVE_WAYBILL_RESULT) { throw new SoapApiError('save_waybill did not return a result'); } return String(response.SAVE_WAYBILL_RESULT); } /** * Send waybill (submit to RS.ge) */ async sendWaybill(waybillId: string): Promise<string> { const response = await this.callSoap<SendWaybillResponse>('send_waybill', { su: this.credentials.user, sp: this.credentials.password, waybill_id: waybillId, }); return response.SEND_WAYBILL_RESULT || 'Success'; } /** * Close waybill */ async closeWaybill(waybillId: string): Promise<string> { const response = await this.callSoap<CloseWaybillResponse>('close_waybill', { su: this.credentials.user, sp: this.credentials.password, waybill_id: waybillId, }); return response.CLOSE_WAYBILL_RESULT || 'Success'; } /** * Confirm waybill (buyer confirms receipt) */ async confirmWaybill(waybillId: string): Promise<string> { const response = await this.callSoap<ConfirmWaybillResponse>('confirm_waybill', { su: this.credentials.user, sp: this.credentials.password, waybill_id: waybillId, }); return response.CONFIRM_WAYBILL_RESULT || 'Success'; } /** * Reject waybill (buyer rejects) */ async rejectWaybill(waybillId: string): Promise<string> { const response = await this.callSoap<RejectWaybillResponse>('reject_waybill', { su: this.credentials.user, sp: this.credentials.password, waybill_id: waybillId, }); return response.REJECT_WAYBILL_RESULT || 'Success'; } /** * Get error codes dictionary */ async getErrorCodes(): Promise<any[]> { const response = await this.callSoap<GetErrorCodesResponse>('get_error_codes', { su: this.credentials.user, sp: this.credentials.password, }); return normalizeToArray(response.ERROR_CODES?.ERROR_CODE); } /** * Get akciz codes (excise codes) * * @param searchText Optional search text filter */ async getAkcizCodes(searchText?: string): Promise<any[]> { const response = await this.callSoap<GetAkcizCodesResponse>('get_akciz_codes', { su: this.credentials.user, sp: this.credentials.password, s_text: searchText || '', }); return normalizeToArray(response.AKCIZ_CODES?.AKCIZ); } /** * Get waybill types */ async getWaybillTypes(): Promise<any[]> { const response = await this.callSoap<GetWaybillTypesResponse>('get_waybill_types', { su: this.credentials.user, sp: this.credentials.password, }); return normalizeToArray(response.WAYBILL_TYPES?.TYPE); } /** * Get company name from TIN */ async getNameFromTin(tin: string): Promise<string> { const response = await this.callSoap<GetNameFromTinResponse>('get_name_from_tin', { su: this.credentials.user, sp: this.credentials.password, tin: tin, }); return response.NAME || ''; } /** * Get service users */ async getServiceUsers(): Promise<any[]> { const response = await this.callSoap<GetServiceUsersResponse>('get_service_users', { user_name: this.credentials.user.split(':')[0], // Extract username part user_password: this.credentials.password, }); return normalizeToArray(response.USERS?.USER); } /** * Check service user credentials */ async checkServiceUser(): Promise<boolean> { try { await this.callSoap('chek_service_user', { su: this.credentials.user, sp: this.credentials.password, }); return true; } catch (error) { if (error instanceof AuthenticationError) { return false; } throw error; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/BorisSolomonia/MCPWaybill'

If you have feedback or need assistance with the MCP directory API, please join our Discord server