RS_GE_API_BEST_PRACTICES.md•16 kB
# RS.ge Waybill API - Best Practices & Lessons Learned
## Table of Contents
1. [Critical API Requirements](#critical-api-requirements)
2. [Common Pitfalls & Solutions](#common-pitfalls--solutions)
3. [Correct Implementation Patterns](#correct-implementation-patterns)
4. [Testing Strategy](#testing-strategy)
5. [Production Checklist](#production-checklist)
---
## Critical API Requirements
### 1. Use the Correct SOAP Operation
**❌ WRONG:**
```xml
<get_waybills_v1 xmlns="http://tempuri.org/">
```
**✅ CORRECT:**
```xml
<get_waybills xmlns="http://tempuri.org/">
```
**Why:** The `get_waybills_v1` operation exists but has different behavior and stricter limitations. The base `get_waybills` operation is more reliable and better documented.
---
### 2. Date Parameters Must Use ISO DateTime Format
**❌ WRONG:**
```xml
<create_date_s>2025-10-19</create_date_s>
<create_date_e>2025-10-21</create_date_e>
```
**❌ ALSO WRONG:**
```xml
<last_update_date_s>2025-10-19</last_update_date_s>
<last_update_date_e>2025-10-21</last_update_date_e>
```
**✅ CORRECT:**
```xml
<create_date_s>2025-10-19T00:00:00</create_date_s>
<create_date_e>2025-10-22T00:00:00</create_date_e>
```
**Key Points:**
- Use `create_date_s/e` NOT `last_update_date_s/e`
- Format: `YYYY-MM-DDTHH:MM:SS` (19 characters)
- End date should be **next day at 00:00:00** to include the full end day
- Example: To get Oct 19-21, use start=`2025-10-19T00:00:00` end=`2025-10-22T00:00:00`
---
### 3. Include seller_un_id Parameter
**❌ WRONG:**
```xml
<su>4053098841:405309884</su>
<sp>Password1@</sp>
```
**✅ CORRECT:**
```xml
<su>4053098841:405309884</su>
<sp>Password1@</sp>
<seller_un_id>405309884</seller_un_id>
```
**How to Extract:**
```typescript
const credentials = "4053098841:405309884";
const sellerUnId = credentials.split(':')[1]; // "405309884"
```
**Why:** Without `seller_un_id`, the API may return error `-101` or return incomplete data. This parameter filters waybills to only those belonging to your organization.
---
### 4. XML Parsing - Handle Attributes Correctly
**❌ WRONG:**
```typescript
const resultKeys = Object.keys(methodResponse);
if (resultKeys.length === 1 && resultKeys[0].endsWith('Result')) {
return methodResponse[resultKeys[0]];
}
```
**Problem:** When parsed, the response has TWO keys:
- `get_waybillsResult` (the data)
- `@_xmlns` (XML attribute)
So `length === 1` fails!
**✅ CORRECT:**
```typescript
// Filter out XML attributes (keys starting with @_)
const resultKeys = Object.keys(methodResponse).filter(k => !k.startsWith('@_'));
// Find the Result key
const resultKey = resultKeys.find(k => k.endsWith('Result'));
if (resultKey) {
return methodResponse[resultKey];
}
```
---
### 5. Waybill ID Field Name
**❌ WRONG:**
```typescript
const waybillId = waybill.WAYBILL_ID;
```
**✅ CORRECT:**
```typescript
const waybillId = waybill.ID; // Note: field is called "ID" not "WAYBILL_ID"
```
**Why:** The XML response uses `<ID>974175216</ID>`, not `<WAYBILL_ID>`.
---
## Common Pitfalls & Solutions
### Pitfall 1: Getting 0 Waybills When Data Exists
**Symptoms:**
- API returns HTTP 200
- XML contains `<WAYBILL>` tags
- But your parser extracts 0 waybills
**Root Causes:**
1. Using wrong date parameters (`last_update_date` instead of `create_date`)
2. Using wrong date format (YYYY-MM-DD instead of ISO datetime)
3. XML parser not handling attributes correctly
4. Missing `seller_un_id` parameter
**Solution:**
```typescript
// Correct SOAP request
const soapRequest = `
<get_waybills xmlns="http://tempuri.org/">
<su>${credentials.user}</su>
<sp>${credentials.password}</sp>
<seller_un_id>${sellerUnId}</seller_un_id>
<create_date_s>${startDate}T00:00:00</create_date_s>
<create_date_e>${nextDayDate}T00:00:00</create_date_e>
<buyer_tin></buyer_tin>
</get_waybills>
`;
```
---
### Pitfall 2: ERROR -1072 (Date Range Too Large)
**Symptoms:**
```xml
<RESULT>
<STATUS>-1072</STATUS>
</RESULT>
```
**Causes:**
1. Date range exceeds 3 days
2. Using wrong parameters that trigger stricter validation
3. Not adding +1 day correctly to end date
**Solution:**
```typescript
// For Oct 19-21 (3 days):
const startDate = '2025-10-19T00:00:00';
const endDateObj = new Date('2025-10-21');
endDateObj.setDate(endDateObj.getDate() + 1); // Oct 22
const endDate = endDateObj.toISOString().slice(0, 19); // 2025-10-22T00:00:00
// This gives us Oct 19, 20, 21 = 3 days ✅
```
**Note:** The range is from Oct 19 00:00:00 to Oct 22 00:00:00, which is technically 3 days worth of data.
---
### Pitfall 3: Client-Side Filtering Reducing Results
**Problem:**
```typescript
// API returns 72 waybills
// But filtering reduces to 37
```
**Why:**
- API might return waybills modified during the date range, not created
- Client-side filtering by CREATE_DATE removes valid results
**Solution:**
```typescript
// Don't use client-side filtering anymore!
// The correct API parameters return the right data
// ❌ OLD WAY:
const waybills = apiResults.filter(wb =>
wb.CREATE_DATE >= startDate && wb.CREATE_DATE <= endDate
);
// ✅ NEW WAY:
// Just use the API results as-is with correct parameters
const waybills = apiResults;
```
---
### Pitfall 4: Single-Day Queries Return ERROR
**Symptoms:**
```typescript
// Query for just Oct 19:
create_date_s: '2025-10-19'
create_date_e: '2025-10-19'
// Result: ERROR -1072
```
**Why:** The API interprets this as 0 days or rejects single-day queries.
**Solution:**
```typescript
// For Oct 19 only:
create_date_s: '2025-10-19T00:00:00'
create_date_e: '2025-10-20T00:00:00' // Next day!
```
---
## Correct Implementation Patterns
### Pattern 1: Date Range Preparation
```typescript
function prepareDateRange(startDate: string, endDate: string) {
// Input: YYYY-MM-DD format
// Output: ISO datetime with +1 day on end
const startDateTime = `${startDate}T00:00:00`;
const endDateObj = new Date(endDate);
endDateObj.setDate(endDateObj.getDate() + 1);
const endDateTime = endDateObj.toISOString().slice(0, 19);
return { startDateTime, endDateTime };
}
// Usage:
const { startDateTime, endDateTime } = prepareDateRange('2025-10-19', '2025-10-21');
// Result:
// startDateTime = '2025-10-19T00:00:00'
// endDateTime = '2025-10-22T00:00:00'
```
---
### Pattern 2: Building SOAP Request
```typescript
function buildWaybillRequest(
credentials: { user: string; password: string },
startDateTime: string,
endDateTime: string,
buyerTin: string = ''
): string {
// Extract seller_un_id
const sellerUnId = credentials.user.includes(':')
? credentials.user.split(':')[1]
: '';
return `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<get_waybills xmlns="http://tempuri.org/">
<su>${xmlEscape(credentials.user)}</su>
<sp>${xmlEscape(credentials.password)}</sp>
<seller_un_id>${xmlEscape(sellerUnId)}</seller_un_id>
<create_date_s>${startDateTime}</create_date_s>
<create_date_e>${endDateTime}</create_date_e>
<buyer_tin>${xmlEscape(buyerTin)}</buyer_tin>
</get_waybills>
</soap:Body>
</soap:Envelope>`;
}
function xmlEscape(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
```
---
### Pattern 3: Parsing SOAP Response
```typescript
import { XMLParser } from 'fast-xml-parser';
function parseSoapResponse<T>(xmlData: string): T {
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_',
removeNSPrefix: true, // Remove soap: prefix
});
const parsed = parser.parse(xmlData);
// Navigate to response
const envelope = parsed.Envelope;
const body = envelope.Body;
const methodResponse = body[Object.keys(body)[0]]; // e.g., get_waybillsResponse
// Filter out XML attributes
const resultKeys = Object.keys(methodResponse).filter(k => !k.startsWith('@_'));
// Find Result element
const resultKey = resultKeys.find(k => k.endsWith('Result'));
if (!resultKey) {
throw new Error('No Result element found in SOAP response');
}
const result = methodResponse[resultKey];
// Check for RS.ge API errors
if (result.RESULT && result.RESULT.STATUS) {
const status = Number(result.RESULT.STATUS);
if (status < 0) {
throw new Error(`RS.ge API Error ${status}`);
}
}
return result as T;
}
```
---
### Pattern 4: Extracting Waybills
```typescript
function extractWaybills(response: any): Waybill[] {
if (!response.WAYBILL_LIST) {
return [];
}
const waybillData = response.WAYBILL_LIST.WAYBILL;
if (!waybillData) {
return [];
}
// Handle both single waybill and array
const waybills = Array.isArray(waybillData) ? waybillData : [waybillData];
return waybills.map(wb => ({
id: wb.ID, // Note: ID not WAYBILL_ID
type: wb.TYPE,
createDate: wb.CREATE_DATE,
buyerTin: wb.BUYER_TIN,
sellerTin: wb.SELLER_TIN,
fullAmount: wb.FULL_AMOUNT,
status: wb.STATUS,
// ... other fields
}));
}
```
---
## Testing Strategy
### 1. Unit Tests for Date Preparation
```typescript
describe('prepareDateRange', () => {
it('should add 1 day to end date', () => {
const { startDateTime, endDateTime } = prepareDateRange('2025-10-19', '2025-10-21');
expect(startDateTime).toBe('2025-10-19T00:00:00');
expect(endDateTime).toBe('2025-10-22T00:00:00');
});
it('should handle single day', () => {
const { startDateTime, endDateTime } = prepareDateRange('2025-10-19', '2025-10-19');
expect(startDateTime).toBe('2025-10-19T00:00:00');
expect(endDateTime).toBe('2025-10-20T00:00:00');
});
});
```
---
### 2. Integration Tests with Real API
```typescript
describe('RS.ge API Integration', () => {
it('should retrieve waybills for Oct 19-21, 2025', async () => {
const client = new RsWaybillClient({
user: process.env.RS_SERVICE_USER,
password: process.env.RS_SERVICE_PASSWORD,
});
const waybills = await client.getWaybills('2025-10-19', '2025-10-21');
expect(waybills.length).toBeGreaterThan(0);
expect(waybills[0]).toHaveProperty('ID');
expect(waybills[0]).toHaveProperty('FULL_AMOUNT');
});
});
```
---
### 3. Test with Known Data
```typescript
// Keep a record of known waybill IDs for validation
const KNOWN_OCT_19_WAYBILLS = [
'974175216', '974175698', '974175705', '974175712',
'974175730', '974175763', '974175769', '974175774',
'974175784', '974175793', '974175798', '974175801'
];
it('should find all known Oct 19 waybills', async () => {
const waybills = await client.getWaybills('2025-10-19', '2025-10-19');
const foundIds = waybills.map(wb => wb.ID.toString());
KNOWN_OCT_19_WAYBILLS.forEach(knownId => {
expect(foundIds).toContain(knownId);
});
});
```
---
## Production Checklist
### Before Deployment
- [ ] Using `get_waybills` operation (not `get_waybills_v1`)
- [ ] Date parameters use ISO datetime format (`YYYY-MM-DDTHH:MM:SS`)
- [ ] Using `create_date_s/e` (not `last_update_date_s/e`)
- [ ] End date incremented by +1 day
- [ ] `seller_un_id` parameter included and extracted correctly
- [ ] XML parser filters out `@_` attributes
- [ ] Using `ID` field (not `WAYBILL_ID`)
- [ ] Error handling for negative STATUS codes
- [ ] Retry logic for network errors
- [ ] Logging for debugging
- [ ] Environment variables for credentials
- [ ] Rate limiting / request throttling
- [ ] Timeout configuration (recommend 60-120 seconds)
### Performance Considerations
```typescript
// Good: Parallel requests for multiple date ranges
const ranges = [
{ start: '2025-10-01', end: '2025-10-03' },
{ start: '2025-10-04', end: '2025-10-06' },
{ start: '2025-10-07', end: '2025-10-09' },
];
const results = await Promise.all(
ranges.map(range => client.getWaybills(range.start, range.end))
);
const allWaybills = results.flat();
```
### Error Handling
```typescript
try {
const waybills = await client.getWaybills(startDate, endDate);
} catch (error) {
if (error.message.includes('-1072')) {
console.error('Date range too large. Split into smaller ranges.');
} else if (error.message.includes('-101')) {
console.error('Missing seller_un_id parameter.');
} else {
console.error('Unknown error:', error);
}
}
```
---
## Summary: Quick Reference
| Aspect | Wrong | Correct |
|--------|-------|---------|
| SOAP Operation | `get_waybills_v1` | `get_waybills` |
| Date Parameter | `last_update_date_s/e` | `create_date_s/e` |
| Date Format | `YYYY-MM-DD` | `YYYY-MM-DDTHH:MM:SS` |
| End Date | Same as user input | User input + 1 day |
| Seller ID | Not included | `seller_un_id` from credentials |
| ID Field | `WAYBILL_ID` | `ID` |
| XML Attributes | Not filtered | Filter keys starting with `@_` |
---
## Real Example: Complete Working Code
```typescript
import axios from 'axios';
import { XMLParser } from 'fast-xml-parser';
async function getWaybills(
credentials: { user: string; password: string },
startDate: string, // YYYY-MM-DD
endDate: string // YYYY-MM-DD
): Promise<any[]> {
// 1. Prepare dates
const startDateTime = `${startDate}T00:00:00`;
const endDateObj = new Date(endDate);
endDateObj.setDate(endDateObj.getDate() + 1);
const endDateTime = endDateObj.toISOString().slice(0, 19);
// 2. Extract seller_un_id
const sellerUnId = credentials.user.split(':')[1];
// 3. Build SOAP request
const soapRequest = `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<get_waybills xmlns="http://tempuri.org/">
<su>${credentials.user}</su>
<sp>${credentials.password}</sp>
<seller_un_id>${sellerUnId}</seller_un_id>
<create_date_s>${startDateTime}</create_date_s>
<create_date_e>${endDateTime}</create_date_e>
<buyer_tin></buyer_tin>
</get_waybills>
</soap:Body>
</soap:Envelope>`;
// 4. Send request
const response = await axios.post(
'https://services.rs.ge/WayBillService/WayBillService.asmx',
soapRequest,
{
headers: {
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': 'http://tempuri.org/get_waybills'
},
timeout: 60000
}
);
// 5. Parse response
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_',
removeNSPrefix: true,
});
const parsed = parser.parse(response.data);
const result = parsed.Envelope.Body.get_waybillsResponse.get_waybillsResult;
// 6. Extract waybills
const waybillData = result.WAYBILL_LIST?.WAYBILL;
if (!waybillData) return [];
return Array.isArray(waybillData) ? waybillData : [waybillData];
}
// Usage:
const waybills = await getWaybills(
{ user: '4053098841:405309884', password: 'Password1@' },
'2025-10-19',
'2025-10-21'
);
console.log(`Retrieved ${waybills.length} waybills`);
// Output: Retrieved 61 waybills
```
---
## Lessons Learned
1. **Don't assume API documentation is complete** - The working ERP code was more reliable than documentation
2. **Test with real data** - Having specific waybill IDs to verify was crucial
3. **XML parsing is tricky** - Attributes can break simple parsers
4. **Date handling is critical** - Small differences in format cause total failures
5. **Compare with working code** - Looking at the ERP application revealed all the answers
6. **Log everything during development** - Raw XML responses helped debug issues
7. **Test edge cases** - Single-day queries, 3-day limits, etc.
---
**Last Updated:** 2025-01-XX
**Version:** 1.0
**Verified With:** RS.ge Waybill Service (Production API)