LoadViewer Integration - V1

V1 is your Current Version.
Manage your Api Keys.

V1 Details

Description
Initial release supporting data in CSV formats.
Release Date
2025-04-01
Status
Active
Upload Settings (ERP → LoadViewer)
details
Code Example
Available in 10 popular languages

Understanding data flow from your ERP to LoadViewer

General guidelines specific to this version of the API.

1. Data Format

LoadViewer expects data from your ERP in a structured CSV format with two special prefix columns:

  • Column 1: Section type — S (Shipment), B (Container), C (Cargo)
  • Column 2: Row type — H (Header), D (Detail)

Structure Example

csv data

S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra
S,D,SANDBOX-01,True,False,False,False
B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM
B,D,5867,2352,2393,27200,0,False,False,0
C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper
C,D,#00246B,700,500,300,91.5,300,0,0,True,1,0,0,0,0,0,0,,0,,,,0,,

Explanation

  • S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra: Shipment-level headers
  • S,D,SANDBOX-01,True,False,False,False: Shipment-level data rows
  • B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM: Container headers
  • B,D,5867,2352,2393,27200,0,False,False,0: Container data rows (can be multiple)
  • C,H,erpId,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,shipper,destination,skuNumber,description,po,color: Cargo headers
  • C,D,,700,500,300,1.5,300,0,0,True,1,0,0,0,0,0,0,,0,,0,,,,#00246B: Cargo data rows (can be multiple)

Expected Fields

We have used the FIXED, Mandatory, and Optional identifiers for the fields. Rules for data values in each of these fields are as follows.
For FIXED fields no other data is accepted. Match the exact upper and lower case. Do not include extra spaces or characters in any such FIXED fields.
For Mandatory fields, the field cannot be blank. Use 0 for numeric fields and meaningful text for string (character) mandatory fields.
For Optional fields, you may leave them blank for text fields and use 0 for numeric values.

Shipment (S) Section:
  • Shipment(S) Header(H) Row - Only one such row per CSV allowed.
    • S : FIXED. Always provide S here. Since this belongs to the Shipment Section.
    • H : FIXED. Always provide H here. Since this is a Header row of the Section.
    • reference : FIXED
    • hangingAllowed : FIXED
    • useMultipleContainers : FIXED
    • uniqueErpId : FIXED
    • suggestExtra : FIXED
  • Shipment(S) Details(D) Row - Only one such row per CSV allowed.
    • Section = S : FIXED. Always provide S here. Since this belongs to the Shipment Section.
    • Row Type = D : FIXED. Always provide D here. Since this is a Data row of the Section.
    • reference = SANDBOX-01 : Mandatory, String: MaxLength[50]. Provide a unique shipment/job reference. Values starting with SANDBOX are for testing purposes and are not chargeable.
    • hangingAllowed = True : Mandatory. Allowed values – True or False. This value tells LoadViewer whether placing any cargo over other cargo requires 100% base support (set False) or 70% base support is sufficient (set True).
    • useMultipleContainers = False : Mandatory. Allowed values – True or False. This is useful to control your coin usage for this shipment.
    • uniqueErpId = False (Default): Mandatory. Allowed values – True or False. When set to True, all cargoes within the request must have a unique value in the erpId field.
    • suggestExtra = False (Default): Mandatory. Allowed values – True or False. Set to True when you want the system to consider loading additional cargo into the containers beyond the initially specified quantities. Set to False otherwise.
Container (B) Section:
  • Container(B) Header(H) Row - Only one such row per CSV allowed.
    • B : FIXED. Always provide B here. Since this belongs to the Container (Bin for you to relate to the Abbreviation B) Section.
    • H : FIXED. Always provide H here. Since this is a Header row of the Section.
    • lengthMm : FIXED.
    • widthMm : FIXED.
    • heightMm : FIXED.
    • maxWeightKg : FIXED
    • backMarginMm : FIXED
    • isRefrigerated : FIXED
    • maxUseOne : FIXED
    • lclBreakEvenCBM : FIXED
  • Container(B) Details(D) Row - Multilple rows per CSV allowed.
    • Section = B : FIXED. Always provide B here. Since this belongs to the Container Section.
    • Row Type = D : FIXED. Always provide D here. Since this is a Data row of the Section.
    • lengthMm = 5867 : Mandatory, Number: Range [250-30000]. Decimals and commas not allowed. Internal Length (Depth) of the Container in Millimeters (mm).
    • widthMm = 2352 : Mandatory, Number: Range [250-3000]. Decimals and commas not allowed. Internal Width of the Container in Millimeters (mm).
    • heightMm = 2393 : Mandatory, Number: Range [250-3000]. Decimals and commas not allowed. Internal Height of the Container in Millimeters (mm).
    • maxWeightKg = 27200 : Mandatory, Decimal (up to three digits after the decimal). Range [0.001, 50000]. Commas not allowed.

      Maximum Weight that can be stuffed in the Container in Kg (Kilogram).

    • backMarginMm = 0 : Mandatory, Number: Range [0-200]. Decimals not allowed. Sometimes you may wish LoadViewer not to plan the area near the door of the Container.
    • isRefrigerated = False : Mandatory. Allowed values: True or False. Indicates whether the Container is Refree container or not.
    • maxUseOne = False (Default): Mandatory. Allowed values: True or False. Indicates whether this container type should be used at most once in the load plan.
    • lclBreakEvenCBM = 0 (Default): Mandatory, Number: Range [0.00 - 100.00]. Represents the CBM threshold below which maximizing orders for a near-empty container might be less practical than optimizing others and considering the remainder for LCL.
Cargo (C) Section:
  • Cargo(C) Header(H) Row - Only one such row per CSV allowed.
    • C : FIXED. Always provide C here. Since this belongs to the Cargo Section.
    • H : FIXED. Always provide H here. Since this is a Header row of the Section.
    • erpId : FIXED.
    • lengthMm, widthMm and heightMm : FIXED.
    • weightKg : FIXED.
    • cartons : FIXED.
    • additionalCartons : FIXED.
    • placement : FIXED.
    • verticalRotationAllowed : FIXED.
    • skuPerCarton : FIXED.
    • emptyPalletHeightMm : FIXED.
    • trayCapMm : FIXED.
    • trayHeightMm : FIXED.
    • marginLenghtMm, marginWidthMm and marginHeightMm : FIXED.
    • nameOfSet : FIXED.
    • cartonRatioInSet : FIXED.
    • shipper : FIXED.
    • destination : FIXED.
    • skuNumber : FIXED.
    • description : FIXED.
    • po : FIXED.
    • color : FIXED.
  • Cargo(C) Details(D) Row - Multilple rows per CSV allowed.
    • Section = C : FIXED. Always provide C here. Since this belongs to the Cargo Section.
    • Row Type = D : FIXED. Always provide D here. Since this is a Data row of the Section.
    • erpId = : Optional, String: MaxLength [40], It should correspond to a unique identifier in your ERP that you wish to receive back in the suggestion response.
    • lengthMm = 700, widthMm = 500 and heightMm = 300 : Mandatory, Number: Range [10–3000]. Decimals and commas are not allowed. External dimensions of the cargo in millimeters (mm): Length, Width, and Height respectively.
    • weightKg = 91.5 : Mandatory, Decimal (up to three digits after the decimal). Range [0.001–1000]. Commas are not allowed.

      Weight of the carton or pallet in kilograms (kg).

    • cartons = 300 : Mandatory, Number: Range [1–30000]. Decimals and commas are not allowed. Number of desired cartons (or pallets) you want to load into containers.
    • additionalCartons = 0 : Mandatory, Number: Range [0–30000]. Decimals and commas are not allowed. LoadViewer will try to place number of additional cargo (cartons or pallets) defined here in any remaining space that cannot be filled by the desired quantities specified in the cartons section.
    • placement = 0 : Mandatory, Number: Range [0–4]. Defines how the item should be vertically placed inside the container.

      This setting determines the vertical placement of the item, mainly for palletized cargo or goods with specific stacking requirements.

    • verticalRotationAllowed = True : Mandatory. Allowed values: True or False. Indicates whether the item can be rotated vertically during load planning.

      When vertical rotation is True, LoadViewer explores all six possible orientations to optimize space inside the container.
      💡 Image showing all six possible rotations :

    • skuPerCarton = 1 : Mandatory, Number: Range[1–10000]. Decimals and commas are not allowed. Represents how many units of the SKU are packed inside one carton or pallet.
    • emptyPalletHeightMm = 0 : Mandatory, Number: Range [90–200] or 0. Use 0 for cartons, and any value between 90 to 200 mm for empty pallet height (i.e., pallet feet only).
    • trayCapMm = 0 : Mandatory, Number: Range [0–200]. Represents the top cap height. This does not reserve any extra space above heightMm but adds a line near the top edge of the cargo for visual reference.
    • trayHeightMm = 0 : Mandatory, Number: Range [0–200]. Represents the tray height in mm. This does not reserve any extra space below heightMm but adds a line near the bottom edge of the cargo for visual reference.
    • marginLenghtMm = 0, marginWidthMm = 0, and marginHeightMm = 0 : Mandatory, Number: Range [0–20]. This can be used to allow LoadViewer to keep margins around your cargo up to 2 cm (20 mm).
    • nameOfSet : Optional, String: MaxLength [1], Range [B–Y]. You can define up to 24 sets per shipment. Values A and Z are reserved. Leave it blank if your cargo does not belong to any set.
    • cartonRatioInSet = 0 : Mandatory, Number: Range [0–100]. Works in conjunction with nameOfSet. When nameOfSet is defined, this must be greater than 0.
    • shipper : Optional, String: MaxLength [20]. You can define your department or division name for use in LoadViewer reports. LoadViewer uses this to group SKUs of the same shipper in a single container, if sizes and quantities allow. If multiple departments are involved in a PO, LoadViewer prioritizes departments with higher volume cargo to be placed deeper in the container.
    • destination = 0 : Mandatory, Number: Range [0–200]. Use 0 when all cargo is for a single destination. This indicates load/unload priority. Lower values are placed closer to the container door for earlier unloading. Higher values are placed deeper inside the container for last unloading. This improves cargo handling efficiency during transit.
    • skuNumber : Optional, String: MaxLength [40]. You can enter SKU number for LoadViewer reports.
    • description : Optional, String: MaxLength [60]. You can enter SKU description, or any meaningful value for LoadViewer reports.
    • po : Optional, String: MaxLength [20]. You can enter the Purchase Order number to appear on LoadViewer reports. LoadViewer uses this to group SKUs of the same PO in the same container, if sizes and quantities allow.
      💡 Practical Use Case – PO (Purchase Order):
    • color = #008000 : Mandatory, String: MaxLength [9], Hex color code for your cargo.

Notes:

  • Separator must be a comma (,)
  • All header and detail rows must strictly follow the S/B/C + H/D format.
  • All header rows are case sensitive and must strictly follow the same format as mentioned in the Headers sections above.
  • Always provide a S,H row followed by at least one S,D row
  • The order of sections can be any. But our example mostly reflects: Shipment → Container(s) → Cargo(s)

Tip: Download SANDBOX-01 and modify the sandbox CSV for accuracy and editing in your text editor / Excel / OpenOffice Calc or other editor of your choice.

2. Authentication

The V1 version uses API Keys for authenticating requests from your ERP system.

How it works:

  • Each user is issued a unique API Key and associated Email.
  • Use these credentials to obtain a JWT token using a secure POST request to our token endpoint.
Manage keys
API Key Manager

Business Email Domain Required
API keys are not available for accounts using free email domains (e.g., Gmail, Hotmail, Yahoo).
If you already have a business email, please log in with that account.
Or, update your email to a business address like your_name@yourcompany.com.

To get your token you need to send a POST request with ApiKey and Email to the token url. Refer to code sections below on how to get the token.
Token Url:
token url

POST https://www.loadviewer.com/api/auth/jwt/token

Request Headers:
header

Content-Type: application/json

Request Body:
body

{
  "email": "your.email@yourcompany.com",
  "apikey": "YOUR_API_KEY_HERE"
}

✅ Success response JSON example:
❌ Error response JSON example:

Key Rotation & Security:
  • Keep your API key secure. Never expose it in frontend code or public repositories.
  • You can regenerate (rotate) your API key at any time. Once rotated, the old key becomes invalid immediately.
  • Always use HTTPS to encrypt the API key and token during transmission.
  • Cache the token on the client side and reuse it for future requests. When the token expires, you'll receive:
    Code/Status = 410, Message = "Token has expired. Please call /api/auth/jwt/token to retrieve a new one."
  • Monitor usage and manage rate limits using the counters associated with your API key.

If a request is made without a valid key, the API will respond with HTTP 401 Unauthorized.


Example: Get Token in Different Languages
Change the YOUR_Email and YOUR_API_KEY_HERE variables in code sections with your actual email and the API Key generated in Api Key Manager.
vb6

' === Constants ===
Const LoadViewerTokenURL As String = "https://www.loadviewer.com/api/auth/jwt/token"
Const LoadViewerAPIEmail As String = "YOUR_Email"
Const LoadViewerAPIKey As String = "YOUR_API_KEY_HERE"

' === Get JWT Token ===
' Returns the token string to be passed in to Authorization Header for every call to 
' LoadViewer endpoints like https://www.loadviewer.com/api/integration/v1/upload
Function getJwtToken() As String
    On Error GoTo ErrHandler

    Dim http As Object
    Set http = CreateObject("WinHttp.WinHttpRequest.5.1")

    Dim jsonBody As String
    jsonBody = "{""email"":""" & LoadViewerAPIEmail & """,""apikey"":""" & LoadViewerAPIKey & """}"

    With http
        .SetTimeouts 3000, 3000, 5000, 5000 ' 3 sec connect/send, 5 sec receive/resolve
        .Open "POST", LoadViewerTokenURL, False
        .SetRequestHeader "Content-Type", "application/json"
        .Send jsonBody
    End With

    If http.Status = 200 Then
        getJwtToken = extractTokenFromResponse(http.ResponseText)
    Else
        getJwtToken = "ERROR: " & http.Status & " - " & http.ResponseText
    End If
    Exit Function

ErrHandler:
    getJwtToken = "Exception: " & Err.Description
End Function

' === Extract "token" field from JSON response ===
Function extractTokenFromResponse(jsonText As String) As String
    Dim tokenStart As Long, tokenEnd As Long
    tokenStart = InStr(jsonText, """token"":""")
    If tokenStart = 0 Then
        extractTokenFromResponse = ""
        Exit Function
    End If
    tokenStart = tokenStart + 9
    tokenEnd = InStr(tokenStart, jsonText, """")
    extractTokenFromResponse = Mid(jsonText, tokenStart, tokenEnd - tokenStart)
End Function

Python

import requests
import json

url = 'https://www.loadviewer.com/api/auth/jwt/token'

payload = {
    'email': 'YOUR_Email',
    'apikey': 'YOUR_API_KEY_HERE'
}

headers = {'Content-Type': 'application/json'}

response = requests.post(url, data=json.dumps(payload), headers=headers)

if response.status_code == 200:
    response_data = response.json()
    token = response_data['token']
    print(f'Token: {token}')
else:
    print(f'Error: {response.status_code} - {response.text}')

C#

using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;

class Program
{
    static async Task Main()
    {
        var url = "https://www.loadviewer.com/api/auth/jwt/token";
        var email = "YOUR_Email";
        var apiKey = "YOUR_API_KEY_HERE";

        var payload = new
        {
            email = email,
            apikey = apiKey
        };

        var json = System.Text.Json.JsonSerializer.Serialize(payload);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        using var client = new HttpClient();
        var response = await client.PostAsync(url, content);
        var responseBody = await response.Content.ReadAsStringAsync();

        if (response.IsSuccessStatusCode)
        {
            var token = JObject.Parse(responseBody)["token"]?.ToString();
            Console.WriteLine("Token: " + token);
        }
        else
        {
            Console.WriteLine($"Error: {(int)response.StatusCode} - {responseBody}");
        }
    }
}

JavaScript

// Using Fetch API
const url = 'https://www.loadviewer.com/api/auth/jwt/token';
const payload = {
  email: 'YOUR_Email',
  apikey: 'YOUR_API_KEY_HERE'
};

fetch(url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
  if (data.token) {
    console.log('Token:', data.token);
  } else {
    console.error('Authentication failed:', data);
  }
})
.catch(error => {
  console.error('Error:', error);
});

Java

import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Scanner;

public class AuthExample {
    public static void main(String[] args) {
        String tokenUrl = "https://www.loadviewer.com/api/auth/jwt/token";
        String email = "YOUR_Email";
        String apiKey = "YOUR_API_KEY_HERE";

        String jsonInputString = String.format("{\"email\":\"%s\",\"apikey\":\"%s\"}", email, apiKey);

        try {
            URL url = new URL(tokenUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type", "application/json");
            conn.setDoOutput(true);

            try (OutputStream os = conn.getOutputStream()) {
                byte[] input = jsonInputString.getBytes("utf-8");
                os.write(input, 0, input.length);
            }

            int status = conn.getResponseCode();
            Scanner scanner;

            if (status == 200) {
                scanner = new Scanner(conn.getInputStream(), "utf-8");
                String responseBody = scanner.useDelimiter("\\A").next();
                System.out.println("Token: " + responseBody);
            } else {
                scanner = new Scanner(conn.getErrorStream(), "utf-8");
                String errorResponse = scanner.useDelimiter("\\A").next();
                System.out.println("Error: " + status + " - " + errorResponse);
            }
            scanner.close();
            conn.disconnect();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

F#

open System
open System.Net.Http
open System.Text
open System.Threading.Tasks

let getTokenAsync () =
    async {
        use client = new HttpClient()
        let url = "https://www.loadviewer.com/api/auth/jwt/token"
        let email = "YOUR_Email"
        let apiKey = "YOUR_API_KEY_HERE"
        let payload = sprintf """{ "email": "%s", "apikey": "%s" }""" email apiKey
        let content = new StringContent(payload, Encoding.UTF8, "application/json")

        try
            let! response = client.PostAsync(url, content) |> Async.AwaitTask
            if response.IsSuccessStatusCode then
                let! body = response.Content.ReadAsStringAsync() |> Async.AwaitTask
                printfn "Token: %s" body
            else
                let! error = response.Content.ReadAsStringAsync() |> Async.AwaitTask
                printfn "Error: %d - %s" (int response.StatusCode) error
        with ex ->
            printfn "Exception: %s" ex.Message
    }

[<EntryPoint>]
let main _ =
    getTokenAsync () |> Async.RunSynchronously
    0

Go

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    url := "https://www.loadviewer.com/api/auth/jwt/token"
    payload := map[string]string{
        "email":  "YOUR_Email",
        "apikey": "YOUR_API_KEY_HERE",
    }

    jsonData, err := json.Marshal(payload)
    if err != nil {
        fmt.Println("Error encoding JSON:", err)
        return
    }

    req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
    if err != nil {
        fmt.Println("Error creating request:", err)
        return
    }
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Error making request:", err)
        return
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error reading response:", err)
        return
    }

    if resp.StatusCode == http.StatusOK {
        var result map[string]interface{}
        if err := json.Unmarshal(body, &result); err != nil {
            fmt.Println("Error decoding JSON:", err)
            return
        }
        fmt.Println("Token:", result["token"])
    } else {
        fmt.Printf("Error: %d - %s\n", resp.StatusCode, string(body))
    }
}

R

library(httr)
library(jsonlite)

url <- "https://www.loadviewer.com/api/auth/jwt/token"
payload <- list(
  email = "YOUR_Email",
  apikey = "YOUR_API_KEY_HERE"
)

response <- POST(
  url,
  add_headers(`Content-Type` = "application/json"),
  body = toJSON(payload, auto_unbox = TRUE)
)

if (status_code(response) == 200) {
  token <- content(response, as = "parsed")$token
  cat("Token:", token, "\n")
} else {
  cat("Error:", status_code(response), "-", content(response, as = "text"), "\n")
}

PHP

<?php
$tokenUrl = 'https://www.loadviewer.com/api/auth/jwt/token';
$email = 'YOUR_Email';
$apiKey = 'YOUR_API_KEY_HERE';

$data = json_encode(['email' => $email, 'apikey' => $apiKey]);

$ch = curl_init($tokenUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode === 200) {
    $responseData = json_decode($response, true);
    echo "Token: " . $responseData['token'];
} else {
    echo "Error: $httpCode - $response";
}
?>

Dart

import 'dart:convert';
import 'package:http/http.dart' as http;

void main() async {
  var url = Uri.parse('https://www.loadviewer.com/api/auth/jwt/token');
  var headers = {'Content-Type': 'application/json'};
  var body = jsonEncode({
    'email': 'YOUR_Email',
    'apikey': 'YOUR_API_KEY_HERE'
  });

  var response = await http.post(url, headers: headers, body: body);

  if (response.statusCode == 200) {
    var responseData = jsonDecode(response.body);
    print('Token: \${responseData['token']}');
  } else {
    print('Error: \${response.statusCode} - \${response.body}');
  }
}


Token Fetch Errors

These errors may occur while requesting a JWT token using your email and API key. They usually relate to key configuration or mismatch.

Status / code ResponseText / message Solution
400 Missing API Key The apiKey field is missing in the request body. Ensure both email and apiKey are provided.
401 Your domain is not Allowed Your email domain is not permitted. Please use a business email (not a free domain like gmail.com). Contact support if unsure.
401 API Key mismatch The API Key provided does not match our records. Double-check the key and try again.
403 API Key is not generated Generate an API Key first using the API Key Manager before making requests.
403 API Key is cleared, please regenerate Your existing API Key was cleared manually. Please regenerate it from the API Key Manager.
403 API Key is disabled The API Key has been disabled. Enable it again from the API Key Manager to proceed.
403 API Key is Expired The API Key has expired. Please generate a new one from the API Key Manager.

3. Error handling

API Error Handling Guide

Every API response includes a standard structure for handling errors: LoadViewer response messages are in JSON format. But if your application has limitied capabilities parsing JSON you may get the response in Plain text.a
To get the Plain Text responses please add Accept header in the http request headers.

Setting Plain Text Accept Header example:
✅ Success JSON format example:
❌ Error JSON format example:
✅ Success Plain Text example:
❌ Error Plain Text example:

Note: Always check the Status / code and ResponseText / message for handling logic on the ERP side. Refer to the suggested Solution below for each error.

1. Errors related to Upload of CSV data to LoadViewer

These errors occur due to missing headers, invalid, expired, or tampered JWT tokens.

Status / code ResponseText / message Solution
400 Missing 'email' in request body. For token request email and APIKey is not present in the request header.
400 Invalid JSON format. For token request email and APIKey is supplied in proper format in the request header.
400 One or more errors occurred while parsing the CSV. CSV file has errors in column names or data type or out of range etc. Specific details are available in errors section of the JSON.
400 Field 'reference' is missing in Shipment Row. Mandatory field for shipment is not supplied. Check your CSV file.
400 Shipment must contain at least one container and one cargo item. Your CSV data is missing the Row with 'C' in first column and 'H' in second column.
409 Shipment Number (Field 'reference') '[XXXXXX]' is used by other Shipment. Check your CSV file and change the Shipment Reference value to not previously used on LoadViewer.
401 Missing or invalid JWT token. Your request is missing a token or has an invalid format. Please obtain a valid token using your credentials.
401 Invalid token. Ensure you are sending a valid Bearer token. Your request is missing a token or has an invalid format. Please obtain a valid token using your credentials.
403 Your domain is not allowed. Your email domain is not allowed. LoadViewer expects a real domain other then gmail.com or hotmail.com etc. Please contact support for more details.
403 Insufficent permissions to perform Upload Action The user has no permission to the resource.
410 Token has expired. Please call /api/auth/jwt/token to retrieve a new one. The token has expired. Please re-authenticate to obtain a new token.
429 Too Many Requests You are exceeding the allowed request rate. Implement retry logic with exponential backoff.

2. Internal & System Errors

These are unexpected system-side issues that may occur temporarily due to backend failures.

Status / code ResponseText / message Solution
500 Internal Server Error Retry the request after a few seconds. If the issue persists, contact LoadViewer support with request details.
503 Service Unavailable The system is temporarily overloaded or under maintenance. Retry after a short delay.

4. Testing

🧪 API Testing Guide

Why Testing is Important

Testing ensures that:

  • Your ERP generates a valid file format
  • Communication with LoadViewer APIs is secure and successful
  • You avoid unnecessary coin consumption during testing.
  • Real-time errors are handled before going live

Your Two-Stage Testing Strategy
  1. Test the File Creation

    Use the manual file upload section in your dashboard to upload a CSV file created by your ERP or a text editor.

    • Make sure the reference does not start with SANDBOX in manual upload mode.
    • Manual uploads are not charged coins unless published.
    • If the upload is successful, your file structure is correct.
    • Repeat this until your ERP consistently generates acceptable files.
  2. Test the API Communication

    Use your tool of choice (Postman, Swagger, curl, script) to post the file to LoadViewer’s API endpoint.

    • Use reference that starts with SANDBOX.
    • No coins are consumed for such files. reference starting with SANDBOX can be provided in mixed case (Upper, Lower, camelCase or else).
    • File is accepted and validated but not published.
    • This lets you test end-to-end without impacting live shipments.

    E.g., SANDBOX-01 is a valid test reference for your dummy data.


Tools & Skills Required
Skill Description
Basic HTTP Understanding Headers, POST, GET, Status Codes
JSON/CSV Correct formatting for request body and payload
Tool usage Postman, curl, Swagger, or scripting languages

ERP Integration Testing
  • Ensure your ERP can generate shipment files in valid CSV format
  • Configure the ERP to send a POST request to:
    https://www.loadviewer.com/api/integration/v1/upload
  • Include ApiKey in the request headers
  • Use a Shipment reference starting with SANDBOX for test uploads
  • Enable logging of request payloads and API responses within the ERP
  • Test various combinations of SKUs and file sizes before going live

Test Code to Upload your CSV data

Note: The example uses integration version V1.
The examples below can be used in planning stage where you may be interested in knowning how many articals can be loaded in a single container.

vb6


' ========================================
' Module: modLoadViewerCSV
' Purpose:
'   1. Generate LoadViewer-compatible CSV from hardcoded or database values
'   2. Upload CSV to LoadViewer using HTTP POST (WinHttp)
' Includes helper functions:
'   - removeComma: for all string fields
'   - formatDecimal3: for weights with decimals
' ========================================

' === Constants ===
Const LoadViewerTokenURL As String = "https://www.loadviewer.com/api/auth/jwt/token"
Const LoadViewerUploadURL As String = "https://www.loadviewer.com/api/integration/v1/upload"
Const LoadViewerDownloadURL As String = "https://www.loadviewer.com/api/integration/v1/download"
Const LoadViewerAPIEmail As String = "YOUR_Email"
Const LoadViewerAPIKey As String = "YOUR_API_KEY_HERE"

' Optional: You can fetch the above values from file or DB:
'   - From INI file in App.Path
'   - From encrypted DB table
'   - From registry or environment variable
'

Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

' === Module level variable to cache the retrieved token ===
Public LoadViewerToken As String

' === Get JWT Token ===
Function getJwtToken() As String

    Dim retry As Integer
    Dim maxRetry As Integer
    Dim logMessage As String
    Dim http As Object
    Dim tokenURL As String
    Dim jsonBody As String
    Dim Wait As Long    ' milliseconds for subsequent retries
    Dim requestTimeout As Long ' milliseconds
    Dim rateLimitWait As Long ' milliseconds for 429

    maxRetry = 3
    retry = maxRetry
    Wait = 5000 ' 5 seconds for general retries
    rateLimitWait = 65000 ' 65 seconds for 429 (slightly over 1 minute)
    requestTimeout = 30000 ' 30 seconds timeout (adjust as needed)
    tokenURL = LoadViewerTokenURL
    jsonBody = "{""email"":""" & LoadViewerAPIEmail & """,""apikey"":""" & LoadViewerAPIKey & """}"

fetchFromServer:
    Set http = CreateObject("WinHttp.WinHttpRequest.5.1")

    On Error GoTo ErrHandler

    http.setTimeouts requestTimeout, requestTimeout, requestTimeout, requestTimeout

    With http
        .Open "POST", tokenURL, False
        .SetRequestHeader "Content-Type", "application/json"
        .Send jsonBody
    End With

    If http.status = 200 Then
        getJwtToken = extractTokenFromResponse(http.ResponseText)
        LoadViewerToken = getJwtToken
        Exit Function
    ElseIf http.status = 429 Then ' Rate limit encountered
        If retry > 0 Then
            retry = retry - 1
            Debug.Print "Rate limit hit on token endpoint. Retrying in " & (rateLimitWait / 1000) & " seconds... (Attempt " & (maxRetry - retry) & " of " & maxRetry & ")"
            Sleep rateLimitWait
            GoTo fetchFromServer
        Else
            getJwtToken = "ERROR: " & http.status & " - Rate limit exceeded on token retrieval." & " - " & http.ResponseText
            Exit Function
        End If
    ElseIf retry > 0 Then
        retry = retry - 1
        Debug.Print "Retrying token retrieval in " & (Wait / 1000) & " seconds... (Attempt " & (maxRetry - retry) & " of " & maxRetry & ")"
        Sleep Wait
        GoTo fetchFromServer
    End If
    getJwtToken = "ERROR: " & http.status & " - " & http.ResponseText
    Exit Function

ErrHandler:
    getJwtToken = "Exception: " & Err.Description
    Set http = Nothing
End Function

' === Upload CSV using Bearer Token ===
Function UploadTOLoadViewer(csvData As String, jwtToken As String) As String

    Dim retry As Integer
    Dim maxRetry As Integer
    Dim logMessage As String
    Dim http As Object
    Dim waitTime As Long ' milliseconds for general retries
    Dim rateLimitWait As Long ' milliseconds for 429
    Dim requestTimeout As Long ' milliseconds

    maxRetry = 3
    retry = maxRetry ' Set the maximum number of retries
    waitTime = 5000 ' 5 seconds in milliseconds
    rateLimitWait = 65000 ' 65 seconds for 429
    requestTimeout = 30000 ' 30 seconds timeout (adjust as needed)

UploadToServer:
    Set http = CreateObject("WinHttp.WinHttpRequest.5.1")

    On Error GoTo ErrHandler

    ' Set the timeout before opening the connection using positional arguments
    http.setTimeouts requestTimeout, requestTimeout, requestTimeout, requestTimeout

    With http
        .Open "POST", LoadViewerUploadURL, False
        .SetRequestHeader "Content-Type", "text/csv"
        .SetRequestHeader "Authorization", "Bearer " & jwtToken
        '.SetRequestHeader "Accept", "text/plain" ' Hint: Comment this line to get JSON format response
        .Send csvData
    End With

    If http.status = 200 Or http.status = 202 Then 'Success (200) or Accepted (202)
        UploadTOLoadViewer = http.status & " - " & http.ResponseText
        Exit Function ' Exit the function if successful
    ElseIf http.status = 429 Then ' Rate limit encountered
        If retry > 0 Then
            retry = retry - 1
            Debug.Print "Rate limit hit on upload endpoint. Retrying in " & (rateLimitWait / 1000) & " seconds... (Attempt " & (maxRetry - retry) & " of " & maxRetry & ")"
            Sleep rateLimitWait
            GoTo UploadToServer
        Else
            UploadTOLoadViewer = "ERROR: " & http.status & " - Rate limit exceeded on upload." & " - " & http.ResponseText
            Exit Function
        End If
    ElseIf http.status = 410 Then ' Token expired
        getJwtToken
        jwtToken = LoadViewerToken
        ' No need to wait, retry immediately after getting a new token
    ElseIf http.status <> 409 And retry > 0 Then ' 409 is duplicate shipment error
        ' Wait before retrying (except after getting a new token)
        retry = retry - 1
        Debug.Print "Retrying Upload in " & (waitTime / 1000) & " seconds... (Attempt " & (maxRetry - retry) & " of " & maxRetry & ")"
        Sleep waitTime  ' Wait 5 seconds
        GoTo UploadToServer
    End If

    ' If we reach here, all retries have failed
    UploadTOLoadViewer = "ERROR: " & http.status & " - " & http.ResponseText
    Exit Function ' Exit the function
ErrHandler:
    UploadTOLoadViewer = "Exception: " & Err.Description
    Set http = Nothing
End Function

' === Get LoadViewer Suggestions CSV ===
Function GetLoadViewerSuggestionsCSV(jwtToken As String, requestBody As String) As String

    Dim retry As Integer
    Dim maxRetries As Integer
    Dim logMessage As String
    Dim http As Object
    Dim waitTime As Long ' milliseconds for general retries
    Dim rateLimitWait As Long ' milliseconds for 429
    Dim requestTimeout As Long ' milliseconds

    maxRetries = 3 ' Set the maximum number of retries
    retry = maxRetries
    waitTime = 5000 ' 5 seconds in milliseconds
    rateLimitWait = 65000 ' 65 seconds for 429
    requestTimeout = 30000 ' 30 seconds timeout (adjust as needed)

TrySuggestionsFetch:
    Set http = CreateObject("WinHttp.WinHttpRequest.5.1")

    On Error GoTo ErrHandler

    ' Set the timeout before opening the connection using positional arguments
    http.setTimeouts requestTimeout, requestTimeout, requestTimeout, requestTimeout

    With http
        .Open "POST", LoadViewerDownloadURL, False ' Corrected URL variable name
        .SetRequestHeader "Content-Type", "application/json"
        .SetRequestHeader "Authorization", "Bearer " & jwtToken
        .Send requestBody

        logMessage = "POST Request to LoadViewer Suggestions URL: " & LoadViewerDownloadURL & vbCrLf ' Corrected URL variable name
        logMessage = logMessage & "Request Body: " & vbCrLf & requestBody & vbCrLf
        logMessage = logMessage & "HTTP Status Code: " & http.status & vbCrLf
        logMessage = logMessage & "Response Text: " & vbCrLf & http.ResponseText & vbCrLf
        Debug.Print logMessage

    End With

    If http.status = 200 Or http.status = 202 Then 'Success (200) or Accepted (202)
        GetLoadViewerSuggestionsCSV = http.ResponseText
        Exit Function ' Exit the function if successful
    ElseIf http.status = 429 Then ' Rate limit encountered
        If retry > 0 Then
            retry = retry - 1
            Debug.Print "Rate limit hit on suggestions endpoint. Retrying in " & (rateLimitWait / 1000) & " seconds... (Attempt " & (maxRetries - retry) & " of " & maxRetries & ")"
            Sleep rateLimitWait
            GoTo TrySuggestionsFetch
        Else
            GetLoadViewerSuggestionsCSV = "ERROR: " & http.status & " - Rate limit exceeded on suggestions retrieval." & " - " & http.ResponseText
            Exit Function
        End If
    ElseIf http.status = 410 Then ' Token expired
        getJwtToken
        jwtToken = LoadViewerToken
        ' No need to wait, retry immediately after getting a new token
    ElseIf retry > 0 Then
        retry = retry - 1
        Debug.Print "Retrying Suggestions Upload in " & (waitTime / 1000) & " seconds... (Attempt " & (maxRetries - retry) & " of " & maxRetries & ")"
        Sleep waitTime  ' Wait 5 seconds
        GoTo TrySuggestionsFetch
    End If


    ' If we reach here, all retries have failed
    GetLoadViewerSuggestionsCSV = "ERROR: " & http.status & " - " & http.ResponseText
    Exit Function ' Exit the function
ErrHandler:
    GetLoadViewerSuggestionsCSV = "Exception: " & Err.Description
    Set http = Nothing
End Function


' === Dynamically generates LoadViewer CSV from ADODB recordsets ===
'For csvGenerate() to work properly you may need to add Reference to
'Microsoft ActiveX Data Object 2.8 Library
'(C:\Program Files (x86)\Common Files\System\ado\msado28.tlb)
'OR any other version for ADODB.Recordset available to you.
Function csvGenerate() As String
    Dim csv As String
    Dim rsShipment As ADODB.Recordset
    Dim rsContainer As ADODB.Recordset
    Dim rsCargo As ADODB.Recordset

    Set rsShipment = New ADODB.Recordset
    Set rsContainer = New ADODB.Recordset
    Set rsCargo = New ADODB.Recordset

    ' --- Define Shipment fields ---
    rsShipment.Fields.Append "reference", adVarChar, 50
    rsShipment.Fields.Append "hangingAllowed", adBoolean
    rsShipment.Fields.Append "useMultipleContainers", adBoolean
    rsShipment.Fields.Append "uniqueErpId", adBoolean
    rsShipment.Fields.Append "suggestExtra", adBoolean

    rsShipment.Open
    rsShipment.AddNew
    rsShipment("reference") = "SANDBOX-01"
    rsShipment("hangingAllowed") = True
    rsShipment("useMultipleContainers") = False
    rsShipment("uniqueErpId") = False
    rsShipment("suggestExtra") = False
    rsShipment.Update

    ' --- Define Container fields ---
    rsContainer.Fields.Append "lengthMm", adInteger
    rsContainer.Fields.Append "widthMm", adInteger
    rsContainer.Fields.Append "heightMm", adInteger
    rsContainer.Fields.Append "maxWeightKg", adDouble
    rsContainer.Fields.Append "backMarginMm", adInteger
    rsContainer.Fields.Append "isRefrigerated", adBoolean
    rsContainer.Fields.Append "maxUseOne", adBoolean
    rsContainer.Fields.Append "lclBreakEvenCBM", adDouble

    rsContainer.Open
    rsContainer.AddNew
    rsContainer("lengthMm") = 5867
    rsContainer("widthMm") = 2352
    rsContainer("heightMm") = 2393
    rsContainer("maxWeightKg") = 27200
    rsContainer("backMarginMm") = 0
    rsContainer("isRefrigerated") = False
    rsContainer("maxUseOne") = False
    rsContainer("lclBreakEvenCBM") = 0
    rsContainer.Update

    ' --- Define Cargo fields ---
    rsCargo.Fields.Append "color", adVarChar, 10
    rsCargo.Fields.Append "lengthMm", adInteger
    rsCargo.Fields.Append "widthMm", adInteger
    rsCargo.Fields.Append "heightMm", adInteger
    rsCargo.Fields.Append "weightKg", adDouble
    rsCargo.Fields.Append "cartons", adInteger
    rsCargo.Fields.Append "additionalCartons", adInteger
    rsCargo.Fields.Append "placement", adInteger
    rsCargo.Fields.Append "verticalRotationAllowed", adBoolean
    rsCargo.Fields.Append "skuPerCarton", adInteger
    rsCargo.Fields.Append "emptyPalletHeightMm", adInteger
    rsCargo.Fields.Append "trayCapMm", adInteger
    rsCargo.Fields.Append "trayHeightMm", adInteger
    rsCargo.Fields.Append "marginLength", adInteger
    rsCargo.Fields.Append "marginWidth", adInteger
    rsCargo.Fields.Append "marginHeight", adInteger
    rsCargo.Fields.Append "nameOfSet", adVarChar, 1
    rsCargo.Fields.Append "cartonRatioInSet", adInteger
    rsCargo.Fields.Append "erpId", adVarChar, 40
    rsCargo.Fields.Append "skuNumber", adVarChar, 40
    rsCargo.Fields.Append "description", adVarChar, 60
    rsCargo.Fields.Append "destination", adInteger
    rsCargo.Fields.Append "po", adVarChar, 20
    rsCargo.Fields.Append "shipper", adVarChar, 20
    rsCargo.Open
    rsCargo.AddNew
    rsCargo("color") = "#008000"
    rsCargo("lengthMm") = 700
    rsCargo("widthMm") = 500
    rsCargo("heightMm") = 300
    rsCargo("weightKg") = 91.5 ' ? "91.5"
    rsCargo("cartons") = 300
    rsCargo("additionalCartons") = 0
    rsCargo("placement") = 0
    rsCargo("verticalRotationAllowed") = True
    rsCargo("skuPerCarton") = 1
    rsCargo("emptyPalletHeightMm") = 0
    rsCargo("trayCapMm") = 0
    rsCargo("trayHeightMm") = 0
    rsCargo("marginLength") = 0
    rsCargo("marginWidth") = 0
    rsCargo("marginHeight") = 0
    rsCargo("nameOfSet") = ""
    rsCargo("cartonRatioInSet") = 0
    rsCargo("erpId") = ""
    rsCargo("skuNumber") = ""
    rsCargo("description") = ""
    rsCargo("destination") = 0
    rsCargo("po") = ""
    rsCargo("shipper") = ""
    rsCargo.Update


    ' --- Begin CSV output ---
    csv = "S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra" & vbCrLf
    csv = csv & "S,D," & removeComma(rsShipment("reference")) & "," & rsShipment("hangingAllowed") & "," & rsShipment("useMultipleContainers") & "," & rsShipment("uniqueErpId") & "," & rsShipment("suggestExtra") & vbCrLf

    csv = csv & "B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM" & vbCrLf

    Do Until rsContainer.EOF
        csv = csv & "B,D," & rsContainer("lengthMm") & "," & rsContainer("widthMm") & "," & rsContainer("heightMm") & "," & formatDecimal3(rsContainer("maxWeightKg")) & "," & rsContainer("backMarginMm") & "," & rsContainer("isRefrigerated") & "," & rsContainer("maxUseOne") & "," & formatDecimal3(rsContainer("lclBreakEvenCBM")) & vbCrLf
        rsContainer.MoveNext
    Loop

    csv = csv & "C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper" & vbCrLf

    Do Until rsCargo.EOF
        csv = csv & "C,D," & removeComma(rsCargo("color")) & "," & rsCargo("lengthMm") & "," & rsCargo("widthMm") & "," & rsCargo("heightMm") & "," & formatDecimal3(rsCargo("weightKg")) & "," & rsCargo("cartons") & "," & rsCargo("additionalCartons") & "," & rsCargo("placement") & "," & rsCargo("verticalRotationAllowed") & "," & rsCargo("skuPerCarton") & "," & rsCargo("emptyPalletHeightMm") & "," & rsCargo("trayCapMm") & "," & rsCargo("trayHeightMm") & "," & rsCargo("marginLength") & "," & rsCargo("marginWidth") & "," & rsCargo("marginHeight") & "," & removeComma(rsCargo("nameOfSet")) & "," & rsCargo("cartonRatioInSet") & "," & removeComma(rsCargo("erpId")) & "," & removeComma(rsCargo("skuNumber")) & "," & removeComma(rsCargo("description")) & "," & rsCargo("destination") & "," & removeComma(rsCargo("po")) & "," & removeComma(rsCargo("shipper")) & vbCrLf
        rsCargo.MoveNext
    Loop

    csvGenerate = csv
End Function

' === Generate CSV string for SANDBOX-01 ===
Function csvCreate() As String
    Dim csv As String
    csv = "S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra" & vbCrLf
    csv = csv & "S,D,SANDBOX-01,True,False,False,False" & vbCrLf
    csv = csv & "B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM" & vbCrLf
    csv = csv & "B,D,5867,2352,2393,27200,0,False,False,0" & vbCrLf
    csv = csv & "C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper" & vbCrLf
    csv = csv & "C,D,#008000,700,500,300,91.5,300,0,0,True,1,0,0,0,0,0,0,,0,,,,0,," & vbCrLf
    csvCreate = csv
End Function

' === Extract "token" field from JSON response ===
Function extractTokenFromResponse(jsonText As String) As String
    Dim tokenStart As Long, tokenEnd As Long
    tokenStart = InStr(jsonText, """token"":""")
    If tokenStart = 0 Then
        extractTokenFromResponse = ""
        Exit Function
    End If
    tokenStart = tokenStart + 9
    tokenEnd = InStr(tokenStart, jsonText, """")
    extractTokenFromResponse = Mid(jsonText, tokenStart, tokenEnd - tokenStart)
End Function

' === Cleans text for CSV: removes commas and replaces line breaks ===
' === Use this for all adVarChar (string) fields before writing to CSV ===
'
' Replaces:
'   ,   ?   (removed)
'   vbCrLf, vbCr, vbLf ? " " (space)
Function removeComma(value As Variant) As String
    If IsNull(value) Then
        removeComma = ""
        Exit Function
    End If

    Dim s As String
    s = Trim(CStr(value))

    s = Replace(s, ",", "")                          ' Remove commas
    s = Replace(s, vbCrLf, " ")                      ' Replace Windows line breaks with space
    s = Replace(s, vbCr, " ")                        ' Replace carriage return
    s = Replace(s, vbLf, " ")                        ' Replace line feed

    removeComma = s
End Function

' === Converts numeric value to string with up to 3 decimal places ===
' === Trailing zeros are removed. Use only for Weight-related fields ===
'
' Examples:
'   Convert 91.5       ? "91.5"
'   Convert 91.56789   ? "91.568"
'   Keep 52            ? "52"   (no .000 added)
Function formatDecimal3(value As Variant) As String
    If IsNull(value) Or Not IsNumeric(value) Then
        formatDecimal3 = "0"
        Exit Function
    End If

    Dim s As String
    s = Trim(CStr(value))

    If InStr(s, ".") > 0 Then
        ' Format with 3 decimals, remove trailing zeroes
        formatDecimal3 = Trim(Str(Val(FormatNumber(value, 3, , , vbFalse))))
    Else
        ' Keep as integer string
        formatDecimal3 = Trim(Str(Val(value)))
    End If
End Function

' === OPTIONAL TEST CALL ===
' Make calls Examples
'    testUpload '->SaveToDisk=No, CSVFromADO=No
'    testUpload True '->SaveToDisk=Yes, CSVFromADO=No
'    testUpload False, True '->SaveToDisk=No, CSVFromADO=Yes
'    testUpload True, True '->SaveToDisk=Yes, CSVFromADO=Yes
'===============================================================
Sub testUpload(Optional saveToDisk As Boolean = False, Optional useADORecordset As Boolean = False)
    Dim csv As String
    If (useADORecordset = False) Then
        csv = csvCreate() '--Using Hard Coded values
    Else
        csv = csvGenerate() '-->Using ADO
    End If

    If saveToDisk Then
        filePath = App.Path & "\SANDBOX-VB6.csv"
        Dim fso As Object
        Set fso = CreateObject("Scripting.FileSystemObject")
        Dim fileOut As Object
        Set fileOut = fso.CreateTextFile(filePath, True)
        fileOut.Write csv
        fileOut.Close
    End If

    Dim token As String
    If LoadViewerToken = "" Then
        token = Trim(getJwtToken())
    Else
        token = LoadViewerToken
    End If
    If token = "" Or Left(token, 5) = "ERROR" Or Left(token, 9) = "Exception" Then
        MsgBox "Token Error or Server Not Reachable: " & vbCrLf & token, vbCritical
        Exit Sub
    End If
    Dim response As String
    response = UploadTOLoadViewer(csv, token)
    MsgBox response
End Sub

' Requires Microsoft WinHTTP Services, version 5.1 (or later) to be referenced
' Requires Microsoft VBScript Regular Expressions 5.5 to be referenced (for email validation)
Sub testSuggestions(shipmentReference As String, Optional solverEmail As String = "solver@loadviewer.com")
    If Trim(shipmentReference) = "" Then
        MsgBox "Shipment Reference is required!", vbCritical
        Exit Sub
    End If

    If Trim(solverEmail) <> "" And Not IsValidEmail(solverEmail) Then
        MsgBox "Invalid Solver Email format!", vbCritical
        Exit Sub
    End If

    Dim token As String
    If LoadViewerToken = "" Then
        token = Trim(getJwtToken())
    Else
        token = LoadViewerToken
    End If

    If token = "" Or Left(token, 5) = "ERROR" Or Left(token, 9) = "Exception" Then
        MsgBox "Token Error or Server Not Reachable: " & vbCrLf & token, vbCritical
        Exit Sub
    End If


    ' --- Create the JSON request body ---
    Dim jsonBody As String
    jsonBody = "{"
    jsonBody = jsonBody & """reference"":""" & Replace(shipmentReference, """", """""") & """"
    jsonBody = jsonBody & ",""solverEmail"":""" & Replace(solverEmail, """", """""") & """"
    jsonBody = jsonBody & "}"

    ' --- Call the GetLoadViewerSuggestionsCSV function with POST ---
    Dim strResponse As String
    strResponse = GetLoadViewerSuggestionsCSV(token, jsonBody)
    If Left(strResponse, 5) = "ERROR" Or Left(strResponse, 9) = "Exception" Then
        MsgBox "Error retrieving data from LoadViewer: " & vbCrLf & strResponse, vbCritical
        Exit Sub
    End If
    MsgBox strResponse, vbInformation

    Dim arrLines As Variant
    Dim results As Object ' Dictionary to hold status and recommendations
    Dim statusInfo As Object ' Dictionary for status details
    Dim recommendations As Collection ' Collection for recommendation data
    Dim i As Long
    Dim arrFields As Variant

    Set results = CreateObject("Scripting.Dictionary")
    Set statusInfo = CreateObject("Scripting.Dictionary")
    Set recommendations = New Collection
    results.Add "status", statusInfo
    results.Add "recommendations", recommendations

    arrLines = Split(strResponse, vbCrLf)

    ' Please adjust the UBound check and the index access (e.g., arrFields(1) to arrFields(0))
    ' according to your project's Option Explicit settings and whether you are using 0-based or 1-based indexing
    If UBound(arrLines) > 0 Then
        For i = LBound(arrLines) To UBound(arrLines)
            arrFields = Split(arrLines(i), ",")
            If UBound(arrFields) >= 1 Then
                Dim recordType As String
                recordType = Trim(arrFields(0))

                Select Case recordType
                    Case "S" ' Status Record
                        If UBound(arrFields) = 8 Then
                            ' Assuming Option Explicit is NOT used, UBound might be 8 for 9 elements (indices 0 to 7).
                            ' Please adjust the UBound check (e.g., to 9) and the index access (e.g., arrFields(1) to arrFields(0))
                            ' according to your project's Option Explicit settings and whether you are using 0-based or 1-based indexing
                            If LCase(Trim(arrFields(1))) = "h" Then
                                ' Recommendation Header - Optional processing
                            ElseIf LCase(Trim(arrFields(1))) = "d" Then
                                statusInfo("status") = Trim(arrFields(2))
                                statusInfo("message") = Trim(arrFields(3))
                                statusInfo("reference") = Trim(arrFields(4))
                                statusInfo("solverEmail") = Trim(arrFields(5))
                                statusInfo("multipleContainers") = Trim(arrFields(6))
                                statusInfo("suggestExtra") = Trim(arrFields(7))
                            End If
                        Else
                            Debug.Print "Invalid Status record format: " & arrLines(i)
                        End If

                    Case "R" ' Recommendation Record
                        If UBound(arrFields) = 15 Then
                            ' Assuming Option Explicit is NOT used, UBound might be 15 for 16 elements (indices 0 to 15).
                            ' Please adjust the UBound check (e.g., to 16) and the index access (e.g., arrFields(1) to arrFields(0))
                            ' according to your project's Option Explicit settings and whether you are using 0-based or 1-based indexing
                            If LCase(Trim(arrFields(1))) = "h" Then
                                ' Recommendation Header - Optional processing
                            ElseIf LCase(Trim(arrFields(1))) = "d" Then
                                Dim recommendation As Object
                                Set recommendation = CreateObject("Scripting.Dictionary")
                                recommendation("size") = Trim(arrFields(2))
                                recommendation("cbm") = Trim(arrFields(3))
                                recommendation("weight") = Trim(arrFields(4))
                                recommendation("noOfPackages") = CLng(Trim(arrFields(5)))
                                recommendation("isPacked") = Trim(arrFields(6))
                                recommendation("packedPackages") = CLng(Trim(arrFields(7)))
                                recommendation("suggestedIncrease") = CLng(Trim(arrFields(8)))
                                recommendation("suggestedDecrease") = CLng(arrFields(9))
                                recommendation("erpId") = Trim(arrFields(10))
                                recommendation("skuNumber") = Trim(arrFields(11))
                                recommendation("description") = Trim(arrFields(12))
                                recommendation("po") = Trim(arrFields(13))
                                recommendation("shipper") = Trim(arrFields(14))
                                recommendations.Add recommendation
                            ElseIf Trim(arrFields(1)) = "" Then
                                'Handle empty row
                            End If
                        Else
                            Debug.Print "Invalid Recommendation record format: " & arrLines(i)
                        End If

                    Case Else
                        If Trim(arrLines(i)) <> "" Then
                            Debug.Print "Unknown record type: " & recordType & " in line: " & arrLines(i)
                        End If
                End Select
            End If
        Next i
    Else
        Debug.Print "Empty suggestions response."
    End If

    If Not results Is Nothing Then
        Dim status As Object
        Set status = results("status")
        Debug.Print "Status: " & status("status") & " - " & status("message")
        Debug.Print "Reference: " & status("reference")
        Debug.Print "Solver Email: " & status("solverEmail")
        Debug.Print "Multiple Containers: " & status("multipleContainers")
        Debug.Print "Extra Suggested: " & status("suggestExtra")

        'Dim recommendations As Collection
        Set recommendations = results("recommendations")
        If recommendations.Count > 0 Then
            Debug.Print "--- Suggestions ---"
            Dim eachRecommendation As Object
            For Each eachRecommendation In recommendations
                Debug.Print "Is Packed: " & eachRecommendation("isPacked") & ", Size LxWxH: " & eachRecommendation("size") & ", CBM: " & eachRecommendation("cbm") & ", Weight Kg: " & eachRecommendation("weight") & ", Packages: " & eachRecommendation("noOfPackages") & ", Packed: " & eachRecommendation("packedPackages") & ", Increase: " & eachRecommendation("suggestedIncrease") & ", Decrease: " & eachRecommendation("suggestedDecrease") & ", ErpId: " & eachRecommendation("erpId") & ", SkuNumber: " & eachRecommendation("skuNumber") & ", Description: " & eachRecommendation("description") & ", PO#: " & eachRecommendation("po") & ", Division: " & eachRecommendation("shipper")
                ' --- Your ERP Processing Logic for Suggestions Here ---
            Next eachRecommendation
        Else
            Debug.Print "No suggestions found for reference: " & shipmentReference
        End If
    Else
        Debug.Print "Error retrieving or parsing suggestions for reference: " & shipmentReference
    End If

End Sub

' === Helper function for Email Validation ===
Function IsValidEmail(email As String) As Boolean
    Dim objRegExp As Object
    Set objRegExp = CreateObject("VBScript.RegExp")
    objRegExp.Pattern = "^[\w\.-]+@[\w\.-]+\.\w+$"
    IsValidEmail = objRegExp.Test(email)
    Set objRegExp = Nothing
End Function

' === OPTIONAL TEST CALL ===
Sub testLoadViewerIntegration_POST()
    Dim token As String
    If LoadViewerToken = "" Then
        token = Trim(getJwtToken())
    Else
        token = LoadViewerToken
    End If

    If token = "" Or Left(token, 5) = "ERROR" Or Left(token, 9) = "Exception" Then
        MsgBox "Token Error or Server Not Reachable: " & vbCrLf & token, vbCritical
        Exit Sub
    End If

    ' --- Test Get Suggestions with POST ---
    Dim shipmentReference As String
    shipmentReference = "CP_NFL_Aug2025_EXT_30" ' Replace with a valid reference
    Dim solverEmail As String
    solverEmail = "test@example.com" ' Replace with a test email

    testSuggestions shipmentReference, solverEmail

End Sub

python

import http.client
import json
import time
import logging
import re  # For email validation
from typing import Optional, Dict, List  # For type hinting

# === Constants ===
LOADVIEWER_TOKEN_URL = "https://www.loadviewer.com/api/auth/jwt/token"
LOADVIEWER_UPLOAD_URL = "https://www.loadviewer.com/api/integration/v1/upload"
LOADVIEWER_DOWNLOAD_URL = "https://www.loadviewer.com/api/integration/v1/download"
LOADVIEWER_API_EMAIL = "YOUR_Email"  # Replace with your actual email
LOADVIEWER_API_KEY = "YOUR_API_KEY_HERE"  # Replace with your actual API key

# === Module level variable to cache the retrieved token ===
loadviewer_token: Optional[str] = None

# Configure logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# --- General Retry and Rate Limit Configuration (can be adjusted locally) ---
DEFAULT_MAX_RETRIES = 3
DEFAULT_RATE_LIMIT_WAIT = 65  # seconds

def get_jwt_token() -> Optional[str]:
    """
    Retrieves a JWT token from the LoadViewer API, handling rate limiting.
    """
    global loadviewer_token
    retry = 0
    max_retry = DEFAULT_MAX_RETRIES
    rate_limit_retry_count = 0
    max_rate_limit_retries = DEFAULT_MAX_RETRIES
    rate_limit_wait = DEFAULT_RATE_LIMIT_WAIT
    request_timeout = 30  # seconds

    token_url = LOADVIEWER_TOKEN_URL
    json_body = json.dumps({"email": LOADVIEWER_API_EMAIL, "apikey": LOADVIEWER_API_KEY})

    while retry <= max_retry:
        try:
            conn = http.client.HTTPSConnection(token_url.split("//")[1], timeout=request_timeout)
            headers = {'Content-Type': 'application/json'}
            conn.request("POST", token_url, body=json_body, headers=headers)
            response = conn.getresponse()
            response_data = response.read().decode('utf-8')

            if response.status == 200:
                token = extract_token_from_response(response_data)
                loadviewer_token = token  # Cache the token
                conn.close()
                return token
            elif response.status == 429:
                if rate_limit_retry_count < max_rate_limit_retries:
                    logger.warning(f"Rate limit hit on token endpoint (attempt {rate_limit_retry_count + 1} of {max_rate_limit_retries}). Waiting {rate_limit_wait} seconds.")
                    time.sleep(rate_limit_wait)
                    rate_limit_retry_count += 1
                    continue
                else:
                    conn.close()
                    return f"ERROR: {response.status} - {response_data} (Max rate limit retries reached)"
            elif retry < max_retry:
                retry += 1
                logger.warning(f"Retrying token retrieval due to error (attempt {retry + 1} of {max_retry}). Waiting 5 seconds.")
                time.sleep(5)
            else:
                conn.close()
                return f"ERROR: {response.status} - {response_data} (Max general retries reached)"

        except Exception as e:
            return f"Exception: {e}"
        finally:
            if 'conn' in locals():
                conn.close()
    return None

def upload_to_loadviewer(csv_data: str, jwt_token: str) -> str:
    """
    Uploads CSV data to LoadViewer using a Bearer token, handling rate limiting.
    """
    retry = 0
    max_retry = DEFAULT_MAX_RETRIES
    rate_limit_retry_count = 0
    max_rate_limit_retries = DEFAULT_MAX_RETRIES
    rate_limit_wait = DEFAULT_RATE_LIMIT_WAIT
    request_timeout = 30  # seconds
    upload_url = LOADVIEWER_UPLOAD_URL

    while retry <= max_retry:
        try:
            conn = http.client.HTTPSConnection(upload_url.split("//")[1], timeout=request_timeout)
            headers = {
                'Content-Type': 'text/csv',
                'Authorization': f'Bearer {jwt_token}'
            }
            conn.request("POST", upload_url, body=csv_data, headers=headers)
            response = conn.getresponse()
            response_data = response.read().decode('utf-8')

            if response.status in (200, 202):  # Success (200) or Accepted (202)
                conn.close()
                return f"{response.status} - {response_data}"
            elif response.status == 410:  # Token expired
                jwt_token = get_jwt_token()
                if jwt_token is None or jwt_token.startswith("ERROR") or jwt_token.startswith("Exception"):
                    conn.close()
                    return jwt_token
                continue  # Retry immediately after getting a new token
            elif response.status == 429:
                if rate_limit_retry_count < max_rate_limit_retries:
                    logger.warning(f"Rate limit hit on upload endpoint (attempt {rate_limit_retry_count + 1} of {max_rate_limit_retries}). Waiting {rate_limit_wait} seconds.")
                    time.sleep(rate_limit_wait)
                    rate_limit_retry_count += 1
                    continue
                else:
                    conn.close()
                    return f"ERROR: {response.status} - {response_data} (Max rate limit retries reached)"
            elif response.status != 409 and retry < max_retry:  # 409 is duplicate shipment error
                retry += 1
                logger.warning(f"Retrying Upload due to error (attempt {retry + 1} of {max_retry}). Waiting 5 seconds.")
                time.sleep(5)
                continue
            else:
                conn.close()
                return f"ERROR: {response.status} - {response_data} (Max general retries reached)"

        except Exception as e:
            return f"Exception: {e}"
        finally:
            if 'conn' in locals():
                conn.close()
    return "ERROR: Upload Failed After all retries or due to persistent rate limiting"

def get_load_viewer_suggestions_csv(jwt_token: str, request_body: str) -> str:
    """
    Retrieves CSV data containing suggestions from LoadViewer, handling rate limiting.
    """
    retry = 0
    max_retries = DEFAULT_MAX_RETRIES
    rate_limit_retry_count = 0
    max_rate_limit_retries = DEFAULT_MAX_RETRIES
    rate_limit_wait = DEFAULT_RATE_LIMIT_WAIT
    request_timeout = 30
    download_url = LOADVIEWER_DOWNLOAD_URL

    while retry <= max_retries:
        try:
            conn = http.client.HTTPSConnection(download_url.split("//")[1], timeout=request_timeout)
            headers = {
                "Content-Type": "application/json",
                "Authorization": f"Bearer {jwt_token}"
            }
            conn.request("POST", download_url, body=request_body, headers=headers)
            response = conn.getresponse()
            response_data = response.read().decode('utf-8')

            log_message = (
                f"POST Request to LoadViewer Suggestions URL: {download_url}\n"
                f"Request Body:\n{request_body}\n"
                f"HTTP Status Code: {response.status}\n"
                f"Response Text:\n{response_data}\n"
            )
            logger.debug(log_message)

            if response.status in (200, 202):
                conn.close()
                return response_data
            elif response.status == 410:  # Token expired
                jwt_token = get_jwt_token()
                if jwt_token is None or jwt_token.startswith("ERROR") or jwt_token.startswith("Exception"):
                    conn.close()
                    return jwt_token
                continue
            elif response.status == 429:
                if rate_limit_retry_count < max_rate_limit_retries:
                    logger.warning(f"Rate limit hit on suggestions endpoint (attempt {rate_limit_retry_count + 1} of {max_rate_limit_retries}). Waiting {rate_limit_wait} seconds.")
                    time.sleep(rate_limit_wait)
                    rate_limit_retry_count += 1
                    continue
                else:
                    conn.close()
                    return f"ERROR: {response.status} - {response_data} (Max rate limit retries reached)"
            elif retry < max_retries:
                retry += 1
                logger.warning(f"Retrying Suggestions Upload due to error (attempt {retry + 1} of {max_retries}). Waiting 5 seconds.")
                time.sleep(5)
                continue
            else:
                conn.close()
                return f"ERROR: {response.status} - {response_data} (Max general retries reached)"

        except Exception as e:
            return f"Exception: {e}"
        finally:
            if 'conn' in locals():
                conn.close()
    return "ERROR: Get Suggestions Failed After all retries or due to persistent rate limiting"

def csv_generate() -> str:
    """
    Dynamically generates LoadViewer CSV.
    """

    rs_shipment = [
        {"reference": "SANDBOX-01", "hangingAllowed": True, "useMultipleContainers": False,
         "uniqueErpId": False, "suggestExtra": False}
    ]

    rs_container = [
        {"lengthMm": 5867, "widthMm": 2352, "heightMm": 2393, "maxWeightKg": 27200,
         "backMarginMm": 0, "isRefrigerated": False, "maxUseOne": False, "lclBreakEvenCBM": 0}
    ]

    rs_cargo = [
        {"color": "#008000", "lengthMm": 700, "widthMm": 500, "heightMm": 300,
         "weightKg": 91.5, "cartons": 300, "additionalCartons": 0, "placement": 0,
         "verticalRotationAllowed": True, "skuPerCarton": 1, "emptyPalletHeightMm": 0,
         "trayCapMm": 0, "trayHeightMm": 0, "marginLength": 0, "marginWidth": 0,
         "marginHeight": 0, "nameOfSet": "", "cartonRatioInSet": 0, "erpId": "",
         "skuNumber": "", "description": "", "destination": 0, "po": "", "shipper": ""}
    ]

    csv_lines = [
        "S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra",
        f"S,D,{remove_comma(rs_shipment[0]['reference'])},{rs_shipment[0]['hangingAllowed']},{rs_shipment[0]['useMultipleContainers']},{rs_shipment[0]['uniqueErpId']},{rs_shipment[0]['suggestExtra']}",
        "B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM"
    ]

    for row in rs_container:
        csv_lines.append(
            f"B,D,{row['lengthMm']},{row['widthMm']},{row['heightMm']},{format_decimal3(row['maxWeightKg'])},{row['backMarginMm']},{row['isRefrigerated']},{row['maxUseOne']},{format_decimal3(row['lclBreakEvenCBM'])}"
        )

    csv_lines.append(
        "C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper")
    for row in rs_cargo:
        csv_lines.append(
            f"C,D,{remove_comma(row['color'])},{row['lengthMm']},{row['widthMm']},{row['heightMm']},{format_decimal3(row['weightKg'])},{row['cartons']},{row['additionalCartons']},{row['placement']},{row['verticalRotationAllowed']},{row['skuPerCarton']},{row['emptyPalletHeightMm']},{row['trayCapMm']},{row['trayHeightMm']},{row['marginLength']},{row['marginWidth']},{row['marginHeight']},{remove_comma(row['nameOfSet'])},{row['cartonRatioInSet']},{remove_comma(row['erpId'])},{remove_comma(row['skuNumber'])},{remove_comma(row['description'])},{row['destination']},{remove_comma(row['po'])},{remove_comma(row['shipper'])}"
        )
    return "\r\n".join(csv_lines)


def csv_create() -> str:
    """
    Generates a simple CSV string.
    """
    csv_content = (
        "S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra\r\n"
        "S,D,SANDBOX-01,True,False,False,False\r\n"
        "B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM\r\n"
        "B,D,5867,2352,2393,27200,0,False,False,0\r\n"
        "C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper\r\n"
        "C,D,#008000,700,500,300,91.5,300,0,0,True,1,0,0,0,0,0,0,,0,,,,0,,\r\n"
    )
    return csv_content


def extract_token_from_response(json_text: str) -> Optional[str]:
    """
    Extracts the 'token' field from a JSON response.
    """
    try:
        json_data = json.loads(json_text)
        return json_data.get('token')
    except json.JSONDecodeError as e:
        logger.error(f"JSONDecodeError: {e}")
        return None


def remove_comma(value: Optional[str]) -> str:
    """
    Cleans text for CSV: removes commas and replaces line breaks.
    """
    if value is None:
        return ""
    s = value.strip()
    s = s.replace(",", "")
    s = s.replace("\r\n", " ")
    s = s.replace("\r", " ")
    s = s.replace("\n", " ")
    return s


def format_decimal3(value: float) -> str:
    """
    Converts a numeric value to a string with up to 3 decimal places.
    """
    return f"{value:.3f}".rstrip('0').rstrip('.')


def test_upload(save_to_disk: bool = False, use_ado_recordset: bool = False) -> None:
    """
    Optional test function to upload CSV data.
    """
    if not use_ado_recordset:
        csv_data = csv_create()
    else:
        csv_data = csv_generate()

    if save_to_disk:
        file_path = "SANDBOX-Python.csv"
        with open(file_path, "w") as f:
            f.write(csv_data)

    token = loadviewer_token or get_jwt_token()  # Use cached token or get a new one
    if token is None or token.startswith("ERROR") or token.startswith("Exception"):
        print(f"Token Error or Server Not Reachable: {token}")
        return

    response = upload_to_loadviewer(csv_data, token)
    print(response)


def is_valid_email(email: str) -> bool:
    """
    Helper function for Email Validation.
    """
    return bool(re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", email))

def test_suggestions(shipment_reference: str, solver_email: str = "solver@loadviewer.com") -> None:
    """
    Tests the LoadViewer suggestions API.
    """
    if not shipment_reference:
        print("Shipment Reference is required!")
        return

    if solver_email and not is_valid_email(solver_email):
        print("Invalid Solver Email format!")
        return

    token = loadviewer_token or get_jwt_token()
    if token is None or token.startswith("ERROR") or token.startswith("Exception"):
        print(f"Token Error or Server Not Reachable: {token}")
        return

    # --- Create the JSON request body ---
    json_body = json.dumps({
        "reference": shipment_reference.replace("\"", "\"\""),  # Escape double quotes
        "solverEmail": solver_email.replace("\"", "\"\"")
    })

    # --- Call the GetLoadViewerSuggestionsCSV function with POST ---
    str_response = get_load_viewer_suggestions_csv(token, json_body)
    if str_response.startswith("ERROR") or str_response.startswith("Exception"):
        print(f"Error retrieving data from LoadViewer: {str_response}")
        return
    print(str_response)

    arr_lines = str_response.splitlines()
    results: Dict[str, object] = {}
    status_info: Dict[str, str] = {}
    recommendations: List[Dict[str, object]] = []

    results["status"] = status_info
    results["recommendations"] = recommendations

    if len(arr_lines) > 0:
        for line in arr_lines:
            arr_fields = line.split(',')
            if len(arr_fields) >= 1:
                record_type = arr_fields[0].strip()

                if record_type == "S":  # Status Record
                    if len(arr_fields) == 9 and arr_fields[1].strip().lower() == "d":
                        status_info["status"] = arr_fields[2].strip()
                        status_info["message"] = arr_fields[3].strip()
                        status_info["reference"] = arr_fields[4].strip()
                        status_info["solverEmail"] = arr_fields[5].strip()
                        status_info["multipleContainers"] = arr_fields[6].strip()
                        status_info["suggestExtra"] = arr_fields[7].strip()
                    elif len(arr_fields) == 9 and arr_fields[1].strip().lower() == "h":
                        pass
                    else:
                        print("Invalid Status record format: " + line)


                elif record_type == "R":  # Recommendation Record
                    if len(arr_fields) == 16 and arr_fields[1].strip().lower() == "d":
                        recommendation: Dict[str, object] = {}
                        recommendation["size"] = arr_fields[2].strip()
                        recommendation["cbm"] = arr_fields[3].strip()
                        recommendation["weight"] = arr_fields[4].strip()
                        recommendation["noOfPackages"] = int(arr_fields[5].strip())
                        recommendation["isPacked"] = arr_fields[6].strip()
                        recommendation["packedPackages"] = int(arr_fields[7].strip())
                        recommendation["suggestedIncrease"] = int(arr_fields[8].strip())
                        recommendation["suggestedDecrease"] = int(arr_fields[9].strip())
                        recommendation["erpId"] = arr_fields[10].strip()
                        recommendation["skuNumber"] = arr_fields[11].strip()
                        recommendation["description"] = arr_fields[12].strip()
                        recommendation["po"] = arr_fields[13].strip()
                        recommendation["shipper"] = arr_fields[14].strip()
                        recommendations.append(recommendation)
                    elif len(arr_fields) == 16 and arr_fields[1].strip().lower() == "h":
                        pass
                    else:
                        print("Invalid Recommendation record format: " + line)
                else:
                    if line.strip() != "":
                        print("Unknown record type: " + record_type + " in line: " + line)

    else:
        print("Empty suggestions response.")

    if results:
        status = results["status"]
        print("Status: " + status["status"] + " - " + status["message"])
        print("Reference: " + status["reference"])
        print("Solver Email: " + status["solverEmail"])
        print("Multiple Containers: " + status["multipleContainers"])
        print("Extra Suggested: " + status["suggestExtra"])

        recs = results["recommendations"]
        if len(recs) > 0:
            print("--- Suggestions ---")
            for recommendation in recs:
                print(
                    f"Is Packed: {recommendation['isPacked']}, Size LxWxH: {recommendation['size']}, CBM: {recommendation['cbm']}, Weight Kg: {recommendation['weight']}, Packages: {recommendation['noOfPackages']}, Packed: {recommendation['packedPackages']}, Increase: {recommendation['suggestedIncrease']}, Decrease: {recommendation['suggestedDecrease']}, ErpId: {recommendation['erpId']}, SkuNumber: {recommendation['skuNumber']}, Description: {recommendation['description']}, PO#: {recommendation['po']}, Division: {recommendation['shipper']}"
                )
                # --- Your ERP Processing Logic for Suggestions Here ---
        else:
            print("No suggestions found for reference: " + shipment_reference)
    else:
        print("Error retrieving or parsing suggestions for reference: " + shipment_reference)

def test_loadviewer_integration_post() -> None:
    """
    Tests the LoadViewer integration (POST requests).
    """
    token = loadviewer_token or get_jwt_token()
    if token is None or token.startswith("ERROR") or token.startswith("Exception"):
        print(f"Token Error or Server Not Reachable: {token}")
        return

    # --- Test Get Suggestions with POST ---
    shipment_reference = "CP_NFL_Aug2025_EXT_30"  # Replace with a valid reference
    solver_email = "test@example.com"  # Replace with a test email

    test_suggestions(shipment_reference, solver_email)

if __name__ == "__main__":
    #test_upload(save_to_disk=True, use_ado_recordset=True)
    test_loadviewer_integration_post()

csharp


using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.Extensions.Logging;

public class LoadViewerIntegration
{
    private readonly string _tokenUrl = "https://www.loadviewer.com/api/auth/jwt/token";
    private readonly string _uploadUrl = "https://www.loadviewer.com/api/integration/v1/upload";
    private readonly string _downloadUrl = "https://www.loadviewer.com/api/integration/v1/download";
    private readonly string _apiEmail = "YOUR_Email"; // Replace with your actual email
    private readonly string _apiKey = "YOUR_API_KEY_HERE"; // Replace with your actual API key
    private string _jwtToken;
    private readonly ILogger _logger;

    private const int DefaultMaxRetries = 3;
    private const int DefaultRateLimitWaitSeconds = 65;
    private const int GeneralRetryDelaySeconds = 5;

    public LoadViewerIntegration(ILogger logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    private bool IsValidEmail(string email)
    {
        // Basic email validation regex
        return System.Text.RegularExpressions.Regex.IsMatch(email, @"^[^\s@]+@[^\s@]+\.[^\s@]+$");
    }

    private async Task GetJwtTokenWithRetries()
    {
        int retryCount = 0;
    GetToken:
        try
        {
            using (var httpClient = new HttpClient())
            {
                var requestBody = new { email = _apiEmail, apikey = _apiKey };
                var jsonBody = JsonSerializer.Serialize(requestBody);
                var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");

                var response = await httpClient.PostAsync(_tokenUrl, content);
                response.EnsureSuccessStatusCode();

                var responseContent = await response.Content.ReadAsStringAsync();
                using (var jsonDocument = JsonDocument.Parse(responseContent))
                {
                    if (jsonDocument.RootElement.TryGetProperty("token", out var tokenElement))
                    {
                        _jwtToken = tokenElement.GetString();
                        return _jwtToken;
                    }
                    else
                    {
                        _logger.LogError($"Failed to extract token from response: {responseContent}");
                        return null;
                    }
                }
            }
        }
        catch (HttpRequestException ex)
        {
            if (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests && retryCount < DefaultMaxRetries)
            {
                retryCount++;
                _logger.LogWarning($"Rate limit hit on token endpoint (attempt {retryCount} of {DefaultMaxRetries}). Waiting {DefaultRateLimitWaitSeconds} seconds before retrying.");
                await Task.Delay(TimeSpan.FromSeconds(DefaultRateLimitWaitSeconds));
                goto GetToken;
            }
            else if (retryCount < DefaultMaxRetries)
            {
                retryCount++;
                _logger.LogError($"Error retrieving token (attempt {retryCount} of {DefaultMaxRetries}): {ex.Message}. Retrying in {GeneralRetryDelaySeconds} seconds.");
                await Task.Delay(TimeSpan.FromSeconds(GeneralRetryDelaySeconds));
                goto GetToken;
            }
            else
            {
                _logger.LogError($"Failed to retrieve token after {DefaultMaxRetries} retries: {ex.Message}");
                return null;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError($"An unexpected error occurred while retrieving token: {ex.Message}");
            return null;
        }
    }

    public async Task UploadToLoadViewerWithRetries(string csvData)
    {
        if (string.IsNullOrEmpty(_jwtToken))
        {
            _jwtToken = await GetJwtTokenWithRetries();
            if (string.IsNullOrEmpty(_jwtToken))
            {
                return "Error: Unable to retrieve JWT token for upload.";
            }
        }

        int retryCount = 0;
    UploadData:
        try
        {
            using (var httpClient = new HttpClient())
            {
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
                var content = new StringContent(csvData, Encoding.UTF8, "text/csv");
                var response = await httpClient.PostAsync(_uploadUrl, content);

                if (response.IsSuccessStatusCode)
                {
                    return $"{response.StatusCode} - {await response.Content.ReadAsStringAsync()}";
                }
                else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Gone)
                {
                    _logger.LogWarning("JWT token expired or invalid. Attempting to retrieve a new token.");
                    _jwtToken = await GetJwtTokenWithRetries();
                    if (string.IsNullOrEmpty(_jwtToken))
                    {
                        return "Error: Unable to retrieve a new JWT token after token expiration.";
                    }
                    goto UploadData;
                }
                else if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests && retryCount < DefaultMaxRetries)
                {
                    retryCount++;
                    _logger.LogWarning($"Rate limit hit on upload endpoint (attempt {retryCount} of {DefaultMaxRetries}). Waiting {DefaultRateLimitWaitSeconds} seconds before retrying.");
                    await Task.Delay(TimeSpan.FromSeconds(DefaultRateLimitWaitSeconds));
                    goto UploadData;
                }
                else if (response.StatusCode != System.Net.HttpStatusCode.Conflict && retryCount < DefaultMaxRetries)
                {
                    retryCount++;
                    _logger.LogError($"Error uploading data (attempt {retryCount} of {DefaultMaxRetries}): {response.StatusCode} - {await response.Content.ReadAsStringAsync()}. Retrying in {GeneralRetryDelaySeconds} seconds.");
                    await Task.Delay(TimeSpan.FromSeconds(GeneralRetryDelaySeconds));
                    goto UploadData;
                }
                else
                {
                    return $"Error: Upload failed after {retryCount + 1} attempts. Status: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}";
                }
            }
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError($"An HTTP error occurred during upload: {ex.Message}");
            return $"Exception: {ex.Message}";
        }
        catch (Exception ex)
        {
            _logger.LogError($"An unexpected error occurred during upload: {ex.Message}");
            return $"Exception: {ex.Message}";
        }
    }

    public async Task GetLoadViewerSuggestionsCsvWithRetries(string requestBody)
    {
        if (string.IsNullOrEmpty(_jwtToken))
        {
            _jwtToken = await GetJwtTokenWithRetries();
            if (string.IsNullOrEmpty(_jwtToken))
            {
                return "Error: Unable to retrieve JWT token for getting suggestions.";
            }
        }

        int retryCount = 0;
    GetSuggestions:
        try
        {
            using (var httpClient = new HttpClient())
            {
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
                var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
                var response = await httpClient.PostAsync(_downloadUrl, content);

                _logger.LogDebug($"POST Request to LoadViewer Suggestions URL: {_downloadUrl}\nRequest Body:\n{requestBody}\nHTTP Status Code: {response.StatusCode}\nResponse Text:\n{await response.Content.ReadAsStringAsync()}");

                if (response.IsSuccessStatusCode)
                {
                    return await response.Content.ReadAsStringAsync();
                }
                else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Gone)
                {
                    _logger.LogWarning("JWT token expired or invalid. Attempting to retrieve a new token.");
                    _jwtToken = await GetJwtTokenWithRetries();
                    if (string.IsNullOrEmpty(_jwtToken))
                    {
                        return "Error: Unable to retrieve a new JWT token after token expiration.";
                    }
                    goto GetSuggestions;
                }
                else if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests && retryCount < DefaultMaxRetries)
                {
                    retryCount++;
                    _logger.LogWarning($"Rate limit hit on suggestions endpoint (attempt {retryCount} of {DefaultMaxRetries}). Waiting {DefaultRateLimitWaitSeconds} seconds before retrying.");
                    await Task.Delay(TimeSpan.FromSeconds(DefaultRateLimitWaitSeconds));
                    goto GetSuggestions;
                }
                else if (retryCount < DefaultMaxRetries)
                {
                    retryCount++;
                    _logger.LogError($"Error retrieving suggestions (attempt {retryCount} of {DefaultMaxRetries}): {response.StatusCode} - {await response.Content.ReadAsStringAsync()}. Retrying in {GeneralRetryDelaySeconds} seconds.");
                    await Task.Delay(TimeSpan.FromSeconds(GeneralRetryDelaySeconds));
                    goto GetSuggestions;
                }
                else
                {
                    return $"Error: Failed to retrieve suggestions after {retryCount + 1} attempts. Status: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}";
                }
            }
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError($"An HTTP error occurred while retrieving suggestions: {ex.Message}");
            return $"Exception: {ex.Message}";
        }
        catch (Exception ex)
        {
            _logger.LogError($"An unexpected error occurred while retrieving suggestions: {ex.Message}");
            return $"Exception: {ex.Message}";
        }
    }

    public async Task TestSuggestions(string shipmentReference, string solverEmail = "solver@loadviewer.com")
    {
        if (string.IsNullOrEmpty(shipmentReference))
        {
            _logger.LogError("Shipment Reference is required!");
            return;
        }

        if (!string.IsNullOrEmpty(solverEmail) && !IsValidEmail(solverEmail))
        {
            _logger.LogError("Invalid Solver Email format!");
            return;
        }

        string token = _jwtToken ?? await GetJwtTokenWithRetries();
        if (string.IsNullOrEmpty(token))
        {
            _logger.LogError("Token Error or Server Not Reachable.");
            return;
        }

        var jsonBody = JsonSerializer.Serialize(new
        {
            reference = shipmentReference.Replace("\"", "\"\""),
            solverEmail = solverEmail.Replace("\"", "\"\"")
        });

        string strResponse = await GetLoadViewerSuggestionsCsvWithRetries(jsonBody);
        if (strResponse.StartsWith("Error") || strResponse.StartsWith("Exception"))
        {
            _logger.LogError($"Error retrieving data from LoadViewer: {strResponse}");
            return;
        }
        _logger.LogInformation($"Raw Suggestions Response:\n{strResponse}");

        var arrLines = strResponse.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
        var results = new Dictionary();
        var statusInfo = new Dictionary();
        var recommendations = new List>();

        results["status"] = statusInfo;
        results["recommendations"] = recommendations;

        if (arrLines.Length > 0)
        {
            foreach (var line in arrLines)
            {
                var arrFields = line.Split(',');
                if (arrFields.Length >= 1)
                {
                    var recordType = arrFields[0].Trim();

                    if (recordType == "S") // Status Record
                    {
                        if (arrFields.Length == 9 && arrFields[1].Trim().ToLowerInvariant() == "d")
                        {
                            statusInfo["status"] = arrFields[2].Trim();
                            statusInfo["message"] = arrFields[3].Trim();
                            statusInfo["reference"] = arrFields[4].Trim();
                            statusInfo["solverEmail"] = arrFields[5].Trim();
                            statusInfo["multipleContainers"] = arrFields[6].Trim();
                            statusInfo["suggestExtra"] = arrFields[7].Trim();
                        }
                        else if (arrFields.Length == 9 && arrFields[1].Trim().ToLowerInvariant() == "h")
                        {
                            // Header row, can be ignored
                        }
                        else
                        {
                            _logger.Warn($"Invalid Status record format: {line}");
                        }
                    }
                    else if (recordType == "R") // Recommendation Record
                    {
                        if (arrFields.Length == 16 && arrFields[1].Trim().ToLowerInvariant() == "d")
                        {
                            var recommendation = new Dictionary
                            {
                                ["size"] = arrFields[2].Trim(),
                                ["cbm"] = arrFields[3].Trim(),
                                ["weight"] = arrFields[4].Trim(),
                                ["noOfPackages"] = int.Parse(arrFields[5].Trim()),
                                ["isPacked"] = arrFields[6].Trim(),
                                ["packedPackages"] = int.Parse(arrFields[7].Trim()),
                                ["suggestedIncrease"] = int.Parse(arrFields[8].Trim()),
                                ["suggestedDecrease"] = int.Parse(arrFields[9].Trim()),
                                ["erpId"] = arrFields[10].Trim(),
                                ["skuNumber"] = arrFields[11].Trim(),
                                ["description"] = arrFields[12].Trim(),
                                ["po"] = arrFields[13].Trim(),
                                ["shipper"] = arrFields[14].Trim()
                            };
                            recommendations.Add(recommendation);
                        }
                        else if (arrFields.Length == 16 && arrFields[1].Trim().ToLowerInvariant() == "h")
                        {
                            // Header row, can be ignored
                        }
                        else
                        {
                            _logger.Warn($"Invalid Recommendation record format: {line}");
                        }
                    }
                    else if (!string.IsNullOrWhiteSpace(recordType))
                    {
                        _logger.Warn($"Unknown record type: {recordType} in line: {line}");
                    }
                }
            }
        }
        else
        {
            _logger.LogInformation("Empty suggestions response.");
        }

        if (results != null && results.ContainsKey("status"))
        {
            var status = (Dictionary)results["status"];
            _logger.LogInformation($"Status: {status.GetValueOrDefault("status")} - {status.GetValueOrDefault("message")}");
            _logger.LogInformation($"Reference: {status.GetValueOrDefault("reference")}");
            _logger.LogInformation($"Solver Email: {status.GetValueOrDefault("solverEmail")}");
            _logger.LogInformation($"Multiple Containers: {status.GetValueOrDefault("multipleContainers")}");
            _logger.LogInformation($"Extra Suggested: {status.GetValueOrDefault("suggestExtra")}");

            if (results.ContainsKey("recommendations") && results["recommendations"] is List> recs && recs.Count > 0)
            {
                _logger.LogInformation("--- Suggestions ---");
                foreach (var recommendation in recs)
                {
                    _logger.LogInformation(
                        $"Is Packed: {recommendation.GetValueOrDefault("isPacked")}, Size LxWxH: {recommendation.GetValueOrDefault("size")}, CBM: {recommendation.GetValueOrDefault("cbm")}, Weight Kg: {recommendation.GetValueOrDefault("weight")}, Packages: {recommendation.GetValueOrDefault("noOfPackages")}, Packed: {recommendation.GetValueOrDefault("packedPackages")}, Increase: {recommendation.GetValueOrDefault("suggestedIncrease")}, Decrease: {recommendation.GetValueOrDefault("suggestedDecrease")}, ErpId: {recommendation.GetValueOrDefault("erpId")}, SkuNumber: {recommendation.GetValueOrDefault("skuNumber")}, Description: {recommendation.GetValueOrDefault("description")}, PO#: {recommendation.GetValueOrDefault("po")}, Division: {recommendation.GetValueOrDefault("shipper")}"
                    );
                    // --- Your ERP Processing Logic for Suggestions Here ---
                }
            }
            else
            {
                _logger.LogInformation($"No suggestions found for reference: {shipmentReference}");
            }
        }
        else
        {
            _logger.LogError($"Error retrieving or parsing suggestions for reference: {shipmentReference}");
        }
    }


    // Example usage (you'll need to inject ILogger in your application)
    // public static async Task Main(string[] args)
    // {
    //     using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
    //     var logger = loggerFactory.CreateLogger();
    //     var integration = new LoadViewerIntegration(logger);
    //
    //     // Example of uploading data
    //     // string csvData = GenerateLoadViewerCsv();
    //     // string uploadResult = await integration.UploadToLoadViewerWithRetries(csvData);
    //     // Console.WriteLine($"Upload Result: {uploadResult}");
    //
    //     // Example of getting suggestions
    //     await integration.TestSuggestions("SANDBOX-01", "test@example.com");
    // }

    private static string RemoveComma(string value)
    {
        return value?.Replace(",", "").Replace(Environment.NewLine, " ") ?? "";
    }

    private static string FormatDecimal3(decimal value)
    {
        return value.ToString("F3").TrimEnd('0').TrimEnd('.');
    }

    private static string GenerateLoadViewerCsv()
    {
        var rs_shipment = new[]
        {
            new { reference = "SANDBOX-01", hangingAllowed = true, useMultipleContainers = false, uniqueErpId = false, suggestExtra = false }
        };

        var rs_container = new[]
        {
            new { lengthMm = 5867, widthMm = 2352, heightMm = 2393, maxWeightKg = 27200m, backMarginMm = 0, isRefrigerated = false, maxUseOne = false, lclBreakEvenCBM = 0m }
        };

        var rs_cargo = new[]
        {
            new { color = "#008000", lengthMm = 700, widthMm = 500, heightMm = 300, weightKg = 91.5m, cartons = 300, additionalCartons = 0, placement = 0, verticalRotationAllowed = true, skuPerCarton = 1, emptyPalletHeightMm = 0, trayCapMm = 0, trayHeightMm = 0, marginLength = 0, marginWidth = 0, marginHeight = 0, nameOfSet = "", cartonRatioInSet = 0, erpId = "", skuNumber = "", description = "", destination = 0, po = "", shipper = "" }
        };

        var csvLines = new List
        {
            "S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra",
            $"S,D,{RemoveComma(rs_shipment[0].reference)},{rs_shipment[0].hangingAllowed},{rs_shipment[0].useMultipleContainers},{rs_shipment[0].uniqueErpId},{rs_shipment[0].suggestExtra}",
            "B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM"
        };

        foreach (var row in rs_container)
        {
            csvLines.Add($"B,D,{row.lengthMm},{row.widthMm},{row.heightMm},{FormatDecimal3(row.maxWeightKg)},{row.backMarginMm},{row.isRefrigerated},{row.maxUseOne},{FormatDecimal3(row.lclBreakEvenCBM)}");
        }

        csvLines.Add("C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper");
        foreach (var row in rs_cargo)
        {
            csvLines.Add($"C,D,{RemoveComma(row.color)},{row.lengthMm},{row.widthMm},{row.heightMm},{FormatDecimal3(row.weightKg)},{row.cartons},{row.additionalCartons},{row.placement},{row.verticalRotationAllowed},{row.skuPerCarton},{row.emptyPalletHeightMm},{row.trayCapMm},{row.trayHeightMm},{row.marginLength},{row.marginWidth},{row.marginHeight},{RemoveComma(row.nameOfSet)},{row.cartonRatioInSet},{RemoveComma(row.erpId)},{RemoveComma(row.skuNumber)},{RemoveComma(row.description)},{row.destination},{RemoveComma(row.po)},{RemoveComma(row.shipper)}");
        }
        return string.Join(Environment.NewLine, csvLines);
    }
}


javascript


// --- Configuration ---
const LOADVIEWER_TOKEN_URL = 'https://www.loadviewer.com/api/auth/jwt/token';
const LOADVIEWER_UPLOAD_URL = 'https://www.loadviewer.com/api/integration/v1/upload';
const LOADVIEWER_DOWNLOAD_URL = 'https://www.loadviewer.com/api/integration/v1/download';
const LOADVIEWER_API_EMAIL = 'YOUR_Email'; // Replace with your actual email
const LOADVIEWER_API_KEY = 'YOUR_API_KEY_HERE'; // Replace with your actual API key

const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_RATE_LIMIT_WAIT_MS = 65000; // 65 seconds in milliseconds
const GENERAL_RETRY_DELAY_MS = 5000; // 5 seconds

let cachedToken = null;

function isValidEmail(email) {
    // Basic email validation regex
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

async function getJwtTokenWithRetries(retries = 0) {
    if (cachedToken) {
        return cachedToken;
    }

    try {
        const response = await fetch(LOADVIEWER_TOKEN_URL, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ email: LOADVIEWER_API_EMAIL, apikey: LOADVIEWER_API_KEY }),
        });

        if (!response.ok) {
            if (response.status === 429 && retries < DEFAULT_MAX_RETRIES) {
                console.warn(`Rate limit hit on token endpoint (attempt ${retries + 1} of ${DEFAULT_MAX_RETRIES}). Waiting ${DEFAULT_RATE_LIMIT_WAIT_MS / 1000} seconds before retrying.`);
                await new Promise(resolve => setTimeout(resolve, DEFAULT_RATE_LIMIT_WAIT_MS));
                return getJwtTokenWithRetries(retries + 1);
            } else if (retries < DEFAULT_MAX_RETRIES) {
                console.error(`Error retrieving token (attempt ${retries + 1} of ${DEFAULT_MAX_RETRIES}): ${response.status} - ${response.statusText}. Retrying in ${GENERAL_RETRY_DELAY_MS / 1000} seconds.`);
                await new Promise(resolve => setTimeout(resolve, GENERAL_RETRY_DELAY_MS));
                return getJwtTokenWithRetries(retries + 1);
            } else {
                console.error(`Failed to retrieve token after ${DEFAULT_MAX_RETRIES} retries: ${response.status} - ${response.statusText}`);
                return null;
            }
        }

        const data = await response.json();
        if (data && data.token) {
            cachedToken = data.token;
            return cachedToken;
        } else {
            console.error('Failed to extract token from response:', data);
            return null;
        }
    } catch (error) {
        console.error('An error occurred while retrieving token:', error);
        if (retries < DEFAULT_MAX_RETRIES) {
            console.error(`Retrying token retrieval in ${GENERAL_RETRY_DELAY_MS / 1000} seconds.`);
            await new Promise(resolve => setTimeout(resolve, GENERAL_RETRY_DELAY_MS));
            return getJwtTokenWithRetries(retries + 1);
        }
        return null;
    }
}

async function uploadToLoadViewerWithRetries(csvData, retries = 0) {
    const token = await getJwtTokenWithRetries();
    if (!token) {
        return 'Error: Unable to retrieve JWT token for upload.';
    }

    try {
        const response = await fetch(LOADVIEWER_UPLOAD_URL, {
            method: 'POST',
            headers: {
                'Content-Type': 'text/csv',
                'Authorization': `Bearer ${token}`,
            },
            body: csvData,
        });

        if (!response.ok) {
            if (response.status === 401 || response.status === 410) {
                console.warn('JWT token expired or invalid. Attempting to retrieve a new token.');
                cachedToken = null; // Invalidate cached token
                const newToken = await getJwtTokenWithRetries();
                if (!newToken) {
                    return 'Error: Unable to retrieve a new JWT token after token expiration.';
                }
                return uploadToLoadViewerWithRetries(csvData, retries); // Retry with new token
            } else if (response.status === 429 && retries < DEFAULT_MAX_RETRIES) {
                console.warn(`Rate limit hit on upload endpoint (attempt ${retries + 1} of ${DEFAULT_MAX_RETRIES}). Waiting ${DEFAULT_RATE_LIMIT_WAIT_MS / 1000} seconds before retrying.`);
                await new Promise(resolve => setTimeout(resolve, DEFAULT_RATE_LIMIT_WAIT_MS));
                return uploadToLoadViewerWithRetries(csvData, retries + 1);
            } else if (response.status !== 409 && retries < DEFAULT_MAX_RETRIES) { // Don't retry on duplicate shipment (409)
                console.error(`Error uploading data (attempt ${retries + 1} of ${DEFAULT_MAX_RETRIES}): ${response.status} - ${response.statusText}. Retrying in ${GENERAL_RETRY_DELAY_MS / 1000} seconds.`);
                await new Promise(resolve => setTimeout(resolve, GENERAL_RETRY_DELAY_MS));
                return uploadToLoadViewerWithRetries(csvData, retries + 1);
            } else {
                return `Error: Upload failed after ${retries + 1} attempts. Status: ${response.status} - ${response.statusText}`;
            }
        }

        return `${response.status} - ${await response.text()}`;
    } catch (error) {
        console.error('An error occurred during upload:', error);
        if (retries < DEFAULT_MAX_RETRIES) {
            console.error(`Retrying upload in ${GENERAL_RETRY_DELAY_MS / 1000} seconds.`);
            await new Promise(resolve => setTimeout(resolve, GENERAL_RETRY_DELAY_MS));
            return uploadToLoadViewerWithRetries(csvData, retries + 1);
        }
        return `Exception: ${error.message}`;
    }
}

async function getLoadViewerSuggestionsCsvWithRetries(requestBody, retries = 0) {
    const token = await getJwtTokenWithRetries();
    if (!token) {
        return 'Error: Unable to retrieve JWT token for getting suggestions.';
    }

    try {
        const response = await fetch(LOADVIEWER_DOWNLOAD_URL, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${token}`,
            },
            body: JSON.stringify(requestBody),
        });

        console.debug(`POST Request to LoadViewer Suggestions URL: ${LOADVIEWER_DOWNLOAD_URL}\nRequest Body:\n${JSON.stringify(requestBody)}\nHTTP Status Code: ${response.status}\nResponse Text:\n${await response.text()}`);

        if (!response.ok) {
            if (response.status === 401 || response.status === 410) {
                console.warn('JWT token expired or invalid. Attempting to retrieve a new token.');
                cachedToken = null; // Invalidate cached token
                const newToken = await getJwtTokenWithRetries();
                if (!newToken) {
                    return 'Error: Unable to retrieve a new JWT token after token expiration.';
                }
                return getLoadViewerSuggestionsCsvWithRetries(requestBody, retries); // Retry with new token
            } else if (response.status === 429 && retries < DEFAULT_MAX_RETRIES) {
                console.warn(`Rate limit hit on suggestions endpoint (attempt ${retries + 1} of ${DEFAULT_MAX_RETRIES}). Waiting ${DEFAULT_RATE_LIMIT_WAIT_MS / 1000} seconds before retrying.`);
                await new Promise(resolve => setTimeout(resolve, DEFAULT_RATE_LIMIT_WAIT_MS));
                return getLoadViewerSuggestionsCsvWithRetries(requestBody, retries + 1);
            } else if (retries < DEFAULT_MAX_RETRIES) {
                console.error(`Error retrieving suggestions (attempt ${retries + 1} of ${DEFAULT_MAX_RETRIES}): ${response.status} - ${response.statusText}. Retrying in ${GENERAL_RETRY_DELAY_MS / 1000} seconds.`);
                await new Promise(resolve => setTimeout(resolve, GENERAL_RETRY_DELAY_MS));
                return getLoadViewerSuggestionsCsvWithRetries(requestBody, retries + 1);
            } else {
                return `Error: Failed to retrieve suggestions after ${retries + 1} attempts. Status: ${response.status} - ${response.statusText}`;
            }
        }

        return await response.text();
    } catch (error) {
        console.error('An error occurred while retrieving suggestions:', error);
        if (retries < DEFAULT_MAX_RETRIES) {
            console.error(`Retrying suggestions retrieval in ${GENERAL_RETRY_DELAY_MS / 1000} seconds.`);
            await new Promise(resolve => setTimeout(resolve, GENERAL_RETRY_DELAY_MS));
            return getLoadViewerSuggestionsCsvWithRetries(requestBody, retries + 1);
        }
        return `Exception: ${error.message}`;
    }
}

async function testSuggestions(shipmentReference, solverEmail = 'solver@loadviewer.com') {
    if (!shipmentReference) {
        console.log('Shipment Reference is required!');
        return;
    }

    if (solverEmail && !isValidEmail(solverEmail)) {
        console.log('Invalid Solver Email format!');
        return;
    }

    const token = await getJwtTokenWithRetries();
    if (!token) {
        console.log('Token Error or Server Not Reachable.');
        return;
    }

    const requestBody = {
        reference: shipmentReference.replace(/"/g, '""'), // Escape double quotes
        solverEmail: solverEmail.replace(/"/g, '""'),
    };

    const strResponse = await getLoadViewerSuggestionsCsvWithRetries(requestBody);
    if (strResponse.startsWith('Error') || strResponse.startsWith('Exception')) {
        console.error('Error retrieving data from LoadViewer:', strResponse);
        return;
    }
    console.log('Raw Suggestions Response:\n', strResponse);

    const arrLines = strResponse.split('\n').map(line => line.trim());
    const results = {
        status: {},
        recommendations: [],
    };

    if (arrLines.length > 0) {
        for (const line of arrLines) {
            const arrFields = line.split(',');
            if (arrFields.length >= 1) {
                const recordType = arrFields[0].trim();

                if (recordType === 'S') { // Status Record
                    if (arrFields.length === 9 && arrFields[1].trim().toLowerCase() === 'd') {
                        results.status.status = arrFields[2].trim();
                        results.status.message = arrFields[3].trim();
                        results.status.reference = arrFields[4].trim();
                        results.status.solverEmail = arrFields[5].trim();
                        results.status.multipleContainers = arrFields[6].trim();
                        results.status.suggestExtra = arrFields[7].trim();
                    } else if (arrFields.length === 9 && arrFields[1].trim().toLowerCase() === 'h') {
                        // Header row, can be ignored for parsing
                    } else {
                        console.warn('Invalid Status record format:', line);
                    }
                } else if (recordType === 'R') { // Recommendation Record
                    if (arrFields.length === 16 && arrFields[1].trim().toLowerCase() === 'd') {
                        const recommendation = {
                            size: arrFields[2].trim(),
                            cbm: arrFields[3].trim(),
                            weight: arrFields[4].trim(),
                            noOfPackages: parseInt(arrFields[5].trim(), 10),
                            isPacked: arrFields[6].trim(),
                            packedPackages: parseInt(arrFields[7].trim(), 10),
                            suggestedIncrease: parseInt(arrFields[8].trim(), 10),
                            suggestedDecrease: parseInt(arrFields[9].trim(), 10),
                            erpId: arrFields[10].trim(),
                            skuNumber: arrFields[11].trim(),
                            description: arrFields[12].trim(),
                            po: arrFields[13].trim(),
                            shipper: arrFields[14].trim(),
                        };
                        results.recommendations.push(recommendation);
                    } else if (arrFields.length === 16 && arrFields[1].trim().toLowerCase() === 'h') {
                        // Header row, can be ignored for parsing
                    } else {
                        console.warn('Invalid Recommendation record format:', line);
                    }
                } else if (recordType && recordType.trim() !== '') {
                    console.warn('Unknown record type:', recordType, 'in line:', line);
                }
            }
        }
    } else {
        console.log('Empty suggestions response.');
    }

    if (results) {
        const status = results.status;
        console.log(`Status: ${status.status} - ${status.message}`);
        console.log(`Reference: ${status.reference}`);
        console.log(`Solver Email: ${status.solverEmail}`);
        console.log(`Multiple Containers: ${status.multipleContainers}`);
        console.log(`Extra Suggested: ${status.suggestExtra}`);

        const recs = results.recommendations;
        if (recs.length > 0) {
            console.log('--- Suggestions ---');
            for (const recommendation of recs) {
                console.log(
                    `Is Packed: ${recommendation.isPacked}, Size LxWxH: ${recommendation.size}, CBM: ${recommendation.cbm}, Weight Kg: ${recommendation.weight}, Packages: ${recommendation.noOfPackages}, Packed: ${recommendation.packedPackages}, Increase: ${recommendation.suggestedIncrease}, Decrease: ${recommendation.suggestedDecrease}, ErpId: ${recommendation.erpId}, SkuNumber: ${recommendation.skuNumber}, Description: ${recommendation.description}, PO#: ${recommendation.po}, Division: ${recommendation.shipper}`
                );
                // --- Your ERP Processing Logic for Suggestions Here ---
            }
        } else {
            console.log(`No suggestions found for reference: ${shipmentReference}`);
        }
    } else {
        console.error(`Error retrieving or parsing suggestions for reference: ${shipmentReference}`);
    }
}

// --- Helper Functions (Adapt as needed for your CSV generation) ---
function removeComma(value) {
    return value ? value.toString().replace(/,/g, '').replace(/\r?\n|\r/g, ' ') : '';
}

function formatDecimal3(value) {
    return parseFloat(value).toFixed(3).replace(/0*$/, '').replace(/\.$/, '');
}

function generateLoadViewerCsv() {
    const rs_shipment = [
        { reference: 'SANDBOX-01', hangingAllowed: true, useMultipleContainers: false, uniqueErpId: false, suggestExtra: false },
    ];

    const rs_container = [
        { lengthMm: 5867, widthMm: 2352, heightMm: 2393, maxWeightKg: 27200, backMarginMm: 0, isRefrigerated: false, maxUseOne: false, lclBreakEvenCBM: 0 },
    ];

    const rs_cargo = [
        { color: '#008000', lengthMm: 700, widthMm: 500, heightMm: 300, weightKg: 91.5, cartons: 300, additionalCartons: 0, placement: 0, verticalRotationAllowed: true, skuPerCarton: 1, emptyPalletHeightMm: 0, trayCapMm: 0, trayHeightMm: 0, marginLength: 0, marginWidth: 0, marginHeight: 0, nameOfSet: '', cartonRatioInSet: 0, erpId: '', skuNumber: '', description: '', destination: 0, po: '', shipper: '' },
    ];

    const csvLines = [
        'S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra',
        `S,D,${removeComma(rs_shipment[0].reference)},${rs_shipment[0].hangingAllowed},${rs_shipment[0].useMultipleContainers},${rs_shipment[0].uniqueErpId},${rs_shipment[0].suggestExtra}`,
        'B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM',
    ];

    for (const row of rs_container) {
        csvLines.push(
            `B,D,${row.lengthMm},${row.widthMm},${row.heightMm},${formatDecimal3(row.maxWeightKg)},${row.backMarginMm},${row.isRefrigerated},${row.maxUseOne},${formatDecimal3(row.lclBreakEvenCBM)}`
        );
    }

    csvLines.push(
        'C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper'
    );
    for (const row of rs_cargo) {
        csvLines.push(
            `C,D,${removeComma(row.color)},${row.lengthMm},${row.widthMm},${row.heightMm},${formatDecimal3(row.weightKg)},${row.cartons},${row.additionalCartons},${row.placement},${row.verticalRotationAllowed},${row.skuPerCarton},${row.emptyPalletHeightMm},${row.trayCapMm},${row.trayHeightMm},${row.marginLength},${row.marginWidth},${row.marginHeight},${removeComma(row.nameOfSet)},${row.cartonRatioInSet},${removeComma(row.erpId)},${removeComma(row.skuNumber)},${removeComma(row.description)},${row.destination},${removeComma(row.po)},${removeComma(row.shipper)}`
        );
    }
    return csvLines.join('\r\n');
}

// --- Example Usage (Node.js or browser environment) ---
async function testLoadViewerIntegration() {
    // Example of uploading data
    const csvData = generateLoadViewerCsv();
    const uploadResult = await uploadToLoadViewerWithRetries(csvData);
    console.log('Upload Result:', uploadResult);

    // Example of getting suggestions
    await testSuggestions('SANDBOX-01', 'test@example.com'); // Replace with a valid reference and email
}

// Only run if this script is executed in a Node.js environment directly
if (typeof require === 'function' && require.main === module) {
    testLoadViewerIntegration();
} else {
    console.log('This script can be run in a Node.js environment or integrated into a web page.');
}

java

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class LoadViewerIntegration {

    private static final String TOKEN_URL = "https://www.loadviewer.com/api/auth/jwt/token";
    private static final String UPLOAD_URL = "https://www.loadviewer.com/api/integration/v1/upload";
    private static final String DOWNLOAD_URL = "https://www.loadviewer.com/api/integration/v1/download";
    private static final String API_EMAIL = "YOUR_Email"; // Replace with your actual email
    private static final String API_KEY = "YOUR_API_KEY_HERE"; // Replace with your actual API key
    private static String jwtToken;

    private static final int DEFAULT_MAX_RETRIES = 3;
    private static final int DEFAULT_RATE_LIMIT_WAIT_SECONDS = 65;
    private static final int GENERAL_RETRY_DELAY_SECONDS = 5;

    private static final HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();

    private static String getJwtTokenWithRetries() throws IOException, InterruptedException {
        int retryCount = 0;
        while (retryCount <= DEFAULT_MAX_RETRIES) {
            try {
                if (jwtToken != null) {
                    return jwtToken;
                }
                Map<String, String> requestBody = new HashMap<>();
                requestBody.put("email", API_EMAIL);
                requestBody.put("apikey", API_KEY);
                String jsonBody = convertMapToJson(requestBody);

                HttpRequest request = HttpRequest.newBuilder()
                        .uri(URI.create(TOKEN_URL))
                        .POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
                        .header("Content-Type", "application/json")
                        .build();

                HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

                if (response.statusCode() == 429) {
                    if (retryCount < DEFAULT_MAX_RETRIES) {
                        retryCount++;
                        System.err.printf("Rate limit hit on token endpoint (attempt %d of %d). Waiting %d seconds before retrying.%n", retryCount, DEFAULT_MAX_RETRIES, DEFAULT_RATE_LIMIT_WAIT_SECONDS);
                        Thread.sleep(DEFAULT_RATE_LIMIT_WAIT_SECONDS * 1000);
                        continue;
                    } else {
                        System.err.println("Failed to retrieve token after multiple rate limits.");
                        return null;
                    }
                } else if (response.statusCode() >= 300) {
                    if (retryCount < DEFAULT_MAX_RETRIES) {
                        retryCount++;
                        System.err.printf("Error retrieving token (attempt %d of %d): %d - %s. Retrying in %d seconds.%n", retryCount, DEFAULT_MAX_RETRIES, response.statusCode(), response.body(), GENERAL_RETRY_DELAY_SECONDS);
                        Thread.sleep(GENERAL_RETRY_DELAY_SECONDS * 1000);
                        continue;
                    } else {
                        System.err.printf("Failed to retrieve token after %d retries: %d - %s%n", DEFAULT_MAX_RETRIES, response.statusCode(), response.body());
                        return null;
                    }
                }

                String responseBody = response.body();
                Map<String, String> jsonResponse = convertJsonToMap(responseBody);
                if (jsonResponse != null && jsonResponse.containsKey("token")) {
                    jwtToken = jsonResponse.get("token");
                    return jwtToken;
                } else {
                    System.err.println("Failed to extract token from response: " + responseBody);
                    return null;
                }

            } catch (IOException | InterruptedException e) {
                if (retryCount < DEFAULT_MAX_RETRIES) {
                    retryCount++;
                    System.err.printf("Exception occurred while retrieving token (attempt %d of %d): %s. Retrying in %d seconds.%n", retryCount, DEFAULT_MAX_RETRIES, e.getMessage(), GENERAL_RETRY_DELAY_SECONDS);
                    Thread.sleep(GENERAL_RETRY_DELAY_SECONDS * 1000);
                    continue;
                } else {
                    System.err.println("An error occurred while retrieving token: " + e.getMessage());
                    return null;
                }
            }
        }
        return null;
    }

    public static String uploadToLoadViewerWithRetries(String csvData) throws IOException, InterruptedException {
        int retryCount = 0;
        while (retryCount <= DEFAULT_MAX_RETRIES) {
            try {
                if (jwtToken == null) {
                    jwtToken = getJwtTokenWithRetries();
                    if (jwtToken == null) {
                        return "Error: Unable to retrieve JWT token for upload.";
                    }
                }

                HttpRequest request = HttpRequest.newBuilder()
                        .uri(URI.create(UPLOAD_URL))
                        .POST(HttpRequest.BodyPublishers.ofString(csvData, StandardCharsets.UTF_8))
                        .header("Content-Type", "text/csv")
                        .header("Authorization", "Bearer " + jwtToken)
                        .build();

                HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

                if (response.statusCode() == 401 || response.statusCode() == 410) {
                    System.err.println("JWT token expired or invalid. Attempting to retrieve a new token.");
                    jwtToken = null;
                    jwtToken = getJwtTokenWithRetries();
                    if (jwtToken == null) {
                        return "Error: Unable to retrieve a new JWT token after token expiration.";
                    }
                    continue;
                } else if (response.statusCode() == 429) {
                    if (retryCount < DEFAULT_MAX_RETRIES) {
                        retryCount++;
                        System.err.printf("Rate limit hit on upload endpoint (attempt %d of %d). Waiting %d seconds before retrying.%n", retryCount, DEFAULT_MAX_RETRIES, DEFAULT_RATE_LIMIT_WAIT_SECONDS);
                        Thread.sleep(DEFAULT_RATE_LIMIT_WAIT_SECONDS * 1000);
                        continue;
                    } else {
                        return "Error: Upload failed after multiple rate limits.";
                    }
                } else if (response.statusCode() != 409 && response.statusCode() >= 300) {
                    if (retryCount < DEFAULT_MAX_RETRIES) {
                        retryCount++;
                        System.err.printf("Error uploading data (attempt %d of %d): %d - %s. Retrying in %d seconds.%n", retryCount, DEFAULT_MAX_RETRIES, response.statusCode(), response.body(), GENERAL_RETRY_DELAY_SECONDS);
                        Thread.sleep(GENERAL_RETRY_DELAY_SECONDS * 1000);
                        continue;
                    } else {
                        return String.format("Error: Upload failed after %d attempts. Status: %d - %s", DEFAULT_MAX_RETRIES, response.statusCode(), response.body());
                    }
                }

                return String.format("%d - %s", response.statusCode(), response.body());

            } catch (IOException | InterruptedException e) {
                if (retryCount < DEFAULT_MAX_RETRIES) {
                    retryCount++;
                    System.err.printf("Exception occurred during upload (attempt %d of %d): %s. Retrying in %d seconds.%n", retryCount, DEFAULT_MAX_RETRIES, e.getMessage(), GENERAL_RETRY_DELAY_SECONDS);
                    Thread.sleep(GENERAL_RETRY_DELAY_SECONDS * 1000);
                    continue;
                } else {
                    return "Exception: " + e.getMessage();
                }
            }
        }
        return "Error: Max upload retries exceeded.";
    }

    public static String getLoadViewerSuggestionsCsvWithRetries(String requestBody) throws IOException, InterruptedException {
        int retryCount = 0;
        while (retryCount <= DEFAULT_MAX_RETRIES) {
            try {
                if (jwtToken == null) {
                    jwtToken = getJwtTokenWithRetries();
                    if (jwtToken == null) {
                        return "Error: Unable to retrieve JWT token for getting suggestions.";
                    }
                }

                HttpRequest request = HttpRequest.newBuilder()
                        .uri(URI.create(DOWNLOAD_URL))
                        .POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
                        .header("Content-Type", "application/json")
                        .header("Authorization", "Bearer " + jwtToken)
                        .build();

                HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
                String responseBody = response.body();
                System.out.printf("POST Request to LoadViewer Suggestions URL: %s%nRequest Body:%n%s%nHTTP Status Code: %d%nResponse Text:%n%s%n", DOWNLOAD_URL, requestBody, response.statusCode(), responseBody);

                if (response.statusCode() == 401 || response.statusCode() == 410) {
                    System.err.println("JWT token expired or invalid. Attempting to retrieve a new token.");
                    jwtToken = null;
                    jwtToken = getJwtTokenWithRetries();
                    if (jwtToken == null) {
                        return "Error: Unable to retrieve a new JWT token after token expiration.";
                    }
                    continue;
                } else if (response.statusCode() == 429) {
                    if (retryCount < DEFAULT_MAX_RETRIES) {
                        retryCount++;
                        System.err.printf("Rate limit hit on suggestions endpoint (attempt %d of %d). Waiting %d seconds before retrying.%n", retryCount, DEFAULT_MAX_RETRIES, DEFAULT_RATE_LIMIT_WAIT_SECONDS);
                        Thread.sleep(DEFAULT_RATE_LIMIT_WAIT_SECONDS * 1000);
                        continue;
                    } else {
                        return "Error: Failed to retrieve suggestions after multiple rate limits.";
                    }
                } else if (response.statusCode() >= 300) {
                    if (retryCount < DEFAULT_MAX_RETRIES) {
                        retryCount++;
                        System.err.printf("Error retrieving suggestions (attempt %d of %d): %d - %s. Retrying in %d seconds.%n", retryCount, DEFAULT_MAX_RETRIES, response.statusCode(), responseBody, GENERAL_RETRY_DELAY_SECONDS);
                        Thread.sleep(GENERAL_RETRY_DELAY_SECONDS * 1000);
                        continue;
                    } else {
                        return String.format("Error: Failed to retrieve suggestions after %d attempts. Status: %d - %s", DEFAULT_MAX_RETRIES, response.statusCode(), responseBody);
                    }
                }

                return responseBody;

            } catch (IOException | InterruptedException e) {
                if (retryCount < DEFAULT_MAX_RETRIES) {
                    retryCount++;
                    System.err.printf("Exception occurred while retrieving suggestions (attempt %d of %d): %s. Retrying in %d seconds.%n", retryCount, DEFAULT_MAX_RETRIES, e.getMessage(), GENERAL_RETRY_DELAY_SECONDS);
                    Thread.sleep(GENERAL_RETRY_DELAY_SECONDS * 1000);
                    continue;
                } else {
                    return "Exception: " + e.getMessage();
                }
            }
        }
        return "Error: Max suggestions retries exceeded.";
    }

    public static void testSuggestions(String shipmentReference, String solverEmail) throws IOException, InterruptedException {
        if (shipmentReference == null || shipmentReference.isEmpty()) {
            System.err.println("Shipment Reference is required!");
            return;
        }

        if (solverEmail != null && !isValidEmail(solverEmail)) {
            System.err.println("Invalid Solver Email format!");
            return;
        }

        String token = jwtToken != null ? jwtToken : getJwtTokenWithRetries();
        if (token == null) {
            System.err.println("Token Error or Server Not Reachable.");
            return;
        }

        Map<String, String> requestBody = new HashMap<>();
        requestBody.put("reference", shipmentReference.replace("\"", "\"\"")); // Escape double quotes
        requestBody.put("solverEmail", solverEmail.replace("\"", "\"\""));
        String jsonBody = convertMapToJson(requestBody);

        String strResponse = getLoadViewerSuggestionsCsvWithRetries(jsonBody);
        if (strResponse.startsWith("Error") || strResponse.startsWith("Exception")) {
            System.err.println("Error retrieving data from LoadViewer: " + strResponse);
            return;
        }
        System.out.println("Raw Suggestions Response:\n" + strResponse);

        String[] arrLines = strResponse.split("\\r?\\n");
        Map<String, Object> results = new HashMap<>();
        Map<String, String> statusInfo = new HashMap<>();
        List<Map<String, Object>> recommendations = new ArrayList<>();

        results.put("status", statusInfo);
        results.put("recommendations", recommendations);

        if (arrLines.length > 0) {
            for (String line : arrLines) {
                String[] arrFields = line.split(",");
                if (arrFields.length >= 1) {
                    String recordType = arrFields[0].trim();

                    if (recordType.equals("S")) { // Status Record
                        if (arrFields.length == 9 && arrFields[1].trim().equalsIgnoreCase("d")) {
                            statusInfo.put("status", arrFields[2].trim());
                            statusInfo.put("message", arrFields[3].trim());
                            statusInfo.put("reference", arrFields[4].trim());
                            statusInfo.put("solverEmail", arrFields[5].trim());
                            statusInfo.put("multipleContainers", arrFields[6].trim());
                            statusInfo.put("suggestExtra", arrFields[7].trim());
                        } else if (arrFields.length == 9 && arrFields[1].trim().equalsIgnoreCase("h")) {
                            // Header row, can be ignored
                        } else {
                            System.err.println("Invalid Status record format: " + line);
                        }
                    } else if (recordType.equals("R")) { // Recommendation Record
                        if (arrFields.length == 16 && arrFields[1].trim().equalsIgnoreCase("d")) {
                            Map<String, Object> recommendation = new HashMap<>();
                            recommendation.put("size", arrFields[2].trim());
                            recommendation.put("cbm", arrFields[3].trim());
                            recommendation.put("weight", arrFields[4].trim());
                            recommendation.put("noOfPackages", Integer.parseInt(arrFields[5].trim()));
                            recommendation.put("isPacked", arrFields[6].trim());
                            recommendation.put("packedPackages", Integer.parseInt(arrFields[7].trim()));
                            recommendation.put("suggestedIncrease", Integer.parseInt(arrFields[8].trim()));
                            recommendation.put("suggestedDecrease", Integer.parseInt(arrFields[9].trim()));
                            recommendation.put("erpId", arrFields[10].trim());
                            recommendation.put("skuNumber", arrFields[11].trim());
                            recommendation.put("description", arrFields[12].trim());
                            recommendation.put("po", arrFields[13].trim());
                            recommendation.put("shipper", arrFields[14].trim());
                            recommendations.add(recommendation);
                        } else if (arrFields.length == 16 && arrFields[1].trim().equalsIgnoreCase("h")) {
                             // Header row, can be ignored.
                        }
                        else {
                            System.err.println("Invalid Recommendation record format: " + line);
                        }
                    } else if (recordType != null && !recordType.trim().isEmpty()) {
                        System.err.println("Unknown record type: " + recordType + " in line: " + line);
                    }
                }
            }
        } else {
            System.out.println("Empty suggestions response.");
        }

        if (results != null && results.containsKey("status")) {
            Map<String, String> status = (Map<String, String>) results.get("status");
            System.out.printf("Status: %s - %s%n", status.get("status"), status.get("message"));
            System.out.printf("Reference: %s%n", status.get("reference"));
            System.out.printf("Solver Email: %s%n", status.get("solverEmail"));
            System.out.printf("Multiple Containers: %s%n", status.get("multipleContainers"));
            System.out.printf("Extra Suggested: %s%n", status.get("suggestExtra"));

            List<Map<String, Object>> recs = (List<Map<String, Object>>) results.get("recommendations");
            if (recs != null && recs.size() > 0) {
                System.out.println("--- Suggestions ---");
                for (Map<String, Object> recommendation : recs) {
                    System.out.printf(
                            "Is Packed: %s, Size LxWxH: %s, CBM: %s, Weight Kg: %s, Packages: %s, Packed: %s, Increase: %s, Decrease: %s, ErpId: %s, SkuNumber: %s, Description: %s, PO#: %s, Division: %s%n",
                            recommendation.get("isPacked"), recommendation.get("size"), recommendation.get("cbm"), recommendation.get("weight"),
                            recommendation.get("noOfPackages"), recommendation.get("packedPackages"), recommendation.get("suggestedIncrease"),
                            recommendation.get("suggestedDecrease"), recommendation.get("erpId"), recommendation.get("skuNumber"),
                            recommendation.get("description"), recommendation.get("po"), recommendation.get("shipper")
                    );
                    // --- Your ERP Processing Logic for Suggestions Here ---
                }
            } else {
                System.out.printf("No suggestions found for reference: %s%n", shipmentReference);
            }
        } else {
            System.err.printf("Error retrieving or parsing suggestions for reference: %s%n", shipmentReference);
        }
    }

    private static String removeComma(String value) {
        return (value != null) ? value.replace(",", "").replace("\r\n", " ") : "";
    }

    private static String formatDecimal3(double value) {
        return String.format("%.3f", value).replaceAll("0*$", "").replaceAll("\\.$", "");
    }

    private static String generateLoadViewerCsv() {
        List<Map<String, Object>> rsShipment = new ArrayList<>();
        Map<String, Object> shipmentData = new HashMap<>();
        shipmentData.put("reference", "SANDBOX-01");
        shipmentData.put("hangingAllowed", true);
        shipmentData.put("useMultipleContainers", false);
        shipmentData.put("uniqueErpId", false);
        shipmentData.put("suggestExtra", false);
        rsShipment.add(shipmentData);

        List<Map<String, Object>> rsContainer = new ArrayList<>();
        Map<String, Object> containerData = new HashMap<>();
        containerData.put("lengthMm", 5867);
        containerData.put("widthMm", 2352);
        containerData.put("heightMm", 2393);
        containerData.put("maxWeightKg", 27200.0);
        containerData.put("backMarginMm", 0);
        containerData.put("isRefrigerated", false);
        containerData.put("maxUseOne", false);
        containerData.put("lclBreakEvenCBM", 0.0);
        rsContainer.add(containerData);

        List<Map<String, Object>> rsCargo = new ArrayList<>();
        Map<String, Object> cargoData = new HashMap<>();
        cargoData.put("color", "#008000");
        cargoData.put("lengthMm", 700);
        cargoData.put("widthMm", 500);
        cargoData.put("heightMm", 300);
        cargoData.put("weightKg", 91.5);
        cargoData.put("cartons", 300);
        cargoData.put("additionalCartons", 0);
        cargoData.put("placement", 0);
        cargoData.put("verticalRotationAllowed", true);
        cargoData.put("skuPerCarton", 1);
        cargoData.put("emptyPalletHeightMm", 0);
        cargoData.put("trayCapMm", 0);
        cargoData.put("trayHeightMm", 0);
        cargoData.put("marginLength", 0);
        cargoData.put("marginWidth", 0);
        cargoData.put("marginHeight", 0);
        cargoData.put("nameOfSet", "");
        cargoData.put("cartonRatioInSet", 0);
        cargoData.put("erpId", "");
        cargoData.put("skuNumber", "");
        cargoData.put("description", "");
        cargoData.put("destination", 0);
        cargoData.put("po", "");
        cargoData.put("shipper", "");
        rsCargo.add(cargoData);

        List<String> csvLines = new ArrayList<>();
        csvLines.add("S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra");
        csvLines.add(String.format("S,D,%s,%s,%s,%s,%s",
                removeComma((String) rsShipment.get(0).get("reference")),
                rsShipment.get(0).get("hangingAllowed"),
                rsShipment.get(0).get("useMultipleContainers"),
                rsShipment.get(0).get("uniqueErpId"),
                rsShipment.get(0).get("suggestExtra")));
        csvLines.add("B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM");

        for (Map<String, Object> row : rsContainer) {
            csvLines.add(String.format("B,D,%s,%s,%s,%s,%s,%s,%s,%s",
                    row.get("lengthMm"), row.get("widthMm"), row.get("heightMm"),
                    formatDecimal3((Double) row.get("maxWeightKg")), row.get("backMarginMm"),
                    row.get("isRefrigerated"), row.get("maxUseOne"),
                    formatDecimal3((Double) row.get("lclBreakEvenCBM"))));
        }

        csvLines.add("C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper");
        for (Map<String, Object> row : rsCargo) {
            csvLines.add(String.format("C,D,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s",
                    removeComma((String) row.get("color")), row.get("lengthMm"), row.get("widthMm"), row.get("heightMm"),
                    formatDecimal3((Double) row.get("weightKg")), row.get("cartons"), row.get("additionalCartons"),
                    row.get("placement"), row.get("verticalRotationAllowed"), row.get("skuPerCarton"),
                    row.get("emptyPalletHeightMm"), row.get("trayCapMm"), row.get("trayHeightMm"),
                    row.get("marginLength"), row.get("marginWidth"), row.get("marginHeight"),
                    removeComma((String) row.get("nameOfSet")), row.get("cartonRatioInSet"),
                    removeComma((String) row.get("erpId")), removeComma((String) row.get("skuNumber")),
                    removeComma((String) row.get("description")), row.get("destination"),
                    removeComma((String) row.get("po")), removeComma((String) row.get("shipper"))));
        }
        return String.join(System.lineSeparator(), csvLines);
    }

    private static boolean isValidEmail(String email) {
        // Basic email validation regex
        String regex = "^[^\s@]+@[^\s@]+\\.[^\s@]+$";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(email);
        return matcher.matches();
    }

    // Helper function to convert a Map to a JSON string
    private static String convertMapToJson(Map<String, String> map) {
        return map.entrySet().stream()
                .map(entry -> String.format("\"%s\":\"%s\"", entry.getKey(), entry.getValue()))
                .collect(Collectors.joining(",", "{", "}"));
    }

    // Helper function to convert a JSON string to a Map
    private static Map<String, String> convertJsonToMap(String json) {
        Map<String, String> map = new HashMap<>();
        json = json.substring(1, json.length() - 1); // Remove curly braces
        String[] pairs = json.split(",");
        for (String pair : pairs) {
            String[] keyValue = pair.split(":");
            if (keyValue.length == 2) {
                String key = keyValue[0].replaceAll("\"", "").trim();
                String value = keyValue[1].replaceAll("\"", "").trim();
                map.put(key, value);
            }
        }
        return map;
    }

    public static void main(String[] args) {
        try {
            // Example of uploading data
            String csvData = generateLoadViewerCsv();
            String uploadResult = uploadToLoadViewerWithRetries(csvData);
            System.out.println("Upload Result: " + uploadResult);

            // Example of getting suggestions
            testSuggestions("SANDBOX-01", "test@example.com");
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}


fsharp


open System
open System.Collections.Generic
open System.Net.Http
open System.Net.Http.Headers
open System.Text
open System.Text.RegularExpressions
open System.Threading.Tasks

// Configuration
let tokenUrl = "https://www.loadviewer.com/api/auth/jwt/token"
let uploadUrl = "https://www.loadviewer.com/api/integration/v1/upload"
let downloadUrl = "https://www.loadviewer.com/api/integration/v1/download"
let apiEmail = "YOUR_Email" // Replace with your actual email
let apiKey = "YOUR_API_KEY_HERE" // Replace with your actual API key
let mutable jwtToken : string option = None

let defaultMaxRetries = 3
let defaultRateLimitWaitSeconds = 65
let generalRetryDelaySeconds = 5

let httpClient = new HttpClient()

// Helper function to convert a Dictionary<string, string> to a JSON string
let convertMapToJson (map : Dictionary<string, string>) =
    map
    |> Seq.map (fun (key, value) -> sprintf """"%s":"%s\"""" key value)
    |> String.concat ","
    |> sprintf "{%s}"

// Helper function to convert a JSON string to a Dictionary<string, string>
let convertJsonToMap (json : string) : Dictionary<string, string> =
    let map = new Dictionary<string, string>()
    let trimmedJson = json.Substring(1, json.Length - 2) // Remove curly braces
    let pairs = trimmedJson.Split(',')
    for pair in pairs do
        let keyValue = pair.Split(':')
        if keyValue.Length = 2 then
            let key = keyValue.[0].Replace("\"", "").Trim()
            let value = keyValue.[1].Replace("\"", "").Trim()
            map.Add(key, value)
    map

// Asynchronous function to get the JWT token with retries
let rec getJwtTokenWithRetries (retryCount : int) : Async<string option> =
    async {
        if jwtToken.IsSome then
            return jwtToken.Value

        if retryCount > defaultMaxRetries then
            eprintln "Failed to retrieve token after multiple retries."
            return None

        try
            let requestBody = new Dictionary<string, string>()
            requestBody.Add("email", apiEmail)
            requestBody.Add("apikey", apiKey)
            let jsonBody = convertMapToJson requestBody
            let content = new StringContent(jsonBody, Encoding.UTF8, "application/json")

            use response = await httpClient.PostAsync(Uri(tokenUrl), content)

            if response.StatusCode = System.Net.HttpStatusCode.TooManyRequests then
                if retryCount < defaultMaxRetries then
                    eprintfn "Rate limit hit on token endpoint (attempt %d of %d). Waiting %d seconds before retrying." retryCount defaultMaxRetries defaultRateLimitWaitSeconds
                    do! Async.Sleep(defaultRateLimitWaitSeconds * 1000)
                    return! getJwtTokenWithRetries (retryCount + 1)
                else
                    eprintln "Failed to retrieve token after multiple rate limits."
                    return None
            elif not response.IsSuccessStatusCode then
                if retryCount < defaultMaxRetries then
                    let errorBody = await response.Content.ReadAsStringAsync()
                    eprintfn "Error retrieving token (attempt %d of %d): %i - %s. Retrying in %d seconds." retryCount defaultMaxRetries (int response.StatusCode) errorBody generalRetryDelaySeconds
                    do! Async.Sleep(generalRetryDelaySeconds * 1000)
                    return! getJwtTokenWithRetries (retryCount + 1)
                else
                    let errorBody = await response.Content.ReadAsStringAsync()
                    eprintfn "Failed to retrieve token after %d retries: %i - %s" defaultMaxRetries (int response.StatusCode) errorBody
                    return None
            else
                let responseBody = await response.Content.ReadAsStringAsync()
                let jsonResponse = convertJsonToMap responseBody
                if jsonResponse.ContainsKey("token") then
                    jwtToken <- Some(jsonResponse.["token"])
                    return jwtToken.Value
                else
                    eprintfn "Failed to extract token from response: %s" responseBody
                    return None
        with
        | ex ->
            if retryCount < defaultMaxRetries then
                eprintfn "Exception occurred while retrieving token (attempt %d of %d): %s. Retrying in %d seconds." retryCount defaultMaxRetries ex.Message generalRetryDelaySeconds
                do! Async.Sleep(generalRetryDelaySeconds * 1000)
                return! getJwtTokenWithRetries (retryCount + 1)
            else
                eprintfn "An error occurred while retrieving token: %s" ex.Message
                return None
    } |> Async.StartAsTask

// Asynchronous function to upload data to LoadViewer with retries
let rec uploadToLoadViewerWithRetries (csvData : string) (retryCount : int) : Async<string> =
    async {
        let currentToken =
            match jwtToken with
            | Some token -> token
            | None ->
                let tokenResult = await getJwtTokenWithRetries 1
                match tokenResult with
                | Some token -> token
                | None -> return "ERROR: Unable to retrieve JWT token for upload."
        jwtToken <- Some(currentToken)

        if retryCount > defaultMaxRetries then
            return "ERROR: Max upload retries exceeded."

        try
            let content = new StringContent(csvData, Encoding.UTF8, "text/csv")
            content.Headers.ContentType <- MediaTypeHeaderValue("text/csv")
            httpClient.DefaultRequestHeaders.Authorization <- new AuthenticationHeaderValue("Bearer", currentToken)

            use response = await httpClient.PostAsync(Uri(uploadUrl), content)

            if response.StatusCode = System.Net.HttpStatusCode.Unauthorized || response.StatusCode = System.Net.HttpStatusCode.Gone then
                eprintln "JWT token expired or invalid. Attempting to retrieve a new token."
                jwtToken <- None
                let newTokenResult = await getJwtTokenWithRetries 1
                match newTokenResult with
                | Some newToken ->
                    httpClient.DefaultRequestHeaders.Authorization <- new AuthenticationHeaderValue("Bearer", newToken)
                    return! uploadToLoadViewerWithRetries csvData (retryCount + 1)
                | None -> return "ERROR: Unable to retrieve a new JWT token after token expiration."
            elif response.StatusCode = System.Net.HttpStatusCode.TooManyRequests then
                if retryCount < defaultMaxRetries then
                    eprintfn "Rate limit hit on upload endpoint (attempt %d of %d). Waiting %d seconds before retrying." retryCount defaultMaxRetries defaultRateLimitWaitSeconds
                    do! Async.Sleep(defaultRateLimitWaitSeconds * 1000)
                    return! uploadToLoadViewerWithRetries csvData (retryCount + 1)
                else
                    return "ERROR: Upload failed after multiple rate limits."
            elif not response.IsSuccessStatusCode && response.StatusCode <> System.Net.HttpStatusCode.Conflict then
                if retryCount < defaultMaxRetries then
                    let errorBody = await response.Content.ReadAsStringAsync()
                    eprintfn "Error uploading data (attempt %d of %d): %i - %s. Retrying in %d seconds." retryCount defaultMaxRetries (int response.StatusCode) errorBody generalRetryDelaySeconds
                    do! Async.Sleep(generalRetryDelaySeconds * 1000)
                    return! uploadToLoadViewerWithRetries csvData (retryCount + 1)
                else
                    let errorBody = await response.Content.ReadAsStringAsync()
                    return sprintf "ERROR: Upload failed after %d attempts. Status: %i - %s" defaultMaxRetries (int response.StatusCode) errorBody
            else
                let responseBody = await response.Content.ReadAsStringAsync()
                return sprintf "%i - %s" (int response.StatusCode) responseBody
        with
        | ex ->
            if retryCount < defaultMaxRetries then
                eprintfn "Exception occurred during upload (attempt %d of %d): %s. Retrying in %d seconds." retryCount defaultMaxRetries ex.Message generalRetryDelaySeconds
                do! Async.Sleep(generalRetryDelaySeconds * 1000)
                return! uploadToLoadViewerWithRetries csvData (retryCount + 1)
            else
                return sprintf "Exception: %s" ex.Message
    } |> Async.StartAsTask |> Async.AwaitTask

// Asynchronous function to get LoadViewer suggestions CSV with retries
let rec getLoadViewerSuggestionsCsvWithRetries (requestBodyJson : string) (retryCount : int) : Async<string> =
    async {
        let currentToken =
            match jwtToken with
            | Some token -> token
            | None ->
                let tokenResult = await getJwtTokenWithRetries 1
                match tokenResult with
                | Some token -> token
                | None -> return "ERROR: Unable to retrieve JWT token for getting suggestions."
        jwtToken <- Some(currentToken)

        if retryCount > defaultMaxRetries then
            return "ERROR: Max suggestions retries exceeded."

        try
            let content = new StringContent(requestBodyJson, Encoding.UTF8, "application/json")
            content.Headers.ContentType <- MediaTypeHeaderValue("application/json")
            httpClient.DefaultRequestHeaders.Authorization <- new AuthenticationHeaderValue("Bearer", currentToken)

            use response = await httpClient.PostAsync(Uri(downloadUrl), content)
            let responseBody = await response.Content.ReadAsStringAsync()
            printfn "POST Request to LoadViewer Suggestions URL: %s\nRequest Body:\n%s\nHTTP Status Code: %i\nResponse Text:\n%s" downloadUrl requestBodyJson (int response.StatusCode) responseBody

            if response.StatusCode = System.Net.HttpStatusCode.Unauthorized || response.StatusCode = System.Net.HttpStatusCode.Gone then
                eprintln "JWT token expired or invalid. Attempting to retrieve a new token."
                jwtToken <- None
                let newTokenResult = await getJwtTokenWithRetries 1
                match newTokenResult with
                | Some newToken ->
                    httpClient.DefaultRequestHeaders.Authorization <- new AuthenticationHeaderValue("Bearer", newToken)
                    return! getLoadViewerSuggestionsCsvWithRetries requestBodyJson (retryCount + 1)
                | None -> return "ERROR: Unable to retrieve a new JWT token after token expiration."
            elif response.StatusCode = System.Net.HttpStatusCode.TooManyRequests then
                if retryCount < defaultMaxRetries then
                    eprintfn "Rate limit hit on suggestions endpoint (attempt %d of %d). Waiting %d seconds before retrying." retryCount defaultMaxRetries defaultRateLimitWaitSeconds
                    do! Async.Sleep(defaultRateLimitWaitSeconds * 1000)
                    return! getLoadViewerSuggestionsCsvWithRetries requestBodyJson (retryCount + 1)
                else
                    return "ERROR: Failed to retrieve suggestions after multiple rate limits."
            elif not response.IsSuccessStatusCode then
                if retryCount < defaultMaxRetries then
                    eprintfn "Error retrieving suggestions (attempt %d of %d): %i - %s. Retrying in %d seconds." retryCount defaultMaxRetries (int response.StatusCode) responseBody generalRetryDelaySeconds
                    do! Async.Sleep(generalRetryDelaySeconds * 1000)
                    return! getLoadViewerSuggestionsCsvWithRetries requestBodyJson (retryCount + 1)
                else
                    return sprintf "ERROR: Failed to retrieve suggestions after %d attempts. Status: %i - %s" defaultMaxRetries (int response.StatusCode) responseBody
            else
                return responseBody
        with
        | ex ->
            if retryCount < defaultMaxRetries then
                eprintfn "Exception occurred while retrieving suggestions (attempt %d of %d): %s. Retrying in %d seconds." retryCount defaultMaxRetries ex.Message generalRetryDelaySeconds
                do! Async.Sleep(generalRetryDelaySeconds * 1000)
                return! getLoadViewerSuggestionsCsvWithRetries requestBodyJson (retryCount + 1)
            else
                return sprintf "Exception: %s" ex.Message
    } |> Async.StartAsTask |> Async.AwaitTask

// Function to test getting suggestions
let testSuggestions (shipmentReference : string) (solverEmail : string) : Async<unit> =
    async {
        if String.IsNullOrWhiteSpace(shipmentReference) then
            eprintln "Shipment Reference is required!"
            return ()

        if not (String.IsNullOrWhiteSpace(solverEmail) || Regex.IsMatch(solverEmail, "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")) then
            eprintln "Invalid Solver Email format!"
            return ()

        let currentToken =
            match jwtToken with
            | Some token -> token
            | None ->
                let tokenResult = await getJwtTokenWithRetries 1
                match tokenResult with
                | Some token -> token
                | None ->
                    eprintln "Token Error or Server Not Reachable."
                    return ()
        jwtToken <- Some(currentToken)

        let requestBody = new Dictionary<string, string>()
        requestBody.Add("reference", shipmentReference.Replace("\"", "\"\"")) // Escape double quotes
        requestBody.Add("solverEmail", solverEmail.Replace("\"", "\"\""))
        let jsonBody = convertMapToJson requestBody

        let strResponse = await getLoadViewerSuggestionsCsvWithRetries jsonBody 1
        if strResponse.StartsWith("ERROR") || strResponse.StartsWith("Exception") then
            eprintfn "Error retrieving data from LoadViewer: %s" strResponse
            return ()

        printfn "Raw Suggestions Response:\n%s" strResponse

        let arrLines = strResponse.Split([|"\r\n"; "\n"|], StringSplitOptions.RemoveEmptyEntries)
        let results = new Dictionary<string, obj>()
        let statusInfo = new Dictionary<string, string>() :> obj
        let recommendations = new List<Dictionary<string, obj>>() :> obj

        results.Add("status", statusInfo)
        results.Add("recommendations", recommendations)

        if arrLines.Length > 0 then
            for line in arrLines do
                let arrFields = line.Split(',')
                if arrFields.Length >= 1 then
                    let recordType = arrFields.[0].Trim()

                    if recordType = "S" then // Status Record
                        if arrFields.Length = 9 && arrFields.[1].Trim().ToLower() = "d" then
                            statusInfo.["status"] <- arrFields.[2].Trim() :> obj
                            statusInfo.["message"] <- arrFields.[3].Trim() :> obj
                            statusInfo.["reference"] <- arrFields.[4].Trim() :> obj
                            statusInfo.["solverEmail"] <- arrFields.[5].Trim() :> obj
                            statusInfo.["multipleContainers"] <- arrFields.[6].Trim() :> obj
                            statusInfo.["suggestExtra"] <- arrFields.[7].Trim() :> obj
                        elif arrFields.Length = 9 && arrFields.[1].Trim().ToLower() = "h" then
                            // Header row, can be ignored
                            ()
                        else
                            eprintln $"Invalid Status record format: {line}"
                    elif recordType = "R" then // Recommendation Record
                        if arrFields.Length = 16 && arrFields.[1].Trim().ToLower() = "d" then
                            let recommendation = new Dictionary<string, obj>()
                            recommendation.["size"] <- arrFields.[2].Trim() :> obj
                            recommendation.["cbm"] <- arrFields.[3].Trim() :> obj
                            recommendation.["weight"] <- arrFields.[4].Trim() :> obj
                            recommendation.["noOfPackages"] <- int arrFields.[5].Trim() :> obj
                            recommendation.["isPacked"] <- arrFields.[6].Trim() :> obj
                            recommendation.["packedPackages"] <- int arrFields.[7].Trim() :> obj
                            recommendation.["suggestedIncrease"] <- int arrFields.[8].Trim() :> obj
                            recommendation.["suggestedDecrease"] <- int arrFields.[9].Trim() :> obj
                            recommendation.["erpId"] <- arrFields.[10].Trim() :> obj
                            recommendation.["skuNumber"] <- arrFields.[11].Trim() :> obj
                            recommendation.["description"] <- arrFields.[12].Trim() :> obj
                            recommendation.["po"] <- arrFields.[13].Trim() :> obj
                            recommendation.["shipper"] <- arrFields.[14].Trim() :> obj
                            (recommendations :?> List<Dictionary<string, obj>>).Add(recommendation)
                        elif arrFields.Length = 16 && arrFields.[1].Trim().ToLower() = "h" then
                            // Header row, can be ignored.
                            ()
                        else
                            eprintln $"Invalid Recommendation record format: {line}"
                    elif not (String.IsNullOrWhiteSpace recordType) then
                        eprintln $"Unknown record type: {recordType} in line: {line}"
        else
            printfn "Empty suggestions response."

        if results.ContainsKey("status") then
            let status = results.["status"] :?> Dictionary<string, string>
            printfn "Status: %s - %s" status.["status"] status.["message"]
            printfn "Reference: %s" status.["reference"]
            printfn "Solver Email: %s" status.["solverEmail"]
            printfn "Multiple Containers: %s" status.["multipleContainers"]
            printfn "Extra Suggested: %s" status.["suggestExtra"]

            if results.ContainsKey("recommendations") then
                let recs = results.["recommendations"] :?> List<Dictionary<string, obj>>
                if recs.Count > 0 then
                    printfn "--- Suggestions ---"
                    for recommendation in recs do
                        printfn "Is Packed: %s, Size LxWxH: %s, CBM: %s, Weight Kg: %s, Packages: %i, Packed: %i, Increase: %i, Decrease: %i, ErpId: %s, SkuNumber: %s, Description: %s, PO#: %s, Division: %s"
                                (recommendation.["isPacked"] :?> string)
                                (recommendation.["size"] :?> string)
                                (recommendation.["cbm"] :?> string)
                                (recommendation.["weight"] :?> string)
                                (recommendation.["noOfPackages"] :?> int)
                                (recommendation.["packedPackages"] :?> int)
                                (recommendation.["suggestedIncrease"] :?> int)
                                (recommendation.["suggestedDecrease"] :?> int)
                                (recommendation.["erpId"] :?> string)
                                (recommendation.["skuNumber"] :?> string)
                                (recommendation.["description"] :?> string)
                                (recommendation.["po"] :?> string)
                                (recommendation.["shipper"] :?> string)
                        // --- Your ERP Processing Logic for Suggestions Here ---
                else
                    printfn $"No suggestions found for reference: {shipmentReference}"
            else
                eprintln $"Error parsing recommendations for reference: {shipmentReference}"
        else
            eprintln $"Error retrieving or parsing suggestions for reference: {shipmentReference}"
    }

let removeComma (value : string) : string =
    if value <> null then value.Replace(",", "").Replace("\r\n", " ") else ""

let formatDecimal3 (value : float) : string =
    sprintf "%.3f" value |> (fun s -> s.TrimEnd('0').TrimEnd('.'))

let generateLoadViewerCsv () : string =
    let rsShipment = ResizeArray()
    let shipmentData = new Dictionary<string, obj>()
    shipmentData.Add("reference", "SANDBOX-01" :> obj)
    shipmentData.Add("hangingAllowed", true :> obj)
    shipmentData.Add("useMultipleContainers", false :> obj)
    shipmentData.Add("uniqueErpId", false :> obj)
    shipmentData.Add("suggestExtra", false :> obj)
    rsShipment.Add(shipmentData)

    let rsContainer = ResizeArray()
    let containerData = new Dictionary<string, obj>()
    containerData.Add("lengthMm", 5867 :> obj)
    containerData.Add("widthMm", 2352 :> obj)
    containerData.Add("heightMm", 2393 :> obj)
    containerData.Add("maxWeightKg", 27200.0 :> obj)
    containerData.Add("backMarginMm", 0 :> obj)
    containerData.Add("isRefrigerated", false :> obj)
    containerData.Add("maxUseOne", false :> obj)
    containerData.Add("lclBreakEvenCBM", 0.0 :> obj)
    rsContainer.Add(containerData)

    let rsCargo = ResizeArray()
    let cargoData = new Dictionary<string, obj>()
    cargoData.Add("color", "#008000" :> obj)
    cargoData.Add("lengthMm", 700 :> obj)
    cargoData.Add("widthMm", 500 :> obj)
    cargoData.Add("heightMm", 300 :> obj)
    cargoData.Add("weightKg", 91.5 :> obj)
    cargoData.Add("cartons", 300 :> obj)
    cargoData.Add("additionalCartons", 0 :> obj)
    cargoData.Add("placement", 0 :> obj)
    cargoData.Add("verticalRotationAllowed", true :> obj)
    cargoData.Add("skuPerCarton", 1 :> obj)
    cargoData.Add("emptyPalletHeightMm", 0 :> obj)
    cargoData.Add("trayCapMm", 0 :> obj)
    cargoData.Add("trayHeightMm", 0 :> obj)
    cargoData.Add("marginLength", 0 :> obj)
    cargoData.Add("marginWidth", 0 :> obj)
    cargoData.Add("marginHeight", 0 :> obj)
    cargoData.Add("nameOfSet", "" :> obj)
    cargoData.Add("cartonRatioInSet", 0 :> obj)
    cargoData.Add("erpId", "" :> obj)
    cargoData.Add("skuNumber", "" :> obj)
    cargoData.Add("description", "" :> obj)
    cargoData.Add("destination", 0 :> obj)
    cargoData.Add("po", "" :> obj)
    cargoData.Add("shipper", "" :> obj)
    rsCargo.Add(cargoData)

    let csvLines = ResizeArray()
    csvLines.Add("S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra")
    csvLines.Add(sprintf "S,D,%s,%b,%b,%b,%b"
                        (removeComma (rsShipment.[0].["reference"] :> string))
                        (rsShipment.[0].["hangingAllowed"] :> bool)
                        (rsShipment.[0].["useMultipleContainers"] :> bool)
                        (rsShipment.[0].["uniqueErpId"] :> bool)
                        (rsShipment.[0].["suggestExtra"] :> bool))
    csvLines.Add("B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM")

    for row in rsContainer do
        csvLines.Add(sprintf "B,D,%i,%i,%i,%s,%i,%b,%b,%s"
                            (row.["lengthMm"] :> int)
                            (row.["widthMm"] :> int)
                            (row.["heightMm"] :> int)
                            (formatDecimal3 (row.["maxWeightKg"] :> float))
                            (row.["backMarginMm"] :> int)
                            (row.["isRefrigerated"] :> bool)
                            (row.["maxUseOne"] :> bool)
                            (formatDecimal3 (row.["lclBreakEvenCBM"] :> float)))

    csvLines.Add("C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper")
    for row in rsCargo do
        csvLines.Add(sprintf "C,D,%s,%i,%i,%i,%s,%i,%i,%i,%b,%i,%i,%i,%i,%i,%i,%i,%s,%i,%s,%s,%s,%i,%s,%s"
                            (removeComma (row.["color"] :> string))
                            (row.["lengthMm"] :> int)
                            (row.["widthMm"] :> int)
                            (row.["heightMm"] :> int)
                            (formatDecimal3 (row.["weightKg"] :> float))
                            (row.["cartons"] :> int)
                            (row.["additionalCartons"] :> int)
                            (row.["placement"] :> int)
                            (row.["verticalRotationAllowed"] :> bool)
                            (row.["skuPerCarton"] :> int)
                            (row.["emptyPalletHeightMm"] :> int)
                            (row.["trayCapMm"] :> int)
                            (row.["trayHeightMm"] :> int)
                            (row.["marginLength"] :> int)
                            (row.["marginWidth"] :> int)
                            (row.["marginHeight"] :> int)
                            (removeComma (row.["nameOfSet"] :> string))
                            (row.["cartonRatioInSet"] :> int)
                            (removeComma (row.["erpId"] :> string))
                            (removeComma (row.["skuNumber"] :> string))
                            (removeComma (row.["description"] :> string))
                            (row.["destination"] :> int)
                            (removeComma (row.["po"] :> string))
                            (removeComma (row.["shipper"] :> string)))
    String.Join(Environment.NewLine, csvLines)

let isValidEmail (email : string) : bool =
    // Basic email validation regex
    let regex = "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"
    Regex.IsMatch(email, regex)

[<EntryPoint>]
let main argv =
    task {
        // Example of uploading data
        let csvData = generateLoadViewerCsv ()
        let uploadResult = await uploadToLoadViewerWithRetries csvData 1
        printfn "Upload Result: %s" uploadResult

        // Example of getting suggestions
        do! testSuggestions "SANDBOX-01" "test@example.com"
    } |> Async.AwaitTask
    0

Go

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"
)

const (
	tokenURL                 = "https://www.loadviewer.com/api/auth/jwt/token"
	uploadURL                = "https://www.loadviewer.com/api/integration/v1/upload"
	downloadURL              = "https://www.loadviewer.com/api/integration/v1/download"
	apiEmail                 = "YOUR_Email"       // Replace with your actual email
	apiKey                   = "YOUR_API_KEY_HERE" // Replace with your actual API key
	defaultMaxRetries        = 3
	defaultRateLimitWaitSeconds = 65
	generalRetryDelaySeconds   = 5
)

var jwtToken string

func getJWTTokenWithRetries(retryCount int) (string, error) {
	if jwtToken != "" {
		return jwtToken, nil
	}

	if retryCount > defaultMaxRetries {
		return "", fmt.Errorf("ERROR: Failed to retrieve token after multiple retries")
	}

	requestBody := map[string]string{
		"email":  apiEmail,
		"apikey": apiKey,
	}
	jsonBody, err := json.Marshal(requestBody)
	if err != nil {
		return "", fmt.Errorf("ERROR: Failed to marshal request body: %w", err)
	}

	resp, err := http.Post(tokenURL, "application/json", bytes.NewBuffer(jsonBody))
	if err != nil {
		time.Sleep(time.Duration(generalRetryDelaySeconds) * time.Second)
		return getJWTTokenWithRetries(retryCount + 1)
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusTooManyRequests {
		if retryCount < defaultMaxRetries {
			fmt.Printf("Rate limit hit on token endpoint (attempt %d of %d). Waiting %d seconds before retrying.\n", retryCount+1, defaultMaxRetries, defaultRateLimitWaitSeconds)
			time.Sleep(time.Duration(defaultRateLimitWaitSeconds) * time.Second)
			return getJWTTokenWithRetries(retryCount + 1)
		} else {
			return "", fmt.Errorf("ERROR: Failed to retrieve token after multiple rate limits")
		}
	} else if resp.StatusCode >= 300 {
		bodyBytes, _ := io.ReadAll(resp.Body)
		bodyString := string(bodyBytes)
		if retryCount < defaultMaxRetries {
			fmt.Printf("Error retrieving token (attempt %d of %d): %d - %s. Retrying in %d seconds.\n", retryCount+1, defaultMaxRetries, resp.StatusCode, bodyString, generalRetryDelaySeconds)
			time.Sleep(time.Duration(generalRetryDelaySeconds) * time.Second)
			return getJWTTokenWithRetries(retryCount + 1)
		} else {
			return "", fmt.Errorf("ERROR: Failed to retrieve token after %d retries: %d - %s", defaultMaxRetries, resp.StatusCode, bodyString)
		}
	}

	bodyBytes, err = io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("ERROR: Failed to read response body: %w", err)
	}
	var result map[string]interface{}
	err = json.Unmarshal(bodyBytes, &result)
	if err != nil {
		return "", fmt.Errorf("ERROR: Failed to unmarshal response body: %w", err)
	}

	if token, ok := result["token"].(string); ok {
		jwtToken = token
		return jwtToken, nil
	} else {
		return "", fmt.Errorf("ERROR: Failed to extract token from response: %s", string(bodyBytes))
	}
}

func uploadToLoadViewerWithRetries(csvData string, retryCount int) (string, error) {
	if jwtToken == "" {
		token, err := getJWTTokenWithRetries(1)
		if err != nil {
			return "", fmt.Errorf("ERROR: Unable to retrieve JWT token for upload: %w", err)
		}
		jwtToken = token
	}

	if retryCount > defaultMaxRetries {
		return "", fmt.Errorf("ERROR: Max upload retries exceeded")
	}

	req, err := http.NewRequest("POST", uploadURL, bytes.NewBufferString(csvData))
	if err != nil {
		return "", fmt.Errorf("ERROR: Failed to create upload request: %w", err)
	}
	req.Header.Set("Content-Type", "text/csv")
	req.Header.Set("Authorization", "Bearer "+jwtToken)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		time.Sleep(time.Duration(generalRetryDelaySeconds) * time.Second)
		return uploadToLoadViewerWithRetries(csvData, retryCount+1)
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusGone {
		fmt.Println("JWT token expired or invalid. Attempting to retrieve a new token.")
		jwtToken = ""
		token, err := getJWTTokenWithRetries(1)
		if err != nil {
			return "", fmt.Errorf("ERROR: Unable to retrieve a new JWT token after token expiration: %w", err)
		}
		jwtToken = token
		return uploadToLoadViewerWithRetries(csvData, retryCount) // Retry with the new token
	} else if resp.StatusCode == http.StatusTooManyRequests {
		if retryCount < defaultMaxRetries {
			fmt.Printf("Rate limit hit on upload endpoint (attempt %d of %d). Waiting %d seconds before retrying.\n", retryCount+1, defaultMaxRetries, defaultRateLimitWaitSeconds)
			time.Sleep(time.Duration(defaultRateLimitWaitSeconds) * time.Second)
			return uploadToLoadViewerWithRetries(csvData, retryCount+1)
		} else {
			return "", fmt.Errorf("ERROR: Upload failed after multiple rate limits")
		}
	} else if resp.StatusCode >= 300 && resp.StatusCode != http.StatusConflict {
		bodyBytes, _ := io.ReadAll(resp.Body)
		bodyString := string(bodyBytes)
		if retryCount < defaultMaxRetries {
			fmt.Printf("Error uploading data (attempt %d of %d): %d - %s. Retrying in %d seconds.\n", retryCount+1, defaultMaxRetries, resp.StatusCode, bodyString, generalRetryDelaySeconds)
			time.Sleep(time.Duration(generalRetryDelaySeconds) * time.Second)
			return uploadToLoadViewerWithRetries(csvData, retryCount+1)
		} else {
			return "", fmt.Errorf("ERROR: Upload failed after %d attempts. Status: %d - %s", defaultMaxRetries, resp.StatusCode, bodyString)
		}
	}

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("ERROR: Failed to read upload response body: %w", err)
	}
	return fmt.Sprintf("%d - %s", resp.StatusCode, string(bodyBytes)), nil
}

func getLoadViewerSuggestionsCsvWithRetries(requestBodyJSON string, retryCount int) (string, error) {
	if jwtToken == "" {
		token, err := getJWTTokenWithRetries(1)
		if err != nil {
			return "", fmt.Errorf("ERROR: Unable to retrieve JWT token for getting suggestions: %w", err)
		}
		jwtToken = token
	}

	if retryCount > defaultMaxRetries {
		return "", fmt.Errorf("ERROR: Max suggestions retries exceeded")
	}

	req, err := http.NewRequest("POST", downloadURL, bytes.NewBufferString(requestBodyJSON))
	if err != nil {
		return "", fmt.Errorf("ERROR: Failed to create suggestions request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+jwtToken)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		time.Sleep(time.Duration(generalRetryDelaySeconds) * time.Second)
		return getLoadViewerSuggestionsCsvWithRetries(requestBodyJSON, retryCount+1)
	}
	defer resp.Body.Close()

	bodyBytes, _ := io.ReadAll(resp.Body)
	bodyString := string(bodyBytes)
	fmt.Printf("POST Request to LoadViewer Suggestions URL: %s\nRequest Body:\n%s\nHTTP Status Code: %d\nResponse Text:\n%s\n", downloadURL, requestBodyJSON, resp.StatusCode, bodyString)

	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusGone {
		fmt.Println("JWT token expired or invalid. Attempting to retrieve a new token.")
		jwtToken = ""
		token, err := getJWTTokenWithRetries(1)
		if err != nil {
			return "", fmt.Errorf("ERROR: Unable to retrieve a new JWT token after token expiration: %w", err)
		}
		jwtToken = token
		return getLoadViewerSuggestionsCsvWithRetries(requestBodyJSON, retryCount) // Retry with the new token
	} else if resp.StatusCode == http.StatusTooManyRequests {
		if retryCount < defaultMaxRetries {
			fmt.Printf("Rate limit hit on suggestions endpoint (attempt %d of %d). Waiting %d seconds before retrying.\n", retryCount+1, defaultMaxRetries, defaultRateLimitWaitSeconds)
			time.Sleep(time.Duration(defaultRateLimitWaitSeconds) * time.Second)
			return getLoadViewerSuggestionsCsvWithRetries(requestBodyJSON, retryCount+1)
		} else {
			return "", fmt.Errorf("ERROR: Failed to retrieve suggestions after multiple rate limits")
		}
	} else if resp.StatusCode >= 300 {
		if retryCount < defaultMaxRetries {
			fmt.Printf("Error retrieving suggestions (attempt %d of %d): %d - %s. Retrying in %d seconds.\n", retryCount+1, defaultMaxRetries, resp.StatusCode, bodyString, generalRetryDelaySeconds)
			time.Sleep(time.Duration(generalRetryDelaySeconds) * time.Second)
			return getLoadViewerSuggestionsCsvWithRetries(requestBodyJSON, retryCount+1)
		} else {
			return "", fmt.Errorf("ERROR: Failed to retrieve suggestions after %d attempts. Status: %d - %s", defaultMaxRetries, resp.StatusCode, bodyString)
		}
	}

	return bodyString, nil
}

func testSuggestions(shipmentReference string, solverEmail string) error {
	if shipmentReference == "" {
		fmt.Println("ERROR: Shipment Reference is required!")
		return nil
	}

	emailRegex := regexp.MustCompile(`^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$`)
	if solverEmail != "" && !emailRegex.MatchString(solverEmail) {
		fmt.Println("ERROR: Invalid Solver Email format!")
		return nil
	}

	if jwtToken == "" {
		token, err := getJWTTokenWithRetries(1)
		if err != nil {
			fmt.Println("ERROR: Token Error or Server Not Reachable:", err)
			return err
		}
		jwtToken = token
	}

	requestBody := map[string]string{
		"reference":   strings.ReplaceAll(shipmentReference, "\"", "\"\""),
		"solverEmail": strings.ReplaceAll(solverEmail, "\"", "\"\""),
	}
	jsonBody, err := json.Marshal(requestBody)
	if err != nil {
		return fmt.Errorf("ERROR: Failed to marshal suggestions request body: %w", err)
	}

	strResponse, err := getLoadViewerSuggestionsCsvWithRetries(string(jsonBody), 1)
	if err != nil {
		fmt.Println("ERROR: retrieving data from LoadViewer:", err)
		return err
	}

	fmt.Printf("Raw Suggestions Response:\n%s\n", strResponse)

	arrLines := strings.Split(strResponse, "\n")
	results := map[string]interface{}{
		"status":        map[string]string{},
		"recommendations": []map[string]interface{}{},
	}

	if len(arrLines) > 0 {
		for _, line := range arrLines {
			arrFields := strings.Split(line, ",")
			if len(arrFields) >= 1 {
				recordType := strings.TrimSpace(arrFields[0])

				switch recordType {
				case "S": // Status Record
					if len(arrFields) == 9 && strings.ToLower(strings.TrimSpace(arrFields[1])) == "d" {
						statusInfo := results["status"].(map[string]string)
						statusInfo["status"] = strings.TrimSpace(arrFields[2])
						statusInfo["message"] = strings.TrimSpace(arrFields[3])
						statusInfo["reference"] = strings.TrimSpace(arrFields[4])
						statusInfo["solverEmail"] = strings.TrimSpace(arrFields[5])
						statusInfo["multipleContainers"] = strings.TrimSpace(arrFields[6])
						statusInfo["suggestExtra"] = strings.TrimSpace(arrFields[7])
					} else if len(arrFields) == 9 && strings.ToLower(strings.TrimSpace(arrFields[1])) == "h" {
						// Header row, can be ignored
					} else if len(strings.TrimSpace(line)) > 0 {
						fmt.Println("ERROR: Invalid Status record format:", line)
					}
				case "R": // Recommendation Record
					if len(arrFields) == 16 && strings.ToLower(strings.TrimSpace(arrFields[1])) == "d" {
						recommendation := map[string]interface{}{
							"size":              strings.TrimSpace(arrFields[2]),
							"cbm":               strings.TrimSpace(arrFields[3]),
							"weight":            strings.TrimSpace(arrFields[4]),
							"noOfPackages":      parseInt(strings.TrimSpace(arrFields[5])),
							"isPacked":          strings.TrimSpace(arrFields[6]),
							"packedPackages":    parseInt(strings.TrimSpace(arrFields[7])),
							"suggestedIncrease": parseInt(strings.TrimSpace(arrFields[8])),
							"suggestedDecrease": parseInt(strings.TrimSpace(arrFields[9])),
							"erpId":             strings.TrimSpace(arrFields[10]),
							"skuNumber":         strings.TrimSpace(arrFields[11]),
							"description":       strings.TrimSpace(arrFields[12]),
							"po":                strings.TrimSpace(arrFields[13]),
							"shipper":           strings.TrimSpace(arrFields[14]),
						}
						recommendations := results["recommendations"].([]map[string]interface{})
						results["recommendations"] = append(recommendations, recommendation)
					} else if len(arrFields) == 16 && strings.ToLower(strings.TrimSpace(arrFields[1])) == "h" {
						// Header row, can be ignored
					} else if len(strings.TrimSpace(line)) > 0 {
						fmt.Println("ERROR: Invalid Recommendation record format:", line)
					}
				default:
					if strings.TrimSpace(line) != "" {
						fmt.Println("ERROR: Unknown record type:", recordType, "in line:", line)
					}
				}
			}
		}
	} else {
		fmt.Println("Empty suggestions response.")
	}

	if statusInfo, ok := results["status"].(map[string]string); ok {
		fmt.Printf("Status: %s - %s\n", statusInfo["status"], statusInfo["message"])
		fmt.Printf("Reference: %s\n", statusInfo["reference"])
		fmt.Printf("Solver Email: %s\n", statusInfo["solverEmail"])
		fmt.Printf("Multiple Containers: %s\n", statusInfo["multipleContainers"])
		fmt.Printf("Extra Suggested: %s\n", statusInfo["suggestExtra"])
	}

	if recs, ok := results["recommendations"].([]map[string]interface{}); ok {
		if len(recs) > 0 {
			fmt.Println("--- Suggestions ---")
			for _, recommendation := range recs {
				fmt.Printf("Is Packed: %s, Size LxWxH: %s, CBM: %s, Weight Kg: %s, Packages: %d, Packed: %d, Increase: %d, Decrease: %d, ErpId: %s, SkuNumber: %s, Description: %s, PO#:%s, Division: %s\n",
					recommendation["isPacked"],
					recommendation["size"],
					recommendation["cbm"],
					recommendation["weight"],
					recommendation["noOfPackages"],
					recommendation["packedPackages"],
					recommendation["suggestedIncrease"],
					recommendation["suggestedDecrease"],
					recommendation["erpId"],
					recommendation["skuNumber"],
					recommendation["description"],
					recommendation["po"],
					recommendation["shipper"],
				)
				// --- Your ERP Processing Logic for Suggestions Here ---
			}
		} else {
			fmt.Printf("No suggestions found for reference: %s\n", shipmentReference)
		}
	} else {
		fmt.Println("Error parsing recommendations.")
	}

	return nil
}

func removeComma(value string) string {
	return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(value, ",", ""), "\r\n", " "), "\n", " ")
}

func formatDecimal3(value float64) string {
	return strings.TrimSuffix(strings.TrimSuffix(fmt.Sprintf("%.3f", value), "0"), ".")
}

func generateLoadViewerCsv() string {
	var csvLines []string

	// Shipment Data
	shipmentData := map[string]interface{}{
		"reference":           "SANDBOX-01",
		"hangingAllowed":      true,
		"useMultipleContainers": false,
		"uniqueErpId":         false,
		"suggestExtra":        false,
	}
	csvLines = append(csvLines, "S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra")
	csvLines = append(csvLines, fmt.Sprintf("S,D,%s,%t,%t,%t,%t",
		removeComma(shipmentData["reference"].(string)),
		shipmentData["hangingAllowed"].(bool),
		shipmentData["useMultipleContainers"].(bool),
		shipmentData["uniqueErpId"].(bool),
		shipmentData["suggestExtra"].(bool),
	))

	// Container Data
	containerData := map[string]interface{}{
		"lengthMm":      5867,
		"widthMm":       2352,
		"heightMm":      2393,
		"maxWeightKg":   27200.0,
		"backMarginMm":  0,
		"isRefrigerated": false,
		"maxUseOne":     false,
		"lclBreakEvenCBM": 0.0,
	}
	csvLines = append(csvLines, "B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM")
	csvLines = append(csvLines, fmt.Sprintf("B,D,%d,%d,%d,%s,%d,%t,%t,%s",
		containerData["lengthMm"].(int),
		containerData["widthMm"].(int),
		containerData["heightMm"].(int),
		formatDecimal3(containerData["maxWeightKg"].(float64)),
		containerData["backMarginMm"].(int),
		containerData["isRefrigerated"].(bool),
		containerData["maxUseOne"].(bool),
		formatDecimal3(containerData["lclBreakEvenCBM"].(float64)),
	))

	// Cargo Data
	cargoData := map[string]interface{}{
		"color":                 "#008000",
		"lengthMm":              700,
		"widthMm":               500,
		"heightMm":              300,
		"weightKg":              91.5,
		"cartons":               300,
		"additionalCartons":     0,
		"placement":             0,
		"verticalRotationAllowed": true,
		"skuPerCarton":          1,
		"emptyPalletHeightMm":   0,
		"trayCapMm":             0,
		"trayHeightMm":          0,
		"marginLength":          0,
		"marginWidth":           0,
		"marginHeight":          0,
		"nameOfSet":             "",
		"cartonRatioInSet":      0,
		"erpId":                 "",
		"skuNumber":             "",
		"description":           "",
		"destination":           0,
		"po":                    "",
		"shipper":               "",
	}
	csvLines = append(csvLines, "C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper")
	csvLines = append(csvLines, fmt.Sprintf("C,D,%s,%d,%d,%d,%s,%d,%d,%d,%t,%d,%d,%d,%d,%d,%d,%d,%s,%d,%s,%s,%s,%d,%s,%s",
		removeComma(cargoData["color"].(string)),
		cargoData["lengthMm"].(int),
		cargoData["widthMm"].(int),
		cargoData["heightMm"].(int),
		formatDecimal3(cargoData["weightKg"].(float64)),
		cargoData["cartons"].(int),
		cargoData["additionalCartons"].(int),
		cargoData["placement"].(int),
		cargoData["verticalRotationAllowed"].(bool),
		cargoData["skuPerCarton"].(int),
		cargoData["emptyPalletHeightMm"].(int),
		cargoData["trayCapMm"].(int),
		cargoData["trayHeightMm"].(int),
		cargoData["marginLength"].(int),
		cargoData["marginWidth"].(int),
		cargoData["marginHeight"].(int),
		removeComma(cargoData["nameOfSet"].(string)),
		cargoData["cartonRatioInSet"].(int),
		removeComma(cargoData["erpId"].(string)),
		removeComma(cargoData["skuNumber"].(string)),
		removeComma(cargoData["description"].(string)),
		cargoData["destination"].(int),
		removeComma(cargoData["po"].(string)),
		removeComma(cargoData["shipper"].(string)),
	))

	return strings.Join(csvLines, "\n")
}

func parseInt(s string) int {
	i, _ := strconv.Atoi(s) // Ignoring error for simplicity, assuming valid integer
	return i
}

func main() {
	csvData := generateLoadViewerCsv()
	uploadResult, err := uploadToLoadViewerWithRetries(csvData, 1)
	if err != nil {
		fmt.Println("Upload Result Error:", err)
	} else {
		fmt.Println("Upload Result:", uploadResult)
	}

	err = testSuggestions("SANDBOX-01", "test@example.com")
	if err != nil {
		fmt.Println("Test Suggestions Error:", err)
	}
}

R


# Define constants
token_url <- "https://www.loadviewer.com/api/auth/jwt/token"
upload_url <- "https://www.loadviewer.com/api/integration/v1/upload"
download_url <- "https://www.loadviewer.com/api/integration/v1/download"
api_email <- "YOUR_Email"       # Replace with your actual email
api_key <- "YOUR_API_KEY_HERE" # Replace with your actual API key
jwt_token <- NULL
default_max_retries <- 3
default_rate_limit_wait_seconds <- 65
general_retry_delay_seconds <- 5

# Function to get JWT token with retries
get_jwt_token_with_retries <- function(retry_count = 0) {
  if (!is.null(jwt_token)) {
    return(jwt_token)
  }
  if (retry_count > default_max_retries) {
    stop("ERROR: Failed to retrieve token after multiple retries.")
  }

  post_data <- list(email = api_email, apikey = api_key)
  response <- tryCatch(
    httr::POST(token_url, body = post_data, encode = "json"),
    error = function(e) {
      Sys.sleep(general_retry_delay_seconds)
      get_jwt_token_with_retries(retry_count + 1)
    }
  )

  if (httr::status_code(response) == 429) {
    if (retry_count < default_max_retries) {
      cat(sprintf("Rate limit hit on token endpoint (attempt %d < %d). Waiting %d seconds before retrying.\n",
                  retry_count + 1, default_max_retries, default_rate_limit_wait_seconds))
      Sys.sleep(default_rate_limit_wait_seconds)
      return(get_jwt_token_with_retries(retry_count + 1))
    } else {
      stop("ERROR: Failed to retrieve token after multiple rate limits.")
    }
  } else if (httr::status_code(response) >= 300) {
    body <- httr::content(response, "text")
    if (retry_count < default_max_retries) {
      cat(sprintf("Error retrieving token (attempt %d < %d): %d - %s. Retrying in %d seconds.\n",
                  retry_count + 1, default_max_retries, httr::status_code(response), body, general_retry_delay_seconds))
      Sys.sleep(general_retry_delay_seconds)
      return(get_jwt_token_with_retries(retry_count + 1))
    } else {
      stop(sprintf("ERROR: Failed to retrieve token after %d retries: %d - %s", default_max_retries, httr::status_code(response), body))
    }
  }

  result <- httr::content(response, "parsed")
  if (!is.null(result$token)) {
    jwt_token <<- result$token
    return(jwt_token)
  } else {
    stop(sprintf("ERROR: Failed to extract token from response: %s", httr::content(response, "text")))
  }
}

# Function to upload to LoadViewer with retries
upload_to_loadviewer_with_retries <- function(csv_data, retry_count = 0) {
  if (is.null(jwt_token)) {
    jwt_token <<- get_jwt_token_with_retries()
    if (is.null(jwt_token)) {
      stop("ERROR: Unable to retrieve JWT token for upload.")
    }
  }
  if (retry_count > default_max_retries) {
    stop("ERROR: Max upload retries exceeded.")
  }

  response <- tryCatch(
    httr::POST(upload_url,
               body = csv_data,
               httr::add_headers("Content-Type" = "text/csv",
                                 "Authorization" = paste("Bearer", jwt_token))),
    error = function(e) {
      Sys.sleep(general_retry_delay_seconds)
      upload_to_loadviewer_with_retries(csv_data, retry_count + 1)
    }
  )

  if (httr::status_code(response) %in% c(401, 410)) {
    cat("JWT token expired or invalid. Attempting to retrieve a new token.\n")
    jwt_token <<- NULL
    jwt_token <<- get_jwt_token_with_retries()
    if (is.null(jwt_token)) {
      stop("ERROR: Unable to retrieve a new JWT token after token expiration.")
    }
    return(upload_to_loadviewer_with_retries(csv_data, retry_count)) # Retry with new token
  } else if (httr::status_code(response) == 429) {
    if (retry_count < default_max_retries) {
      cat(sprintf("Rate limit hit on upload endpoint (attempt %d < %d). Waiting %d seconds before retrying.\n",
                  retry_count + 1, default_max_retries, default_rate_limit_wait_seconds))
      Sys.sleep(default_rate_limit_wait_seconds)
      return(upload_to_loadviewer_with_retries(csv_data, retry_count + 1))
    } else {
      stop("ERROR: Upload failed after multiple rate limits.")
    }
  } else if (httr::status_code(response) >= 300 && httr::status_code(response) != 409) {
    body <- httr::content(response, "text")
    if (retry_count < default_max_retries) {
      cat(sprintf("Error uploading data (attempt %d < %d): %d - %s. Retrying in %d seconds.\n",
                  retry_count + 1, default_max_retries, httr::status_code(response), body, general_retry_delay_seconds))
      Sys.sleep(general_retry_delay_seconds)
      return(upload_to_loadviewer_with_retries(csv_data, retry_count + 1))
    } else {
      stop(sprintf("ERROR: Upload failed after %d attempts. Status: %d - %s", default_max_retries, httr::status_code(response), body))
    }
  }

  return(sprintf("%d - %s", httr::status_code(response), httr::content(response, "text")))
}

# Function to get LoadViewer suggestions CSV with retries
get_loadviewer_suggestions_csv_with_retries <- function(request_body_json, retry_count = 0) {
  if (is.null(jwt_token)) {
    jwt_token <<- get_jwt_token_with_retries()
    if (is.null(jwt_token)) {
      stop("ERROR: Unable to retrieve JWT token for getting suggestions.")
    }
  }
  if (retry_count > default_max_retries) {
    stop("ERROR: Max suggestions retries exceeded.")
  }

  response <- tryCatch(
    httr::POST(download_url,
               body = request_body_json,
               httr::add_headers("Content-Type" = "application/json",
                                 "Authorization" = paste("Bearer", jwt_token))),
    error = function(e) {
      Sys.sleep(general_retry_delay_seconds)
      get_loadviewer_suggestions_csv_with_retries(request_body_json, retry_count + 1)
    }
  )

  cat(sprintf("POST Request to LoadViewer Suggestions URL: %s\nRequest Body:\n%s\nHTTP Status Code: %d\nResponse Text:\n%s\n",
              download_url, request_body_json, httr::status_code(response), httr::content(response, "text")))

  if (httr::status_code(response) %in% c(401, 410)) {
    cat("JWT token expired or invalid. Attempting to retrieve a new token.\n")
    jwt_token <<- NULL
    jwt_token <<- get_jwt_token_with_retries()
    if (is.null(jwt_token)) {
      stop("ERROR: Unable to retrieve a new JWT token after token expiration.")
    }
    return(get_loadviewer_suggestions_csv_with_retries(request_body_json, retry_count)) # Retry with new token
  } else if (httr::status_code(response) == 429) {
    if (retry_count < default_max_retries) {
      cat(sprintf("Rate limit hit on suggestions endpoint (attempt %d < %d). Waiting %d seconds before retrying.\n",
                  retry_count + 1, default_max_retries, default_rate_limit_wait_seconds))
      Sys.sleep(default_rate_limit_wait_seconds)
      return(get_loadviewer_suggestions_csv_with_retries(request_body_json, retry_count + 1))
    } else {
      stop("ERROR: Failed to retrieve suggestions after multiple rate limits.")
    }
  } else if (httr::status_code(response) >= 300) {
    body <- httr::content(response, "text")
    if (retry_count < default_max_retries) {
      cat(sprintf("Error retrieving suggestions (attempt %d < %d): %d - %s. Retrying in %d seconds.\n",
                  retry_count + 1, default_max_retries, httr::status_code(response), body, general_retry_delay_seconds))
      Sys.sleep(general_retry_delay_seconds)
      return(get_loadviewer_suggestions_csv_with_retries(request_body_json, retry_count + 1))
    } else {
      stop(sprintf("ERROR: Failed to retrieve suggestions after %d attempts. Status: %d - %s", default_max_retries, httr::status_code(response), body))
    }
  }

  return(httr::content(response, "text"))
}

# Function to test suggestions
test_suggestions <- function(shipment_reference, solver_email) {
  if (nchar(shipment_reference) == 0) {
    cat("ERROR: Shipment Reference is required!\n")
    return()
  }
  if (nchar(solver_email) > 0 && !grepl("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$", solver_email)) {
    cat("ERROR: Invalid Solver Email format!\n")
    return()
  }
  if (is.null(jwt_token)) {
    jwt_token <<- get_jwt_token_with_retries()
    if (is.null(jwt_token)) {
      cat("ERROR: Token Error or Server Not Reachable.\n")
      return()
    }
  }

  request_body <- jsonlite::toJSON(list(reference = gsub("\"", "\"\"", shipment_reference),
                                       solverEmail = gsub("\"", "\"\"", solver_email)), auto_unbox = TRUE)
  str_response <- get_loadviewer_suggestions_csv_with_retries(request_body)

  if (startsWith(str_response, "ERROR:")) {
    cat(sprintf("ERROR: retrieving data from LoadViewer: %s\n", str_response))
    return()
  }

  cat(sprintf("Raw Suggestions Response:\n%s\n", str_response))

  arr_lines <- strsplit(str_response, "\n")[[1]]
  results <- list(status = list(), recommendations = list())

  if (length(arr_lines) > 0) {
    for (line in arr_lines) {
      arr_fields <- strsplit(line, ",")[[1]]
      if (length(arr_fields) >= 1) {
        record_type <- trimws(arr_fields[1])

        if (trimws(arr_fields[1]) == 'S') { # Status Record
          if (length(arr_fields) == 9 && tolower(trimws(arr_fields[2])) == 'd') {
            results$status$status <- trimws(arr_fields[3])
            results$status$message <- trimws(arr_fields[4])
            results$status$reference <- trimws(arr_fields[5])
            results$status$solverEmail <- trimws(arr_fields[6])
            results$status$multipleContainers <- trimws(arr_fields[7])
            results$status$suggestExtra <- trimws(arr_fields[8])
          } else if (length(arr_fields) == 9 && tolower(trimws(arr_fields[2])) == 'h') {
            # Header row, can be ignored
          } else if (nchar(trimws(line)) > 0) {
            cat(sprintf("ERROR: Invalid Status record format: %s\n", line))
          }
        } else if (trimws(arr_fields[1]) == 'R') { # Recommendation Record
          if (length(arr_fields) == 16 && tolower(trimws(arr_fields[2])) == 'd') {
            recommendation <- list(
              size = trimws(arr_fields[3]),
              cbm = trimws(arr_fields[4]),
              weight = trimws(arr_fields[5]),
              noOfPackages = as.integer(trimws(arr_fields[6])),
              isPacked = trimws(arr_fields[7]),
              packedPackages = as.integer(trimws(arr_fields[8])),
              suggestedIncrease = as.integer(trimws(arr_fields[9])),
              suggestedDecrease = as.integer(trimws(arr_fields[10])),
              erpId = trimws(arr_fields[11]),
              skuNumber = trimws(arr_fields[12]),
              description = trimws(arr_fields[13]),
              po = trimws(arr_fields[14]),
              shipper = trimws(arr_fields[15])
            )
            results$recommendations <- append(results$recommendations, list(recommendation))
          } else if (length(arr_fields) == 16 && tolower(trimws(arr_fields[2])) == 'h') {
            # Header row, can be ignored
          } else if (nchar(trimws(line)) > 0) {
            cat(sprintf("ERROR: Invalid Recommendation record format: %s\n", line))
          }
        } else if (nchar(trimws(line)) > 0) {
          cat(sprintf("ERROR: Unknown record type: %s in line: %s\n", record_type, line))
        }
      }
    }
  } else {
    cat("Empty suggestions response.\n")
  }

  if (length(results$status) > 0) {
    cat(sprintf("Status: %s - %s\n", results$status$status, results$status$message))
    cat(sprintf("Reference: %s\n", results$status$reference))
    cat(sprintf("Solver Email: %s\n", results$status$solverEmail))
    cat(sprintf("Multiple Containers: %s\n", results$status$multipleContainers))
    cat(sprintf("Extra Suggested: %s\n", results$status$suggestExtra))
  }

  if (length(results$recommendations) > 0) {
    cat("--- Suggestions ---\n")
    for (recommendation in results$recommendations) {
      cat(sprintf("Is Packed: %s, Size LxWxH: %s, CBM: %s, Weight Kg: %s, Packages: %d, Packed: %d, Increase: %d, Decrease: %d, ErpId: %s, SkuNumber: %s, Description: %s, PO#: %s, Division: %s \n",
                  recommendation$isPacked,
                  recommendation$size,
                  recommendation$cbm,
                  recommendation$weight,
                  recommendation$noOfPackages,
                  recommendation$packedPackages,
                  recommendation$suggestedIncrease,
                  recommendation$suggestedDecrease,
                  recommendation$erpId,
                  recommendation$skuNumber,
                  recommendation$description,
                  recommendation$po,
                  recommendation$shipper
                  ))
      # --- Your ERP Processing Logic for Suggestions Here ---
    }
  } else {
    cat(sprintf("No suggestions found for reference: %s\n", shipment_reference))
  }
}

# Function to remove comma
remove_comma <- function(value) {
  gsub(",", "", gsub("[\r\n]", " ", value))
}

# Function to format decimal with 3 places
format_decimal_3 <- function(value) {
  sprintf("%.3f", value)
}

# Function to generate LoadViewer CSV data (example)
generate_loadviewer_csv <- function() {
  shipment_data <- data.frame(
    record_type = "S",
    header_detail = c("H", "D"),
    reference = c(NA, remove_comma("SANDBOX-01")),
    hangingAllowed = c(NA, TRUE),
    useMultipleContainers = c(NA, FALSE),
    uniqueErpId = c(NA, FALSE),
    suggestExtra = c(NA, FALSE)
  )

  container_data <- data.frame(
    record_type = "B",
    header_detail = c("H", "D"),
    lengthMm = c(NA, 5867),
    widthMm = c(NA, 2352),
    heightMm = c(NA, 2393),
    maxWeightKg = c(NA, format_decimal_3(27200.0)),
    backMarginMm = c(NA, 0),
    isRefrigerated = c(NA, FALSE),
    maxUseOne = c(NA, FALSE),
    lclBreakEvenCBM = c(NA, format_decimal_3(0.0))
  )

  cargo_data <- data.frame(
    record_type = "C",
    header_detail = c("H", "D"),
    color = c(NA, remove_comma("#008000")),
    lengthMm = c(NA, 700),
    widthMm = c(NA, 500),
    heightMm = c(NA, 300),
    weightKg = c(NA, format_decimal_3(91.5)),
    cartons = c(NA, 300),
    additionalCartons = c(NA, 0),
    placement = c(NA, 0),
    verticalRotationAllowed = c(NA, TRUE),
    skuPerCarton = c(NA, 1),
    emptyPalletHeightMm = c(NA, 0),
    trayCapMm = c(NA, 0),
    trayHeightMm = c(NA, 0),
    marginLenghtMm = c(NA, 0),
    marginWidthMm = c(NA, 0),
    marginHeightMm = c(NA, 0),
    nameOfSet = c(NA, remove_comma("")),
    cartonRatioInSet = c(NA, 0),
    erpId = c(NA, remove_comma("")),
    skuNumber = c(NA, remove_comma("")),
    description = c(NA, remove_comma("")),
    destination = c(NA, 0),
    po = c(NA, remove_comma("")),
    shipper = c(NA, remove_comma(""))
  )

  shipment_csv <- paste0(apply(shipment_data, 1, paste, collapse = ","), collapse = "\n")
  container_csv <- paste0(apply(container_data, 1, paste, collapse = ","), collapse = "\n")
  cargo_csv <- paste0(apply(cargo_data, 1, paste, collapse = ","), collapse = "\n")

  return(paste(shipment_csv, container_csv, cargo_csv, sep = "\n"))
}

# Main execution
if (interactive()) {
  csv_data <- generate_loadviewer_csv()
  upload_result <- upload_to_loadviewer_with_retries(csv_data)
  cat(sprintf("Upload Result: %s\n", upload_result))

  test_suggestions("SANDBOX-01", "test@example.com")
}

PHP

<?php

$token_url = "https://www.loadviewer.com/api/auth/jwt/token";
$upload_url = "https://www.loadviewer.com/api/integration/v1/upload";
$download_url = "https://www.loadviewer.com/api/integration/v1/download";
$api_email = "YOUR_Email"; // Replace with your actual email
$api_key = "YOUR_API_KEY_HERE"; // Replace with your actual API key
$jwt_token = null;

const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_RATE_LIMIT_WAIT_SECONDS = 65;
const GENERAL_RETRY_DELAY_SECONDS = 5;

function get_jwt_token_with_retries($retry_count = 0) {
    global $jwt_token, $token_url, $api_email, $api_key;

    if ($jwt_token !== null) {
        return $jwt_token;
    }

    if ($retry_count > DEFAULT_MAX_RETRIES) {
        error_log("ERROR: Failed to retrieve token after multiple retries.");
        return null;
    }

    $post_data = json_encode(['email' => $api_email, 'apikey' => $api_key]);

    $ch = curl_init($token_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);

    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    if ($http_code === 429) {
        if ($retry_count < DEFAULT_MAX_RETRIES) {
            $retry_count++;
            error_log("Rate limit hit on token endpoint (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "). Waiting " . DEFAULT_RATE_LIMIT_WAIT_SECONDS . " seconds before retrying.");
            sleep(DEFAULT_RATE_LIMIT_WAIT_SECONDS);
            curl_close($ch);
            return get_jwt_token_with_retries($retry_count);
        } else {
            error_log("ERROR: Failed to retrieve token after multiple rate limits.");
            curl_close($ch);
            return null;
        }
    } elseif ($http_code >= 300) {
        if ($retry_count < DEFAULT_MAX_RETRIES) {
            $retry_count++;
            error_log("Error retrieving token (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "): {$http_code} - {$response}. Retrying in " . GENERAL_RETRY_DELAY_SECONDS . " seconds.");
            sleep(GENERAL_RETRY_DELAY_SECONDS);
            curl_close($ch);
            return get_jwt_token_with_retries($retry_count);
        } else {
            error_log("ERROR: Failed to retrieve token after " . DEFAULT_MAX_RETRIES . " retries: {$http_code} - {$response}");
            curl_close($ch);
            return null;
        }
    }

    if ($response === false) {
        if ($retry_count < DEFAULT_MAX_RETRIES) {
            $retry_count++;
            error_log("ERROR: cURL error while retrieving token (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "): " . curl_error($ch) . ". Retrying in " . GENERAL_RETRY_DELAY_SECONDS . " seconds.");
            sleep(GENERAL_RETRY_DELAY_SECONDS);
            curl_close($ch);
            return get_jwt_token_with_retries($retry_count);
        } else {
            error_log("ERROR: cURL error while retrieving token after " . DEFAULT_MAX_RETRIES . " retries: " . curl_error($ch));
            curl_close($ch);
            return null;
        }
    }

    curl_close($ch);
    $result = json_decode($response, true);
    if (isset($result['token'])) {
        $jwt_token = $result['token'];
        return $jwt_token;
    } else {
        error_log("ERROR: Failed to extract token from response: {$response}");
        return null;
    }
}

function upload_to_loadviewer_with_retries($csv_data, $retry_count = 0) {
    global $jwt_token, $upload_url;

    if ($jwt_token === null) {
        $jwt_token = get_jwt_token_with_retries();
        if ($jwt_token === null) {
            return "ERROR: Unable to retrieve JWT token for upload.";
        }
    }

    if ($retry_count > DEFAULT_MAX_RETRIES) {
        return "ERROR: Max upload retries exceeded.";
    }

    $ch = curl_init($upload_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $csv_data);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: text/csv',
        'Authorization: Bearer ' . $jwt_token,
    ]);

    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    if ($http_code === 401 || $http_code === 410) {
        error_log("JWT token expired or invalid. Attempting to retrieve a new token.");
        $jwt_token = null;
        $jwt_token = get_jwt_token_with_retries();
        if ($jwt_token === null) {
            curl_close($ch);
            return "ERROR: Unable to retrieve a new JWT token after token expiration.";
        }
        curl_close($ch);
        return upload_to_loadviewer_with_retries($csv_data, $retry_count); // Retry with the new token
    } elseif ($http_code === 429) {
        if ($retry_count < DEFAULT_MAX_RETRIES) {
            $retry_count++;
            error_log("Rate limit hit on upload endpoint (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "). Waiting " . DEFAULT_RATE_LIMIT_WAIT_SECONDS . " seconds before retrying.");
            sleep(DEFAULT_RATE_LIMIT_WAIT_SECONDS);
            curl_close($ch);
            return upload_to_loadviewer_with_retries($csv_data, $retry_count);
        } else {
            curl_close($ch);
            return "ERROR: Upload failed after multiple rate limits.";
        }
    } elseif ($http_code >= 300 && $http_code !== 409) {
        if ($retry_count < DEFAULT_MAX_RETRIES) {
            $retry_count++;
            error_log("Error uploading data (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "): {$http_code} - {$response}. Retrying in " . GENERAL_RETRY_DELAY_SECONDS . " seconds.");
            sleep(GENERAL_RETRY_DELAY_SECONDS);
            curl_close($ch);
            return upload_to_loadviewer_with_retries($csv_data, $retry_count);
        } else {
            curl_close($ch);
            return "ERROR: Upload failed after " . DEFAULT_MAX_RETRIES . " attempts. Status: {$http_code} - {$response}";
        }
    }

    if ($response === false) {
        if ($retry_count < DEFAULT_MAX_RETRIES) {
            $retry_count++;
            error_log("ERROR: cURL error during upload (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "): " . curl_error($ch) . ". Retrying in " . GENERAL_RETRY_DELAY_SECONDS . " seconds.");
            sleep(GENERAL_RETRY_DELAY_SECONDS);
            curl_close($ch);
            return upload_to_loadviewer_with_retries($csv_data, $retry_count);
        } else {
            curl_close($ch);
            return "ERROR: cURL error during upload after " . DEFAULT_MAX_RETRIES . " retries: " . curl_error($ch);
        }
    }

    curl_close($ch);
    return "{$http_code} - {$response}";
}

function get_loadviewer_suggestions_csv_with_retries($request_body_json, $retry_count = 0) {
    global $jwt_token, $download_url;

    if ($jwt_token === null) {
        $jwt_token = get_jwt_token_with_retries();
        if ($jwt_token === null) {
            return "ERROR: Unable to retrieve JWT token for getting suggestions.";
        }
    }

    if ($retry_count > DEFAULT_MAX_RETRIES) {
        return "ERROR: Max suggestions retries exceeded.";
    }

    $ch = curl_init($download_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $request_body_json);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $jwt_token,
    ]);

    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    error_log("POST Request to LoadViewer Suggestions URL: {$download_url}\nRequest Body:\n{$request_body_json}\nHTTP Status Code: {$http_code}\nResponse Text:\n{$response}");

    if ($http_code === 401 || $http_code === 410) {
        error_log("JWT token expired or invalid. Attempting to retrieve a new token.");
        $jwt_token = null;
        $jwt_token = get_jwt_token_with_retries();
        if ($jwt_token === null) {
            curl_close($ch);
            return "ERROR: Unable to retrieve a new JWT token after token expiration.";
        }
        curl_close($ch);
        return get_loadviewer_suggestions_csv_with_retries($request_body_json, $retry_count); // Retry with the new token
    } elseif ($http_code === 429) {
        if ($retry_count < DEFAULT_MAX_RETRIES) {
            $retry_count++;
            error_log("Rate limit hit on suggestions endpoint (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "). Waiting " . DEFAULT_RATE_LIMIT_WAIT_SECONDS . " seconds before retrying.");
            sleep(DEFAULT_RATE_LIMIT_WAIT_SECONDS);
            curl_close($ch);
            return get_loadviewer_suggestions_csv_with_retries($request_body_json, $retry_count);
        } else {
            curl_close($ch);
            return "ERROR: Failed to retrieve suggestions after multiple rate limits.";
        }
    } elseif ($http_code >= 300) {
        if ($retry_count < DEFAULT_MAX_RETRIES) {
            $retry_count++;
            error_log("Error retrieving suggestions (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "): {$http_code} - {$response}. Retrying in " . GENERAL_RETRY_DELAY_SECONDS . " seconds.");
            sleep(GENERAL_RETRY_DELAY_SECONDS);
            curl_close($ch);
            return get_loadviewer_suggestions_csv_with_retries($request_body_json, $retry_count);
        } else {
            curl_close($ch);
            return "ERROR: Failed to retrieve suggestions after " . DEFAULT_MAX_RETRIES . " attempts. Status: {$http_code} - {$response}";
        }
    }

    if ($response === false) {
        if ($retry_count < DEFAULT_MAX_RETRIES) {
            $retry_count++;
            error_log("ERROR: cURL error while retrieving suggestions (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "): " . curl_error($ch) . ". Retrying in " . GENERAL_RETRY_DELAY_SECONDS . " seconds.");
            sleep(GENERAL_RETRY_DELAY_SECONDS);
            curl_close($ch);
            return get_loadviewer_suggestions_csv_with_retries($request_body_json, $retry_count);
        } else {
            curl_close($ch);
            return "ERROR: cURL error while retrieving suggestions after " . DEFAULT_MAX_RETRIES . " retries: " . curl_error($ch);
        }
    }

    curl_close($ch);
    return $response;
}

function test_suggestions($shipment_reference, $solver_email) {
    if (empty($shipment_reference)) {
        error_log("ERROR: Shipment Reference is required!");
        return;
    }

    if (!empty($solver_email) && !filter_var($solver_email, FILTER_VALIDATE_EMAIL)) {
        error_log("ERROR: Invalid Solver Email format!");
        return;
    }

    global $jwt_token;
    if ($jwt_token === null) {
        $jwt_token = get_jwt_token_with_retries();
        if ($jwt_token === null) {
            error_log("ERROR: Token Error or Server Not Reachable.");
            return;
        }
    }

    $request_body = json_encode(['reference' => str_replace('"', '""', $shipment_reference), 'solverEmail' => str_replace('"', '""', $solver_email)]);
    $str_response = get_loadviewer_suggestions_csv_with_retries($request_body);

    if (strpos($str_response, "ERROR:") === 0) {
        error_log("ERROR: retrieving data from LoadViewer: " . $str_response);
        return;
    }

    echo "Raw Suggestions Response:\n" . $str_response . "\n";

    $arr_lines = preg_split('/\r\n|\r|\n/', $str_response);
    $results = [];
    $status_info = [];
    $recommendations = [];

    $results['status'] = &$status_info;
    $results['recommendations'] = &$recommendations;

    if (count($arr_lines) > 0) {
        foreach ($arr_lines as $line) {
            $arr_fields = explode(',', $line);
            if (count($arr_fields) >= 1) {
                $record_type = trim($arr_fields[0]);

                if ($record_type === 'S') { // Status Record
                    if (count($arr_fields) === 9 && strtolower(trim($arr_fields[1])) === 'd') {
                        $status_info['status'] = trim($arr_fields[2]);
                        $status_info['message'] = trim($arr_fields[3]);
                        $status_info['reference'] = trim($arr_fields[4]);
                        $status_info['solverEmail'] = trim($arr_fields[5]);
                        $status_info['multipleContainers'] = trim($arr_fields[6]);
                        $status_info['suggestExtra'] = trim($arr_fields[7]);
                    } elseif (count($arr_fields) === 9 && strtolower(trim($arr_fields[1])) === 'h') {
                        // Header row, can be ignored
                    } else {
                        error_log("ERROR: Invalid Status record format: " . $line);
                    }
                } elseif ($record_type === 'R') { // Recommendation Record
                    if (count($arr_fields) === 16 && strtolower(trim($arr_fields[1])) === 'd') {
                        $recommendation = [];
                        $recommendation['size'] = trim($arr_fields[2]);
                        $recommendation['cbm'] = trim($arr_fields[3]);
                        $recommendation['weight'] = trim($arr_fields[4]);
                        $recommendation['noOfPackages'] = intval(trim($arr_fields[5]));
                        $recommendation['isPacked'] = trim($arr_fields[6]);
                        $recommendation['packedPackages'] = intval(trim($arr_fields[7]));
                        $recommendation['suggestedIncrease'] = intval(trim($arr_fields[8]));
                        $recommendation['suggestedDecrease'] = intval(trim($arr_fields[9]));
                        $recommendation['erpId'] = trim($arr_fields[10]);
                        $recommendation['skuNumber'] = trim($arr_fields[11]);
                        $recommendation['description'] = trim($arr_fields[12]);
                        $recommendation['po'] = trim($arr_fields[13]);
                        $recommendation['shipper'] = trim($arr_fields[14]);
                        $recommendations[] = $recommendation;
                    } elseif (count($arr_fields) === 16 && strtolower(trim($arr_fields[1])) === 'h') {// Header row, can be ignored.
                    } else {
                        error_log("ERROR: Invalid Recommendation record format: " . $line);
                    }
                } elseif (!empty($record_type)) {
                    error_log("ERROR: Unknown record type: " . $record_type . " in line: " . $line);
                }
            }
        }
    } else {
        echo "Empty suggestions response.\n";
    }

    if (isset($results['status'])) {
        $status = $results['status'];
        echo "Status: " . (isset($status['status']) ? $status['status'] : '') . " - " . (isset($status['message']) ? $status['message'] : '') . "\n";
        echo "Reference: " . (isset($status['reference']) ? $status['reference'] : '') . "\n";
        echo "Solver Email: " . (isset($status['solverEmail']) ? $status['solverEmail'] : '') . "\n";
        echo "Multiple Containers: " . (isset($status['multipleContainers']) ? $status['multipleContainers'] : '') . "\n";
        echo "Extra Suggested: " . (isset($status['suggestExtra']) ? $status['suggestExtra'] : '') . "\n";

        if (isset($results['recommendations']) && count($results['recommendations']) > 0) {
            echo "--- Suggestions ---\n";
            foreach ($results['recommendations'] as $recommendation) {
                echo "Is Packed: " . (isset($recommendation['isPacked']) ? $recommendation['isPacked'] : '') . ", ";
                echo "Size LxWxH: " . (isset($recommendation['size']) ? $recommendation['size'] : '') . ", ";
                echo "CBM: " . (isset($recommendation['cbm']) ? $recommendation['cbm'] : '') . ", ";
                echo "Weight Kg: " . (isset($recommendation['weight']) ? $recommendation['weight'] : '') . ", ";
                echo "Packages: " . (isset($recommendation['noOfPackages']) ? $recommendation['noOfPackages'] : '') . ", ";
                echo "Packed: " . (isset($recommendation['packedPackages']) ? $recommendation['packedPackages'] : '') . ", ";
                echo "Increase: " . (isset($recommendation['suggestedIncrease']) ? $recommendation['suggestedIncrease'] : '') . ", ";
                echo "Decrease: " . (isset($recommendation['suggestedDecrease']) ? $recommendation['suggestedDecrease'] : '') . ", ";
                echo "ErpId: " . (isset($recommendation['erpId']) ? $recommendation['erpId'] : '') . ", ";
                echo "SkuNumber: " . (isset($recommendation['skuNumber']) ? $recommendation['skuNumber'] : '') . ", ";
                echo "Description: " . (isset($recommendation['description']) ? $recommendation['description'] : '') . ", ";
                echo "PO#: " . (isset($recommendation['po']) ? $recommendation['po'] : '') . ", ";
                echo "Division: " . (isset($recommendation['shipper']) ? $recommendation['shipper'] : '') . "\n";
                // --- Your ERP Processing Logic for Suggestions Here ---
            }
        } else {
            echo "No suggestions found for reference: " . $shipment_reference . "\n";
        }
    } else {
        error_log("ERROR: retrieving or parsing suggestions for reference: " . $shipment_reference);
    }
}

function remove_comma($value) {
    return ($value !== null) ? str_replace(["\r\n", "\r", "\n", ","], [" ", " ", " ", ""], $value) : '';
}

function format_decimal_3($value) {
    return rtrim(rtrim(sprintf('%.3f', $value), '0'), '.');
}

function generate_loadviewer_csv() {
    $rs_shipment = [];
    $shipment_data = [
        'reference' => 'SANDBOX-01',
        'hangingAllowed' => true,
        'useMultipleContainers' => false,
        'uniqueErpId' => false,
        'suggestExtra' => false,
    ];
    $rs_shipment[] = $shipment_data;

    $rs_container = [];
    $container_data = [
        'lengthMm' => 5867,
        'widthMm' => 2352,
        'heightMm' => 2393,
        'maxWeightKg' => 27200.0,
        'backMarginMm' => 0,
        'isRefrigerated' => false,
        'maxUseOne' => false,
        'lclBreakEvenCBM' => 0.0,
    ];
    $rs_container[] = $container_data;

    $rs_cargo = [];
    $cargo_data = [
        'color' => '#008000',
        'lengthMm' => 700,
        'widthMm' => 500,
        'heightMm' => 300,
        'weightKg' => 91.5,
        'cartons' => 300,
        'additionalCartons' => 0,
        'placement' => 0,
        'verticalRotationAllowed' => true,
        'skuPerCarton' => 1,
        'emptyPalletHeightMm' => 0,
        'trayCapMm' => 0,
        'trayHeightMm' => 0,
        'marginLength' => 0,
        'marginWidth' => 0,
        'marginHeight' => 0,
        'nameOfSet' => '',
        'cartonRatioInSet' => 0,
        'erpId' => '',
        'skuNumber' => '',
        'description' => '',
        'destination' => 0,
        'po' => '',
        'shipper' => '',
    ];
    $rs_cargo[] = $cargo_data;

    $csv_lines = [];
    $csv_lines[] = "S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra";
    $csv_lines[] = "S,D," . remove_comma($rs_shipment[0]['reference']) . "," . ($rs_shipment[0]['hangingAllowed'] ? 'true' : 'false') . "," . ($rs_shipment[0]['useMultipleContainers'] ? 'true' : 'false') . "," . ($rs_shipment[0]['uniqueErpId'] ? 'true' : 'false') . "," . ($rs_shipment[0]['suggestExtra'] ? 'true' : 'false');
    $csv_lines[] = "B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM";

    foreach ($rs_container as $row) {
        $csv_lines[] = "B,D," . $row['lengthMm'] . "," . $row['widthMm'] . "," . $row['heightMm'] . "," . format_decimal_3($row['maxWeightKg']) . "," . $row['backMarginMm'] . "," . ($row['isRefrigerated'] ? 'true' : 'false') . "," . ($row['maxUseOne'] ? 'true' : 'false') . "," . format_decimal_3($row['lclBreakEvenCBM']);
    }

    $csv_lines[] = "C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper";
    foreach ($rs_cargo as $row) {
        $csv_lines[] = "C,D," . remove_comma($row['color']) . "," . $row['lengthMm'] . "," . $row['widthMm'] . "," . $row['heightMm'] . "," . format_decimal_3($row['weightKg']) . "," . $row['cartons'] . "," . $row['additionalCartons'] . "," . $row['placement'] . "," . ($row['verticalRotationAllowed'] ? 'true' : 'false') . "," . $row['skuPerCarton'] . "," . $row['emptyPalletHeightMm'] . "," . $row['trayCapMm'] . "," . $row['trayHeightMm'] . "," . $row['marginLength'] . "," . $row['marginWidth'] . "," . $row['marginHeight'] . "," . remove_comma($row['nameOfSet']) . "," . $row['cartonRatioInSet'] . "," . remove_comma($row['erpId']) . "," . remove_comma($row['skuNumber']) . "," . remove_comma($row['description']) . "," . $row['destination'] . "," . remove_comma($row['po']) . "," . remove_comma($row['shipper']);
    }
    return implode("\n", $csv_lines);
}

// Example usage:
$csv_data = generate_loadviewer_csv();
$upload_result = upload_to_loadviewer_with_retries($csv_data);
echo "Upload Result: " . htmlspecialchars($upload_result) . "\n";

test_suggestions("SANDBOX-01", "test@example.com");

?>

Dart


import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart'; // Import the intl package

// Constants
const String tokenUrl = 'https://www.loadviewer.com/api/auth/jwt/token';
const String uploadUrl = 'https://www.loadviewer.com/api/integration/v1/upload';
const String downloadUrl = 'https://www.loadviewer.com/api/integration/v1/download';
const String apiEmail = 'YOUR_Email'; // Replace with your actual email
const String apiKey = 'YOUR_API_KEY_HERE'; // Replace with your actual API key
const int defaultMaxRetries = 3;
const int defaultRateLimitWaitSeconds = 65;
const int generalRetryDelaySeconds = 5;

String? jwtToken; // Keep it nullable

// Helper function for exponential backoff
Future<void> _delay(int retryCount) async {
  await Future.delayed(Duration(seconds: generalRetryDelaySeconds * retryCount));
}

// Function to get JWT token with retries
Future<String?> getJwtTokenWithRetries({int retryCount = 0}) async {
  if (jwtToken != null) {
    return jwtToken;
  }

  if (retryCount > defaultMaxRetries) {
    throw Exception('ERROR: Failed to retrieve token after multiple retries');
  }

  final Map<String, String> requestBody = {
    'email': apiEmail,
    'apikey': apiKey,
  };
  final String jsonBody = jsonEncode(requestBody);

  try {
    final response = await http.post(
      Uri.parse(tokenUrl),
      headers: {'Content-Type': 'application/json'},
      body: jsonBody,
    );

    if (response.statusCode == 429) {
      if (retryCount < defaultMaxRetries) {
        print(
            'Rate limit hit on token endpoint (attempt ${retryCount + 1} of $defaultMaxRetries). Waiting $defaultRateLimitWaitSeconds seconds before retrying.');
        await Future.delayed(
            const Duration(seconds: defaultRateLimitWaitSeconds));
        return getJwtTokenWithRetries(retryCount: retryCount + 1);
      } else {
        throw Exception('ERROR: Failed to retrieve token after multiple rate limits');
      }
    } else if (response.statusCode >= 300) {
      if (retryCount < defaultMaxRetries) {
        print(
            'Error retrieving token (attempt ${retryCount + 1} of $defaultMaxRetries): ${response.statusCode} - ${response.body}. Retrying in $generalRetryDelaySeconds seconds.');
        await _delay(retryCount);
        return getJwtTokenWithRetries(retryCount: retryCount + 1);
      } else {
        throw Exception(
            'ERROR: Failed to retrieve token after $defaultMaxRetries attempts: ${response.statusCode} - ${response.body}');
      }
    }

    final Map<String, dynamic> result = jsonDecode(response.body);
    if (result.containsKey('token')) {
      jwtToken = result['token'];
      return jwtToken;
    } else {
      throw Exception(
          'ERROR: Failed to extract token from response: ${response.body}');
    }
  } catch (e) {
    await _delay(retryCount);
    return getJwtTokenWithRetries(retryCount: retryCount + 1);
  }
}

// Function to upload to LoadViewer with retries
Future<String> uploadToLoadViewerWithRetries(String csvData,
    {int retryCount = 0}) async {
  if (jwtToken == null) {
    jwtToken = await getJwtTokenWithRetries();
    if (jwtToken == null) {
      throw Exception('ERROR: Unable to retrieve JWT token for upload');
    }
  }

  if (retryCount > defaultMaxRetries) {
    throw Exception('ERROR: Max upload retries exceeded');
  }

  try {
    final response = await http.post(
      Uri.parse(uploadUrl),
      headers: {
        'Content-Type': 'text/csv',
        'Authorization': 'Bearer $jwtToken',
      },
      body: csvData,
    );

    if (response.statusCode == 401 || response.statusCode == 410) {
      print('JWT token expired or invalid. Attempting to retrieve a new token.');
      jwtToken = null;
      jwtToken = await getJwtTokenWithRetries();
      if (jwtToken == null) {
        throw Exception(
            'ERROR: Unable to retrieve a new JWT token after token expiration');
      }
      return uploadToLoadViewerWithRetries(csvData,
          retryCount:
              retryCount); // Retry with the same retryCount, not incremented
    } else if (response.statusCode == 429) {
      if (retryCount < defaultMaxRetries) {
        print(
            'Rate limit hit on upload endpoint (attempt ${retryCount + 1} of $defaultMaxRetries). Waiting $defaultRateLimitWaitSeconds seconds before retrying.');
        await Future.delayed(
            const Duration(seconds: defaultRateLimitWaitSeconds));
        return uploadToLoadViewerWithRetries(csvData,
            retryCount: retryCount + 1);
      } else {
        throw Exception('ERROR: Upload failed after multiple rate limits');
      }
    } else if (response.statusCode >= 300 &&
        response.statusCode != 409) {
      if (retryCount < defaultMaxRetries) {
        print(
            'Error uploading data (attempt ${retryCount + 1} of $defaultMaxRetries): ${response.statusCode} - ${response.body}. Retrying in $generalRetryDelaySeconds seconds.');
        await _delay(retryCount);
        return uploadToLoadViewerWithRetries(csvData,
            retryCount: retryCount + 1);
      } else {
        throw Exception(
            'ERROR: Upload failed after $defaultMaxRetries attempts. Status: ${response.statusCode} - ${response.body}');
      }
    }

    return '${response.statusCode} - ${response.body}';
  } catch (e) {
    await _delay(retryCount);
    return uploadToLoadViewerWithRetries(csvData,
        retryCount: retryCount + 1);
  }
}

// Function to get LoadViewer suggestions CSV with retries
Future<String> getLoadViewerSuggestionsCsvWithRetries(String requestBodyJson,
    {int retryCount = 0}) async {
  if (jwtToken == null) {
    jwtToken = await getJwtTokenWithRetries();
    if (jwtToken == null) {
      throw Exception(
          'ERROR: Unable to retrieve JWT token for getting suggestions');
    }
  }

  if (retryCount > defaultMaxRetries) {
    throw Exception('ERROR: Max suggestions retries exceeded');
  }

  try {
    final response = await http.post(
      Uri.parse(downloadUrl),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer $jwtToken',
      },
      body: requestBodyJson,
    );

    print(
        'POST Request to LoadViewer Suggestions URL: $downloadUrl\nRequest Body:\n$requestBodyJson\nHTTP Status Code: ${response.statusCode}\nResponse Text:\n${response.body}\n');

    if (response.statusCode == 401 || response.statusCode == 410) {
      print('JWT token expired or invalid. Attempting to retrieve a new token.');
      jwtToken = null;
      jwtToken = await getJwtTokenWithRetries();
      if (jwtToken == null) {
        throw Exception(
            'ERROR: Unable to retrieve a new JWT token after token expiration');
      }
      return getLoadViewerSuggestionsCsvWithRetries(requestBodyJson,
          retryCount:
              retryCount); // Retry with same count, not incremented.
    } else if (response.statusCode == 429) {
      if (retryCount < defaultMaxRetries) {
        print(
            'Rate limit hit on suggestions endpoint (attempt ${retryCount + 1} of $defaultMaxRetries). Waiting $defaultRateLimitWaitSeconds seconds before retrying.');
        await Future.delayed(
            const Duration(seconds: defaultRateLimitWaitSeconds));
        return getLoadViewerSuggestionsCsvWithRetries(requestBodyJson,
            retryCount: retryCount + 1);
      } else {
        throw Exception('ERROR: Failed to retrieve suggestions after multiple rate limits');
      }
    } else if (response.statusCode >= 300) {
      if (retryCount < defaultMaxRetries) {
        print(
            'Error retrieving suggestions (attempt ${retryCount + 1} of $defaultMaxRetries): ${response.statusCode} - ${response.body}. Retrying in $generalRetryDelaySeconds seconds.');
        await _delay(retryCount);
        return getLoadViewerSuggestionsCsvWithRetries(requestBodyJson,
            retryCount: retryCount + 1);
      } else {
        throw Exception(
            'ERROR: Failed to retrieve suggestions after $defaultMaxRetries attempts. Status: ${response.statusCode} - ${response.body}');
      }
    }

    return response.body;
  } catch (e) {
    await _delay(retryCount);
    return getLoadViewerSuggestionsCsvWithRetries(requestBodyJson,
        retryCount: retryCount + 1);
  }
}

// Function to test suggestions
Future<void> testSuggestions(String shipmentReference, String solverEmail) async {
  if (shipmentReference.isEmpty) {
    print('ERROR: Shipment Reference is required!');
    return;
  }

  final emailRegex =
      RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$'); // More robust email regex
  if (solverEmail.isNotEmpty && !emailRegex.hasMatch(solverEmail)) {
    print('ERROR: Invalid Solver Email format!');
    return;
  }

  if (jwtToken == null) {
    jwtToken = await getJwtTokenWithRetries();
    if (jwtToken == null) {
      print('ERROR: Token Error or Server Not Reachable.');
      return;
    }
  }

  final Map<String, String> requestBody = {
    'reference': shipmentReference.replaceAll('"', '""'),
    'solverEmail': solverEmail.replaceAll('"', '""'),
  };
  final String jsonBody = jsonEncode(requestBody);

  final String strResponse =
      await getLoadViewerSuggestionsCsvWithRetries(jsonBody);

  print('Raw Suggestions Response:\n$strResponse\n');

  final List<String> arrLines = strResponse.split('\n');
  final Map<String, dynamic> results = {
    'status': <String, String>{},
    'recommendations': <Map<String, dynamic>>[],
  };

  if (arrLines.isNotEmpty) {
    for (String line in arrLines) {
      final List<String> arrFields = line.split(',');
      if (arrFields.isNotEmpty) {
        final String recordType = arrFields[0].trim();

        switch (recordType) {
          case 'S': // Status Record
            if (arrFields.length == 9 &&
                arrFields[1].trim().toLowerCase() == 'd') {
              final Map<String, String> statusInfo =
                  results['status'].cast<String, String>();
              statusInfo['status'] = arrFields[2].trim();
              statusInfo['message'] = arrFields[3].trim();
              statusInfo['reference'] = arrFields[4].trim();
              statusInfo['solverEmail'] = arrFields[5].trim();
              statusInfo['multipleContainers'] = arrFields[6].trim();
              statusInfo['suggestExtra'] = arrFields[7].trim();
            } else if (arrFields.length == 9 &&
                arrFields[1].trim().toLowerCase() == 'h') {
              // Header row, can be ignored
            } else if (line.trim().isNotEmpty) {
              print('ERROR: Invalid Status record format: $line');
            }
            break;
          case 'R': // Recommendation Record
            if (arrFields.length == 16 &&
                arrFields[1].trim().toLowerCase() == 'd') {
              final Map<String, dynamic> recommendation = {
                'size': arrFields[2].trim(),
                'cbm': arrFields[3].trim(),
                'weight': arrFields[4].trim(),
                'noOfPackages': int.tryParse(arrFields[5].trim()) ?? 0,
                'isPacked': arrFields[6].trim(),
                'packedPackages': int.tryParse(arrFields[7].trim()) ?? 0,
                'suggestedIncrease': int.tryParse(arrFields[8].trim()) ?? 0,
                'suggestedDecrease': int.tryParse(arrFields[9].trim()) ?? 0,
                'erpId': arrFields[10].trim(),
                'skuNumber': arrFields[11].trim(),
                'description': arrFields[12].trim(),
                'po': arrFields[13].trim(),
                'shipper': arrFields[14].trim(),
              };
              List<Map<String, dynamic>> recommendations =
                  List<Map<String, dynamic>>.from(results['recommendations']);
              recommendations.add(recommendation);
              results['recommendations'] = recommendations;
            } else if (arrFields.length == 16 &&
                arrFields[1].trim().toLowerCase() == 'h') {
              // Header row, can be ignored.
            } else if (line.trim().isNotEmpty) {
              print('ERROR: Invalid Recommendation record format: $line');
            }
            break;
          default:
            if (line.trim().isNotEmpty) {
              print('ERROR: Unknown record type: $recordType in line: $line');
            }
        }
      }
    }
  } else {
    print('Empty suggestions response.');
  }

  final Map<String, String>? statusInfo =
      results['status'] as Map<String, String>?;
  if (statusInfo != null) {
    print('Status: ${statusInfo['status']} - ${statusInfo['message']}');
    print('Reference: ${statusInfo['reference']}');
    print('Solver Email: ${statusInfo['solverEmail']}');
    print('Multiple Containers: ${statusInfo['multipleContainers']}');
    print('Extra Suggested: ${statusInfo['suggestExtra']}');
  }

  final List<Map<String, dynamic>>? recommendations =
      results['recommendations'] as List<Map<String, dynamic>>?;
  if (recommendations != null && recommendations.isNotEmpty) {
    print('--- Suggestions ---');
    for (var recommendation in recommendations) {
      print(
          'Is Packed: ${recommendation['isPacked']}, Size LxWxH: ${recommendation['size']}, CBM: ${recommendation['cbm']}, Weight Kg: ${recommendation['weight']}, Packages: ${recommendation['noOfPackages']}, Packed: ${recommendation['packedPackages']}, Increase: ${recommendation['suggestedIncrease']}, Decrease: ${recommendation['suggestedDecrease']}, ErpId: ${recommendation['erpId']}, SkuNumber: ${recommendation['skuNumber']}, Description: ${recommendation['description']}, PO#: ${recommendation['po']}, Division: ${recommendation['shipper']} ');
      // --- Your ERP Processing Logic for Suggestions Here ---
    }
  } else {
    print('No suggestions found for reference: $shipmentReference');
  }
}

// Function to remove commas and newlines
String removeComma(String value) {
  return value.replaceAll(',', '').replaceAll('\r\n', ' ').replaceAll('\n', ' ');
}

// Function to format decimal with 3 places
String formatDecimal3(double value) {
  final formatter = NumberFormat('#.###', 'en_US'); // Use en_US for consistent formatting
  String formatted = formatter.format(value);
  if (formatted.endsWith('.000')) {
    formatted = formatted.substring(0, formatted.length - 4);
  } else if (formatted.endsWith('00')) {
    formatted = formatted.substring(0, formatted.length - 2);
  } else if (formatted.endsWith('0')) {
    formatted = formatted.substring(0, formatted.length - 1);
  }
  return formatted;
}

// Function to generate LoadViewer CSV data (example)
String generateLoadViewerCsv() {
  final List<List<String>> csvData = [];

  // Shipment Data
  csvData.add([
    'S',
    'H',
    'reference',
    'hangingAllowed',
    'useMultipleContainers',
    'uniqueErpId',
    'suggestExtra'
  ]);
  csvData.add([
    'S',
    'D',
    removeComma('SANDBOX-01'),
    'true',
    'false',
    'false',
    'false'
  ]);

  // Container Data
  csvData.add([
    'B',
    'H',
    'lengthMm',
    'widthMm',
    'heightMm',
    'maxWeightKg',
    'backMarginMm',
    'isRefrigerated',
    'maxUseOne',
    'lclBreakEvenCBM'
  ]);
  csvData.add([
    'B',
    'D',
    '5867',
    '2352',
    '2393',
    formatDecimal3(27200.0),
    '0',
    'false',
    'false',
    formatDecimal3(0.0)
  ]);

  // Cargo Data
  csvData.add([
    'C',
    'H',
    'color',
    'lengthMm',
    'widthMm',
    'heightMm',
    'weightKg',
    'cartons',
    'additionalCartons',
    'placement',
    'verticalRotationAllowed',
    'skuPerCarton',
    'emptyPalletHeightMm',
    'trayCapMm',
    'trayHeightMm',
    'marginLenghtMm',
    'marginWidthMm',
    'marginHeightMm',
    'nameOfSet',
    'cartonRatioInSet',
    'erpId',
    'skuNumber',
    'description',
    'destination',
    'po',
    'shipper'
  ]);
  csvData.add([
    'C',
    'D',
    removeComma('#008000'),
    '700',
    '500',
    '300',
    formatDecimal3(91.5),
    '300',
    '0',
    '0',
    'true',
    '1',
    '0',
    '0',
    '0',
    '0',
    '0',
    '0',
    removeComma(''),
    '0',
    removeComma(''),
    removeComma(''),
    removeComma(''),
    '0',
    removeComma(''),
    removeComma('')
  ]);

  // Join the rows with newlines
  return csvData.map((row) => row.join(',')).join('\n');
}

// Main function
Future<void> main() async {
  try {
    String csvData = generateLoadViewerCsv();
    String uploadResult = await uploadToLoadViewerWithRetries(csvData);
    print('Upload Result: $uploadResult');

    await testSuggestions('SANDBOX-01', 'test@example.com');
  } catch (e) {
    print('Error: $e'); // Catch and print any exceptions during the process.
  }
}




Important Reminders
  • Requests without API Key will return HTTP 400 or 401
  • Requests with expired taken shall return HTTP 410 you should replace your cached token with new one from LoadViewer.
  • Use SANDBOX-* Shipment References to avoid real charges, these shipments are not published.
  • Accepted Shipment always retrun Success with code 202 with Draft status. SANDBOX shipments and other valid shipment that cannot be published are Saved with Draft Status.
  • Accepted and Published Shipment always retrun Success with code 200 with Coins Charges and Coin Balance after charge.
  • Use the manual file upload method as your first validation step
  • Use the Shipments page to delete the SHIPMENTS to try again.
  • Always verify success and no error structure in responses