LoadViewer Integration - V1
V
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
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,shape,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,rotationsAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper
C,D,#00246B,0,700,500,300,91.5,300,0,0,abcdef,1,0,0,0,0,0,0,,0,,,,0,,
Explanation
S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra: Shipment-level headersS,D,SANDBOX-01,True,False,False,False: Shipment-level data rowsB,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM: Container headersB,D,5867,2352,2393,27200,0,False,False,0: Container data rows (can be multiple)C,H,erpId,color,shape,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,rotationsAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,shipper,destination,skuNumber,description,po: Cargo headersC,D,,#00246B,0,700,500,300,1.5,300,0,0,abcdef,1,0,0,0,0,0,0,,0,,0,,,: 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) Header(H) Row - Only one such row per CSV allowed.
S: FIXED. Always provideShere. Since this belongs to the Shipment Section.H: FIXED. Always provideHhere. Since this is a Header row of the Section.reference: FIXEDhangingAllowed: FIXEDuseMultipleContainers: FIXEDuniqueErpId: FIXEDsuggestExtra: FIXED
-
Shipment(S) Details(D) Row - Only one such row per CSV allowed.
- Section =
S: FIXED. Always provideShere. Since this belongs to the Shipment Section. - Row Type =
D: FIXED. Always provideDhere. Since this is a Data row of the Section. -
reference =
SANDBOX-01: Mandatory, String: MaxLength[50]. Provide a unique shipment/job reference. Values starting withSANDBOXare for testing purposes and are not chargeable. -
hangingAllowed =
True: Mandatory. Allowed values –TrueorFalse. This value tells LoadViewer whether placing any cargo over other cargo requires 100% base support (setFalse) or 70% base support is sufficient (setTrue). -
useMultipleContainers =
False: Mandatory. Allowed values –TrueorFalse. This is useful to control your coin usage for this shipment. -
uniqueErpId =
False(Default): Mandatory. Allowed values –TrueorFalse. When set toTrue, all cargoes within the request must have a unique value in theerpIdfield. -
suggestExtra =
False(Default): Mandatory. Allowed values –TrueorFalse. Set toTruewhen you want the system to consider loading additional cargo into the containers beyond the initially specified quantities. Set toFalseotherwise.
- Section =
-
Container(B) Header(H) Row - Only one such row per CSV allowed.
B: FIXED. Always provideBhere. Since this belongs to the Container (Bin for you to relate to the Abbreviation B) Section.H: FIXED. Always provideHhere. Since this is a Header row of the Section.lengthMm: FIXED.widthMm: FIXED.heightMm: FIXED.maxWeightKg: FIXEDbackMarginMm: FIXEDisRefrigerated: FIXEDmaxUseOne: FIXEDlclBreakEvenCBM: FIXED
-
Container(B) Details(D) Row - Multilple rows per CSV allowed.
- Section =
B: FIXED. Always provideBhere. Since this belongs to the Container Section. - Row Type =
D: FIXED. Always provideDhere. 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:TrueorFalse. Indicates whether the Container is Refree container or not. -
maxUseOne =
False(Default): Mandatory. Allowed values:TrueorFalse. 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.
- Section =
-
Cargo(C) Header(H) Row - Only one such row per CSV allowed.
C: FIXED. Always provideChere. Since this belongs to the Cargo Section.H: FIXED. Always provideHhere. Since this is a Header row of the Section.erpId: FIXED.shape: FIXED.lengthMm,widthMmandheightMm: FIXED.weightKg: FIXED.cartons: FIXED.additionalCartons: FIXED.placement: FIXED.rotationsAllowed: FIXED.skuPerCarton: FIXED.emptyPalletHeightMm: FIXED.trayCapMm: FIXED.trayHeightMm: FIXED.marginLenghtMm,marginWidthMmandmarginHeightMm: 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 provideChere. Since this belongs to the Cargo Section. - Row Type =
D: FIXED. Always provideDhere. 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. -
shape =
: Mandatory, Number: 0 for Cubiods and 1 for Cylindrical shapes. -
lengthMm =
700, widthMm =500and 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.
-
rotationsAllowed =
abcdef: Mandatory. Allowed values: combination ofa,b,c,d,e and f. Indicates what specific rotations are allowed for the package during load planning.When vertical rotation is
abcdef, 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. Use0for cartons, and any value between90to200mm for empty pallet height (i.e., pallet feet only). This applies to Cubiod shapes 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. This applies to Cubiod shapes only. -
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. This applies to Cubiod shapes only. -
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). This applies to Cubiod shapes only. - 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 than0. - 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]. Use0when 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.
- Section =
Notes:
- Separator must be a comma (,)
- All header and detail rows must strictly follow the
S/B/C+H/Dformat. - All header rows are case sensitive and must strictly follow the same format as mentioned in the Headers sections above.
- Always provide a
S,Hrow followed by at least oneS,Drow - 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 V
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
Create Your Business Account
A business profile is required to generate API keys. It also enables you to:
- Create and manage shipments
- Buy and earn coins
- Access referral rewards
Token Url:
POST https://www.loadviewer.com/api/auth/jwt/token
Request Headers:
Content-Type: application/json
Request Body:
{
"email": "your.email@yourcompany.com",
"apikey": "YOUR_API_KEY_HERE"
}
{
"token": "eyJh...pljBlI"
}
{
"status":"error",
"message": "Your domain is not allowed",
"code":401,
"data": null,
"errors":
[
{
"errorCode":"401",
"errorMessage":"Your domain is not Allowed"
}
],
"meta": null
}
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
YOUR_Email and YOUR_API_KEY_HERE variables in code sections with your actual email and the API Key generated in
Api Key Manager. You may refer to complete example code after understanding the Authentication flow.
' === 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
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}')
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}");
}
}
}
// 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);
});
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();
}
}
}
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
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))
}
}
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
$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";
}
?>
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. |
https://www.loadviewer.com/api/auth/jwt/token endpoint.
You may refer to the Errors
section for guidance on sending and receiving data with other API endpoints.
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.
Dim http As Object
Set http = CreateObject("WinHttp.WinHttpRequest.5.1")
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. The token endpoint shall always return the json format in case of success response.
.Send csvData
End With
{
"status":"success",
"message":"Shipment Number 'SANDBOX-01' is accepted. 1 out of 1 Container(s) added. 1 out of 1 cargoes added. 1 Cargo added with new Colors.",
"code":202,
"data":
{
"shipmentNumber":"SANDBOX-01",
"shipmentStatus":"Draft"
},
"errors":null,
"meta":
{
"coinsCharged":0,
"coinsBalance":0
}
}
{
"status":"error",
"message": "Shipment Number (Field 'reference') 'SANDBOX-01' is used by other Shipment.",
"code":409,
"data":
{
"shipmentNumber":"SANDBOX-01"
},
"errors":
[
{
"errorCode":"409",
"errorMessage":"Shipment Number (Field 'reference') 'SANDBOX-01' is used by other Shipment."
}
],
"meta":
{
"coinsBalance":0
}
}
Status = 202
ResponseText = Shipment Number 'SANDBOX-01' is accepted.
Status = 409
ResponseText = Shipment Number (Field 'reference') 'SANDBOX-01' is used by other Shipment.
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
-
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
referencedoes not start withSANDBOXin 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.
- Make sure the
-
Test the API Communication
Use your tool of choice (Postman, Swagger, curl, script) to post the file to LoadViewer’s API endpoint.
- Use
referencethat starts withSANDBOX. - No coins are consumed for such files.
referencestarting withSANDBOXcan 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-01is a valid test reference for your dummy data. - Use
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
ApiKeyin the request headers - Use a Shipment
referencestarting withSANDBOXfor 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/execution stage where you may be interested in knowning how many articals can be loaded in a single container.
' ========================================
' 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
Dim newToken As String
newToken = extractTokenFromResponse(http.ResponseText)
If newToken <> "" Then
LoadViewerToken = newToken
getJwtToken = newToken
Else
getJwtToken = "ERROR: Token not found in server response."
End If
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
If retry > 0 Then
retry = retry - 1
jwtToken = Trim(getJwtToken())
If jwtToken <> "" _
And Left(jwtToken, 5) <> "ERROR" _
And Left(jwtToken, 9) <> "Exception" Then
GoTo UploadToServer
Else
UploadTOLoadViewer = "ERROR: Unable to retrieve new JWT token."
Exit Function
End If
Else
UploadTOLoadViewer = _
"ERROR: Token expired and maximum retries exceeded. " & _
http.ResponseText
Exit Function
End If
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
If retry > 0 Then
retry = retry - 1
jwtToken = Trim(getJwtToken())
If jwtToken <> "" _
And Left(jwtToken, 5) <> "ERROR" _
And Left(jwtToken, 9) <> "Exception" Then
GoTo TrySuggestionsFetch
Else
GetLoadViewerSuggestionsCSV = "ERROR: Unable to retrieve new JWT token."
Exit Function
End If
Else
GetLoadViewerSuggestionsCSV = _
"ERROR: Token expired and maximum retries exceeded. " & _
http.ResponseText
Exit Function
End If
ElseIf retry > 0 Then
retry = retry - 1
Debug.Print "Retrying Suggestions 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 "shape", adInteger
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 "rotationsAllowed", adVarChar, 6
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("shape") = 0
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("rotationsAllowed") = "abcdef"
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,shape,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,rotationsAllowed,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("shape")& "," & rsCargo("lengthMm") & "," & rsCargo("widthMm") & "," & rsCargo("heightMm") & "," & formatDecimal3(rsCargo("weightKg")) & "," & rsCargo("cartons") & "," & rsCargo("additionalCartons") & "," & rsCargo("placement") & "," & rsCargo("rotationsAllowed") & "," & 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,shape,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,rotationsAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper" & vbCrLf
csv = csv & "C,D,#008000,0,700,500,300,91.5,300,0,0,abcdef,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 recommendations As Collection ' Collection for Container Load Plan rows
Dim i As Long
Dim arrFields As Variant
Set recommendations = New Collection
arrLines = Split(strResponse, vbCrLf)
If UBound(arrLines) > 0 Then
For i = LBound(arrLines) To UBound(arrLines)
If Trim(arrLines(i)) <> "" Then
arrFields = Split(arrLines(i), ",")
' Check for 26 fields (UBound = 25 in zero-based arrays)
If UBound(arrFields) >= 25 Then
' Skip the Header Row
If LCase(Trim(arrFields(0))) <> "so" Then
Dim clpRow As Object
Set clpRow = CreateObject("Scripting.Dictionary")
clpRow("so") = Trim(arrFields(0))
clpRow("line") = CLng(Val(Trim(arrFields(1))))
clpRow("erpId") = Trim(arrFields(2))
clpRow("container") = CLng(Val(Trim(arrFields(3))))
clpRow("dimensionsLxWxH_mm") = Trim(arrFields(4))
clpRow("maxWeight_Kg") = CDbl(Val(Trim(arrFields(5))))
clpRow("skuNumber") = Trim(arrFields(6))
clpRow("description") = Trim(arrFields(7))
clpRow("po") = Trim(arrFields(8))
clpRow("shipper") = Trim(arrFields(9))
clpRow("pkgType") = Trim(arrFields(10))
clpRow("length_mm") = CDbl(Val(Trim(arrFields(11))))
clpRow("width_mm") = CDbl(Val(Trim(arrFields(12))))
clpRow("height_mm") = CDbl(Val(Trim(arrFields(13))))
clpRow("keepStraight") = Trim(arrFields(14))
clpRow("rotations") = Trim(arrFields(15))
clpRow("set") = Trim(arrFields(16))
clpRow("innerQty") = CLng(Val(Trim(arrFields(17))))
clpRow("pkgGrossWt") = CDbl(Val(Trim(arrFields(18))))
clpRow("stuffedPkgs") = CLng(Val(Trim(arrFields(19))))
clpRow("cbm") = CDbl(Val(Trim(arrFields(20))))
clpRow("weight_Kg") = CDbl(Val(Trim(arrFields(21))))
clpRow("color") = Trim(arrFields(22))
clpRow("stuffedSku") = CLng(Val(Trim(arrFields(23))))
clpRow("skusInPo") = CLng(Val(Trim(arrFields(24))))
clpRow("poVariation") = CLng(Val(Trim(arrFields(25))))
recommendations.Add clpRow
End If
End If
End If
Next i
Else
Debug.Print "Empty suggestions response."
End If
' --- Output Debug Results ---
If recommendations.Count > 0 Then
Debug.Print "--- Container Load Plan Rows ---"
Dim eachRow As Object
For Each eachRow In recommendations
Debug.Print "SO: " & eachRow("so") & _
", Line: " & eachRow("line") & _
", Container: " & eachRow("container") & _
", SKU: " & eachRow("skuNumber") & _
", Stuffed SKU Qty: " & eachRow("stuffedSku") & _
", SKUs In PO: " & eachRow("skusInPo") & _
", PO Variation: " & eachRow("poVariation") & _
", CBM: " & eachRow("cbm")
' --- Your ERP Processing / SO Split Logic Here ---
Next eachRow
Else
Debug.Print "No Container Load Plan rows found 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
'===========================================================
' Escapes a value according to RFC 4180 CSV rules.
'
' Rules:
' 1. Null becomes empty string.
' 2. Embedded quotes are doubled.
' 3. If the value contains comma, quote, CR or LF,
' the entire value is enclosed in quotes.
'
' Examples:
' ABC -> ABC
' ABC,DEF -> "ABC,DEF"
' ABC"DEF -> "ABC""DEF"
' Line1<CRLF> Line2 -> "Line1
' Line2"
'===========================================================
Function CsvEscape(value As Variant) As String
If IsNull(value) Then
CsvEscape = ""
Exit Function
End If
Dim s As String
s = CStr(value)
' Escape embedded quotes
s = Replace(s, """", """""")
' Quote field if required
If InStr(s, ",") > 0 _
Or InStr(s, """") > 0 _
Or InStr(s, vbCr) > 0 _
Or InStr(s, vbLf) > 0 Then
s = """" & s & """"
End If
CsvEscape = s
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 = "solver@loadviewer.com" ' Replace with a test email
testSuggestions shipmentReference, solverEmail
End Sub
import http.client
import json
import time
import logging
import re # For email validation
from typing import Optional, Dict, List # For type hinting
from urllib.parse import urlparse
# === 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_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 # seconds
token_url = LOADVIEWER_TOKEN_URL
json_body = json.dumps({"email": LOADVIEWER_API_EMAIL, "apikey": LOADVIEWER_API_KEY})
while retry <= max_retries:
try:
parsed_url = urlparse(token_url)
conn = http.client.HTTPSConnection(parsed_url.netloc, timeout=request_timeout)
headers = {'Content-Type': 'application/json'}
conn.request("POST", parsed_url.path, 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
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:
return f"ERROR: {response.status} - {response_data} (Max rate limit retries reached)"
elif retry < max_retries:
retry += 1
logger.warning(f"Retrying token retrieval due to error (attempt {retry + 1} of {max_retries}). Waiting 5 seconds.")
time.sleep(5)
else:
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_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 # seconds
upload_url = LOADVIEWER_UPLOAD_URL
while retry <= max_retries:
try:
parsed_url = urlparse(upload_url)
conn = http.client.HTTPSConnection(parsed_url.netloc, timeout=request_timeout)
headers = {
'Content-Type': 'text/csv',
'Authorization': f'Bearer {jwt_token}'
}
conn.request("POST", parsed_url.path, 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)
return f"{response.status} - {response_data}"
elif response.status in (401, 410):
if retry < max_retries:
retry += 1
logger.warning("JWT token expired or invalid. Attempting to retrieve a new token.")
jwt_token = get_jwt_token()
if jwt_token is None or jwt_token.startswith(("ERROR", "Exception")):
return jwt_token
continue
return f"ERROR: Token expired/invalid. Upload failed after {retry + 1} attempts. Status: {response.status} - {response_data}"
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:
return f"ERROR: {response.status} - {response_data} (Max rate limit retries reached)"
elif response.status != 409 and retry < max_retries: # 409 is duplicate shipment error
retry += 1
logger.warning(f"Retrying Upload due to error (attempt {retry + 1} of {max_retries}). Waiting 5 seconds.")
time.sleep(5)
continue
else:
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:
parsed_url = urlparse(download_url)
conn = http.client.HTTPSConnection(parsed_url.netloc, timeout=request_timeout)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {jwt_token}"
}
conn.request("POST", parsed_url.path, 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):
return response_data
elif response.status in (401, 410):
if retry < max_retries:
retry += 1
logger.warning("JWT token expired or invalid. Attempting to retrieve a new token.")
jwt_token = get_jwt_token()
if jwt_token is None or jwt_token.startswith(("ERROR", "Exception")):
return jwt_token
continue
return f"ERROR: Token expired/invalid. Download Suggestions failed after {retry + 1} attempts. Status: {response.status} - {response_data}"
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:
return f"ERROR: {response.status} - {response_data} (Max rate limit retries reached)"
elif retry < max_retries:
retry += 1
logger.warning(f"Retrying Suggestions download due to error (attempt {retry + 1} of {max_retries}). Waiting 5 seconds.")
time.sleep(5)
continue
else:
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", "shape": 0, "lengthMm": 700, "widthMm": 500, "heightMm": 300,
"weightKg": 91.5, "cartons": 300, "additionalCartons": 0, "placement": 0,
"rotationsAllowed": "abcdef", "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,shape,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,rotationsAllowed,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['shape']},{row['lengthMm']},{row['widthMm']},{row['heightMm']},{format_decimal3(row['weightKg'])},{row['cartons']},{row['additionalCartons']},{row['placement']},{row['rotationsAllowed']},{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,shape,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,rotationsAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper\r\n"
"C,D,#008000,0,700,500,300,91.5,300,0,0,abcdef,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,
"solverEmail": solver_email
})
# --- 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()
recommendations: List[Dict[str, object]] = []
if len(arr_lines) > 0:
for line in arr_lines:
arr_fields = line.split(',')
if len(arr_fields) >= 26 and arr_fields[0].strip().lower() != "so":
recommendation: Dict[str, object] = {}
recommendation["so"] = arr_fields[0].strip()
recommendation["line"] = int(arr_fields[1].strip())
recommendation["erpid"] = arr_fields[2].strip()
recommendation["container"] = int(arr_fields[3].strip())
recommendation["dimenionslxwxh_mm"] = arr_fields[4].strip()
recommendation["maxweight_kg"] = float(arr_fields[5].strip())
recommendation["sku_number"] = arr_fields[6].strip()
recommendation["sku_description"] = arr_fields[7].strip()
recommendation["po"] = arr_fields[8].strip()
recommendation["shipper"] = arr_fields[9].strip()
recommendation["pkg_type"] = arr_fields[10].strip()
recommendation["length_mm"] = int(arr_fields[11].strip())
recommendation["width_mm"] = int(arr_fields[12].strip())
recommendation["height_mm"] = int(arr_fields[13].strip())
recommendation["keepstraight"] = arr_fields[14].strip()
recommendation["rotations"] = arr_fields[15].strip()
recommendation["set"] = arr_fields[16].strip()
recommendation["innerqty"] = int(arr_fields[17].strip())
recommendation["pkggrosswt"] = float(arr_fields[18].strip())
recommendation["stuffedpkgs"] = int(arr_fields[19].strip())
recommendation["cbm"] = float(arr_fields[20].strip())
recommendation["weight_kg"] = float(arr_fields[21].strip())
recommendation["color"] = arr_fields[22].strip()
recommendation["stuffedsku"] = int(arr_fields[23].strip())
recommendation["skusinpo"] = int(arr_fields[24].strip())
recommendation["povariation"] = int(arr_fields[25].strip())
recommendations.append(recommendation)
else:
print("Empty suggestions response.")
if len(recommendations) > 0:
print("--- Suggestions ---")
for recommendation in recommendations:
print(
f"SO: {recommendation['so']}, Line: {recommendation['line']}, Container: {recommendation['container']}, SKU: {recommendation['sku_number']}, Stuffed SKU Qty: {recommendation['stuffedsku']}, SKUs In PO: {recommendation['skusinpo']}, PO Variation: {recommendation['povariation']}, CBM: {recommendation['cbm']}"
)
# --- Your ERP Processing Logic for Suggestions Here ---
else:
print("No suggestions found 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 = "solver@loadviewer.com" # Replace with a your email, if advisor plan is there it shall be provided on solver@loadviewer.com
test_suggestions(shipment_reference, solver_email)
if __name__ == "__main__":
#test_upload(save_to_disk=True, use_ado_recordset=True)
test_loadviewer_integration_post()
using Microsoft.Extensions.Logging;
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;
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<LoadViewerIntegration> _logger;
private const int DefaultMaxRetries = 3;
private const int DefaultRateLimitWaitSeconds = 65;
private const int GeneralRetryDelaySeconds = 5;
public LoadViewerIntegration(ILogger<LoadViewerIntegration> 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<string?> 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<string> 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)
{
if (retryCount < DefaultMaxRetries)
{
_logger.LogWarning("JWT token expired or invalid. Attempting to retrieve a new token.");
retryCount++;
_jwtToken = await GetJwtTokenWithRetries();
if (string.IsNullOrEmpty(_jwtToken))
{
return "Error: Unable to retrieve a new JWT token after token expiration.";
}
goto UploadData;
}
return $"Error: Token expired/ invalid. Upload failed after {retryCount + 1} attempts. Status: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}";
}
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<string> 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);
var responseText = await response.Content.ReadAsStringAsync();
_logger.LogDebug($"POST Request to LoadViewer Suggestions URL: {_downloadUrl}\nRequest Body:\n{requestBody}\nHTTP Status Code: {response.StatusCode}\nResponse Text:\n{responseText}");
if (response.IsSuccessStatusCode)
{
return responseText;
}
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Gone)
{
if (retryCount < DefaultMaxRetries)
{
_logger.LogWarning("JWT token expired or invalid. Attempting to retrieve a new token.");
retryCount++;
_jwtToken = await GetJwtTokenWithRetries();
if (string.IsNullOrEmpty(_jwtToken))
{
return "Error: Unable to retrieve a new JWT token after token expiration.";
}
goto GetSuggestions;
}
return $"Error: Token expired/invalid. Download Suggestions failed after {retryCount + 1} attempts. Status: {response.StatusCode} - {responseText}";
}
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} - {responseText}. 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} - {responseText}";
}
}
}
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))
{
Console.WriteLine("[ERROR] Shipment Reference is required!");
return;
}
if (!string.IsNullOrEmpty(solverEmail) && !IsValidEmail(solverEmail))
{
Console.WriteLine("[ERROR] Invalid Solver Email format!");
return;
}
string token = _jwtToken ?? await GetJwtTokenWithRetries();
if (string.IsNullOrEmpty(token))
{
Console.WriteLine("[ERROR] Token Error or Server Not Reachable.");
return;
}
// --- Create the JSON request body ---
var jsonBody = JsonSerializer.Serialize(new
{
reference = shipmentReference,
solverEmail = solverEmail
});
// --- Call the GetLoadViewerSuggestionsCsvWithRetries function with POST ---
string strResponse = await GetLoadViewerSuggestionsCsvWithRetries(jsonBody);
if (strResponse.StartsWith("ERROR") || strResponse.StartsWith("Exception"))
{
Console.WriteLine($"[ERROR] Error retrieving data from LoadViewer: {strResponse}");
return;
}
Console.WriteLine($"[INFO] Raw Suggestions Response:\n{strResponse}");
var arrLines = strResponse.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
var recommendations = new List<Dictionary<string, object>>();
if (arrLines.Length > 0)
{
foreach (var line in arrLines)
{
if (string.IsNullOrWhiteSpace(line)) continue;
var arrFields = line.Split(',');
// Parse Container Manifest Rows (26 columns)
if (arrFields.Length >= 26)
{
// Skip the Header Row
if (arrFields[0].Trim().ToLowerInvariant() != "so")
{
long.TryParse(arrFields[1].Trim(), out long lineNum);
long.TryParse(arrFields[3].Trim(), out long containerNum);
double.TryParse(arrFields[5].Trim(), out double maxWeight);
double.TryParse(arrFields[11].Trim(), out double lengthMm);
double.TryParse(arrFields[12].Trim(), out double widthMm);
double.TryParse(arrFields[13].Trim(), out double heightMm);
long.TryParse(arrFields[17].Trim(), out long innerQty);
double.TryParse(arrFields[18].Trim(), out double pkgGrossWt);
long.TryParse(arrFields[19].Trim(), out long stuffedPkgs);
double.TryParse(arrFields[20].Trim(), out double cbm);
double.TryParse(arrFields[21].Trim(), out double weightKg);
long.TryParse(arrFields[23].Trim(), out long stuffedSku);
long.TryParse(arrFields[24].Trim(), out long skusInPo);
long.TryParse(arrFields[25].Trim(), out long poVariation);
var clpRow = new Dictionary<string, object>
{
["so"] = arrFields[0].Trim(),
["line"] = lineNum,
["erpId"] = arrFields[2].Trim(),
["container"] = containerNum,
["dimensionsLxWxH_mm"] = arrFields[4].Trim(),
["maxWeight_Kg"] = maxWeight,
["skuNumber"] = arrFields[6].Trim(),
["description"] = arrFields[7].Trim(),
["po"] = arrFields[8].Trim(),
["shipper"] = arrFields[9].Trim(),
["pkgType"] = arrFields[10].Trim(),
["length_mm"] = lengthMm,
["width_mm"] = widthMm,
["height_mm"] = heightMm,
["keepStraight"] = arrFields[14].Trim(),
["rotations"] = arrFields[15].Trim(),
["set"] = arrFields[16].Trim(),
["innerQty"] = innerQty,
["pkgGrossWt"] = pkgGrossWt,
["stuffedPkgs"] = stuffedPkgs,
["cbm"] = cbm,
["weight_Kg"] = weightKg,
["color"] = arrFields[22].Trim(),
["stuffedSku"] = stuffedSku,
["skusInPo"] = skusInPo,
["poVariation"] = poVariation
};
recommendations.Add(clpRow);
}
}
}
}
else
{
Console.WriteLine("[INFO] Empty suggestions response.");
}
// --- Output Debug Results ---
if (recommendations.Count > 0)
{
Console.WriteLine("[INFO] --- Container Load Plan Rows ---");
foreach (var eachRow in recommendations)
{
Console.WriteLine(
$"[INFO] SO: {eachRow.GetValueOrDefault("so")}, " +
$"Line: {eachRow.GetValueOrDefault("line")}, " +
$"Container: {eachRow.GetValueOrDefault("container")}, " +
$"SKU: {eachRow.GetValueOrDefault("skuNumber")}, " +
$"Stuffed SKU Qty: {eachRow.GetValueOrDefault("stuffedSku")}, " +
$"SKUs In PO: {eachRow.GetValueOrDefault("skusInPo")}, " +
$"Shortfall/Variation: {eachRow.GetValueOrDefault("poVariation")}, " +
$"CBM: {eachRow.GetValueOrDefault("cbm")}"
);
// --- Your ERP Processing / SO Split Logic Here ---
}
}
else
{
Console.WriteLine($"[INFO] No Container Load Plan rows found 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", "solver@loadviewer.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", shape = 0, lengthMm = 700, widthMm = 500, heightMm = 300, weightKg = 91.5m, cartons = 300, additionalCartons = 0, placement = 0, rotationsAllowed = "abcdef", 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<string>
{
"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,shape,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,rotationsAllowed,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.shape},{row.lengthMm},{row.widthMm},{row.heightMm},{FormatDecimal3(row.weightKg)},{row.cartons},{row.additionalCartons},{row.placement},{row.rotationsAllowed},{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);
}
}
// --- 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) {
if (retries < DEFAULT_MAX_RETRIES) {
console.warn('JWT token expired or invalid. Attempting to retrieve a new token.');
cachedToken = null;
const newToken = await getJwtTokenWithRetries();
if (!newToken) {
return 'Error: Unable to retrieve a new JWT token after token expiration.';
}
return uploadToLoadViewerWithRetries(csvData, retries + 1);
}
return `Error: Token expired/invalid and maximum retries exceeded. Status: ${response.status} - ${response.statusText}`;
} 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),
});
const responseText = await response.text();
console.debug(
`POST Request to LoadViewer Suggestions URL: ${LOADVIEWER_DOWNLOAD_URL}\n` +
`Request Body:\n${JSON.stringify(requestBody)}\n` +
`HTTP Status Code: ${response.status}\n` +
`Response Text:\n${responseText}`
);
if (!response.ok) {
if (response.status === 401 || response.status === 410) {
if (retries < DEFAULT_MAX_RETRIES) {
console.warn('JWT token expired or invalid. Attempting to retrieve a new token.');
cachedToken = null;
const newToken = await getJwtTokenWithRetries();
if (!newToken) {
return 'Error: Unable to retrieve a new JWT token after token expiration.';
}
return getLoadViewerSuggestionsCsvWithRetries(requestBody, retries + 1);
}
return `Error: Token expired/invalid and maximum retries exceeded. Status: ${response.status} - ${response.statusText}`;
} 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 responseText;
} 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}`;
}
}
function parseCsvLine(line) {
const fields = [];
let field = "";
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const ch = line[i];
if (ch === '"') {
if (inQuotes && line[i + 1] === '"') {
field += '"';
i++;
} else {
inQuotes = !inQuotes;
}
} else if (ch === ',' && !inQuotes) {
fields.push(field);
field = "";
} else {
field += ch;
}
}
fields.push(field);
return fields;
}
function csvFieldsToClpRow(f) {
return {
so: f[0].trim(),
line: Number.parseInt(f[1].trim(), 10),
erpId: f[2].trim(),
container: Number.parseInt(f[3].trim(), 10),
dimensionsLxWxH_mm: f[4].trim(),
maxWeight_Kg: Number.parseFloat(f[5].trim()),
skuNumber: f[6].trim(),
description: f[7].trim(),
po: f[8].trim(),
shipper: f[9].trim(),
pkgType: f[10].trim(),
length_mm: Number.parseInt(f[11].trim(), 10),
width_mm: Number.parseInt(f[12].trim(), 10),
height_mm: Number.parseInt(f[13].trim(), 10),
keepStraight: f[14].trim(),
rotations: f[15].trim(),
set: f[16].trim(),
innerQty: Number.parseInt(f[17].trim(), 10),
pkgGrossWt: Number.parseFloat(f[18].trim()),
stuffedPkgs: Number.parseInt(f[19].trim(), 10),
cbm: Number.parseFloat(f[20].trim()),
weight_Kg: Number.parseFloat(f[21].trim()),
color: f[22].trim(),
stuffedSku: Number.parseInt(f[23].trim(), 10),
skusInPo: Number.parseInt(f[24].trim(), 10),
poVariation: Number.parseInt(f[25].trim(), 10)
};
}
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,
solverEmail: solverEmail
};
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 recommendations = [];
const arrLines = strResponse.split(/\r?\n/);
for (const line of arrLines) {
if (line.trim() === "") {
continue;
}
const fields = parseCsvLine(line);
if (fields.length < 26) {
continue;
}
// Header
if (fields[0].trim().toLowerCase() === "so") {
continue;
}
recommendations.push(csvFieldsToClpRow(fields));
}
// --- Output Debug Results ---
if (recommendations.length > 0) {
console.log("--- Container Load Plan Rows ---");
for (const eachRow of recommendations) {
console.log(
"SO: " + eachRow.so +
", Line: " + eachRow.line +
", Container: " + eachRow.container +
", SKU: " + eachRow.skuNumber +
", Stuffed SKU Qty: " + eachRow.stuffedSku +
", SKUs In PO: " + eachRow.skusInPo +
", Shortfall/Variation: " + eachRow.poVariation +
", CBM: " + eachRow.cbm
);
// --- Your ERP Processing / SO Split Logic Here ---
}
} else {
console.log(
"No Container Load Plan rows found 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', shape: 0, lengthMm: 700, widthMm: 500, heightMm: 300, weightKg: 91.5, cartons: 300, additionalCartons: 0, placement: 0, rotationsAllowed: "abcdef", 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,shape,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,rotationsAllowed,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.shape},${row.lengthMm},${row.widthMm},${row.heightMm},${formatDecimal3(row.weightKg)},${row.cartons},${row.additionalCartons},${row.placement},${row.rotationsAllowed},${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', 'solver@loadviewer.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.');
}
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) {
if (retryCount < DEFAULT_MAX_RETRIES) {
retryCount++;
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 {
return "Error: Token expired/invalid. Maximum retries exceeded.";
}
} 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) {
if (retryCount < DEFAULT_MAX_RETRIES) {
retryCount++;
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 {
return "Error: Token expired/invalid. Maximum retries exceeded.";
}
} 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);
requestBody.put("solverEmail", solverEmail);
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");
recommendations = new ArrayList<>();
if (arrLines.length > 0) {
for (String line : arrLines) {
String[] arrFields = line.split(",");
if (arrFields.length >= 26 && !arrFields[0].trim().equalsIgnoreCase("so")) {
Map<String, Object> recommendation = new HashMap<>();
//so,line,erpid,container,dimenionslxwxh_mm,maxweight_kg,sku_number,sku_description,po,shipper,pkg_type,length_mm,width_mm,height_mm,
//keepstraight,rotations,set,innerqty,pkggrosswt,stuffedpkgs,cbm,weight_kg,color,stuffedsku,skusinpo,povariation
recommendation.put("so", arrFields[0].trim());
recommendation.put("line", Integer.parseInt(arrFields[1].trim()));
recommendation.put("erpid", arrFields[2].trim());
recommendation.put("container", arrFields[3].trim());
recommendation.put("dimenionslxwxh_mm", arrFields[4].trim());
recommendation.put("maxweight_kg", Double.parseDouble(arrFields[5].trim()));
recommendation.put("sku_number", arrFields[6].trim());
recommendation.put("sku_description", arrFields[7].trim());
recommendation.put("po", arrFields[8].trim());
recommendation.put("shipper", arrFields[9].trim());
recommendation.put("pkg_type", arrFields[10].trim());
recommendation.put("length_mm", Integer.parseInt(arrFields[11].trim()));
recommendation.put("width_mm", Integer.parseInt(arrFields[12].trim()));
recommendation.put("height_mm", Integer.parseInt(arrFields[13].trim()));
recommendation.put("keepstraight", arrFields[14].trim());
recommendation.put("rotations", arrFields[15].trim());
recommendation.put("set", arrFields[16].trim());
recommendation.put("innerqty", Integer.parseInt(arrFields[17].trim()));
recommendation.put("pkggrosswt", Double.parseDouble(arrFields[18].trim()));
recommendation.put("stuffedpkgs", Integer.parseInt(arrFields[19].trim()));
recommendation.put("cbm", Double.parseDouble(arrFields[20].trim()));
recommendation.put("weight_kg", Double.parseDouble(arrFields[21].trim()));
recommendation.put("color", arrFields[22].trim());
recommendation.put("stuffedsku", Integer.parseInt(arrFields[23].trim()));
recommendation.put("skusinpo", Integer.parseInt(arrFields[24].trim()));
recommendation.put("povariation", Integer.parseInt(arrFields[25].trim()));
recommendations.add(recommendation);
}
else {
System.err.println("Invalid Recommendation record format: " + line);
}
}
} else {
System.out.println("Empty suggestions response.");
}
if (recommendations != null && recommendations.size() > 0) {
System.out.println("--- Suggestions ---");
for (Map<String, Object> recommendation : recommendations) {
System.out.printf(
"SO: %s, Line: %s, Container: %s, SKU: %s, Stuffed SKU Qty: %s, SKUs In PO: %s, Shortfall/Variation: %s, CBM: %s%n",
recommendation.get("so"), recommendation.get("line"), recommendation.get("container"), recommendation.get("sku_number"),
recommendation.get("stuffedsku"), recommendation.get("skusinpo"), recommendation.get("povariation"),recommendation.get("cbm")
);
// --- Your ERP Processing Logic for Suggestions Here ---
}
} else {
System.out.printf("No suggestions found 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(java.util.Locale.ROOT, "%.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("shape", 0);
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("rotationsAllowed", "abcdef");
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,shape,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,rotationsAllowed,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,%s",
removeComma((String) row.get("color")), row.get("shape"), 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("rotationsAllowed"), 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) {
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", "solver@loadviewer.com");
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
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 {
match jwtToken with
| Some token when not (String.IsNullOrWhiteSpace(token)) ->
return Some token
| _ ->
if retryCount > defaultMaxRetries then
eprintfn "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 = 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
eprintfn "Failed to retrieve token after multiple rate limits."
return None
elif not response.IsSuccessStatusCode then
if retryCount < defaultMaxRetries then
let! errorBody = 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 = response.Content.ReadAsStringAsync()
eprintfn "Failed to retrieve token after %d retries: %i - %s" defaultMaxRetries (int response.StatusCode) errorBody
return None
else
let! responseBody = response.Content.ReadAsStringAsync()
let jsonResponse = convertJsonToMap responseBody
if jsonResponse.ContainsKey("token") then
jwtToken <- Some(jsonResponse.["token"])
return jwtToken
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 |> Async.AwaitTask
// 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 = 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 = httpClient.PostAsync(Uri(uploadUrl), content)
if response.StatusCode = System.Net.HttpStatusCode.Unauthorized || response.StatusCode = System.Net.HttpStatusCode.Gone then
eprintfn "JWT token expired or invalid. Attempting to retrieve a new token."
jwtToken <- None
let! newTokenResult = 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 = 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 = response.Content.ReadAsStringAsync()
return sprintf "ERROR: Upload failed after %d attempts. Status: %i - %s" defaultMaxRetries (int response.StatusCode) errorBody
else
let! responseBody = 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 = 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 = httpClient.PostAsync(Uri(downloadUrl), content)
let! responseBody = 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
eprintfn "JWT token expired or invalid. Attempting to retrieve a new token."
jwtToken <- None
let! newTokenResult = 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
// Helper function to safely parse ints
let inline tryParseInt (s: string) defaultVal =
match Int32.TryParse(s) with
| true, v -> v
| _ -> defaultVal
// Helper function to safely parse doubles
let inline tryParseDouble (s: string) defaultVal =
match Double.TryParse(s, Globalization.NumberStyles.Float, Globalization.CultureInfo.InvariantCulture) with
| true, v -> v
| _ -> defaultVal
// Function to test getting suggestions
let testSuggestions (shipmentReference : string) (solverEmail : string) : Async<unit> =
async {
if String.IsNullOrWhiteSpace(shipmentReference) then
eprintfn "[ERROR] Shipment Reference is required!"
return ()
if String.IsNullOrWhiteSpace(solverEmail) ||
not (Regex.IsMatch(solverEmail, @"^[^\s@]+@[^\s@]+\.[^\s@]+$")) then
eprintfn "[ERROR] Invalid Solver Email format!"
return ()
let currentToken =
match jwtToken with
| Some token -> token
| None ->
let! tokenResult = getJwtTokenWithRetries 1
match tokenResult with
| Some token -> token
| None ->
eprintfn "[ERROR] Token Error or Server Not Reachable."
return ()
jwtToken <- Some(currentToken)
// --- Create the JSON request body ---
let requestBody = new Dictionary<string, string>()
requestBody.Add("reference", shipmentReference.Replace("\"", "\\\""))
requestBody.Add("solverEmail", solverEmail.Replace("\"", "\\\""))
let jsonBody = convertMapToJson requestBody
// --- Call the getLoadViewerSuggestionsCsvWithRetries function with POST ---
let! strResponse = getLoadViewerSuggestionsCsvWithRetries jsonBody 1
if strResponse.StartsWith("ERROR:") || strResponse.StartsWith("Exception:") then
eprintfn "[ERROR] Error retrieving data from LoadViewer: %s" strResponse
return ()
printfn "[INFO] Raw Suggestions Response:\n%s" strResponse
let arrLines = strResponse.Split([|"\r\n"; "\n"|], StringSplitOptions.None)
let recommendations = new List<Dictionary<string, obj>>()
if arrLines.Length > 0 then
for line in arrLines do
if not (String.IsNullOrWhiteSpace(line)) then
let arrFields = line.Split(',')
// Parse Container Manifest Rows (26 columns)
if arrFields.Length >= 26 then
// Skip the Header Row
if arrFields.[0].Trim().ToLower() <> "so" then
let lineNum = tryParseInt (arrFields.[1].Trim()) 0
let containerNum = tryParseInt (arrFields.[3].Trim()) 0
let maxWeight = tryParseDouble (arrFields.[5].Trim()) 0.0
let lengthMm = tryParseInt (arrFields.[11].Trim()) 0
let widthMm = tryParseInt (arrFields.[12].Trim()) 0
let heightMm = tryParseInt (arrFields.[13].Trim()) 0
let innerQty = tryParseInt (arrFields.[17].Trim()) 0
let pkgGrossWt = tryParseDouble (arrFields.[18].Trim()) 0.0
let stuffedPkgs = tryParseInt (arrFields.[19].Trim()) 0
let cbm = tryParseDouble (arrFields.[20].Trim()) 0.0
let weightKg = tryParseDouble (arrFields.[21].Trim()) 0.0
let stuffedSku = tryParseInt (arrFields.[23].Trim()) 0
let skusInPo = tryParseInt (arrFields.[24].Trim()) 0
let poVariation = tryParseInt (arrFields.[25].Trim()) 0
let clpRow = new Dictionary<string, obj>()
clpRow.["so"] <- arrFields.[0].Trim() :> obj
clpRow.["line"] <- lineNum :> obj
clpRow.["erpId"] <- arrFields.[2].Trim() :> obj
clpRow.["container"] <- containerNum :> obj
clpRow.["dimensionsLxWxH_mm"] <- arrFields.[4].Trim() :> obj
clpRow.["maxWeight_Kg"] <- maxWeight :> obj
clpRow.["skuNumber"] <- arrFields.[6].Trim() :> obj
clpRow.["description"] <- arrFields.[7].Trim() :> obj
clpRow.["po"] <- arrFields.[8].Trim() :> obj
clpRow.["shipper"] <- arrFields.[9].Trim() :> obj
clpRow.["pkgType"] <- arrFields.[10].Trim() :> obj
clpRow.["length_mm"] <- lengthMm :> obj
clpRow.["width_mm"] <- widthMm :> obj
clpRow.["height_mm"] <- heightMm :> obj
clpRow.["keepStraight"] <- arrFields.[14].Trim() :> obj
clpRow.["rotations"] <- arrFields.[15].Trim() :> obj
clpRow.["set"] <- arrFields.[16].Trim() :> obj
clpRow.["innerQty"] <- innerQty :> obj
clpRow.["pkgGrossWt"] <- pkgGrossWt :> obj
clpRow.["stuffedPkgs"] <- stuffedPkgs :> obj
clpRow.["cbm"] <- cbm :> obj
clpRow.["weight_Kg"] <- weightKg :> obj
clpRow.["color"] <- arrFields.[22].Trim() :> obj
clpRow.["stuffedSku"] <- stuffedSku :> obj
clpRow.["skusInPo"] <- skusInPo :> obj
clpRow.["poVariation"] <- poVariation :> obj
recommendations.Add(clpRow)
else
printfn "[INFO] Empty suggestions response."
// --- Output Debug Results ---
if recommendations.Count > 0 then
printfn "[INFO] --- Container Load Plan Rows ---"
for eachRow in recommendations do
printfn "[INFO] SO: %s, Line: %i, Container: %i, SKU: %s, Stuffed SKU Qty: %i, SKUs In PO: %i, Shortfall/Variation: %i, CBM: %f"
(eachRow.["so"] :?> string)
(eachRow.["line"] :?> int)
(eachRow.["container"] :?> int)
(eachRow.["skuNumber"] :?> string)
(eachRow.["stuffedSku"] :?> int)
(eachRow.["skusInPo"] :?> int)
(eachRow.["poVariation"] :?> int)
(eachRow.["cbm"] :?> float)
else
printfn "[INFO] No Container Load Plan rows found for reference: %s" 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("shape", 0 :> 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("rotationsAllowed", "abcdef" :> 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,shape,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,rotationsAllowed,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,%i,%s,%i,%i,%i,%s,%i,%i,%i,%i,%i,%i,%i,%s,%i,%s,%s,%s,%i,%s,%s"
(removeComma (row.["color"] :> string))
(row.["shape"] :> int)
(row.["lengthMm"] :> int)
(row.["widthMm"] :> int)
(row.["heightMm"] :> int)
(formatDecimal3 (row.["weightKg"] :> float))
(row.["cartons"] :> int)
(row.["additionalCartons"] :> int)
(row.["placement"] :> int)
(row.["rotationsAllowed"] :> string)
(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 = uploadToLoadViewerWithRetries csvData 1
printfn "Upload Result: %s" uploadResult
// Example of getting suggestions
do! testSuggestions "SANDBOX-01" "solver@loadviewer.com"
} |> Async.AwaitTask
0
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"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{
Timeout: 60 * time.Second,
}
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+1) // 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{
Timeout: 60 * time.Second,
}
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+1) // 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": shipmentReference,
"solverEmail": 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")
recommendations := []map[string]interface{}{}
for _, line := range arrLines {
if strings.TrimSpace(line) == "" {
continue
}
arrFields := strings.Split(line, ",")
// Container Manifest response contains 26 columns.
if len(arrFields) < 26 {
fmt.Println("ERROR: Invalid Container Manifest record:", line)
continue
}
// Skip the header row.
if strings.EqualFold(strings.TrimSpace(arrFields[0]), "SO") {
continue
}
lineNum := parseInt(strings.TrimSpace(arrFields[1]))
containerNum := parseInt(strings.TrimSpace(arrFields[3]))
maxWeight := parseFloat(strings.TrimSpace(arrFields[5]))
lengthMm := parseInt(strings.TrimSpace(arrFields[11]))
widthMm := parseInt(strings.TrimSpace(arrFields[12]))
heightMm := parseInt(strings.TrimSpace(arrFields[13]))
innerQty := parseInt(strings.TrimSpace(arrFields[17]))
pkgGrossWt := parseFloat(strings.TrimSpace(arrFields[18]))
stuffedPkgs := parseInt(strings.TrimSpace(arrFields[19]))
cbm := parseFloat(strings.TrimSpace(arrFields[20]))
weightKg := parseFloat(strings.TrimSpace(arrFields[21]))
stuffedSku := parseInt(strings.TrimSpace(arrFields[23]))
skusInPo := parseInt(strings.TrimSpace(arrFields[24]))
poVariation := parseInt(strings.TrimSpace(arrFields[25]))
recommendation := map[string]interface{}{
"so": strings.TrimSpace(arrFields[0]),
"line": lineNum,
"erpId": strings.TrimSpace(arrFields[2]),
"container": containerNum,
"dimensionsLxWxH_mm": strings.TrimSpace(arrFields[4]),
"maxWeight_Kg": maxWeight,
"skuNumber": strings.TrimSpace(arrFields[6]),
"description": strings.TrimSpace(arrFields[7]),
"po": strings.TrimSpace(arrFields[8]),
"shipper": strings.TrimSpace(arrFields[9]),
"pkgType": strings.TrimSpace(arrFields[10]),
"length_mm": lengthMm,
"width_mm": widthMm,
"height_mm": heightMm,
"keepStraight": strings.TrimSpace(arrFields[14]),
"rotations": strings.TrimSpace(arrFields[15]),
"set": strings.TrimSpace(arrFields[16]),
"innerQty": innerQty,
"pkgGrossWt": pkgGrossWt,
"stuffedPkgs": stuffedPkgs,
"cbm": cbm,
"weight_Kg": weightKg,
"color": strings.TrimSpace(arrFields[22]),
"stuffedSku": stuffedSku,
"skusInPo": skusInPo,
"poVariation": poVariation,
}
recommendations = append(recommendations, recommendation)
}
if len(recommendations) > 0 {
fmt.Println("--- Container Load Plan Rows ---")
for _, recommendation := range recommendations {
fmt.Printf(
"SO: %s, Line: %d, Container: %d, SKU: %s, Stuffed SKU Qty: %d, SKUs In PO: %d, Shortfall/Variation: %d, CBM: %f\n",
recommendation["so"],
recommendation["line"],
recommendation["container"],
recommendation["skuNumber"],
recommendation["stuffedSku"],
recommendation["skusInPo"],
recommendation["poVariation"],
recommendation["cbm"],
)
}
} else {
fmt.Printf("No Container Load Plan rows found for reference: %s\n", shipmentReference)
}
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.TrimRight(strings.TrimRight(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",
"shape": 0,
"lengthMm": 700,
"widthMm": 500,
"heightMm": 300,
"weightKg": 91.5,
"cartons": 300,
"additionalCartons": 0,
"placement": 0,
"rotationsAllowed": "abcdef",
"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,shape,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,rotationsAllowed,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,%d,%s,%d,%d,%d,%s,%d,%d,%d,%d,%d,%d,%d,%s,%d,%s,%s,%s,%d,%s,%s",
removeComma(cargoData["color"].(string)),
cargoData["shape"].(int),
cargoData["lengthMm"].(int),
cargoData["widthMm"].(int),
cargoData["heightMm"].(int),
formatDecimal3(cargoData["weightKg"].(float64)),
cargoData["cartons"].(int),
cargoData["additionalCartons"].(int),
cargoData["placement"].(int),
cargoData["rotationsAllowed"].(string),
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 parseFloat(s string) float64 {
f, _ := strconv.ParseFloat(s, 64)
return f
}
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", "solver@loadviewer.com")
if err != nil {
fmt.Println("Test Suggestions Error:", err)
}
}
# 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+1)) # 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+1)) # 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]]
recommendations <- list()
if (length(arr_lines) > 0) {
for (line in arr_lines) {
if (trimws(line) == "") {
next
}
arr_fields <- strsplit(line, ",")[[1]]
if (length(arr_fields) < 26) {
cat(sprintf("ERROR: Invalid suggestion record format: %s\n", line))
next
}
# Header row
if (toupper(trimws(arr_fields[1])) == "SO") {
next
}
recommendation <- list(
so = trimws(arr_fields[1]),
line = as.integer(trimws(arr_fields[2])),
erpId = trimws(arr_fields[3]),
container = as.integer(trimws(arr_fields[4])),
dimensionsLxWxH_mm = trimws(arr_fields[5]),
maxWeight_Kg = as.numeric(trimws(arr_fields[6])),
skuNumber = trimws(arr_fields[7]),
description = trimws(arr_fields[8]),
po = trimws(arr_fields[9]),
shipper = trimws(arr_fields[10]),
pkgType = trimws(arr_fields[11]),
length_mm = as.integer(trimws(arr_fields[12])),
width_mm = as.integer(trimws(arr_fields[13])),
height_mm = as.integer(trimws(arr_fields[14])),
keepStraight = trimws(arr_fields[15]),
rotations = trimws(arr_fields[16]),
set = trimws(arr_fields[17]),
innerQty = as.integer(trimws(arr_fields[18])),
pkgGrossWt = as.numeric(trimws(arr_fields[19])),
stuffedPkgs = as.integer(trimws(arr_fields[20])),
cbm = as.numeric(trimws(arr_fields[21])),
weight_Kg = as.numeric(trimws(arr_fields[22])),
color = trimws(arr_fields[23]),
stuffedSku = as.integer(trimws(arr_fields[24])),
skusInPo = as.integer(trimws(arr_fields[25])),
poVariation = as.integer(trimws(arr_fields[26]))
)
recommendations <- append(recommendations, list(recommendation))
}
}
if (length(recommendations) > 0) {
cat("--- Suggestions ---\n")
for (recommendation in recommendations) {
cat(sprintf(
"SO: %s, Line: %d, Container: %d, SKU: %s, Stuffed SKU Qty: %d, SKUs In PO: %d, PO Variation: %d, CBM: %s\n",
recommendation$so,
recommendation$line,
recommendation$container,
recommendation$skuNumber,
recommendation$stuffedSku,
recommendation$skusInPo,
recommendation$poVariation,
recommendation$cbm
))
# --- 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")),
shape = c(NA, 0),
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),
rotationsAllowed = c(NA, remove_comma("abcdef")),
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", "solver@loadviewer.com")
}
<?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_TIMEOUT, 60);
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_TIMEOUT, 60);
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) {
if ($retry_count < DEFAULT_MAX_RETRIES) {
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.";
}
$retry_count++;
curl_close($ch);
return upload_to_loadviewer_with_retries($csv_data, $retry_count); // Retry with the new token
}
curl_close($ch);
return "ERROR: Upload failed after multiple retries.";
} 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_TIMEOUT, 60);
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) {
if ($retry_count < DEFAULT_MAX_RETRIES) {
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.";
}
$retry_count++;
curl_close($ch);
return get_loadviewer_suggestions_csv_with_retries($request_body_json, $retry_count); // Retry with the new token
}
curl_close($ch);
return "ERROR: Failed to retrieve suggestions after multiple retries.";
} 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' => $shipment_reference,
'solverEmail' => $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);
$recommendations = [];
foreach ($arr_lines as $line) {
if (trim($line) === '') {
continue;
}
$arr_fields = explode(',', $line);
// Container Manifest response contains 26 columns.
if (count($arr_fields) < 26) {
error_log("ERROR: Invalid Container Manifest record: " . $line);
continue;
}
// Skip the header row.
if (strcasecmp(trim($arr_fields[0]), 'SO') === 0) {
continue;
}
$recommendation = [
'so' => trim($arr_fields[0]),
'line' => intval(trim($arr_fields[1])),
'erpId' => trim($arr_fields[2]),
'container' => intval(trim($arr_fields[3])),
'dimensionsLxWxH_mm' => trim($arr_fields[4]),
'maxWeight_Kg' => floatval(trim($arr_fields[5])),
'skuNumber' => trim($arr_fields[6]),
'description' => trim($arr_fields[7]),
'po' => trim($arr_fields[8]),
'shipper' => trim($arr_fields[9]),
'pkgType' => trim($arr_fields[10]),
'length_mm' => intval(trim($arr_fields[11])),
'width_mm' => intval(trim($arr_fields[12])),
'height_mm' => intval(trim($arr_fields[13])),
'keepStraight' => trim($arr_fields[14]),
'rotations' => trim($arr_fields[15]),
'set' => trim($arr_fields[16]),
'innerQty' => intval(trim($arr_fields[17])),
'pkgGrossWt' => floatval(trim($arr_fields[18])),
'stuffedPkgs' => intval(trim($arr_fields[19])),
'cbm' => floatval(trim($arr_fields[20])),
'weight_Kg' => floatval(trim($arr_fields[21])),
'color' => trim($arr_fields[22]),
'stuffedSku' => intval(trim($arr_fields[23])),
'skusInPo' => intval(trim($arr_fields[24])),
'poVariation' => intval(trim($arr_fields[25]))
];
$recommendations[] = $recommendation;
}
if (count($recommendations) > 0) {
echo "--- Container Load Plan Rows ---\n";
foreach ($recommendations as $recommendation) {
echo "SO: " . $recommendation['so']
. ", Line: " . $recommendation['line']
. ", Container: " . $recommendation['container']
. ", SKU: " . $recommendation['skuNumber']
. ", Stuffed SKU Qty: " . $recommendation['stuffedSku']
. ", SKUs In PO: " . $recommendation['skusInPo']
. ", Shortfall/Variation: " . $recommendation['poVariation']
. ", CBM: " . $recommendation['cbm'] . "\n";
// --- Your ERP Processing Logic for Container Manifest Here ---
}
} else {
echo "No Container Load Plan rows found for reference: " . $shipment_reference . "\n";
}
}
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',
'shape' => 0,
'lengthMm' => 700,
'widthMm' => 500,
'heightMm' => 300,
'weightKg' => 91.5,
'cartons' => 300,
'additionalCartons' => 0,
'placement' => 0,
'rotationsAllowed' => "abcdef",
'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,shape,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,rotationsAllowed,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['shape']. "," . $row['lengthMm'] . "," . $row['widthMm'] . "," . $row['heightMm'] . "," . format_decimal_3($row['weightKg']) . "," . $row['cartons'] . "," . $row['additionalCartons'] . "," . $row['placement'] . "," . remove_comma($row['rotationsAllowed']) . "," . $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", "solver@loadviewer.com");
?>
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+1); // 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+1); // 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@]+$');
if (solverEmail.isNotEmpty && !emailRegex.hasMatch(solverEmail)) {
print('[ERROR] Invalid Solver Email format!');
return;
}
String? token = jwtToken ?? await getJwtTokenWithRetries();
if (token == null || token.isEmpty) {
print('[ERROR] Token Error or Server Not Reachable.');
return;
}
// --- Create the JSON request body ---
final Map<String, String> requestBody = {
'reference': shipmentReference,
'solverEmail': solverEmail,
};
final String jsonBody = jsonEncode(requestBody);
// --- Call the getLoadViewerSuggestionsCsvWithRetries function with POST ---
final String strResponse =
await getLoadViewerSuggestionsCsvWithRetries(jsonBody);
if (strResponse.startsWith('Error:') || strResponse.startsWith('Exception:')) {
print('[ERROR] Error retrieving data from LoadViewer: $strResponse');
return;
}
print('[INFO] Raw Suggestions Response:\n$strResponse');
final List<String> arrLines = LineSplitter.split(strResponse).toList();
final List<Map<String, dynamic>> recommendations = [];
if (arrLines.isNotEmpty) {
for (String line in arrLines) {
if (line.trim().isEmpty) continue;
final List<String> arrFields = line.split(',');
if (arrFields.length >= 26) {
// Skip the Header Row
if (arrFields[0].trim().toLowerCase() != 'so') {
final int lineNum = int.tryParse(arrFields[1].trim()) ?? 0;
final int containerNum = int.tryParse(arrFields[3].trim()) ?? 0;
final double maxWeight = double.tryParse(arrFields[5].trim()) ?? 0.0;
final double lengthMm = double.tryParse(arrFields[11].trim()) ?? 0.0;
final double widthMm = double.tryParse(arrFields[12].trim()) ?? 0.0;
final double heightMm = double.tryParse(arrFields[13].trim()) ?? 0.0;
final int innerQty = int.tryParse(arrFields[17].trim()) ?? 0;
final double pkgGrossWt = double.tryParse(arrFields[18].trim()) ?? 0.0;
final int stuffedPkgs = int.tryParse(arrFields[19].trim()) ?? 0;
final double cbm = double.tryParse(arrFields[20].trim()) ?? 0.0;
final double weightKg = double.tryParse(arrFields[21].trim()) ?? 0.0;
final int stuffedSku = int.tryParse(arrFields[23].trim()) ?? 0;
final int skusInPo = int.tryParse(arrFields[24].trim()) ?? 0;
final int poVariation = int.tryParse(arrFields[25].trim()) ?? 0;
final Map<String, dynamic> clpRow = {
'so': arrFields[0].trim(),
'line': lineNum,
'erpId': arrFields[2].trim(),
'container': containerNum,
'dimensionsLxWxH_mm': arrFields[4].trim(),
'maxWeight_Kg': maxWeight,
'skuNumber': arrFields[6].trim(),
'description': arrFields[7].trim(),
'po': arrFields[8].trim(),
'shipper': arrFields[9].trim(),
'pkgType': arrFields[10].trim(),
'length_mm': lengthMm,
'width_mm': widthMm,
'height_mm': heightMm,
'keepStraight': arrFields[14].trim(),
'rotations': arrFields[15].trim(),
'set': arrFields[16].trim(),
'innerQty': innerQty,
'pkgGrossWt': pkgGrossWt,
'stuffedPkgs': stuffedPkgs,
'cbm': cbm,
'weight_Kg': weightKg,
'color': arrFields[22].trim(),
'stuffedSku': stuffedSku,
'skusInPo': skusInPo,
'poVariation': poVariation,
};
recommendations.add(clpRow);
}
}
}
} else {
print('[INFO] Empty suggestions response.');
}
// --- Output Debug Results ---
if (recommendations.isNotEmpty) {
print('[INFO] --- Container Load Plan Rows ---');
for (var eachRow in recommendations) {
print(
'[INFO] SO: ${eachRow['so']}, Line: ${eachRow['line']}, Container: ${eachRow['container']}, SKU: ${eachRow['skuNumber']}, Stuffed SKU Qty: ${eachRow['stuffedSku']}, SKUs In PO: ${eachRow['skusInPo']}, Shortfall/Variation: ${eachRow['poVariation']}, CBM: ${eachRow['cbm']}');
// --- your erp processiong logic here ---
}
} else {
print('[INFO] No Container Load Plan rows 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',
'shape',
'lengthMm',
'widthMm',
'heightMm',
'weightKg',
'cartons',
'additionalCartons',
'placement',
'rotationsAllowed',
'skuPerCarton',
'emptyPalletHeightMm',
'trayCapMm',
'trayHeightMm',
'marginLenghtMm',
'marginWidthMm',
'marginHeightMm',
'nameOfSet',
'cartonRatioInSet',
'erpId',
'skuNumber',
'description',
'destination',
'po',
'shipper'
]);
csvData.add([
'C',
'D',
removeComma('#008000'),
'0',
'700',
'500',
'300',
formatDecimal3(91.5),
'300',
'0',
'0',
'abcdef',
'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', 'solver@loadviewer.com');
} catch (e) {
print('Error: $e'); // Catch and print any exceptions during the process.
}
}
Important Reminders
- Requests without API Key will return HTTP
400or401 - Requests with expired taken shall return HTTP
410you 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
successand noerrorstructure in responses