UNPKG

eudr-api-client

Version:

Enterprise-grade Node.js library for the EU Deforestation Regulation (EUDR) TRACES system. It provides seamless integration for submitting, amending, retrieving, and managing Due Diligence Statements (DDS) with support for both V1 and V2 APIs.

1,443 lines (1,227 loc) β€’ 82.7 kB
# 🌲 EUDR API Client [![npm version](https://img.shields.io/npm/v/eudr-api-client.svg)](https://www.npmjs.com/package/eudr-api-client) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) [![Node.js Version](https://img.shields.io/node/v/eudr-api-client.svg)](https://nodejs.org) [![Test Status](https://img.shields.io/badge/tests-passing-brightgreen.svg)](./tests/README.md) > **Enterprise-grade Node.js library for EU Deforestation Regulation (EUDR) compliance** > Complete integration with EUDR TRACES system for Due Diligence Statements (DDS) management ## EUDR Systems The EUDR system operates on two environments: - **🟒 Production (LIVE)**: [https://eudr.webcloud.ec.europa.eu/tracesnt/](https://eudr.webcloud.ec.europa.eu/tracesnt/) - **Purpose**: Real submissions with legal value - **Web Service Client ID**: `eudr` - **Use**: Only for products to be placed on the market or exported after entry into application - **Note**: Submissions have legal value and can be subject to checks by Competent Authorities - **🟑 Acceptance (Training)**: [https://acceptance.eudr.webcloud.ec.europa.eu/tracesnt/](https://acceptance.eudr.webcloud.ec.europa.eu/tracesnt/) - **Purpose**: Training and familiarization platform - **Web Service Client ID**: `eudr-test` - **Use**: Testing and getting familiar with the system - **Note**: Submissions have no legal value ## Why EUDR API Client? The EU Deforestation Regulation (EUDR) requires operators and traders to submit Due Diligence Statements for commodities like wood, cocoa, coffee, and more. This library provides: - βœ… **100% API Coverage** - Both V1 and V2 EUDR APIs fully implemented - βœ… **Production-Ready** - Battle-tested with real EUDR systems - βœ… **Well-Documented** - Comprehensive documentation with real examples - βœ… **Enterprise Features** - Robust error handling, logging, and comprehensive validation - βœ… **Easy Integration** - Simple API with real-world examples - βœ… **Smart Endpoint Management** - Automatic endpoint generation for standard environments - βœ… **Flexible Configuration** - Manual endpoint override when needed - βœ… **πŸš€ NEW: Flexible Array Fields** - Array properties accept both single objects and arrays for maximum flexibility ## Table of Contents - [Quick Start](#quick-start) - [Installation](#installation) - [Basic Setup](#basic-setup) - [Configuration](#configuration) - [Real-World Examples](#real-world-examples) - [Trade Operations](#trade-operations) - [Import Operations](#import-operations) - [Domestic Production](#domestic-production) - [Authorized Representatives](#authorized-representatives) - [API Reference](#api-reference) - [Services Overview](#services-overview) - [Echo Service](#echo-service) - [Submission Service](#submission-service) - [Retrieval Service](#retrieval-service) - [Data Types](#data-types) - [Advanced Usage](#advanced-usage) - [Testing](#testing) - [Troubleshooting](#troubleshooting) - [Common Issues](#common-issues) - [Debug Mode](#debug-mode) - [FAQ](#faq) - [Contributing](#contributing) - [License](#license) - [Support](#support) ## Quick Start ### Installation ```bash npm install eudr-api-client ``` ### Basic Setup ```javascript const { EudrSubmissionClient } = require('eudr-api-client'); // Initialize the client with automatic endpoint generation const client = new EudrSubmissionClient({ username: 'your-username', password: 'your-password', // Use "eudr-test" EUDR Traces acceptance environment, use "eudr-repository" for production environment webServiceClientId: 'eudr-test', // See [Configuration](#configuration) section for details ssl: false // SSL configuration: true for secure (production), false for development }); ``` **Submit your first DDS:** ```javascript const result = await client.submitDds({ operatorType: 'TRADER', statement: { internalReferenceNumber: 'REF-001', activityType: 'TRADE', countryOfActivity: 'HR', borderCrossCountry: 'HR', commodities: [{ descriptors: { descriptionOfGoods: 'Traded wood products', goodsMeasure: { netWeight: 20, volume: 15 } }, hsHeading: '4401', speciesInfo: { scientificName: 'Fagus silvatica', commonName: 'European Beech' } // πŸš€ NEW: speciesInfo can also be an array for multiple species }], operator: { nameAndAddress: { name: 'Your Company Ltd.', country: 'HR', address: 'Your Address 123, 10000 Zagreb' }, email: 'info@yourcompany.com', phone: '+385 1 234 5678' }, geoLocationConfidential: false, associatedStatements: [{ referenceNumber: '25NLSN6LX69730', verificationNumber: 'K7R8LA90' }] // πŸš€ NEW: All array fields support both single objects and arrays } }); console.log('βœ… DDS Submitted. Identifier:', result.ddsIdentifier); ``` ## Configuration ### Environment Variables Create a `.env` file in your project root: ```bash # EUDR API Credentials EUDR_TRACES_USERNAME=your-username EUDR_TRACES_PASSWORD=your-password EUDR_WEB_SERVICE_CLIENT_ID=eudr-test # Optional: SSL Configuration EUDR_SSL_ENABLED=false # true for production (secure), false for development # Optional: Logging EUDR_LOG_LEVEL=info # trace, debug, info, warn, error, fatal ``` ### Configuration Options **Automatic Endpoint Generation (Recommended):** ```javascript const config = { // Required username: 'your-username', password: 'your-password', webServiceClientId: 'eudr-test', // Automatically generates acceptance endpoint // Optional ssl: false, // true for production (secure), false for development timestampValidity: 60, // seconds timeout: 10000, // milliseconds }; ``` **Manual Endpoint Override (Legacy/Advanced):** ```javascript const config = { // Required endpoint: 'https://custom-endpoint.com/ws/service', username: 'your-username', password: 'your-password', webServiceClientId: 'custom-client', // Custom ID requires manual endpoint // Optional ssl: true, // true for production (secure), false for development timestampValidity: 60, timeout: 10000, }; ``` ### Configuration Priority **Priority Order for Endpoint Resolution:** 1. **Manual `endpoint`** (if provided) β†’ Uses specified endpoint 2. **Standard `webServiceClientId`** β†’ Automatically generates endpoint 3. **Custom `webServiceClientId`** β†’ Requires manual `endpoint` configuration **What happens automatically:** - **`webServiceClientId: 'eudr-repository'`** β†’ Uses production environment - **`webServiceClientId: 'eudr-test'`** β†’ Uses acceptance environment - **Custom `webServiceClientId`** β†’ Requires manual `endpoint` configuration ### Example Configuration Scenarios ```javascript // Scenario 1: Automatic endpoint generation (Recommended) const autoClient = new EudrSubmissionClient({ username: 'user', password: 'pass', webServiceClientId: 'eudr-test', // Automatically generates acceptance endpoint ssl: false // Development environment - allow self-signed certificates }); // Scenario 2: Manual endpoint override const manualClient = new EudrSubmissionClient({ endpoint: 'https://custom-server.com/ws/service', username: 'user', password: 'pass', webServiceClientId: 'custom-id', ssl: false // Custom development server }); // Scenario 3: Production environment const productionClient = new EudrSubmissionClient({ username: 'user', password: 'pass', webServiceClientId: 'eudr-repository', // Automatically generates production endpoint ssl: true // Production environment - validate SSL certificates }); ``` ### Accessing Configuration Information **You can access endpoint configuration information through the `config` export:** ```javascript const { config } = require('eudr-api-client'); // Get supported client IDs const supportedIds = config.getSupportedClientIds(); console.log('Supported IDs:', supportedIds); // ['eudr-repository', 'eudr-test'] // Get supported services const supportedServices = config.getSupportedServices(); console.log('Supported Services:', supportedServices); // ['echo', 'retrieval', 'submission'] // Get supported versions for a service const echoVersions = config.getSupportedVersions('echo'); console.log('Echo Service Versions:', echoVersions); // ['v1', 'v2'] // Check if a client ID is standard const isStandard = config.isStandardClientId('eudr-repository'); console.log('Is eudr standard?', isStandard); // true // Generate endpoint manually (if needed) const endpoint = config.generateEndpoint('submission', 'v2', 'eudr-test'); console.log('Generated endpoint:', endpoint); ``` ## Real-World Examples ### Trade Operations **Scenario**: Trading wood products with references to existing DDS statements ```javascript const { EudrSubmissionClient } = require('eudr-api-client'); // πŸš€ NEW: Automatic endpoint generation - no need to specify endpoint! const client = new EudrSubmissionClient({ username: process.env.EUDR_USERNAME, password: process.env.EUDR_PASSWORD, webServiceClientId: process.env.EUDR_CLIENT_ID, // Automatically generates endpoint ssl: process.env.EUDR_SSL_ENABLED === 'true' // SSL configuration from environment }); // See [Configuration](#configuration) section for detailed options // Trade submission with multiple associated statements const tradeResult = await client.submitDds({ operatorType: "TRADER", statement: { internalReferenceNumber: "DLE20/358", activityType: "TRADE", countryOfActivity: "HR", borderCrossCountry: "HR", comment: "Trade submission with multiple associated statements", commodities: [{ descriptors: { descriptionOfGoods: "Traded wood products from main warehouse", goodsMeasure: { netWeight: 20, volume: 15 } }, hsHeading: "4401", speciesInfo: { scientificName: "Fagus silvatica", commonName: "BUKVA OBIČNA" } }], operator: { nameAndAddress: { name: "GreenWood Solutions Ltd.", country: "HR", address: "Trg Republike 15, 10000 Zagreb" }, email: "info@greenwood-solutions.hr", phone: "+385 (001) 480-4111" }, geoLocationConfidential: false, associatedStatements: [ { referenceNumber: "25NLSN6LX69730", verificationNumber: "K7R8LA90" }, { referenceNumber: "25NLWPAZWQ8865", verificationNumber: "GLE9SMMM" } ] } }); console.log(`βœ… Trade DDS submitted. Identifier: ${tradeResult.ddsIdentifier}`); ``` ### Import Operations **Scenario**: Importing wood products with geolocation data ```javascript // πŸš€ NEW: Automatic endpoint generation for import operations const importClient = new EudrSubmissionClient({ username: process.env.EUDR_USERNAME, password: process.env.EUDR_PASSWORD, webServiceClientId: 'eudr-test', // Automatically generates acceptance endpoint ssl: false // Development environment - allow self-signed certificates }); // See [Configuration](#configuration) section for detailed options // Import submission with producer geolocations const importResult = await importClient.submitDds({ operatorType: "OPERATOR", statement: { internalReferenceNumber: "DLE20/359", activityType: "IMPORT", countryOfActivity: "HR", borderCrossCountry: "HR", comment: "Import with geolocations", commodities: [{ descriptors: { descriptionOfGoods: "Imported wood products from France", goodsMeasure: { netWeight: 30, volume: 15 } }, hsHeading: "4401", speciesInfo: { scientificName: "Fagus silvatica", commonName: "BUKVA OBIČNA" }, producers: [{ country: "FR", name: "French Wood Producer", // Base64 encoded GeoJSON polygon geometryGeojson: "eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9seWdvbiIsImNvb3JkaW5hdGVzIjpbW1sxNC45NzA0NTk4MzIsNDUuMTkyMzk4MjUyXSxbMTQuOTY5ODU4Mjc1LDQ1LjE4ODM0NDEwNl0sWzE4Ljk2ODIyMzYzMSw0NS4xODY4NjQzMTRdLFsxNC45NjI0NDc0NjQsNDUuMTg1Njg0NTJdLFsxNC45NjM2MzE4MzksNDUuMTkxMTExMzkxXSxbMTQuOTY2MTQ1ODEzLDQ1LjE5MDg2MjIzNF0sWzE0Ljk2NzU4NDQwMyw0NS4xOTIyODAxMDZdLFsxNC45NzA0NTk4MzIsNDUuMTkyMzk4MjUyXV1dfSwicHJvcGVydGllcyI6eyJnamlkIjoiNTgwIiwiZ29kaW5hIjoyMDE2LCJwb3Zyc2luYSI6MzEuMjQsIm96bmFrYSI6IjQyIGEifX1dfQ==" }] }], operator: { nameAndAddress: { name: "GreenWood Solutions Ltd.", country: "HR", address: "Trg Republike 15, 10000 Zagreb" }, email: "info@greenwood-solutions.hr", phone: "+385 (001) 480-4111" }, geoLocationConfidential: false } }); console.log(`βœ… Import DDS submitted. Identifier: ${importResult.ddsIdentifier}`); ``` ### Domestic Production **Scenario**: Domestic wood production with multiple species ```javascript // Domestic production with multiple commodities const domesticResult = await client.submitDds({ operatorType: "OPERATOR", statement: { internalReferenceNumber: "DLE20/357", activityType: "DOMESTIC", countryOfActivity: "HR", borderCrossCountry: "HR", comment: "", commodities: [ { descriptors: { descriptionOfGoods: "Otprema prostornog drva s glavnog stovariΕ‘ta (popratnica DLE20/357) - BUKVA OBIČNA", goodsMeasure: { volume: 20, netWeight: 16 } }, hsHeading: "4401", speciesInfo: { scientificName: "Fagus silvatica", commonName: "BUKVA OBIČNA" }, producers: [{ country: "HR", name: "GreenWood Solutions Ltd.", geometryGeojson: "eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9seWdvbiIsImNvb3JkaW5hdGVzIjpbW1sxNC45NzA0NTk4MzIsNDUuMTkyMzk4MjUyXSxbMTQuOTY5ODU4Mjc1LDQ1LjE4ODM0NDEwNl0sWzE4Ljk2ODIyMzYzMSw0NS4xODY4NjQzMTRdLFsxNC45NjI0NDc0NjQsNDUuMTg1Njg0NTJdLFsxNC45NjM2MzE4MzksNDUuMTkxMTExMzkxXSxbMTQuOTY2MTQ1ODEzLDQ1LjE5MDg2MjIzNF0sWzE0Ljk2NzU4NDQwMyw0NS4xOTIyODAxMDZdLFsxNC45NzA0NTk4MzIsNDUuMTkyMzk4MjUyXV1dfSwicHJvcGVydGllcyI6eyJnamlkIjoiNTgwIiwiZ29kaW5hIjoyMDE2LCJwb3Zyc2luYSI6MzEuMjQsIm96bmFrYSI6IjQyIGEifX1dfQ==" }] }, { descriptors: { descriptionOfGoods: "Otprema prostornog drva s glavnog stovariΕ‘ta (popratnica DLE20/357) - BUKVA OSTALE", goodsMeasure: { volume: 15, netWeight: 12 } }, hsHeading: "4401", speciesInfo: { scientificName: "Fagus sp.", commonName: "BUKVA OSTALE" }, producers: [{ country: "HR", name: "GreenWood Solutions Ltd.", geometryGeojson: "eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9seWdvbiIsImNvb3JkaW5hdGVzIjpbW1sxNC45NzA0NTk4MzIsNDUuMTkyMzk4MjUyXSxbMTQuOTY5ODU4Mjc1LDQ1LjE4ODM0NDEwNl0sWzE4Ljk2ODIyMzYzMSw0NS4xODY4NjQzMTRdLFsxNC45NjI0NDc0NjQsNDUuMTg1Njg0NTJdLFsxNC45NjM2MzE4MzksNDUuMTkxMTExMzkxXSxbMTQuOTY2MTQ1ODEzLDQ1LjE5MDg2MjIzNF0sWzE0Ljk2NzU4NDQwMyw0NS4xOTIyODAxMDZdLFsxNC45NzA0NTk4MzIsNDUuMTkyMzk4MjUyXV1dfSwicHJvcGVydGllcyI6eyJnamlkIjoiNTgwIiwiZ29kaW5hIjoyMDE2LCJwb3Zyc2luYSI6MzEuMjQsIm96bmFrYSI6IjQyIGEifX1dfQ==" }] } ], operator: { nameAndAddress: { name: "GreenWood Solutions Ltd.", country: "HR", address: "Trg Republike 15, 10000 Zagreb" }, email: "info@greenwood-solutions.hr", phone: "+385 (001) 480-4111" }, geoLocationConfidential: false } }); console.log(`βœ… Domestic DDS submitted. Identifier: ${domesticResult.ddsIdentifier}`); ``` ### Authorized Representatives **Scenario**: Submitting on behalf of another operator ```javascript // Authorized representative submission const representativeResult = await client.submitDds({ operatorType: "OPERATOR", statement: { internalReferenceNumber: "DLE20/360", activityType: "IMPORT", operator: { // Reference to the actual operator referenceNumber: { identifierType: "eori", identifierValue: "HR123456789" }, nameAndAddress: { name: "Croatian Import Company", country: "HR", address: "Ulica Kneza Branimira 2, 10000 Zagreb" }, email: "contact@croatianimport.hr", phone: "+385 (001) 480-4111" }, countryOfActivity: "HR", borderCrossCountry: "HR", comment: "Import by authorized representative", commodities: [{ descriptors: { descriptionOfGoods: "Wood products imported by representative", goodsMeasure: { netWeight: 25, volume: 12 } }, hsHeading: "4401", speciesInfo: { scientificName: "Fagus silvatica", commonName: "BUKVA OBIČNA" }, producers: [{ country: "GH", name: "Ghana Wood Board", geometryGeojson: "eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJnZW9tZXRyeSI6eyJ0eXBlIjoiUG9seWdvbiIsImNvb3JkaW5hdGVzIjpbW1stMS4xODY0LDYuNTI0NF0sWy0xLjE4NzQsNi41MjQ0XSxbLTEuMTg3NCw2LjUzNDRdLFstMS4xODY0LDYuNTM0NF0sWy0xLjE4NjQsNi41MjQ0XV1dfSwicHJvcGVydGllcyI6eyJuYW1lIjoiR2hhbmEgV29vZCBCb2FyZCJ9fV19" }] }], geoLocationConfidential: false } }); console.log(`βœ… Representative DDS submitted. Identifier: ${representativeResult.ddsIdentifier}`); ``` ## API Reference ### Services Overview **πŸš€ NEW: All services now support automatic endpoint generation!** | Service | Class | Automatic Endpoint | Manual Override | CF Specification | |---------|-------|-------------------|-----------------|------------------| | **Echo Service** | `EudrEchoClient` | βœ… Yes | βœ… Yes | CF1 v1.4 | | **Submission Service V1** | `EudrSubmissionClient` | βœ… Yes | βœ… Yes | CF2 v1.4 | | **Submission Service V2** | `EudrSubmissionClientV2` | βœ… Yes | βœ… Yes | CF2 v1.4 | | **Retrieval Service V1** | `EudrRetrievalClient` | βœ… Yes | βœ… Yes | CF3 & CF7 v1.4 | | **Retrieval Service V2** | `EudrRetrievalClientV2` | βœ… Yes | βœ… Yes | CF3 & CF7 v1.4 | **Endpoint Generation Rules:** - **`webServiceClientId: 'eudr-repository'`** β†’ Production environment endpoints - **`webServiceClientId: 'eudr-test'`** β†’ Acceptance environment endpoints - **Custom `webServiceClientId`** β†’ Requires manual `endpoint` configuration **Example:** ```javascript const { EudrEchoClient, EudrSubmissionClient, EudrSubmissionClientV2, EudrRetrievalClient, EudrRetrievalClientV2 } = require('eudr-api-client'); // All services automatically generate endpoints const echoClient = new EudrEchoClient({ username: 'user', password: 'pass', webServiceClientId: 'eudr-test', ssl: false }); const submissionV1Client = new EudrSubmissionClient({ username: 'user', password: 'pass', webServiceClientId: 'eudr-repository', ssl: true }); const submissionV2Client = new EudrSubmissionClientV2({ username: 'user', password: 'pass', webServiceClientId: 'eudr-test', ssl: false }); const retrievalClient = new EudrRetrievalClient({ username: 'user', password: 'pass', webServiceClientId: 'eudr-repository', ssl: true }); const retrievalV2Client = new EudrRetrievalClientV2({ username: 'user', password: 'pass', webServiceClientId: 'eudr-test', ssl: false }); ``` **For detailed endpoint configuration options, see the [Configuration](#configuration) section.** ### Echo Service Test connectivity and authentication with the EUDR system. ```javascript const { EudrEchoClient } = require('eudr-api-client'); const echoClient = new EudrEchoClient(config); // Test connection const response = await echoClient.echo('Hello EUDR'); console.log('Echo response:', response.status); ``` #### Methods | Method | Description | Parameters | Returns | |--------|-------------|------------|---------| | `echo(params)` | Test service connectivity | `message` (String) | Promise with echo response | #### Example ```javascript const response = await echoClient.echo('Hello EUDR'); // Returns: { message: 'Hello EUDR' } ``` ### Submission Service Submit, amend, and retract DDS statements. Available in both V1 and V2 APIs with different validation requirements. #### V1 Client (`EudrSubmissionClient`) ```javascript const { EudrSubmissionClient } = require('eudr-api-client'); const submissionClient = new EudrSubmissionClient(config); // Submit DDS const submitResult = await submissionClient.submitDds(requestObject, { rawResponse: false }); // Amend existing DDS const amendResult = await submissionClient.amendDds(submitResult.ddsIdentifier, updatedStatement); // Retract DDS const retractResult = await submissionClient.retractDds(submitResult.ddsIdentifier); ``` #### V2 Client (`EudrSubmissionClientV2`) ```javascript const { EudrSubmissionClientV2 } = require('eudr-api-client'); const submissionClientV2 = new EudrSubmissionClientV2(configV2); // Submit DDS const submitResultV2 = await submissionClientV2.submitDds(requestObjectV2); // Amend existing DDS const amendResultV2 = await submissionClientV2.amendDds(submitResultV2.ddsIdentifier, updatedStatementV2); // Retract DDS const retractResultV2 = await submissionClientV2.retractDds(submitResultV2.ddsIdentifier); ``` ### Retrieval Service Retrieve DDS information and supply chain data with automatic endpoint generation. #### V1 Client (`EudrRetrievalClient`) ```javascript const { EudrRetrievalClient } = require('eudr-api-client'); // πŸš€ NEW: Automatic endpoint generation - no need to specify endpoint! const retrievalClient = new EudrRetrievalClient({ username: process.env.EUDR_USERNAME, password: process.env.EUDR_PASSWORD, webServiceClientId: 'eudr-test', // Automatically generates acceptance endpoint ssl: false // Development environment - allow self-signed certificates }); // Get DDS info by UUID (supports single UUID or array of UUIDs, max 100) const ddsInfo = await retrievalClient.getDdsInfo('some-uuid-string'); const multipleDds = await retrievalClient.getDdsInfo(['uuid-1', 'uuid-2', 'uuid-3']); // Get DDS info by internal reference number (CF3 v1.4) const ddsList = await retrievalClient.getDdsInfoByInternalReferenceNumber('DLE20/357'); // Get full DDS statement by reference and verification number (CF7 v1.4) const fullDds = await retrievalClient.getStatementByIdentifiers('25NLSN6LX69730', 'K7R8LA90'); // Note: getReferencedDDS is only available in V2 - use EudrRetrievalClientV2 ``` #### Key Features - βœ… **CF3 v1.4 Support**: `getDdsInfo`, `getDdsInfoByInternalReferenceNumber` with rejection reason & CA communication - βœ… **CF7 v1.4 Support**: `getStatementByIdentifiers` for complete DDS retrieval - βœ… **Automatic Endpoint Generation**: No manual endpoint configuration needed for standard client IDs - βœ… **Batch Retrieval**: Support for up to 100 UUIDs in a single `getDdsInfo` call - βœ… **Smart Error Handling**: Converts SOAP authentication faults to proper HTTP 401 status codes - βœ… **Flexible SSL Configuration**: Configurable SSL certificate validation - βœ… **πŸš€ NEW: Consistent Array Fields**: `commodities`, `producers`, `speciesInfo`, `referenceNumber` always returned as arrays --- ### πŸ“ EudrSubmissionClient The main client for submitting, amending, and retracting DDS statements (V1 API). #### Methods | Method | Description | Parameters | Returns | |--------|-------------|------------|---------| | `submitDds(request, options)` | Submit a new Due Diligence Statement | `request` (Object), `options` (Object) | Promise with DDS identifier | | `amendDds(ddsIdentifier, statement, options)` | Amend an existing DDS | `ddsIdentifier` (String), `statement` (Object), `options` (Object) | Promise with success status | | `retractDds(ddsIdentifier, options)` | Retract a submitted DDS | `ddsIdentifier` (String), `options` (Object) | Promise with success status | #### Detailed Method Reference **`submitDds(request, options)`** ```javascript const result = await client.submitDds({ operatorType: 'TRADER', // 'OPERATOR' or 'TRADER' statement: { internalReferenceNumber: 'REF-001', activityType: 'TRADE', // ... other statement data } }, { rawResponse: false, // Set to true to get raw XML response encodeGeojson: true // Encode plain geometryGeojson strings to base64 }); // Returns: { httpStatus: 200, status: 200, ddsIdentifier: 'uuid-string', raw: 'xml...' } ``` **`amendDds(ddsIdentifier, statement, options)`** ```javascript const result = await client.amendDds( 'existing-dds-uuid', // DDS identifier from previous submission { // dds statement data internalReferenceNumber: 'REF-001-UPDATED', // ... other updated fields }, { rawResponse: false, // Set to true to get raw XML response encodeGeojson: true // Encode plain geometryGeojson strings to base64 } ); // Returns: { httpStatus: 200, status: 200, success: true, message: 'DDS amended successfully' } ``` **`retractDds(ddsIdentifier, options)`** ```javascript const result = await client.retractDds( 'dds-uuid-to-retract', { debug: true, // Enable debug logging rawResponse: false // Set to true to get raw XML response } ); // Returns: { httpStatus: 200, success: true, status: 'SC_200_OK', message: 'DDS retracted successfully' } ``` --- ### πŸš€ EudrSubmissionClientV2 The V2 client for submitting, amending, and retracting DDS statements with enhanced validation and structure. #### Methods | Method | Description | Parameters | Returns | |--------|-------------|------------|---------| | `submitDds(request, options)` | Submit a new DDS (V2) | `request` (Object), `options` (Object) | Promise with DDS identifier | | `amendDds(ddsIdentifier, statement, options)` | Amend existing DDS (V2) | `ddsIdentifier` (String), `statement` (Object), `options` (Object) | Promise with success status | | `retractDds(ddsIdentifier, options)` | Retract DDS (V2) | `ddsIdentifier` (String), `options` (Object) | Promise with success status | #### Detailed Method Reference **`submitDds(request, options)`** ```javascript const result = await clientV2.submitDds({ operatorType: 'OPERATOR', statement: { internalReferenceNumber: 'DLE20/357', activityType: 'DOMESTIC', operator: { operatorAddress: { // V2 uses operatorAddress instead of nameAndAddress name: 'GreenWood Solutions Ltd.', country: 'HR', street: 'Trg Republike 15', postalCode: '10000', city: 'Zagreb', fullAddress: 'Trg Republike 15, 10000 Zagreb' }, email: 'info@greenwood-solutions.hr', phone: '+385 (001) 480-4111' }, countryOfActivity: 'HR', borderCrossCountry: 'HR', commodities: [{ descriptors: { descriptionOfGoods: 'Wood products', goodsMeasure: { supplementaryUnit: 20, // V2 uses supplementaryUnit for DOMESTIC supplementaryUnitQualifier: 'MTQ' // Cubic meters } }, hsHeading: '4401', speciesInfo: { scientificName: 'Fagus silvatica', commonName: 'BUKVA OBIČNA' }, producers: [{ country: 'HR', name: 'GreenWood Solutions Ltd.', geometryGeojson: 'plain-json-string' // Will be encoded to base64 if encodeGeojson: true }] }], geoLocationConfidential: false } }, { rawResponse: false, // Set to true to get raw XML response encodeGeojson: true // Encode plain geometryGeojson strings to base64 }); // Returns: { httpStatus: 200, status: 200, ddsIdentifier: 'uuid-string', raw: 'xml...' } ``` **`amendDds(ddsIdentifier, statement, options)`** ```javascript const result = await clientV2.amendDds( 'existing-dds-uuid', // DDS identifier from previous submission { // Updated statement data internalReferenceNumber: 'DLE20/357-UPDATED', // ... other updated fields }, { rawResponse: false, // Set to true to get raw XML response encodeGeojson: true // Encode plain geometryGeojson strings to base64 } ); // Returns: { httpStatus: 200, status: 200, success: true, message: 'DDS amended successfully' } ``` **`retractDds(ddsIdentifier, options)`** ```javascript const result = await clientV2.retractDds( 'dds-uuid-to-retract', { debug: true, // Enable debug logging rawResponse: false // Set to true to get raw XML response } ); // Returns: { httpStatus: 200, success: true, status: 'SC_200_OK', message: 'DDS retracted successfully' } ``` --- ### πŸ” EudrRetrievalClient (V1) Service for retrieving DDS information and supply chain data with automatic endpoint generation and smart error handling. #### Methods | Method | Description | CF Spec | Parameters | Returns | |--------|-------------|---------|------------|---------| | `getDdsInfo(uuids, options)` | Retrieve DDS info by UUID(s) | CF3 v1.4 | `uuids` (String or Array), `options` (Object) | Promise with DDS details | | `getDdsInfoByInternalReferenceNumber(internalReferenceNumber, options)` | Retrieve DDS by internal reference | CF3 v1.4 | `internalReferenceNumber` (String, 3-50 chars), `options` (Object) | Promise with DDS array | | `getStatementByIdentifiers(referenceNumber, verificationNumber, options)` | Get full DDS statement | CF7 v1.4 | `referenceNumber` (String), `verificationNumber` (String), `options` (Object) | Promise with complete DDS | | ~~`getReferencedDDS()`~~ | ❌ Not available in V1 | N/A | Use `EudrRetrievalClientV2` instead | V2 only | #### Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `rawResponse` | boolean | false | Whether to return the raw XML response | | `decodeGeojson` | boolean | false | Whether to decode base64 geometryGeojson to plain string | #### Detailed Method Reference **`getDdsInfo(uuids, options)` - CF3 v1.4** Retrieve DDS information by UUID with v1.4 enhancements including rejection reasons and CA communication. ```javascript // Single UUID const ddsInfo = await retrievalClient.getDdsInfo('550e8400-e29b-41d4-a716-446655440000'); // Multiple UUIDs (max 100 per call) const multipleDds = await retrievalClient.getDdsInfo([ '550e8400-e29b-41d4-a716-446655440000', '6ba7b810-9dad-11d1-80b4-00c04fd430c8' ]); // With options const ddsWithRaw = await retrievalClient.getDdsInfo('some-uuid', { rawResponse: true // Get raw XML response }); // Returns: // { // httpStatus: 200, // status: 200, // ddsInfo: [ // { // identifier: 'uuid-string', // status: 'Accepted' | 'Rejected' | 'Processing', // referenceNumber: '25NLSN6LX69730', // verificationNumber: 'K7R8LA90', // rejectionReason: 'Reason if status is Rejected', // v1.4 feature // caCommunication: 'CA communication if provided', // v1.4 feature // // ... other DDS info // } // ], // raw: 'xml-response', // if rawResponse: true // parsed: { /* parsed XML object */ } // } ``` **`getDdsInfoByInternalReferenceNumber(internalReferenceNumber, options)` - CF3 v1.4** Search for DDS by internal reference number with v1.4 enhancements. ```javascript // Search by internal reference (3-50 characters) const ddsList = await retrievalClient.getDdsInfoByInternalReferenceNumber('DLE20/357'); // Returns: Array of matching DDS statements with rejection reasons and CA communication // { // httpStatus: 200, // status: 200, // ddsInfo: [ // { // identifier: 'uuid-string', // internalReferenceNumber: 'DLE20/357', // status: 'Accepted', // rejectionReason: null, // v1.4 feature // caCommunication: 'text', // v1.4 feature // // ... other DDS details // } // ] // } ``` **`getStatementByIdentifiers(referenceNumber, verificationNumber, options)` - CF7 v1.4** Retrieve complete DDS statement content including geolocation and activity data. ```javascript // Get full DDS statement const fullDds = await retrievalClient.getStatementByIdentifiers( '25NLSN6LX69730', 'K7R8LA90' ); // With decodeGeojson option to decode base64 geometryGeojson to plain string const fullDdsDecoded = await retrievalClient.getStatementByIdentifiers( '25NLSN6LX69730', 'K7R8LA90', { decodeGeojson: true // Decode base64 geometryGeojson to plain string } ); // Returns: Complete DDS object with v1.4 fields // { // httpStatus: 200, // status: 200, // ddsInfo: [ // { // // Full DDS content including: // geolocation: { /* geolocation data */ }, // activity: { /* activity details */ }, // commodities: [ /* complete commodity info */ ], // referencedStatements: [ // { // referenceNumber: '25NLWPAZWQ8865', // securityNumber: 'GLE9SMMM' // For supply chain traversal // } // ], // availabilityDate: '2024-01-15', // v1.4 feature // // ... complete DDS data // } // ] // } ``` **Options:** - `rawResponse` (boolean): Whether to return the raw XML response - `decodeGeojson` (boolean): Whether to decode base64 geometryGeojson to plain string (default: false) **Supply Chain Traversal (V2 Only)** ⚠️ **Note**: The `getReferencedDDS()` method is not available in V1. For supply chain traversal, use `EudrRetrievalClientV2`: ```javascript // V1 can get the referenced statement info from getStatementByIdentifiers const fullDds = await retrievalClient.getStatementByIdentifiers('25NLSN6LX69730', 'K7R8LA90'); const referencedStatements = fullDds.ddsInfo[0].referencedStatements; // For actual traversal, use V2: // const { EudrRetrievalClientV2 } = require('eudr-api-client'); // const v2Client = new EudrRetrievalClientV2(config); // const referencedDds = await v2Client.getReferencedDds(ref.referenceNumber, ref.securityNumber); ``` #### Error Handling The V1 Retrieval Client includes smart error handling that converts SOAP authentication faults to proper HTTP status codes: ```javascript try { const result = await retrievalClient.getDdsInfo('some-uuid'); console.log('Success:', result.ddsInfo); } catch (error) { if (error.details.status === 401) { console.error('Authentication failed:', error.message); // Handle invalid credentials } else if (error.details.status === 404) { console.error('DDS not found:', error.message); // Handle missing DDS (covers both EUDR-API-NO-DDS and EUDR-WEBSERVICE-STATEMENT-NOT-FOUND) } else if (error.details.status === 400) { console.error('Invalid verification number:', error.message); // Handle invalid verification number (EUDR-VERIFICATION-NUMBER-INVALID) } else if (error.details.status === 500) { console.error('Server error:', error.message); // Handle server issues } else { console.error('Network error:', error.message); // Handle network issues } } ``` #### Configuration Examples ```javascript // Production environment with SSL validation const productionClient = new EudrRetrievalClient({ username: process.env.EUDR_USERNAME, password: process.env.EUDR_PASSWORD, webServiceClientId: 'eudr-repository', // Production environment ssl: true, // Validate SSL certificates timeout: 30000 // 30 seconds timeout }); // Development environment with relaxed SSL const devClient = new EudrRetrievalClient({ username: process.env.EUDR_USERNAME, password: process.env.EUDR_PASSWORD, webServiceClientId: 'eudr-test', // Acceptance environment ssl: false, // Allow self-signed certificates timeout: 10000 // 10 seconds timeout }); // Manual endpoint override const customClient = new EudrRetrievalClient({ endpoint: 'https://custom-endpoint.com/ws/EUDRRetrievalServiceV1', username: 'user', password: 'pass', webServiceClientId: 'custom-client', ssl: false }); ``` --- ### πŸš€ EudrRetrievalClientV2 (V2) Advanced service for retrieving DDS information and supply chain data with enhanced features including supply chain traversal. #### Methods | Method | Description | CF Spec | Parameters | Returns | |--------|-------------|---------|------------|---------| | `getDdsInfo(uuids, options)` | Retrieve DDS info by UUID(s) | CF3 v1.4 | `uuids` (String or Array), `options` (Object) | Promise with DDS details | | `getDdsInfoByInternalReferenceNumber(internalReferenceNumber, options)` | Retrieve DDS by internal reference | CF3 v1.4 | `internalReferenceNumber` (String, 3-50 chars), `options` (Object) | Promise with DDS array | | `getStatementByIdentifiers(referenceNumber, verificationNumber, options)` | Get full DDS statement | CF7 v1.4 | `referenceNumber` (String), `verificationNumber` (String), `options` (Object) | Promise with complete DDS | | `getReferencedDds(referenceNumber, securityNumber, options)` | πŸš€ **V2 Only**: Supply chain traversal | CF7 v1.4 | `referenceNumber` (String), `securityNumber` (String), `options` (Object) | Promise with referenced DDS | #### Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `rawResponse` | boolean | false | Whether to return the raw XML response | | `decodeGeojson` | boolean | false | Whether to decode base64 geometryGeojson to plain string | #### Key Features - βœ… **All V1 Features**: CF3 v1.4 Support, CF7 v1.4 Support, Batch Retrieval, Smart Error Handling - βœ… **πŸš€ NEW: Supply Chain Traversal**: `getReferencedDds()` method for following DDS references - βœ… **Enhanced V2 Namespaces**: Updated SOAP namespaces for V2 compatibility - βœ… **Automatic Endpoint Generation**: V2-specific endpoint generation - βœ… **Consistent Response Format**: Includes both `httpStatus` and `status` fields for compatibility - βœ… **πŸš€ NEW: Consistent Array Fields**: `commodities`, `producers`, `speciesInfo`, `referenceNumber` always returned as arrays - βœ… **πŸš€ NEW: Business Rules Validation**: Comprehensive input validation with detailed error messages #### Detailed Method Reference **Basic Usage:** ```javascript const { EudrRetrievalClientV2 } = require('eudr-api-client'); // πŸš€ NEW: Automatic V2 endpoint generation const retrievalClientV2 = new EudrRetrievalClientV2({ username: process.env.EUDR_USERNAME, password: process.env.EUDR_PASSWORD, webServiceClientId: 'eudr-test', // Automatically generates V2 acceptance endpoint ssl: false // Development environment - allow self-signed certificates }); // All V1 methods work the same way in V2 const ddsInfo = await retrievalClientV2.getDdsInfo('some-uuid-string'); const ddsList = await retrievalClientV2.getDdsInfoByInternalReferenceNumber('DLE20/357'); const fullDds = await retrievalClientV2.getStatementByIdentifiers('25NLSN6LX69730', 'K7R8LA90'); // πŸš€ NEW V2-only feature: Supply chain traversal const referencedDds = await retrievalClientV2.getReferencedDds( '25NLWPAZWQ8865', 'XtZ7C6t3lFHnOhAqN9fw5w==:dRES/NzB0xL4nkf5nmRrb/5SMARFHoDK53PaCJFPNRA=' ); ``` **πŸš€ Business Rules Validation** The V2 Retrieval Service includes comprehensive input validation with detailed error messages for all methods: ```javascript try { const result = await retrievalClientV2.getStatementByIdentifiers('25HRW9IURY3412', 'COAASVYH'); console.log('Success:', result.ddsInfo); } catch (error) { if (error.errorType === 'BUSINESS_RULES_VALIDATION') { console.error('Validation Error:', error.message); // Handle validation errors with specific error messages: // - "Reference number too long" (if > 15 characters) // - "Reference number too short" (if < 8 characters) // - "Verification number must be exactly 8 characters" // - "Reference number must contain only uppercase letters and numbers" // - "Verification number must contain only uppercase letters and numbers" } } ``` **Validation Rules:** - **Reference Number**: 8-15 characters, uppercase letters and numbers only - **Verification Number**: Exactly 8 characters, uppercase letters and numbers only - **Internal Reference Number**: 3-50 characters (for `getDdsInfoByInternalReferenceNumber`) **πŸš€ Supply Chain Traversal - `getReferencedDds(referenceNumber, securityNumber, options)`** The key V2 enhancement allows you to traverse the supply chain by following referenced DDS statements: ```javascript // Step 1: Get a DDS statement that references other statements const mainDds = await retrievalClientV2.getStatementByIdentifiers( '25NLSN6LX69730', 'K7R8LA90' ); // Step 2: Extract referenced statements info const referencedStatements = mainDds.ddsInfo[0].referencedStatements; console.log('Found referenced statements:', referencedStatements); // [ // { // referenceNumber: '25NLWPAZWQ8865', // securityNumber: 'XtZ7C6t3lFHnOhAqN9fw5w==:dRES/NzB0xL4nkf5nmRrb/5SMARFHoDK53PaCJFPNRA=' // } // ] // Step 3: Follow the supply chain using getReferencedDds for (const ref of referencedStatements) { const referencedDds = await retrievalClientV2.getReferencedDds( ref.referenceNumber, ref.securityNumber, // Use securityNumber, not verificationNumber { decodeGeojson: true // Decode base64 geometryGeojson to plain string } ); console.log('Referenced DDS content:', referencedDds.ddsInfo); // Continue traversing if this DDS also has references if (referencedDds.ddsInfo[0].referencedStatements?.length > 0) { // Recursive traversal possible console.log('This DDS has further references...'); } } // Returns: Same format as other retrieval methods // { // httpStatus: 200, // status: 200, // status: 200, // πŸš€ NEW: Added for consistency // ddsInfo: [ // { // // Complete DDS content of the referenced statement // geolocation: { /* geolocation data */ }, // activity: { /* activity details */ }, // commodities: [ /* complete commodity info */ ], // // ... complete referenced DDS data // } // ], // raw: 'xml-response' // if rawResponse: true // } ``` **Complete Supply Chain Analysis Example:** ```javascript async function analyzeSupplyChain(referenceNumber, verificationNumber) { const chain = []; const visited = new Set(); // Start with the main statement let currentDds = await retrievalClientV2.getStatementByIdentifiers( referenceNumber, verificationNumber ); chain.push({ level: 0, referenceNumber: referenceNumber, dds: currentDds.ddsInfo[0] }); // Follow the chain const toProcess = currentDds.ddsInfo[0].referencedStatements?.map(ref => ({ ...ref, level: 1 })) || []; while (toProcess.length > 0) { const ref = toProcess.shift(); // Avoid circular references if (visited.has(ref.referenceNumber)) continue; visited.add(ref.referenceNumber); try { const referencedDds = await retrievalClientV2.getReferencedDds( ref.referenceNumber, ref.securityNumber ); chain.push({ level: ref.level, referenceNumber: ref.referenceNumber, dds: referencedDds.ddsInfo[0] }); // Add next level references const nextRefs = referencedDds.ddsInfo[0].referencedStatements?.map(nextRef => ({ ...nextRef, level: ref.level + 1 })) || []; toProcess.push(...nextRefs); } catch (error) { console.warn(`Failed to retrieve ${ref.referenceNumber}:`, error.message); } } return chain; } // Usage const supplyChain = await analyzeSupplyChain('25NLSN6LX69730', 'K7R8LA90'); console.log(`Supply chain has ${supplyChain.length} levels:`); supplyChain.forEach((item, index) => { console.log(`Level ${item.level}: ${item.referenceNumber} - ${item.dds.activity?.activityType}`); }); ``` #### Error Handling V2 includes the same smart error handling as V1, with enhanced error details: ```javascript try { const result = await retrievalClientV2.getReferencedDds(referenceNumber, securityNumber); console.log('Success:', result.ddsInfo); } catch (error) { // Same error handling as V1, with both httpStatus and status fields if (error.details.status === 401) { console.error('Authentication failed:', error.message); } else if (error.details.status === 404) { console.error('DDS not found:', error.message); // Handle missing DDS (covers both EUDR-API-NO-DDS and EUDR-WEBSERVICE-STATEMENT-NOT-FOUND) } else if (error.details.status === 400) { console.error('Invalid verification number:', error.message); // Handle invalid verification number (EUDR-VERIFICATION-NUMBER-INVALID) } else if (error.details.status === 500) { console.error('Server error:', error.message); } // error.details contains both httpStatus and status for compatibility } ``` #### Configuration Examples ```javascript // Production environment with V2 const productionV2Client = new EudrRetrievalClientV2({ username: process.env.EUDR_USERNAME, password: process.env.EUDR_PASSWORD, webServiceClientId: 'eudr-repository', // Production V2 environment ssl: true, // Validate SSL certificates timeout: 30000 }); // Development environment with V2 const devV2Client = new EudrRetrievalClientV2({ username: process.env.EUDR_USERNAME, password: process.env.EUDR_PASSWORD, webServiceClientId: 'eudr-test', // Acceptance V2 environment ssl: false, // Allow self-signed certificates timeout: 10000 }); // Manual V2 endpoint override const customV2Client = new EudrRetrievalClientV2({ endpoint: 'https://custom-endpoint.com/ws/EUDRRetrievalServiceV2', username: 'user', password: 'pass', webServiceClientId: 'custom-client', ssl: false }); ``` ### πŸš€ NEW: Flexible Array Fields The EUDR API Client now supports **maximum flexibility** for array properties. All fields that can contain multiple items according to the XSD schema can be provided as either: - **Single Object**: `{ property: { ... } }` - **Array of Objects**: `{ property: [{ ... }, { ... }] }` #### Supported Flexible Fields | Field | XSD maxOccurs | V1 Support | V2 Support | Description | |-------|---------------|------------|------------|-------------| | **`commodities`** | 200 | βœ… | βœ… | Commodity information | | **`producers`** | 1000 | βœ… | βœ… | Producer information | | **`speciesInfo`** | 500 | βœ… | βœ… | Species information | | **`associatedStatements`** | 2000 | βœ… | βœ… | Referenced DDS statements | | **`referenceNumber`** | 12 | βœ… | βœ… | Operator reference numbers | #### Examples **Single Object (Traditional):** ```javascript const request = { statement: { commodities: [{ speciesInfo: { // Single species object scientificName: 'Fagus sylvatica', commonName: 'European Beech' }, producers: { // Single producer object country: 'HR', name: 'Forest Company' } }], operator: { referenceNumber: { // Single reference object identifierType: 'eori', identifierValue: 'HR123456789' } } } }; ``` **Array Format (Multiple Items):** ```javascript const request = { statement: { commodities: [{ speciesInfo: [ // Array of species { scientificName: 'Fagus sylvatica', commonName: 'European Beech' }, { scientificName: 'Quercus robur', commonName: 'English Oak' } ], producers: [ // Array of producers { country: 'HR', name: 'Croatian Forest Company' }, { country: 'DE', name: 'German Wood Supplier' } ] }], operator: { referenceNumber: [ // Array of references { identifierType: 'eori', identifierValue: 'HR123456789' }, { identifierType: 'vat', identifierValue: 'HR12345678901' } ] }, associatedStatements: [ // Array of associated statements { referenceNumber: '25NLSN6LX69730', verificationNumber: 'K7R8LA90' }, { referenceNumber: '25NLWPAZWQ8865', verificationNumber: 'GLE9SMMM' } ] } }; ``` **Mixed Usage (Maximum Flexibility):** ```javascript const request = { statement: { commodities: [ // Array of commodities { speciesInfo: { // Single species in first commodity scientificName: 'Fagus sylvatica', commonName: 'European Beech' }, producers: [ // Multiple producers in first commodity { country: 'HR', name: 'Croatian Producer' }, { country: 'DE', name: 'German Producer' } ] }, { speciesInfo: [ // Multiple species in second commodity { scientificName: 'Quercus robur', commonName: 'English Oak' }, { scientificName: 'Pinus sylvestris', commonName: 'Scots Pine' } ], producers: { // Single producer in second commodity country: 'AT', name: 'Austrian Producer' } } ] } }; ``` #### Benefits - **πŸ”„ Backward Compatibility**: Existing code continues to work unchanged - **πŸ“ˆ Scalability**: Easy to add multiple items when needed - **🎯 Consistency**: Same pattern across all array fields - **⚑ Performance**: No overhead for single-item arrays - **πŸ›‘οΈ Type Safety**: Full TypeScript support for both formats ### Data Types #### DDS Statement Structure (V1) ```javascript { operatorType: 'TRADER' | 'OPERATOR', statement: { internalReferenceNumber: String, activityType: 'TRADE' | 'IMPORT' | 'EXPORT' | 'DOMESTIC', countryOfActivity: String, // ISO country code (e.g., 'HR', 'FR') borderCrossCountry: String, // ISO country code comment: String, commodities: [{ descriptors: { descriptionOfGoods: String, goodsMeasure: { netWeight?: Number, // in kg volume?: Number, // in mΒ³ } }, hsHeading: String, // HS code (e.g., '4401') speciesInfo: { scientificName: String, commonName: String }, producers: [{ country: String, name: