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,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper
C,D,#00246B,700,500,300,91.5,300,0,0,True,1,0,0,0,0,0,0,,0,,,,0,,
Explanation
S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra
: Shipment-level 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,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,shipper,destination,skuNumber,description,po,color
: Cargo headersC,D,,700,500,300,1.5,300,0,0,True,1,0,0,0,0,0,0,,0,,0,,,,#00246B
: Cargo data rows (can be multiple)
Expected Fields
We have used the FIXED, Mandatory, and Optional identifiers for the fields. Rules for data values in each of these fields are as follows.
For FIXED fields no other data is accepted. Match the exact upper and lower case. Do not include extra spaces or characters in any such FIXED fields.
For Mandatory fields, the field cannot be blank. Use 0 for numeric fields and meaningful text for string (character) mandatory fields.
For Optional fields, you may leave them blank for text fields and use 0 for numeric values.
-
Shipment(S) Header(H) Row - Only one such row per CSV allowed.
S
: FIXED. Always provideS
here. Since this belongs to the Shipment Section.H
: FIXED. Always provideH
here. 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 provideS
here. Since this belongs to the Shipment Section. - Row Type =
D
: FIXED. Always provideD
here. Since this is a Data row of the Section. -
reference =
SANDBOX-01
: Mandatory, String: MaxLength[50]. Provide a unique shipment/job reference. Values starting withSANDBOX
are for testing purposes and are not chargeable. -
hangingAllowed =
True
: Mandatory. Allowed values –True
orFalse
. 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 –True
orFalse
. This is useful to control your coin usage for this shipment. -
uniqueErpId =
False
(Default): Mandatory. Allowed values –True
orFalse
. When set toTrue
, all cargoes within the request must have a unique value in theerpId
field. -
suggestExtra =
False
(Default): Mandatory. Allowed values –True
orFalse
. Set toTrue
when you want the system to consider loading additional cargo into the containers beyond the initially specified quantities. Set toFalse
otherwise.
- Section =
-
Container(B) Header(H) Row - Only one such row per CSV allowed.
B
: FIXED. Always provideB
here. Since this belongs to the Container (Bin for you to relate to the Abbreviation B) Section.H
: FIXED. Always provideH
here. 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 provideB
here. Since this belongs to the Container Section. - Row Type =
D
: FIXED. Always provideD
here. Since this is a Data row of the Section. -
lengthMm =
5867
: Mandatory, Number: Range [250-30000]. Decimals and commas not allowed. Internal Length (Depth) of the Container in Millimeters (mm). -
widthMm =
2352
: Mandatory, Number: Range [250-3000]. Decimals and commas not allowed. Internal Width of the Container in Millimeters (mm). -
heightMm =
2393
: Mandatory, Number: Range [250-3000]. Decimals and commas not allowed. Internal Height of the Container in Millimeters (mm). -
maxWeightKg =
27200
: Mandatory, Decimal (up to three digits after the decimal). Range [0.001, 50000]. Commas not allowed.Maximum Weight that can be stuffed in the Container in Kg (Kilogram).
-
backMarginMm =
0
: Mandatory, Number: Range [0-200]. Decimals not allowed. Sometimes you may wish LoadViewer not to plan the area near the door of the Container. -
isRefrigerated =
False
: Mandatory. Allowed values:True
orFalse
. Indicates whether the Container is Refree container or not. -
maxUseOne =
False
(Default): Mandatory. Allowed values:True
orFalse
. 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 provideC
here. Since this belongs to the Cargo Section.H
: FIXED. Always provideH
here. Since this is a Header row of the Section.erpId
: FIXED.lengthMm
,widthMm
andheightMm
: FIXED.weightKg
: FIXED.cartons
: FIXED.additionalCartons
: FIXED.placement
: FIXED.verticalRotationAllowed
: FIXED.skuPerCarton
: FIXED.emptyPalletHeightMm
: FIXED.trayCapMm
: FIXED.trayHeightMm
: FIXED.marginLenghtMm
,marginWidthMm
andmarginHeightMm
: 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 provideC
here. Since this belongs to the Cargo Section. - Row Type =
D
: FIXED. Always provideD
here. Since this is a Data row of the Section. -
erpId =
: Optional, String: MaxLength [40], It should correspond to a unique identifier in your ERP that you wish to receive back in the suggestion response.
-
lengthMm =
700
, widthMm =500
and heightMm =300
: Mandatory, Number: Range [10–3000]. Decimals and commas are not allowed. External dimensions of the cargo in millimeters (mm): Length, Width, and Height respectively. -
weightKg =
91.5
: Mandatory, Decimal (up to three digits after the decimal). Range [0.001–1000]. Commas are not allowed.Weight of the carton or pallet in kilograms (kg).
-
cartons =
300
: Mandatory, Number: Range [1–30000]. Decimals and commas are not allowed. Number of desired cartons (or pallets) you want to load into containers. -
additionalCartons =
0
: Mandatory, Number: Range [0–30000]. Decimals and commas are not allowed. LoadViewer will try to place number of additional cargo (cartons or pallets) defined here in any remaining space that cannot be filled by the desired quantities specified in the cartons section. -
placement =
0
: Mandatory, Number: Range [0–4]. Defines how the item should be vertically placed inside the container.This setting determines the vertical placement of the item, mainly for palletized cargo or goods with specific stacking requirements.
-
verticalRotationAllowed =
True
: Mandatory. Allowed values:True
orFalse
. Indicates whether the item can be rotated vertically during load planning.When vertical rotation is
True
, LoadViewer explores all six possible orientations to optimize space inside the container.
💡 Image showing all six possible rotations : -
skuPerCarton =
1
: Mandatory, Number: Range[1–10000]. Decimals and commas are not allowed. Represents how many units of the SKU are packed inside one carton or pallet. -
emptyPalletHeightMm =
0
: Mandatory, Number: Range [90–200] or 0. Use0
for cartons, and any value between90
to200
mm for empty pallet height (i.e., pallet feet only). -
trayCapMm =
0
: Mandatory, Number: Range [0–200]. Represents the top cap height. This does not reserve any extra space above heightMm but adds a line near the top edge of the cargo for visual reference. -
trayHeightMm =
0
: Mandatory, Number: Range [0–200]. Represents the tray height in mm. This does not reserve any extra space below heightMm but adds a line near the bottom edge of the cargo for visual reference. -
marginLenghtMm =
0
, marginWidthMm =0
, and marginHeightMm =0
: Mandatory, Number: Range [0–20]. This can be used to allow LoadViewer to keep margins around your cargo up to 2 cm (20 mm). - nameOfSet : Optional, String: MaxLength [1], Range [B–Y]. You can define up to 24 sets per shipment. Values A and Z are reserved. Leave it blank if your cargo does not belong to any set.
-
cartonRatioInSet =
0
: Mandatory, Number: Range [0–100]. Works in conjunction with nameOfSet. When nameOfSet is defined, this must be greater 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]. Use0
when all cargo is for a single destination. This indicates load/unload priority. Lower values are placed closer to the container door for earlier unloading. Higher values are placed deeper inside the container for last unloading. This improves cargo handling efficiency during transit. - skuNumber : Optional, String: MaxLength [40]. You can enter SKU number for LoadViewer reports.
- description : Optional, String: MaxLength [60]. You can enter SKU description, or any meaningful value for LoadViewer reports.
-
po : Optional, String: MaxLength [20]. You can enter the Purchase Order number to appear on LoadViewer reports.
LoadViewer uses this to group SKUs of the same PO in the same container, if sizes and quantities allow.
💡 Practical Use Case – PO (Purchase Order): -
color =
#008000
: Mandatory, String: MaxLength [9], Hex color code for your cargo.
- Section =
Notes:
- Separator must be a comma (,)
- All header and detail rows must strictly follow the
S/B/C
+H/D
format. - All header rows are case sensitive and must strictly follow the same format as mentioned in the Headers sections above.
- Always provide a
S,H
row followed by at least oneS,D
row - The order of sections can be any. But our example mostly reflects: Shipment → Container(s) → Cargo(s)
Tip: Download SANDBOX-01 and modify the sandbox CSV for accuracy and editing in your text editor / Excel / OpenOffice Calc or other editor of your choice.
2. Authentication
The 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
Business Email Domain Required
API keys are not available for accounts using free email domains (e.g., Gmail, Hotmail, Yahoo).
If you already have a business email, please log in with that account.
Or, update your email to a business address like your_name@yourcompany.com
.
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.
' === 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
reference
does not start withSANDBOX
in manual upload mode. - Manual uploads are not charged coins unless published.
- If the upload is successful, your file structure is correct.
- Repeat this until your ERP consistently generates acceptable files.
- 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
reference
that starts withSANDBOX
. - No coins are consumed for such files.
reference
starting withSANDBOX
can be provided in mixed case (Upper, Lower, camelCase or else). - File is accepted and validated but not published.
- This lets you test end-to-end without impacting live shipments.
E.g.,
SANDBOX-01
is a valid test reference for your dummy data. - 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
ApiKey
in the request headers - Use a Shipment
reference
starting withSANDBOX
for test uploads - Enable logging of request payloads and API responses within the ERP
- Test various combinations of SKUs and file sizes before going live
Test Code to Upload your CSV data
Note: The example uses integration version V1
.
The examples below can be used in planning stage where you may be interested in knowning how many articals can be loaded in a single container.
' ========================================
' Module: modLoadViewerCSV
' Purpose:
' 1. Generate LoadViewer-compatible CSV from hardcoded or database values
' 2. Upload CSV to LoadViewer using HTTP POST (WinHttp)
' Includes helper functions:
' - removeComma: for all string fields
' - formatDecimal3: for weights with decimals
' ========================================
' === Constants ===
Const LoadViewerTokenURL As String = "https://www.loadviewer.com/api/auth/jwt/token"
Const LoadViewerUploadURL As String = "https://www.loadviewer.com/api/integration/v1/upload"
Const LoadViewerDownloadURL As String = "https://www.loadviewer.com/api/integration/v1/download"
Const LoadViewerAPIEmail As String = "YOUR_Email"
Const LoadViewerAPIKey As String = "YOUR_API_KEY_HERE"
' Optional: You can fetch the above values from file or DB:
' - From INI file in App.Path
' - From encrypted DB table
' - From registry or environment variable
'
Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
' === Module level variable to cache the retrieved token ===
Public LoadViewerToken As String
' === Get JWT Token ===
Function getJwtToken() As String
Dim retry As Integer
Dim maxRetry As Integer
Dim logMessage As String
Dim http As Object
Dim tokenURL As String
Dim jsonBody As String
Dim Wait As Long ' milliseconds for subsequent retries
Dim requestTimeout As Long ' milliseconds
Dim rateLimitWait As Long ' milliseconds for 429
maxRetry = 3
retry = maxRetry
Wait = 5000 ' 5 seconds for general retries
rateLimitWait = 65000 ' 65 seconds for 429 (slightly over 1 minute)
requestTimeout = 30000 ' 30 seconds timeout (adjust as needed)
tokenURL = LoadViewerTokenURL
jsonBody = "{""email"":""" & LoadViewerAPIEmail & """,""apikey"":""" & LoadViewerAPIKey & """}"
fetchFromServer:
Set http = CreateObject("WinHttp.WinHttpRequest.5.1")
On Error GoTo ErrHandler
http.setTimeouts requestTimeout, requestTimeout, requestTimeout, requestTimeout
With http
.Open "POST", tokenURL, False
.SetRequestHeader "Content-Type", "application/json"
.Send jsonBody
End With
If http.status = 200 Then
getJwtToken = extractTokenFromResponse(http.ResponseText)
LoadViewerToken = getJwtToken
Exit Function
ElseIf http.status = 429 Then ' Rate limit encountered
If retry > 0 Then
retry = retry - 1
Debug.Print "Rate limit hit on token endpoint. Retrying in " & (rateLimitWait / 1000) & " seconds... (Attempt " & (maxRetry - retry) & " of " & maxRetry & ")"
Sleep rateLimitWait
GoTo fetchFromServer
Else
getJwtToken = "ERROR: " & http.status & " - Rate limit exceeded on token retrieval." & " - " & http.ResponseText
Exit Function
End If
ElseIf retry > 0 Then
retry = retry - 1
Debug.Print "Retrying token retrieval in " & (Wait / 1000) & " seconds... (Attempt " & (maxRetry - retry) & " of " & maxRetry & ")"
Sleep Wait
GoTo fetchFromServer
End If
getJwtToken = "ERROR: " & http.status & " - " & http.ResponseText
Exit Function
ErrHandler:
getJwtToken = "Exception: " & Err.Description
Set http = Nothing
End Function
' === Upload CSV using Bearer Token ===
Function UploadTOLoadViewer(csvData As String, jwtToken As String) As String
Dim retry As Integer
Dim maxRetry As Integer
Dim logMessage As String
Dim http As Object
Dim waitTime As Long ' milliseconds for general retries
Dim rateLimitWait As Long ' milliseconds for 429
Dim requestTimeout As Long ' milliseconds
maxRetry = 3
retry = maxRetry ' Set the maximum number of retries
waitTime = 5000 ' 5 seconds in milliseconds
rateLimitWait = 65000 ' 65 seconds for 429
requestTimeout = 30000 ' 30 seconds timeout (adjust as needed)
UploadToServer:
Set http = CreateObject("WinHttp.WinHttpRequest.5.1")
On Error GoTo ErrHandler
' Set the timeout before opening the connection using positional arguments
http.setTimeouts requestTimeout, requestTimeout, requestTimeout, requestTimeout
With http
.Open "POST", LoadViewerUploadURL, False
.SetRequestHeader "Content-Type", "text/csv"
.SetRequestHeader "Authorization", "Bearer " & jwtToken
'.SetRequestHeader "Accept", "text/plain" ' Hint: Comment this line to get JSON format response
.Send csvData
End With
If http.status = 200 Or http.status = 202 Then 'Success (200) or Accepted (202)
UploadTOLoadViewer = http.status & " - " & http.ResponseText
Exit Function ' Exit the function if successful
ElseIf http.status = 429 Then ' Rate limit encountered
If retry > 0 Then
retry = retry - 1
Debug.Print "Rate limit hit on upload endpoint. Retrying in " & (rateLimitWait / 1000) & " seconds... (Attempt " & (maxRetry - retry) & " of " & maxRetry & ")"
Sleep rateLimitWait
GoTo UploadToServer
Else
UploadTOLoadViewer = "ERROR: " & http.status & " - Rate limit exceeded on upload." & " - " & http.ResponseText
Exit Function
End If
ElseIf http.status = 410 Then ' Token expired
getJwtToken
jwtToken = LoadViewerToken
' No need to wait, retry immediately after getting a new token
ElseIf http.status <> 409 And retry > 0 Then ' 409 is duplicate shipment error
' Wait before retrying (except after getting a new token)
retry = retry - 1
Debug.Print "Retrying Upload in " & (waitTime / 1000) & " seconds... (Attempt " & (maxRetry - retry) & " of " & maxRetry & ")"
Sleep waitTime ' Wait 5 seconds
GoTo UploadToServer
End If
' If we reach here, all retries have failed
UploadTOLoadViewer = "ERROR: " & http.status & " - " & http.ResponseText
Exit Function ' Exit the function
ErrHandler:
UploadTOLoadViewer = "Exception: " & Err.Description
Set http = Nothing
End Function
' === Get LoadViewer Suggestions CSV ===
Function GetLoadViewerSuggestionsCSV(jwtToken As String, requestBody As String) As String
Dim retry As Integer
Dim maxRetries As Integer
Dim logMessage As String
Dim http As Object
Dim waitTime As Long ' milliseconds for general retries
Dim rateLimitWait As Long ' milliseconds for 429
Dim requestTimeout As Long ' milliseconds
maxRetries = 3 ' Set the maximum number of retries
retry = maxRetries
waitTime = 5000 ' 5 seconds in milliseconds
rateLimitWait = 65000 ' 65 seconds for 429
requestTimeout = 30000 ' 30 seconds timeout (adjust as needed)
TrySuggestionsFetch:
Set http = CreateObject("WinHttp.WinHttpRequest.5.1")
On Error GoTo ErrHandler
' Set the timeout before opening the connection using positional arguments
http.setTimeouts requestTimeout, requestTimeout, requestTimeout, requestTimeout
With http
.Open "POST", LoadViewerDownloadURL, False ' Corrected URL variable name
.SetRequestHeader "Content-Type", "application/json"
.SetRequestHeader "Authorization", "Bearer " & jwtToken
.Send requestBody
logMessage = "POST Request to LoadViewer Suggestions URL: " & LoadViewerDownloadURL & vbCrLf ' Corrected URL variable name
logMessage = logMessage & "Request Body: " & vbCrLf & requestBody & vbCrLf
logMessage = logMessage & "HTTP Status Code: " & http.status & vbCrLf
logMessage = logMessage & "Response Text: " & vbCrLf & http.ResponseText & vbCrLf
Debug.Print logMessage
End With
If http.status = 200 Or http.status = 202 Then 'Success (200) or Accepted (202)
GetLoadViewerSuggestionsCSV = http.ResponseText
Exit Function ' Exit the function if successful
ElseIf http.status = 429 Then ' Rate limit encountered
If retry > 0 Then
retry = retry - 1
Debug.Print "Rate limit hit on suggestions endpoint. Retrying in " & (rateLimitWait / 1000) & " seconds... (Attempt " & (maxRetries - retry) & " of " & maxRetries & ")"
Sleep rateLimitWait
GoTo TrySuggestionsFetch
Else
GetLoadViewerSuggestionsCSV = "ERROR: " & http.status & " - Rate limit exceeded on suggestions retrieval." & " - " & http.ResponseText
Exit Function
End If
ElseIf http.status = 410 Then ' Token expired
getJwtToken
jwtToken = LoadViewerToken
' No need to wait, retry immediately after getting a new token
ElseIf retry > 0 Then
retry = retry - 1
Debug.Print "Retrying Suggestions Upload in " & (waitTime / 1000) & " seconds... (Attempt " & (maxRetries - retry) & " of " & maxRetries & ")"
Sleep waitTime ' Wait 5 seconds
GoTo TrySuggestionsFetch
End If
' If we reach here, all retries have failed
GetLoadViewerSuggestionsCSV = "ERROR: " & http.status & " - " & http.ResponseText
Exit Function ' Exit the function
ErrHandler:
GetLoadViewerSuggestionsCSV = "Exception: " & Err.Description
Set http = Nothing
End Function
' === Dynamically generates LoadViewer CSV from ADODB recordsets ===
'For csvGenerate() to work properly you may need to add Reference to
'Microsoft ActiveX Data Object 2.8 Library
'(C:\Program Files (x86)\Common Files\System\ado\msado28.tlb)
'OR any other version for ADODB.Recordset available to you.
Function csvGenerate() As String
Dim csv As String
Dim rsShipment As ADODB.Recordset
Dim rsContainer As ADODB.Recordset
Dim rsCargo As ADODB.Recordset
Set rsShipment = New ADODB.Recordset
Set rsContainer = New ADODB.Recordset
Set rsCargo = New ADODB.Recordset
' --- Define Shipment fields ---
rsShipment.Fields.Append "reference", adVarChar, 50
rsShipment.Fields.Append "hangingAllowed", adBoolean
rsShipment.Fields.Append "useMultipleContainers", adBoolean
rsShipment.Fields.Append "uniqueErpId", adBoolean
rsShipment.Fields.Append "suggestExtra", adBoolean
rsShipment.Open
rsShipment.AddNew
rsShipment("reference") = "SANDBOX-01"
rsShipment("hangingAllowed") = True
rsShipment("useMultipleContainers") = False
rsShipment("uniqueErpId") = False
rsShipment("suggestExtra") = False
rsShipment.Update
' --- Define Container fields ---
rsContainer.Fields.Append "lengthMm", adInteger
rsContainer.Fields.Append "widthMm", adInteger
rsContainer.Fields.Append "heightMm", adInteger
rsContainer.Fields.Append "maxWeightKg", adDouble
rsContainer.Fields.Append "backMarginMm", adInteger
rsContainer.Fields.Append "isRefrigerated", adBoolean
rsContainer.Fields.Append "maxUseOne", adBoolean
rsContainer.Fields.Append "lclBreakEvenCBM", adDouble
rsContainer.Open
rsContainer.AddNew
rsContainer("lengthMm") = 5867
rsContainer("widthMm") = 2352
rsContainer("heightMm") = 2393
rsContainer("maxWeightKg") = 27200
rsContainer("backMarginMm") = 0
rsContainer("isRefrigerated") = False
rsContainer("maxUseOne") = False
rsContainer("lclBreakEvenCBM") = 0
rsContainer.Update
' --- Define Cargo fields ---
rsCargo.Fields.Append "color", adVarChar, 10
rsCargo.Fields.Append "lengthMm", adInteger
rsCargo.Fields.Append "widthMm", adInteger
rsCargo.Fields.Append "heightMm", adInteger
rsCargo.Fields.Append "weightKg", adDouble
rsCargo.Fields.Append "cartons", adInteger
rsCargo.Fields.Append "additionalCartons", adInteger
rsCargo.Fields.Append "placement", adInteger
rsCargo.Fields.Append "verticalRotationAllowed", adBoolean
rsCargo.Fields.Append "skuPerCarton", adInteger
rsCargo.Fields.Append "emptyPalletHeightMm", adInteger
rsCargo.Fields.Append "trayCapMm", adInteger
rsCargo.Fields.Append "trayHeightMm", adInteger
rsCargo.Fields.Append "marginLength", adInteger
rsCargo.Fields.Append "marginWidth", adInteger
rsCargo.Fields.Append "marginHeight", adInteger
rsCargo.Fields.Append "nameOfSet", adVarChar, 1
rsCargo.Fields.Append "cartonRatioInSet", adInteger
rsCargo.Fields.Append "erpId", adVarChar, 40
rsCargo.Fields.Append "skuNumber", adVarChar, 40
rsCargo.Fields.Append "description", adVarChar, 60
rsCargo.Fields.Append "destination", adInteger
rsCargo.Fields.Append "po", adVarChar, 20
rsCargo.Fields.Append "shipper", adVarChar, 20
rsCargo.Open
rsCargo.AddNew
rsCargo("color") = "#008000"
rsCargo("lengthMm") = 700
rsCargo("widthMm") = 500
rsCargo("heightMm") = 300
rsCargo("weightKg") = 91.5 ' ? "91.5"
rsCargo("cartons") = 300
rsCargo("additionalCartons") = 0
rsCargo("placement") = 0
rsCargo("verticalRotationAllowed") = True
rsCargo("skuPerCarton") = 1
rsCargo("emptyPalletHeightMm") = 0
rsCargo("trayCapMm") = 0
rsCargo("trayHeightMm") = 0
rsCargo("marginLength") = 0
rsCargo("marginWidth") = 0
rsCargo("marginHeight") = 0
rsCargo("nameOfSet") = ""
rsCargo("cartonRatioInSet") = 0
rsCargo("erpId") = ""
rsCargo("skuNumber") = ""
rsCargo("description") = ""
rsCargo("destination") = 0
rsCargo("po") = ""
rsCargo("shipper") = ""
rsCargo.Update
' --- Begin CSV output ---
csv = "S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra" & vbCrLf
csv = csv & "S,D," & removeComma(rsShipment("reference")) & "," & rsShipment("hangingAllowed") & "," & rsShipment("useMultipleContainers") & "," & rsShipment("uniqueErpId") & "," & rsShipment("suggestExtra") & vbCrLf
csv = csv & "B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM" & vbCrLf
Do Until rsContainer.EOF
csv = csv & "B,D," & rsContainer("lengthMm") & "," & rsContainer("widthMm") & "," & rsContainer("heightMm") & "," & formatDecimal3(rsContainer("maxWeightKg")) & "," & rsContainer("backMarginMm") & "," & rsContainer("isRefrigerated") & "," & rsContainer("maxUseOne") & "," & formatDecimal3(rsContainer("lclBreakEvenCBM")) & vbCrLf
rsContainer.MoveNext
Loop
csv = csv & "C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper" & vbCrLf
Do Until rsCargo.EOF
csv = csv & "C,D," & removeComma(rsCargo("color")) & "," & rsCargo("lengthMm") & "," & rsCargo("widthMm") & "," & rsCargo("heightMm") & "," & formatDecimal3(rsCargo("weightKg")) & "," & rsCargo("cartons") & "," & rsCargo("additionalCartons") & "," & rsCargo("placement") & "," & rsCargo("verticalRotationAllowed") & "," & rsCargo("skuPerCarton") & "," & rsCargo("emptyPalletHeightMm") & "," & rsCargo("trayCapMm") & "," & rsCargo("trayHeightMm") & "," & rsCargo("marginLength") & "," & rsCargo("marginWidth") & "," & rsCargo("marginHeight") & "," & removeComma(rsCargo("nameOfSet")) & "," & rsCargo("cartonRatioInSet") & "," & removeComma(rsCargo("erpId")) & "," & removeComma(rsCargo("skuNumber")) & "," & removeComma(rsCargo("description")) & "," & rsCargo("destination") & "," & removeComma(rsCargo("po")) & "," & removeComma(rsCargo("shipper")) & vbCrLf
rsCargo.MoveNext
Loop
csvGenerate = csv
End Function
' === Generate CSV string for SANDBOX-01 ===
Function csvCreate() As String
Dim csv As String
csv = "S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra" & vbCrLf
csv = csv & "S,D,SANDBOX-01,True,False,False,False" & vbCrLf
csv = csv & "B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM" & vbCrLf
csv = csv & "B,D,5867,2352,2393,27200,0,False,False,0" & vbCrLf
csv = csv & "C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper" & vbCrLf
csv = csv & "C,D,#008000,700,500,300,91.5,300,0,0,True,1,0,0,0,0,0,0,,0,,,,0,," & vbCrLf
csvCreate = csv
End Function
' === Extract "token" field from JSON response ===
Function extractTokenFromResponse(jsonText As String) As String
Dim tokenStart As Long, tokenEnd As Long
tokenStart = InStr(jsonText, """token"":""")
If tokenStart = 0 Then
extractTokenFromResponse = ""
Exit Function
End If
tokenStart = tokenStart + 9
tokenEnd = InStr(tokenStart, jsonText, """")
extractTokenFromResponse = Mid(jsonText, tokenStart, tokenEnd - tokenStart)
End Function
' === Cleans text for CSV: removes commas and replaces line breaks ===
' === Use this for all adVarChar (string) fields before writing to CSV ===
'
' Replaces:
' , ? (removed)
' vbCrLf, vbCr, vbLf ? " " (space)
Function removeComma(value As Variant) As String
If IsNull(value) Then
removeComma = ""
Exit Function
End If
Dim s As String
s = Trim(CStr(value))
s = Replace(s, ",", "") ' Remove commas
s = Replace(s, vbCrLf, " ") ' Replace Windows line breaks with space
s = Replace(s, vbCr, " ") ' Replace carriage return
s = Replace(s, vbLf, " ") ' Replace line feed
removeComma = s
End Function
' === Converts numeric value to string with up to 3 decimal places ===
' === Trailing zeros are removed. Use only for Weight-related fields ===
'
' Examples:
' Convert 91.5 ? "91.5"
' Convert 91.56789 ? "91.568"
' Keep 52 ? "52" (no .000 added)
Function formatDecimal3(value As Variant) As String
If IsNull(value) Or Not IsNumeric(value) Then
formatDecimal3 = "0"
Exit Function
End If
Dim s As String
s = Trim(CStr(value))
If InStr(s, ".") > 0 Then
' Format with 3 decimals, remove trailing zeroes
formatDecimal3 = Trim(Str(Val(FormatNumber(value, 3, , , vbFalse))))
Else
' Keep as integer string
formatDecimal3 = Trim(Str(Val(value)))
End If
End Function
' === OPTIONAL TEST CALL ===
' Make calls Examples
' testUpload '->SaveToDisk=No, CSVFromADO=No
' testUpload True '->SaveToDisk=Yes, CSVFromADO=No
' testUpload False, True '->SaveToDisk=No, CSVFromADO=Yes
' testUpload True, True '->SaveToDisk=Yes, CSVFromADO=Yes
'===============================================================
Sub testUpload(Optional saveToDisk As Boolean = False, Optional useADORecordset As Boolean = False)
Dim csv As String
If (useADORecordset = False) Then
csv = csvCreate() '--Using Hard Coded values
Else
csv = csvGenerate() '-->Using ADO
End If
If saveToDisk Then
filePath = App.Path & "\SANDBOX-VB6.csv"
Dim fso As Object
Set fso = CreateObject("Scripting.FileSystemObject")
Dim fileOut As Object
Set fileOut = fso.CreateTextFile(filePath, True)
fileOut.Write csv
fileOut.Close
End If
Dim token As String
If LoadViewerToken = "" Then
token = Trim(getJwtToken())
Else
token = LoadViewerToken
End If
If token = "" Or Left(token, 5) = "ERROR" Or Left(token, 9) = "Exception" Then
MsgBox "Token Error or Server Not Reachable: " & vbCrLf & token, vbCritical
Exit Sub
End If
Dim response As String
response = UploadTOLoadViewer(csv, token)
MsgBox response
End Sub
' Requires Microsoft WinHTTP Services, version 5.1 (or later) to be referenced
' Requires Microsoft VBScript Regular Expressions 5.5 to be referenced (for email validation)
Sub testSuggestions(shipmentReference As String, Optional solverEmail As String = "solver@loadviewer.com")
If Trim(shipmentReference) = "" Then
MsgBox "Shipment Reference is required!", vbCritical
Exit Sub
End If
If Trim(solverEmail) <> "" And Not IsValidEmail(solverEmail) Then
MsgBox "Invalid Solver Email format!", vbCritical
Exit Sub
End If
Dim token As String
If LoadViewerToken = "" Then
token = Trim(getJwtToken())
Else
token = LoadViewerToken
End If
If token = "" Or Left(token, 5) = "ERROR" Or Left(token, 9) = "Exception" Then
MsgBox "Token Error or Server Not Reachable: " & vbCrLf & token, vbCritical
Exit Sub
End If
' --- Create the JSON request body ---
Dim jsonBody As String
jsonBody = "{"
jsonBody = jsonBody & """reference"":""" & Replace(shipmentReference, """", """""") & """"
jsonBody = jsonBody & ",""solverEmail"":""" & Replace(solverEmail, """", """""") & """"
jsonBody = jsonBody & "}"
' --- Call the GetLoadViewerSuggestionsCSV function with POST ---
Dim strResponse As String
strResponse = GetLoadViewerSuggestionsCSV(token, jsonBody)
If Left(strResponse, 5) = "ERROR" Or Left(strResponse, 9) = "Exception" Then
MsgBox "Error retrieving data from LoadViewer: " & vbCrLf & strResponse, vbCritical
Exit Sub
End If
MsgBox strResponse, vbInformation
Dim arrLines As Variant
Dim results As Object ' Dictionary to hold status and recommendations
Dim statusInfo As Object ' Dictionary for status details
Dim recommendations As Collection ' Collection for recommendation data
Dim i As Long
Dim arrFields As Variant
Set results = CreateObject("Scripting.Dictionary")
Set statusInfo = CreateObject("Scripting.Dictionary")
Set recommendations = New Collection
results.Add "status", statusInfo
results.Add "recommendations", recommendations
arrLines = Split(strResponse, vbCrLf)
' Please adjust the UBound check and the index access (e.g., arrFields(1) to arrFields(0))
' according to your project's Option Explicit settings and whether you are using 0-based or 1-based indexing
If UBound(arrLines) > 0 Then
For i = LBound(arrLines) To UBound(arrLines)
arrFields = Split(arrLines(i), ",")
If UBound(arrFields) >= 1 Then
Dim recordType As String
recordType = Trim(arrFields(0))
Select Case recordType
Case "S" ' Status Record
If UBound(arrFields) = 8 Then
' Assuming Option Explicit is NOT used, UBound might be 8 for 9 elements (indices 0 to 7).
' Please adjust the UBound check (e.g., to 9) and the index access (e.g., arrFields(1) to arrFields(0))
' according to your project's Option Explicit settings and whether you are using 0-based or 1-based indexing
If LCase(Trim(arrFields(1))) = "h" Then
' Recommendation Header - Optional processing
ElseIf LCase(Trim(arrFields(1))) = "d" Then
statusInfo("status") = Trim(arrFields(2))
statusInfo("message") = Trim(arrFields(3))
statusInfo("reference") = Trim(arrFields(4))
statusInfo("solverEmail") = Trim(arrFields(5))
statusInfo("multipleContainers") = Trim(arrFields(6))
statusInfo("suggestExtra") = Trim(arrFields(7))
End If
Else
Debug.Print "Invalid Status record format: " & arrLines(i)
End If
Case "R" ' Recommendation Record
If UBound(arrFields) = 15 Then
' Assuming Option Explicit is NOT used, UBound might be 15 for 16 elements (indices 0 to 15).
' Please adjust the UBound check (e.g., to 16) and the index access (e.g., arrFields(1) to arrFields(0))
' according to your project's Option Explicit settings and whether you are using 0-based or 1-based indexing
If LCase(Trim(arrFields(1))) = "h" Then
' Recommendation Header - Optional processing
ElseIf LCase(Trim(arrFields(1))) = "d" Then
Dim recommendation As Object
Set recommendation = CreateObject("Scripting.Dictionary")
recommendation("size") = Trim(arrFields(2))
recommendation("cbm") = Trim(arrFields(3))
recommendation("weight") = Trim(arrFields(4))
recommendation("noOfPackages") = CLng(Trim(arrFields(5)))
recommendation("isPacked") = Trim(arrFields(6))
recommendation("packedPackages") = CLng(Trim(arrFields(7)))
recommendation("suggestedIncrease") = CLng(Trim(arrFields(8)))
recommendation("suggestedDecrease") = CLng(arrFields(9))
recommendation("erpId") = Trim(arrFields(10))
recommendation("skuNumber") = Trim(arrFields(11))
recommendation("description") = Trim(arrFields(12))
recommendation("po") = Trim(arrFields(13))
recommendation("shipper") = Trim(arrFields(14))
recommendations.Add recommendation
ElseIf Trim(arrFields(1)) = "" Then
'Handle empty row
End If
Else
Debug.Print "Invalid Recommendation record format: " & arrLines(i)
End If
Case Else
If Trim(arrLines(i)) <> "" Then
Debug.Print "Unknown record type: " & recordType & " in line: " & arrLines(i)
End If
End Select
End If
Next i
Else
Debug.Print "Empty suggestions response."
End If
If Not results Is Nothing Then
Dim status As Object
Set status = results("status")
Debug.Print "Status: " & status("status") & " - " & status("message")
Debug.Print "Reference: " & status("reference")
Debug.Print "Solver Email: " & status("solverEmail")
Debug.Print "Multiple Containers: " & status("multipleContainers")
Debug.Print "Extra Suggested: " & status("suggestExtra")
'Dim recommendations As Collection
Set recommendations = results("recommendations")
If recommendations.Count > 0 Then
Debug.Print "--- Suggestions ---"
Dim eachRecommendation As Object
For Each eachRecommendation In recommendations
Debug.Print "Is Packed: " & eachRecommendation("isPacked") & ", Size LxWxH: " & eachRecommendation("size") & ", CBM: " & eachRecommendation("cbm") & ", Weight Kg: " & eachRecommendation("weight") & ", Packages: " & eachRecommendation("noOfPackages") & ", Packed: " & eachRecommendation("packedPackages") & ", Increase: " & eachRecommendation("suggestedIncrease") & ", Decrease: " & eachRecommendation("suggestedDecrease") & ", ErpId: " & eachRecommendation("erpId") & ", SkuNumber: " & eachRecommendation("skuNumber") & ", Description: " & eachRecommendation("description") & ", PO#: " & eachRecommendation("po") & ", Division: " & eachRecommendation("shipper")
' --- Your ERP Processing Logic for Suggestions Here ---
Next eachRecommendation
Else
Debug.Print "No suggestions found for reference: " & shipmentReference
End If
Else
Debug.Print "Error retrieving or parsing suggestions for reference: " & shipmentReference
End If
End Sub
' === Helper function for Email Validation ===
Function IsValidEmail(email As String) As Boolean
Dim objRegExp As Object
Set objRegExp = CreateObject("VBScript.RegExp")
objRegExp.Pattern = "^[\w\.-]+@[\w\.-]+\.\w+$"
IsValidEmail = objRegExp.Test(email)
Set objRegExp = Nothing
End Function
' === OPTIONAL TEST CALL ===
Sub testLoadViewerIntegration_POST()
Dim token As String
If LoadViewerToken = "" Then
token = Trim(getJwtToken())
Else
token = LoadViewerToken
End If
If token = "" Or Left(token, 5) = "ERROR" Or Left(token, 9) = "Exception" Then
MsgBox "Token Error or Server Not Reachable: " & vbCrLf & token, vbCritical
Exit Sub
End If
' --- Test Get Suggestions with POST ---
Dim shipmentReference As String
shipmentReference = "CP_NFL_Aug2025_EXT_30" ' Replace with a valid reference
Dim solverEmail As String
solverEmail = "test@example.com" ' Replace with a test email
testSuggestions shipmentReference, solverEmail
End Sub
import http.client
import json
import time
import logging
import re # For email validation
from typing import Optional, Dict, List # For type hinting
# === Constants ===
LOADVIEWER_TOKEN_URL = "https://www.loadviewer.com/api/auth/jwt/token"
LOADVIEWER_UPLOAD_URL = "https://www.loadviewer.com/api/integration/v1/upload"
LOADVIEWER_DOWNLOAD_URL = "https://www.loadviewer.com/api/integration/v1/download"
LOADVIEWER_API_EMAIL = "YOUR_Email" # Replace with your actual email
LOADVIEWER_API_KEY = "YOUR_API_KEY_HERE" # Replace with your actual API key
# === Module level variable to cache the retrieved token ===
loadviewer_token: Optional[str] = None
# Configure logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# --- General Retry and Rate Limit Configuration (can be adjusted locally) ---
DEFAULT_MAX_RETRIES = 3
DEFAULT_RATE_LIMIT_WAIT = 65 # seconds
def get_jwt_token() -> Optional[str]:
"""
Retrieves a JWT token from the LoadViewer API, handling rate limiting.
"""
global loadviewer_token
retry = 0
max_retry = DEFAULT_MAX_RETRIES
rate_limit_retry_count = 0
max_rate_limit_retries = DEFAULT_MAX_RETRIES
rate_limit_wait = DEFAULT_RATE_LIMIT_WAIT
request_timeout = 30 # seconds
token_url = LOADVIEWER_TOKEN_URL
json_body = json.dumps({"email": LOADVIEWER_API_EMAIL, "apikey": LOADVIEWER_API_KEY})
while retry <= max_retry:
try:
conn = http.client.HTTPSConnection(token_url.split("//")[1], timeout=request_timeout)
headers = {'Content-Type': 'application/json'}
conn.request("POST", token_url, body=json_body, headers=headers)
response = conn.getresponse()
response_data = response.read().decode('utf-8')
if response.status == 200:
token = extract_token_from_response(response_data)
loadviewer_token = token # Cache the token
conn.close()
return token
elif response.status == 429:
if rate_limit_retry_count < max_rate_limit_retries:
logger.warning(f"Rate limit hit on token endpoint (attempt {rate_limit_retry_count + 1} of {max_rate_limit_retries}). Waiting {rate_limit_wait} seconds.")
time.sleep(rate_limit_wait)
rate_limit_retry_count += 1
continue
else:
conn.close()
return f"ERROR: {response.status} - {response_data} (Max rate limit retries reached)"
elif retry < max_retry:
retry += 1
logger.warning(f"Retrying token retrieval due to error (attempt {retry + 1} of {max_retry}). Waiting 5 seconds.")
time.sleep(5)
else:
conn.close()
return f"ERROR: {response.status} - {response_data} (Max general retries reached)"
except Exception as e:
return f"Exception: {e}"
finally:
if 'conn' in locals():
conn.close()
return None
def upload_to_loadviewer(csv_data: str, jwt_token: str) -> str:
"""
Uploads CSV data to LoadViewer using a Bearer token, handling rate limiting.
"""
retry = 0
max_retry = DEFAULT_MAX_RETRIES
rate_limit_retry_count = 0
max_rate_limit_retries = DEFAULT_MAX_RETRIES
rate_limit_wait = DEFAULT_RATE_LIMIT_WAIT
request_timeout = 30 # seconds
upload_url = LOADVIEWER_UPLOAD_URL
while retry <= max_retry:
try:
conn = http.client.HTTPSConnection(upload_url.split("//")[1], timeout=request_timeout)
headers = {
'Content-Type': 'text/csv',
'Authorization': f'Bearer {jwt_token}'
}
conn.request("POST", upload_url, body=csv_data, headers=headers)
response = conn.getresponse()
response_data = response.read().decode('utf-8')
if response.status in (200, 202): # Success (200) or Accepted (202)
conn.close()
return f"{response.status} - {response_data}"
elif response.status == 410: # Token expired
jwt_token = get_jwt_token()
if jwt_token is None or jwt_token.startswith("ERROR") or jwt_token.startswith("Exception"):
conn.close()
return jwt_token
continue # Retry immediately after getting a new token
elif response.status == 429:
if rate_limit_retry_count < max_rate_limit_retries:
logger.warning(f"Rate limit hit on upload endpoint (attempt {rate_limit_retry_count + 1} of {max_rate_limit_retries}). Waiting {rate_limit_wait} seconds.")
time.sleep(rate_limit_wait)
rate_limit_retry_count += 1
continue
else:
conn.close()
return f"ERROR: {response.status} - {response_data} (Max rate limit retries reached)"
elif response.status != 409 and retry < max_retry: # 409 is duplicate shipment error
retry += 1
logger.warning(f"Retrying Upload due to error (attempt {retry + 1} of {max_retry}). Waiting 5 seconds.")
time.sleep(5)
continue
else:
conn.close()
return f"ERROR: {response.status} - {response_data} (Max general retries reached)"
except Exception as e:
return f"Exception: {e}"
finally:
if 'conn' in locals():
conn.close()
return "ERROR: Upload Failed After all retries or due to persistent rate limiting"
def get_load_viewer_suggestions_csv(jwt_token: str, request_body: str) -> str:
"""
Retrieves CSV data containing suggestions from LoadViewer, handling rate limiting.
"""
retry = 0
max_retries = DEFAULT_MAX_RETRIES
rate_limit_retry_count = 0
max_rate_limit_retries = DEFAULT_MAX_RETRIES
rate_limit_wait = DEFAULT_RATE_LIMIT_WAIT
request_timeout = 30
download_url = LOADVIEWER_DOWNLOAD_URL
while retry <= max_retries:
try:
conn = http.client.HTTPSConnection(download_url.split("//")[1], timeout=request_timeout)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {jwt_token}"
}
conn.request("POST", download_url, body=request_body, headers=headers)
response = conn.getresponse()
response_data = response.read().decode('utf-8')
log_message = (
f"POST Request to LoadViewer Suggestions URL: {download_url}\n"
f"Request Body:\n{request_body}\n"
f"HTTP Status Code: {response.status}\n"
f"Response Text:\n{response_data}\n"
)
logger.debug(log_message)
if response.status in (200, 202):
conn.close()
return response_data
elif response.status == 410: # Token expired
jwt_token = get_jwt_token()
if jwt_token is None or jwt_token.startswith("ERROR") or jwt_token.startswith("Exception"):
conn.close()
return jwt_token
continue
elif response.status == 429:
if rate_limit_retry_count < max_rate_limit_retries:
logger.warning(f"Rate limit hit on suggestions endpoint (attempt {rate_limit_retry_count + 1} of {max_rate_limit_retries}). Waiting {rate_limit_wait} seconds.")
time.sleep(rate_limit_wait)
rate_limit_retry_count += 1
continue
else:
conn.close()
return f"ERROR: {response.status} - {response_data} (Max rate limit retries reached)"
elif retry < max_retries:
retry += 1
logger.warning(f"Retrying Suggestions Upload due to error (attempt {retry + 1} of {max_retries}). Waiting 5 seconds.")
time.sleep(5)
continue
else:
conn.close()
return f"ERROR: {response.status} - {response_data} (Max general retries reached)"
except Exception as e:
return f"Exception: {e}"
finally:
if 'conn' in locals():
conn.close()
return "ERROR: Get Suggestions Failed After all retries or due to persistent rate limiting"
def csv_generate() -> str:
"""
Dynamically generates LoadViewer CSV.
"""
rs_shipment = [
{"reference": "SANDBOX-01", "hangingAllowed": True, "useMultipleContainers": False,
"uniqueErpId": False, "suggestExtra": False}
]
rs_container = [
{"lengthMm": 5867, "widthMm": 2352, "heightMm": 2393, "maxWeightKg": 27200,
"backMarginMm": 0, "isRefrigerated": False, "maxUseOne": False, "lclBreakEvenCBM": 0}
]
rs_cargo = [
{"color": "#008000", "lengthMm": 700, "widthMm": 500, "heightMm": 300,
"weightKg": 91.5, "cartons": 300, "additionalCartons": 0, "placement": 0,
"verticalRotationAllowed": True, "skuPerCarton": 1, "emptyPalletHeightMm": 0,
"trayCapMm": 0, "trayHeightMm": 0, "marginLength": 0, "marginWidth": 0,
"marginHeight": 0, "nameOfSet": "", "cartonRatioInSet": 0, "erpId": "",
"skuNumber": "", "description": "", "destination": 0, "po": "", "shipper": ""}
]
csv_lines = [
"S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra",
f"S,D,{remove_comma(rs_shipment[0]['reference'])},{rs_shipment[0]['hangingAllowed']},{rs_shipment[0]['useMultipleContainers']},{rs_shipment[0]['uniqueErpId']},{rs_shipment[0]['suggestExtra']}",
"B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM"
]
for row in rs_container:
csv_lines.append(
f"B,D,{row['lengthMm']},{row['widthMm']},{row['heightMm']},{format_decimal3(row['maxWeightKg'])},{row['backMarginMm']},{row['isRefrigerated']},{row['maxUseOne']},{format_decimal3(row['lclBreakEvenCBM'])}"
)
csv_lines.append(
"C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper")
for row in rs_cargo:
csv_lines.append(
f"C,D,{remove_comma(row['color'])},{row['lengthMm']},{row['widthMm']},{row['heightMm']},{format_decimal3(row['weightKg'])},{row['cartons']},{row['additionalCartons']},{row['placement']},{row['verticalRotationAllowed']},{row['skuPerCarton']},{row['emptyPalletHeightMm']},{row['trayCapMm']},{row['trayHeightMm']},{row['marginLength']},{row['marginWidth']},{row['marginHeight']},{remove_comma(row['nameOfSet'])},{row['cartonRatioInSet']},{remove_comma(row['erpId'])},{remove_comma(row['skuNumber'])},{remove_comma(row['description'])},{row['destination']},{remove_comma(row['po'])},{remove_comma(row['shipper'])}"
)
return "\r\n".join(csv_lines)
def csv_create() -> str:
"""
Generates a simple CSV string.
"""
csv_content = (
"S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra\r\n"
"S,D,SANDBOX-01,True,False,False,False\r\n"
"B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM\r\n"
"B,D,5867,2352,2393,27200,0,False,False,0\r\n"
"C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper\r\n"
"C,D,#008000,700,500,300,91.5,300,0,0,True,1,0,0,0,0,0,0,,0,,,,0,,\r\n"
)
return csv_content
def extract_token_from_response(json_text: str) -> Optional[str]:
"""
Extracts the 'token' field from a JSON response.
"""
try:
json_data = json.loads(json_text)
return json_data.get('token')
except json.JSONDecodeError as e:
logger.error(f"JSONDecodeError: {e}")
return None
def remove_comma(value: Optional[str]) -> str:
"""
Cleans text for CSV: removes commas and replaces line breaks.
"""
if value is None:
return ""
s = value.strip()
s = s.replace(",", "")
s = s.replace("\r\n", " ")
s = s.replace("\r", " ")
s = s.replace("\n", " ")
return s
def format_decimal3(value: float) -> str:
"""
Converts a numeric value to a string with up to 3 decimal places.
"""
return f"{value:.3f}".rstrip('0').rstrip('.')
def test_upload(save_to_disk: bool = False, use_ado_recordset: bool = False) -> None:
"""
Optional test function to upload CSV data.
"""
if not use_ado_recordset:
csv_data = csv_create()
else:
csv_data = csv_generate()
if save_to_disk:
file_path = "SANDBOX-Python.csv"
with open(file_path, "w") as f:
f.write(csv_data)
token = loadviewer_token or get_jwt_token() # Use cached token or get a new one
if token is None or token.startswith("ERROR") or token.startswith("Exception"):
print(f"Token Error or Server Not Reachable: {token}")
return
response = upload_to_loadviewer(csv_data, token)
print(response)
def is_valid_email(email: str) -> bool:
"""
Helper function for Email Validation.
"""
return bool(re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", email))
def test_suggestions(shipment_reference: str, solver_email: str = "solver@loadviewer.com") -> None:
"""
Tests the LoadViewer suggestions API.
"""
if not shipment_reference:
print("Shipment Reference is required!")
return
if solver_email and not is_valid_email(solver_email):
print("Invalid Solver Email format!")
return
token = loadviewer_token or get_jwt_token()
if token is None or token.startswith("ERROR") or token.startswith("Exception"):
print(f"Token Error or Server Not Reachable: {token}")
return
# --- Create the JSON request body ---
json_body = json.dumps({
"reference": shipment_reference.replace("\"", "\"\""), # Escape double quotes
"solverEmail": solver_email.replace("\"", "\"\"")
})
# --- Call the GetLoadViewerSuggestionsCSV function with POST ---
str_response = get_load_viewer_suggestions_csv(token, json_body)
if str_response.startswith("ERROR") or str_response.startswith("Exception"):
print(f"Error retrieving data from LoadViewer: {str_response}")
return
print(str_response)
arr_lines = str_response.splitlines()
results: Dict[str, object] = {}
status_info: Dict[str, str] = {}
recommendations: List[Dict[str, object]] = []
results["status"] = status_info
results["recommendations"] = recommendations
if len(arr_lines) > 0:
for line in arr_lines:
arr_fields = line.split(',')
if len(arr_fields) >= 1:
record_type = arr_fields[0].strip()
if record_type == "S": # Status Record
if len(arr_fields) == 9 and arr_fields[1].strip().lower() == "d":
status_info["status"] = arr_fields[2].strip()
status_info["message"] = arr_fields[3].strip()
status_info["reference"] = arr_fields[4].strip()
status_info["solverEmail"] = arr_fields[5].strip()
status_info["multipleContainers"] = arr_fields[6].strip()
status_info["suggestExtra"] = arr_fields[7].strip()
elif len(arr_fields) == 9 and arr_fields[1].strip().lower() == "h":
pass
else:
print("Invalid Status record format: " + line)
elif record_type == "R": # Recommendation Record
if len(arr_fields) == 16 and arr_fields[1].strip().lower() == "d":
recommendation: Dict[str, object] = {}
recommendation["size"] = arr_fields[2].strip()
recommendation["cbm"] = arr_fields[3].strip()
recommendation["weight"] = arr_fields[4].strip()
recommendation["noOfPackages"] = int(arr_fields[5].strip())
recommendation["isPacked"] = arr_fields[6].strip()
recommendation["packedPackages"] = int(arr_fields[7].strip())
recommendation["suggestedIncrease"] = int(arr_fields[8].strip())
recommendation["suggestedDecrease"] = int(arr_fields[9].strip())
recommendation["erpId"] = arr_fields[10].strip()
recommendation["skuNumber"] = arr_fields[11].strip()
recommendation["description"] = arr_fields[12].strip()
recommendation["po"] = arr_fields[13].strip()
recommendation["shipper"] = arr_fields[14].strip()
recommendations.append(recommendation)
elif len(arr_fields) == 16 and arr_fields[1].strip().lower() == "h":
pass
else:
print("Invalid Recommendation record format: " + line)
else:
if line.strip() != "":
print("Unknown record type: " + record_type + " in line: " + line)
else:
print("Empty suggestions response.")
if results:
status = results["status"]
print("Status: " + status["status"] + " - " + status["message"])
print("Reference: " + status["reference"])
print("Solver Email: " + status["solverEmail"])
print("Multiple Containers: " + status["multipleContainers"])
print("Extra Suggested: " + status["suggestExtra"])
recs = results["recommendations"]
if len(recs) > 0:
print("--- Suggestions ---")
for recommendation in recs:
print(
f"Is Packed: {recommendation['isPacked']}, Size LxWxH: {recommendation['size']}, CBM: {recommendation['cbm']}, Weight Kg: {recommendation['weight']}, Packages: {recommendation['noOfPackages']}, Packed: {recommendation['packedPackages']}, Increase: {recommendation['suggestedIncrease']}, Decrease: {recommendation['suggestedDecrease']}, ErpId: {recommendation['erpId']}, SkuNumber: {recommendation['skuNumber']}, Description: {recommendation['description']}, PO#: {recommendation['po']}, Division: {recommendation['shipper']}"
)
# --- Your ERP Processing Logic for Suggestions Here ---
else:
print("No suggestions found for reference: " + shipment_reference)
else:
print("Error retrieving or parsing suggestions for reference: " + shipment_reference)
def test_loadviewer_integration_post() -> None:
"""
Tests the LoadViewer integration (POST requests).
"""
token = loadviewer_token or get_jwt_token()
if token is None or token.startswith("ERROR") or token.startswith("Exception"):
print(f"Token Error or Server Not Reachable: {token}")
return
# --- Test Get Suggestions with POST ---
shipment_reference = "CP_NFL_Aug2025_EXT_30" # Replace with a valid reference
solver_email = "test@example.com" # Replace with a test email
test_suggestions(shipment_reference, solver_email)
if __name__ == "__main__":
#test_upload(save_to_disk=True, use_ado_recordset=True)
test_loadviewer_integration_post()
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.Extensions.Logging;
public class LoadViewerIntegration
{
private readonly string _tokenUrl = "https://www.loadviewer.com/api/auth/jwt/token";
private readonly string _uploadUrl = "https://www.loadviewer.com/api/integration/v1/upload";
private readonly string _downloadUrl = "https://www.loadviewer.com/api/integration/v1/download";
private readonly string _apiEmail = "YOUR_Email"; // Replace with your actual email
private readonly string _apiKey = "YOUR_API_KEY_HERE"; // Replace with your actual API key
private string _jwtToken;
private readonly ILogger _logger;
private const int DefaultMaxRetries = 3;
private const int DefaultRateLimitWaitSeconds = 65;
private const int GeneralRetryDelaySeconds = 5;
public LoadViewerIntegration(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
private bool IsValidEmail(string email)
{
// Basic email validation regex
return System.Text.RegularExpressions.Regex.IsMatch(email, @"^[^\s@]+@[^\s@]+\.[^\s@]+$");
}
private async Task GetJwtTokenWithRetries()
{
int retryCount = 0;
GetToken:
try
{
using (var httpClient = new HttpClient())
{
var requestBody = new { email = _apiEmail, apikey = _apiKey };
var jsonBody = JsonSerializer.Serialize(requestBody);
var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(_tokenUrl, content);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
using (var jsonDocument = JsonDocument.Parse(responseContent))
{
if (jsonDocument.RootElement.TryGetProperty("token", out var tokenElement))
{
_jwtToken = tokenElement.GetString();
return _jwtToken;
}
else
{
_logger.LogError($"Failed to extract token from response: {responseContent}");
return null;
}
}
}
}
catch (HttpRequestException ex)
{
if (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests && retryCount < DefaultMaxRetries)
{
retryCount++;
_logger.LogWarning($"Rate limit hit on token endpoint (attempt {retryCount} of {DefaultMaxRetries}). Waiting {DefaultRateLimitWaitSeconds} seconds before retrying.");
await Task.Delay(TimeSpan.FromSeconds(DefaultRateLimitWaitSeconds));
goto GetToken;
}
else if (retryCount < DefaultMaxRetries)
{
retryCount++;
_logger.LogError($"Error retrieving token (attempt {retryCount} of {DefaultMaxRetries}): {ex.Message}. Retrying in {GeneralRetryDelaySeconds} seconds.");
await Task.Delay(TimeSpan.FromSeconds(GeneralRetryDelaySeconds));
goto GetToken;
}
else
{
_logger.LogError($"Failed to retrieve token after {DefaultMaxRetries} retries: {ex.Message}");
return null;
}
}
catch (Exception ex)
{
_logger.LogError($"An unexpected error occurred while retrieving token: {ex.Message}");
return null;
}
}
public async Task UploadToLoadViewerWithRetries(string csvData)
{
if (string.IsNullOrEmpty(_jwtToken))
{
_jwtToken = await GetJwtTokenWithRetries();
if (string.IsNullOrEmpty(_jwtToken))
{
return "Error: Unable to retrieve JWT token for upload.";
}
}
int retryCount = 0;
UploadData:
try
{
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
var content = new StringContent(csvData, Encoding.UTF8, "text/csv");
var response = await httpClient.PostAsync(_uploadUrl, content);
if (response.IsSuccessStatusCode)
{
return $"{response.StatusCode} - {await response.Content.ReadAsStringAsync()}";
}
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Gone)
{
_logger.LogWarning("JWT token expired or invalid. Attempting to retrieve a new token.");
_jwtToken = await GetJwtTokenWithRetries();
if (string.IsNullOrEmpty(_jwtToken))
{
return "Error: Unable to retrieve a new JWT token after token expiration.";
}
goto UploadData;
}
else if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests && retryCount < DefaultMaxRetries)
{
retryCount++;
_logger.LogWarning($"Rate limit hit on upload endpoint (attempt {retryCount} of {DefaultMaxRetries}). Waiting {DefaultRateLimitWaitSeconds} seconds before retrying.");
await Task.Delay(TimeSpan.FromSeconds(DefaultRateLimitWaitSeconds));
goto UploadData;
}
else if (response.StatusCode != System.Net.HttpStatusCode.Conflict && retryCount < DefaultMaxRetries)
{
retryCount++;
_logger.LogError($"Error uploading data (attempt {retryCount} of {DefaultMaxRetries}): {response.StatusCode} - {await response.Content.ReadAsStringAsync()}. Retrying in {GeneralRetryDelaySeconds} seconds.");
await Task.Delay(TimeSpan.FromSeconds(GeneralRetryDelaySeconds));
goto UploadData;
}
else
{
return $"Error: Upload failed after {retryCount + 1} attempts. Status: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}";
}
}
}
catch (HttpRequestException ex)
{
_logger.LogError($"An HTTP error occurred during upload: {ex.Message}");
return $"Exception: {ex.Message}";
}
catch (Exception ex)
{
_logger.LogError($"An unexpected error occurred during upload: {ex.Message}");
return $"Exception: {ex.Message}";
}
}
public async Task GetLoadViewerSuggestionsCsvWithRetries(string requestBody)
{
if (string.IsNullOrEmpty(_jwtToken))
{
_jwtToken = await GetJwtTokenWithRetries();
if (string.IsNullOrEmpty(_jwtToken))
{
return "Error: Unable to retrieve JWT token for getting suggestions.";
}
}
int retryCount = 0;
GetSuggestions:
try
{
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(_downloadUrl, content);
_logger.LogDebug($"POST Request to LoadViewer Suggestions URL: {_downloadUrl}\nRequest Body:\n{requestBody}\nHTTP Status Code: {response.StatusCode}\nResponse Text:\n{await response.Content.ReadAsStringAsync()}");
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsStringAsync();
}
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Gone)
{
_logger.LogWarning("JWT token expired or invalid. Attempting to retrieve a new token.");
_jwtToken = await GetJwtTokenWithRetries();
if (string.IsNullOrEmpty(_jwtToken))
{
return "Error: Unable to retrieve a new JWT token after token expiration.";
}
goto GetSuggestions;
}
else if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests && retryCount < DefaultMaxRetries)
{
retryCount++;
_logger.LogWarning($"Rate limit hit on suggestions endpoint (attempt {retryCount} of {DefaultMaxRetries}). Waiting {DefaultRateLimitWaitSeconds} seconds before retrying.");
await Task.Delay(TimeSpan.FromSeconds(DefaultRateLimitWaitSeconds));
goto GetSuggestions;
}
else if (retryCount < DefaultMaxRetries)
{
retryCount++;
_logger.LogError($"Error retrieving suggestions (attempt {retryCount} of {DefaultMaxRetries}): {response.StatusCode} - {await response.Content.ReadAsStringAsync()}. Retrying in {GeneralRetryDelaySeconds} seconds.");
await Task.Delay(TimeSpan.FromSeconds(GeneralRetryDelaySeconds));
goto GetSuggestions;
}
else
{
return $"Error: Failed to retrieve suggestions after {retryCount + 1} attempts. Status: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}";
}
}
}
catch (HttpRequestException ex)
{
_logger.LogError($"An HTTP error occurred while retrieving suggestions: {ex.Message}");
return $"Exception: {ex.Message}";
}
catch (Exception ex)
{
_logger.LogError($"An unexpected error occurred while retrieving suggestions: {ex.Message}");
return $"Exception: {ex.Message}";
}
}
public async Task TestSuggestions(string shipmentReference, string solverEmail = "solver@loadviewer.com")
{
if (string.IsNullOrEmpty(shipmentReference))
{
_logger.LogError("Shipment Reference is required!");
return;
}
if (!string.IsNullOrEmpty(solverEmail) && !IsValidEmail(solverEmail))
{
_logger.LogError("Invalid Solver Email format!");
return;
}
string token = _jwtToken ?? await GetJwtTokenWithRetries();
if (string.IsNullOrEmpty(token))
{
_logger.LogError("Token Error or Server Not Reachable.");
return;
}
var jsonBody = JsonSerializer.Serialize(new
{
reference = shipmentReference.Replace("\"", "\"\""),
solverEmail = solverEmail.Replace("\"", "\"\"")
});
string strResponse = await GetLoadViewerSuggestionsCsvWithRetries(jsonBody);
if (strResponse.StartsWith("Error") || strResponse.StartsWith("Exception"))
{
_logger.LogError($"Error retrieving data from LoadViewer: {strResponse}");
return;
}
_logger.LogInformation($"Raw Suggestions Response:\n{strResponse}");
var arrLines = strResponse.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
var results = new Dictionary();
var statusInfo = new Dictionary();
var recommendations = new List>();
results["status"] = statusInfo;
results["recommendations"] = recommendations;
if (arrLines.Length > 0)
{
foreach (var line in arrLines)
{
var arrFields = line.Split(',');
if (arrFields.Length >= 1)
{
var recordType = arrFields[0].Trim();
if (recordType == "S") // Status Record
{
if (arrFields.Length == 9 && arrFields[1].Trim().ToLowerInvariant() == "d")
{
statusInfo["status"] = arrFields[2].Trim();
statusInfo["message"] = arrFields[3].Trim();
statusInfo["reference"] = arrFields[4].Trim();
statusInfo["solverEmail"] = arrFields[5].Trim();
statusInfo["multipleContainers"] = arrFields[6].Trim();
statusInfo["suggestExtra"] = arrFields[7].Trim();
}
else if (arrFields.Length == 9 && arrFields[1].Trim().ToLowerInvariant() == "h")
{
// Header row, can be ignored
}
else
{
_logger.Warn($"Invalid Status record format: {line}");
}
}
else if (recordType == "R") // Recommendation Record
{
if (arrFields.Length == 16 && arrFields[1].Trim().ToLowerInvariant() == "d")
{
var recommendation = new Dictionary
{
["size"] = arrFields[2].Trim(),
["cbm"] = arrFields[3].Trim(),
["weight"] = arrFields[4].Trim(),
["noOfPackages"] = int.Parse(arrFields[5].Trim()),
["isPacked"] = arrFields[6].Trim(),
["packedPackages"] = int.Parse(arrFields[7].Trim()),
["suggestedIncrease"] = int.Parse(arrFields[8].Trim()),
["suggestedDecrease"] = int.Parse(arrFields[9].Trim()),
["erpId"] = arrFields[10].Trim(),
["skuNumber"] = arrFields[11].Trim(),
["description"] = arrFields[12].Trim(),
["po"] = arrFields[13].Trim(),
["shipper"] = arrFields[14].Trim()
};
recommendations.Add(recommendation);
}
else if (arrFields.Length == 16 && arrFields[1].Trim().ToLowerInvariant() == "h")
{
// Header row, can be ignored
}
else
{
_logger.Warn($"Invalid Recommendation record format: {line}");
}
}
else if (!string.IsNullOrWhiteSpace(recordType))
{
_logger.Warn($"Unknown record type: {recordType} in line: {line}");
}
}
}
}
else
{
_logger.LogInformation("Empty suggestions response.");
}
if (results != null && results.ContainsKey("status"))
{
var status = (Dictionary)results["status"];
_logger.LogInformation($"Status: {status.GetValueOrDefault("status")} - {status.GetValueOrDefault("message")}");
_logger.LogInformation($"Reference: {status.GetValueOrDefault("reference")}");
_logger.LogInformation($"Solver Email: {status.GetValueOrDefault("solverEmail")}");
_logger.LogInformation($"Multiple Containers: {status.GetValueOrDefault("multipleContainers")}");
_logger.LogInformation($"Extra Suggested: {status.GetValueOrDefault("suggestExtra")}");
if (results.ContainsKey("recommendations") && results["recommendations"] is List> recs && recs.Count > 0)
{
_logger.LogInformation("--- Suggestions ---");
foreach (var recommendation in recs)
{
_logger.LogInformation(
$"Is Packed: {recommendation.GetValueOrDefault("isPacked")}, Size LxWxH: {recommendation.GetValueOrDefault("size")}, CBM: {recommendation.GetValueOrDefault("cbm")}, Weight Kg: {recommendation.GetValueOrDefault("weight")}, Packages: {recommendation.GetValueOrDefault("noOfPackages")}, Packed: {recommendation.GetValueOrDefault("packedPackages")}, Increase: {recommendation.GetValueOrDefault("suggestedIncrease")}, Decrease: {recommendation.GetValueOrDefault("suggestedDecrease")}, ErpId: {recommendation.GetValueOrDefault("erpId")}, SkuNumber: {recommendation.GetValueOrDefault("skuNumber")}, Description: {recommendation.GetValueOrDefault("description")}, PO#: {recommendation.GetValueOrDefault("po")}, Division: {recommendation.GetValueOrDefault("shipper")}"
);
// --- Your ERP Processing Logic for Suggestions Here ---
}
}
else
{
_logger.LogInformation($"No suggestions found for reference: {shipmentReference}");
}
}
else
{
_logger.LogError($"Error retrieving or parsing suggestions for reference: {shipmentReference}");
}
}
// Example usage (you'll need to inject ILogger in your application)
// public static async Task Main(string[] args)
// {
// using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
// var logger = loggerFactory.CreateLogger();
// var integration = new LoadViewerIntegration(logger);
//
// // Example of uploading data
// // string csvData = GenerateLoadViewerCsv();
// // string uploadResult = await integration.UploadToLoadViewerWithRetries(csvData);
// // Console.WriteLine($"Upload Result: {uploadResult}");
//
// // Example of getting suggestions
// await integration.TestSuggestions("SANDBOX-01", "test@example.com");
// }
private static string RemoveComma(string value)
{
return value?.Replace(",", "").Replace(Environment.NewLine, " ") ?? "";
}
private static string FormatDecimal3(decimal value)
{
return value.ToString("F3").TrimEnd('0').TrimEnd('.');
}
private static string GenerateLoadViewerCsv()
{
var rs_shipment = new[]
{
new { reference = "SANDBOX-01", hangingAllowed = true, useMultipleContainers = false, uniqueErpId = false, suggestExtra = false }
};
var rs_container = new[]
{
new { lengthMm = 5867, widthMm = 2352, heightMm = 2393, maxWeightKg = 27200m, backMarginMm = 0, isRefrigerated = false, maxUseOne = false, lclBreakEvenCBM = 0m }
};
var rs_cargo = new[]
{
new { color = "#008000", lengthMm = 700, widthMm = 500, heightMm = 300, weightKg = 91.5m, cartons = 300, additionalCartons = 0, placement = 0, verticalRotationAllowed = true, skuPerCarton = 1, emptyPalletHeightMm = 0, trayCapMm = 0, trayHeightMm = 0, marginLength = 0, marginWidth = 0, marginHeight = 0, nameOfSet = "", cartonRatioInSet = 0, erpId = "", skuNumber = "", description = "", destination = 0, po = "", shipper = "" }
};
var csvLines = new List
{
"S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra",
$"S,D,{RemoveComma(rs_shipment[0].reference)},{rs_shipment[0].hangingAllowed},{rs_shipment[0].useMultipleContainers},{rs_shipment[0].uniqueErpId},{rs_shipment[0].suggestExtra}",
"B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM"
};
foreach (var row in rs_container)
{
csvLines.Add($"B,D,{row.lengthMm},{row.widthMm},{row.heightMm},{FormatDecimal3(row.maxWeightKg)},{row.backMarginMm},{row.isRefrigerated},{row.maxUseOne},{FormatDecimal3(row.lclBreakEvenCBM)}");
}
csvLines.Add("C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper");
foreach (var row in rs_cargo)
{
csvLines.Add($"C,D,{RemoveComma(row.color)},{row.lengthMm},{row.widthMm},{row.heightMm},{FormatDecimal3(row.weightKg)},{row.cartons},{row.additionalCartons},{row.placement},{row.verticalRotationAllowed},{row.skuPerCarton},{row.emptyPalletHeightMm},{row.trayCapMm},{row.trayHeightMm},{row.marginLength},{row.marginWidth},{row.marginHeight},{RemoveComma(row.nameOfSet)},{row.cartonRatioInSet},{RemoveComma(row.erpId)},{RemoveComma(row.skuNumber)},{RemoveComma(row.description)},{row.destination},{RemoveComma(row.po)},{RemoveComma(row.shipper)}");
}
return string.Join(Environment.NewLine, csvLines);
}
}
// --- Configuration ---
const LOADVIEWER_TOKEN_URL = 'https://www.loadviewer.com/api/auth/jwt/token';
const LOADVIEWER_UPLOAD_URL = 'https://www.loadviewer.com/api/integration/v1/upload';
const LOADVIEWER_DOWNLOAD_URL = 'https://www.loadviewer.com/api/integration/v1/download';
const LOADVIEWER_API_EMAIL = 'YOUR_Email'; // Replace with your actual email
const LOADVIEWER_API_KEY = 'YOUR_API_KEY_HERE'; // Replace with your actual API key
const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_RATE_LIMIT_WAIT_MS = 65000; // 65 seconds in milliseconds
const GENERAL_RETRY_DELAY_MS = 5000; // 5 seconds
let cachedToken = null;
function isValidEmail(email) {
// Basic email validation regex
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
async function getJwtTokenWithRetries(retries = 0) {
if (cachedToken) {
return cachedToken;
}
try {
const response = await fetch(LOADVIEWER_TOKEN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: LOADVIEWER_API_EMAIL, apikey: LOADVIEWER_API_KEY }),
});
if (!response.ok) {
if (response.status === 429 && retries < DEFAULT_MAX_RETRIES) {
console.warn(`Rate limit hit on token endpoint (attempt ${retries + 1} of ${DEFAULT_MAX_RETRIES}). Waiting ${DEFAULT_RATE_LIMIT_WAIT_MS / 1000} seconds before retrying.`);
await new Promise(resolve => setTimeout(resolve, DEFAULT_RATE_LIMIT_WAIT_MS));
return getJwtTokenWithRetries(retries + 1);
} else if (retries < DEFAULT_MAX_RETRIES) {
console.error(`Error retrieving token (attempt ${retries + 1} of ${DEFAULT_MAX_RETRIES}): ${response.status} - ${response.statusText}. Retrying in ${GENERAL_RETRY_DELAY_MS / 1000} seconds.`);
await new Promise(resolve => setTimeout(resolve, GENERAL_RETRY_DELAY_MS));
return getJwtTokenWithRetries(retries + 1);
} else {
console.error(`Failed to retrieve token after ${DEFAULT_MAX_RETRIES} retries: ${response.status} - ${response.statusText}`);
return null;
}
}
const data = await response.json();
if (data && data.token) {
cachedToken = data.token;
return cachedToken;
} else {
console.error('Failed to extract token from response:', data);
return null;
}
} catch (error) {
console.error('An error occurred while retrieving token:', error);
if (retries < DEFAULT_MAX_RETRIES) {
console.error(`Retrying token retrieval in ${GENERAL_RETRY_DELAY_MS / 1000} seconds.`);
await new Promise(resolve => setTimeout(resolve, GENERAL_RETRY_DELAY_MS));
return getJwtTokenWithRetries(retries + 1);
}
return null;
}
}
async function uploadToLoadViewerWithRetries(csvData, retries = 0) {
const token = await getJwtTokenWithRetries();
if (!token) {
return 'Error: Unable to retrieve JWT token for upload.';
}
try {
const response = await fetch(LOADVIEWER_UPLOAD_URL, {
method: 'POST',
headers: {
'Content-Type': 'text/csv',
'Authorization': `Bearer ${token}`,
},
body: csvData,
});
if (!response.ok) {
if (response.status === 401 || response.status === 410) {
console.warn('JWT token expired or invalid. Attempting to retrieve a new token.');
cachedToken = null; // Invalidate cached token
const newToken = await getJwtTokenWithRetries();
if (!newToken) {
return 'Error: Unable to retrieve a new JWT token after token expiration.';
}
return uploadToLoadViewerWithRetries(csvData, retries); // Retry with new token
} else if (response.status === 429 && retries < DEFAULT_MAX_RETRIES) {
console.warn(`Rate limit hit on upload endpoint (attempt ${retries + 1} of ${DEFAULT_MAX_RETRIES}). Waiting ${DEFAULT_RATE_LIMIT_WAIT_MS / 1000} seconds before retrying.`);
await new Promise(resolve => setTimeout(resolve, DEFAULT_RATE_LIMIT_WAIT_MS));
return uploadToLoadViewerWithRetries(csvData, retries + 1);
} else if (response.status !== 409 && retries < DEFAULT_MAX_RETRIES) { // Don't retry on duplicate shipment (409)
console.error(`Error uploading data (attempt ${retries + 1} of ${DEFAULT_MAX_RETRIES}): ${response.status} - ${response.statusText}. Retrying in ${GENERAL_RETRY_DELAY_MS / 1000} seconds.`);
await new Promise(resolve => setTimeout(resolve, GENERAL_RETRY_DELAY_MS));
return uploadToLoadViewerWithRetries(csvData, retries + 1);
} else {
return `Error: Upload failed after ${retries + 1} attempts. Status: ${response.status} - ${response.statusText}`;
}
}
return `${response.status} - ${await response.text()}`;
} catch (error) {
console.error('An error occurred during upload:', error);
if (retries < DEFAULT_MAX_RETRIES) {
console.error(`Retrying upload in ${GENERAL_RETRY_DELAY_MS / 1000} seconds.`);
await new Promise(resolve => setTimeout(resolve, GENERAL_RETRY_DELAY_MS));
return uploadToLoadViewerWithRetries(csvData, retries + 1);
}
return `Exception: ${error.message}`;
}
}
async function getLoadViewerSuggestionsCsvWithRetries(requestBody, retries = 0) {
const token = await getJwtTokenWithRetries();
if (!token) {
return 'Error: Unable to retrieve JWT token for getting suggestions.';
}
try {
const response = await fetch(LOADVIEWER_DOWNLOAD_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(requestBody),
});
console.debug(`POST Request to LoadViewer Suggestions URL: ${LOADVIEWER_DOWNLOAD_URL}\nRequest Body:\n${JSON.stringify(requestBody)}\nHTTP Status Code: ${response.status}\nResponse Text:\n${await response.text()}`);
if (!response.ok) {
if (response.status === 401 || response.status === 410) {
console.warn('JWT token expired or invalid. Attempting to retrieve a new token.');
cachedToken = null; // Invalidate cached token
const newToken = await getJwtTokenWithRetries();
if (!newToken) {
return 'Error: Unable to retrieve a new JWT token after token expiration.';
}
return getLoadViewerSuggestionsCsvWithRetries(requestBody, retries); // Retry with new token
} else if (response.status === 429 && retries < DEFAULT_MAX_RETRIES) {
console.warn(`Rate limit hit on suggestions endpoint (attempt ${retries + 1} of ${DEFAULT_MAX_RETRIES}). Waiting ${DEFAULT_RATE_LIMIT_WAIT_MS / 1000} seconds before retrying.`);
await new Promise(resolve => setTimeout(resolve, DEFAULT_RATE_LIMIT_WAIT_MS));
return getLoadViewerSuggestionsCsvWithRetries(requestBody, retries + 1);
} else if (retries < DEFAULT_MAX_RETRIES) {
console.error(`Error retrieving suggestions (attempt ${retries + 1} of ${DEFAULT_MAX_RETRIES}): ${response.status} - ${response.statusText}. Retrying in ${GENERAL_RETRY_DELAY_MS / 1000} seconds.`);
await new Promise(resolve => setTimeout(resolve, GENERAL_RETRY_DELAY_MS));
return getLoadViewerSuggestionsCsvWithRetries(requestBody, retries + 1);
} else {
return `Error: Failed to retrieve suggestions after ${retries + 1} attempts. Status: ${response.status} - ${response.statusText}`;
}
}
return await response.text();
} catch (error) {
console.error('An error occurred while retrieving suggestions:', error);
if (retries < DEFAULT_MAX_RETRIES) {
console.error(`Retrying suggestions retrieval in ${GENERAL_RETRY_DELAY_MS / 1000} seconds.`);
await new Promise(resolve => setTimeout(resolve, GENERAL_RETRY_DELAY_MS));
return getLoadViewerSuggestionsCsvWithRetries(requestBody, retries + 1);
}
return `Exception: ${error.message}`;
}
}
async function testSuggestions(shipmentReference, solverEmail = 'solver@loadviewer.com') {
if (!shipmentReference) {
console.log('Shipment Reference is required!');
return;
}
if (solverEmail && !isValidEmail(solverEmail)) {
console.log('Invalid Solver Email format!');
return;
}
const token = await getJwtTokenWithRetries();
if (!token) {
console.log('Token Error or Server Not Reachable.');
return;
}
const requestBody = {
reference: shipmentReference.replace(/"/g, '""'), // Escape double quotes
solverEmail: solverEmail.replace(/"/g, '""'),
};
const strResponse = await getLoadViewerSuggestionsCsvWithRetries(requestBody);
if (strResponse.startsWith('Error') || strResponse.startsWith('Exception')) {
console.error('Error retrieving data from LoadViewer:', strResponse);
return;
}
console.log('Raw Suggestions Response:\n', strResponse);
const arrLines = strResponse.split('\n').map(line => line.trim());
const results = {
status: {},
recommendations: [],
};
if (arrLines.length > 0) {
for (const line of arrLines) {
const arrFields = line.split(',');
if (arrFields.length >= 1) {
const recordType = arrFields[0].trim();
if (recordType === 'S') { // Status Record
if (arrFields.length === 9 && arrFields[1].trim().toLowerCase() === 'd') {
results.status.status = arrFields[2].trim();
results.status.message = arrFields[3].trim();
results.status.reference = arrFields[4].trim();
results.status.solverEmail = arrFields[5].trim();
results.status.multipleContainers = arrFields[6].trim();
results.status.suggestExtra = arrFields[7].trim();
} else if (arrFields.length === 9 && arrFields[1].trim().toLowerCase() === 'h') {
// Header row, can be ignored for parsing
} else {
console.warn('Invalid Status record format:', line);
}
} else if (recordType === 'R') { // Recommendation Record
if (arrFields.length === 16 && arrFields[1].trim().toLowerCase() === 'd') {
const recommendation = {
size: arrFields[2].trim(),
cbm: arrFields[3].trim(),
weight: arrFields[4].trim(),
noOfPackages: parseInt(arrFields[5].trim(), 10),
isPacked: arrFields[6].trim(),
packedPackages: parseInt(arrFields[7].trim(), 10),
suggestedIncrease: parseInt(arrFields[8].trim(), 10),
suggestedDecrease: parseInt(arrFields[9].trim(), 10),
erpId: arrFields[10].trim(),
skuNumber: arrFields[11].trim(),
description: arrFields[12].trim(),
po: arrFields[13].trim(),
shipper: arrFields[14].trim(),
};
results.recommendations.push(recommendation);
} else if (arrFields.length === 16 && arrFields[1].trim().toLowerCase() === 'h') {
// Header row, can be ignored for parsing
} else {
console.warn('Invalid Recommendation record format:', line);
}
} else if (recordType && recordType.trim() !== '') {
console.warn('Unknown record type:', recordType, 'in line:', line);
}
}
}
} else {
console.log('Empty suggestions response.');
}
if (results) {
const status = results.status;
console.log(`Status: ${status.status} - ${status.message}`);
console.log(`Reference: ${status.reference}`);
console.log(`Solver Email: ${status.solverEmail}`);
console.log(`Multiple Containers: ${status.multipleContainers}`);
console.log(`Extra Suggested: ${status.suggestExtra}`);
const recs = results.recommendations;
if (recs.length > 0) {
console.log('--- Suggestions ---');
for (const recommendation of recs) {
console.log(
`Is Packed: ${recommendation.isPacked}, Size LxWxH: ${recommendation.size}, CBM: ${recommendation.cbm}, Weight Kg: ${recommendation.weight}, Packages: ${recommendation.noOfPackages}, Packed: ${recommendation.packedPackages}, Increase: ${recommendation.suggestedIncrease}, Decrease: ${recommendation.suggestedDecrease}, ErpId: ${recommendation.erpId}, SkuNumber: ${recommendation.skuNumber}, Description: ${recommendation.description}, PO#: ${recommendation.po}, Division: ${recommendation.shipper}`
);
// --- Your ERP Processing Logic for Suggestions Here ---
}
} else {
console.log(`No suggestions found for reference: ${shipmentReference}`);
}
} else {
console.error(`Error retrieving or parsing suggestions for reference: ${shipmentReference}`);
}
}
// --- Helper Functions (Adapt as needed for your CSV generation) ---
function removeComma(value) {
return value ? value.toString().replace(/,/g, '').replace(/\r?\n|\r/g, ' ') : '';
}
function formatDecimal3(value) {
return parseFloat(value).toFixed(3).replace(/0*$/, '').replace(/\.$/, '');
}
function generateLoadViewerCsv() {
const rs_shipment = [
{ reference: 'SANDBOX-01', hangingAllowed: true, useMultipleContainers: false, uniqueErpId: false, suggestExtra: false },
];
const rs_container = [
{ lengthMm: 5867, widthMm: 2352, heightMm: 2393, maxWeightKg: 27200, backMarginMm: 0, isRefrigerated: false, maxUseOne: false, lclBreakEvenCBM: 0 },
];
const rs_cargo = [
{ color: '#008000', lengthMm: 700, widthMm: 500, heightMm: 300, weightKg: 91.5, cartons: 300, additionalCartons: 0, placement: 0, verticalRotationAllowed: true, skuPerCarton: 1, emptyPalletHeightMm: 0, trayCapMm: 0, trayHeightMm: 0, marginLength: 0, marginWidth: 0, marginHeight: 0, nameOfSet: '', cartonRatioInSet: 0, erpId: '', skuNumber: '', description: '', destination: 0, po: '', shipper: '' },
];
const csvLines = [
'S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra',
`S,D,${removeComma(rs_shipment[0].reference)},${rs_shipment[0].hangingAllowed},${rs_shipment[0].useMultipleContainers},${rs_shipment[0].uniqueErpId},${rs_shipment[0].suggestExtra}`,
'B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM',
];
for (const row of rs_container) {
csvLines.push(
`B,D,${row.lengthMm},${row.widthMm},${row.heightMm},${formatDecimal3(row.maxWeightKg)},${row.backMarginMm},${row.isRefrigerated},${row.maxUseOne},${formatDecimal3(row.lclBreakEvenCBM)}`
);
}
csvLines.push(
'C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper'
);
for (const row of rs_cargo) {
csvLines.push(
`C,D,${removeComma(row.color)},${row.lengthMm},${row.widthMm},${row.heightMm},${formatDecimal3(row.weightKg)},${row.cartons},${row.additionalCartons},${row.placement},${row.verticalRotationAllowed},${row.skuPerCarton},${row.emptyPalletHeightMm},${row.trayCapMm},${row.trayHeightMm},${row.marginLength},${row.marginWidth},${row.marginHeight},${removeComma(row.nameOfSet)},${row.cartonRatioInSet},${removeComma(row.erpId)},${removeComma(row.skuNumber)},${removeComma(row.description)},${row.destination},${removeComma(row.po)},${removeComma(row.shipper)}`
);
}
return csvLines.join('\r\n');
}
// --- Example Usage (Node.js or browser environment) ---
async function testLoadViewerIntegration() {
// Example of uploading data
const csvData = generateLoadViewerCsv();
const uploadResult = await uploadToLoadViewerWithRetries(csvData);
console.log('Upload Result:', uploadResult);
// Example of getting suggestions
await testSuggestions('SANDBOX-01', 'test@example.com'); // Replace with a valid reference and email
}
// Only run if this script is executed in a Node.js environment directly
if (typeof require === 'function' && require.main === module) {
testLoadViewerIntegration();
} else {
console.log('This script can be run in a Node.js environment or integrated into a web page.');
}
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class LoadViewerIntegration {
private static final String TOKEN_URL = "https://www.loadviewer.com/api/auth/jwt/token";
private static final String UPLOAD_URL = "https://www.loadviewer.com/api/integration/v1/upload";
private static final String DOWNLOAD_URL = "https://www.loadviewer.com/api/integration/v1/download";
private static final String API_EMAIL = "YOUR_Email"; // Replace with your actual email
private static final String API_KEY = "YOUR_API_KEY_HERE"; // Replace with your actual API key
private static String jwtToken;
private static final int DEFAULT_MAX_RETRIES = 3;
private static final int DEFAULT_RATE_LIMIT_WAIT_SECONDS = 65;
private static final int GENERAL_RETRY_DELAY_SECONDS = 5;
private static final HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
private static String getJwtTokenWithRetries() throws IOException, InterruptedException {
int retryCount = 0;
while (retryCount <= DEFAULT_MAX_RETRIES) {
try {
if (jwtToken != null) {
return jwtToken;
}
Map<String, String> requestBody = new HashMap<>();
requestBody.put("email", API_EMAIL);
requestBody.put("apikey", API_KEY);
String jsonBody = convertMapToJson(requestBody);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(TOKEN_URL))
.POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
.header("Content-Type", "application/json")
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 429) {
if (retryCount < DEFAULT_MAX_RETRIES) {
retryCount++;
System.err.printf("Rate limit hit on token endpoint (attempt %d of %d). Waiting %d seconds before retrying.%n", retryCount, DEFAULT_MAX_RETRIES, DEFAULT_RATE_LIMIT_WAIT_SECONDS);
Thread.sleep(DEFAULT_RATE_LIMIT_WAIT_SECONDS * 1000);
continue;
} else {
System.err.println("Failed to retrieve token after multiple rate limits.");
return null;
}
} else if (response.statusCode() >= 300) {
if (retryCount < DEFAULT_MAX_RETRIES) {
retryCount++;
System.err.printf("Error retrieving token (attempt %d of %d): %d - %s. Retrying in %d seconds.%n", retryCount, DEFAULT_MAX_RETRIES, response.statusCode(), response.body(), GENERAL_RETRY_DELAY_SECONDS);
Thread.sleep(GENERAL_RETRY_DELAY_SECONDS * 1000);
continue;
} else {
System.err.printf("Failed to retrieve token after %d retries: %d - %s%n", DEFAULT_MAX_RETRIES, response.statusCode(), response.body());
return null;
}
}
String responseBody = response.body();
Map<String, String> jsonResponse = convertJsonToMap(responseBody);
if (jsonResponse != null && jsonResponse.containsKey("token")) {
jwtToken = jsonResponse.get("token");
return jwtToken;
} else {
System.err.println("Failed to extract token from response: " + responseBody);
return null;
}
} catch (IOException | InterruptedException e) {
if (retryCount < DEFAULT_MAX_RETRIES) {
retryCount++;
System.err.printf("Exception occurred while retrieving token (attempt %d of %d): %s. Retrying in %d seconds.%n", retryCount, DEFAULT_MAX_RETRIES, e.getMessage(), GENERAL_RETRY_DELAY_SECONDS);
Thread.sleep(GENERAL_RETRY_DELAY_SECONDS * 1000);
continue;
} else {
System.err.println("An error occurred while retrieving token: " + e.getMessage());
return null;
}
}
}
return null;
}
public static String uploadToLoadViewerWithRetries(String csvData) throws IOException, InterruptedException {
int retryCount = 0;
while (retryCount <= DEFAULT_MAX_RETRIES) {
try {
if (jwtToken == null) {
jwtToken = getJwtTokenWithRetries();
if (jwtToken == null) {
return "Error: Unable to retrieve JWT token for upload.";
}
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(UPLOAD_URL))
.POST(HttpRequest.BodyPublishers.ofString(csvData, StandardCharsets.UTF_8))
.header("Content-Type", "text/csv")
.header("Authorization", "Bearer " + jwtToken)
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 401 || response.statusCode() == 410) {
System.err.println("JWT token expired or invalid. Attempting to retrieve a new token.");
jwtToken = null;
jwtToken = getJwtTokenWithRetries();
if (jwtToken == null) {
return "Error: Unable to retrieve a new JWT token after token expiration.";
}
continue;
} else if (response.statusCode() == 429) {
if (retryCount < DEFAULT_MAX_RETRIES) {
retryCount++;
System.err.printf("Rate limit hit on upload endpoint (attempt %d of %d). Waiting %d seconds before retrying.%n", retryCount, DEFAULT_MAX_RETRIES, DEFAULT_RATE_LIMIT_WAIT_SECONDS);
Thread.sleep(DEFAULT_RATE_LIMIT_WAIT_SECONDS * 1000);
continue;
} else {
return "Error: Upload failed after multiple rate limits.";
}
} else if (response.statusCode() != 409 && response.statusCode() >= 300) {
if (retryCount < DEFAULT_MAX_RETRIES) {
retryCount++;
System.err.printf("Error uploading data (attempt %d of %d): %d - %s. Retrying in %d seconds.%n", retryCount, DEFAULT_MAX_RETRIES, response.statusCode(), response.body(), GENERAL_RETRY_DELAY_SECONDS);
Thread.sleep(GENERAL_RETRY_DELAY_SECONDS * 1000);
continue;
} else {
return String.format("Error: Upload failed after %d attempts. Status: %d - %s", DEFAULT_MAX_RETRIES, response.statusCode(), response.body());
}
}
return String.format("%d - %s", response.statusCode(), response.body());
} catch (IOException | InterruptedException e) {
if (retryCount < DEFAULT_MAX_RETRIES) {
retryCount++;
System.err.printf("Exception occurred during upload (attempt %d of %d): %s. Retrying in %d seconds.%n", retryCount, DEFAULT_MAX_RETRIES, e.getMessage(), GENERAL_RETRY_DELAY_SECONDS);
Thread.sleep(GENERAL_RETRY_DELAY_SECONDS * 1000);
continue;
} else {
return "Exception: " + e.getMessage();
}
}
}
return "Error: Max upload retries exceeded.";
}
public static String getLoadViewerSuggestionsCsvWithRetries(String requestBody) throws IOException, InterruptedException {
int retryCount = 0;
while (retryCount <= DEFAULT_MAX_RETRIES) {
try {
if (jwtToken == null) {
jwtToken = getJwtTokenWithRetries();
if (jwtToken == null) {
return "Error: Unable to retrieve JWT token for getting suggestions.";
}
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(DOWNLOAD_URL))
.POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + jwtToken)
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
String responseBody = response.body();
System.out.printf("POST Request to LoadViewer Suggestions URL: %s%nRequest Body:%n%s%nHTTP Status Code: %d%nResponse Text:%n%s%n", DOWNLOAD_URL, requestBody, response.statusCode(), responseBody);
if (response.statusCode() == 401 || response.statusCode() == 410) {
System.err.println("JWT token expired or invalid. Attempting to retrieve a new token.");
jwtToken = null;
jwtToken = getJwtTokenWithRetries();
if (jwtToken == null) {
return "Error: Unable to retrieve a new JWT token after token expiration.";
}
continue;
} else if (response.statusCode() == 429) {
if (retryCount < DEFAULT_MAX_RETRIES) {
retryCount++;
System.err.printf("Rate limit hit on suggestions endpoint (attempt %d of %d). Waiting %d seconds before retrying.%n", retryCount, DEFAULT_MAX_RETRIES, DEFAULT_RATE_LIMIT_WAIT_SECONDS);
Thread.sleep(DEFAULT_RATE_LIMIT_WAIT_SECONDS * 1000);
continue;
} else {
return "Error: Failed to retrieve suggestions after multiple rate limits.";
}
} else if (response.statusCode() >= 300) {
if (retryCount < DEFAULT_MAX_RETRIES) {
retryCount++;
System.err.printf("Error retrieving suggestions (attempt %d of %d): %d - %s. Retrying in %d seconds.%n", retryCount, DEFAULT_MAX_RETRIES, response.statusCode(), responseBody, GENERAL_RETRY_DELAY_SECONDS);
Thread.sleep(GENERAL_RETRY_DELAY_SECONDS * 1000);
continue;
} else {
return String.format("Error: Failed to retrieve suggestions after %d attempts. Status: %d - %s", DEFAULT_MAX_RETRIES, response.statusCode(), responseBody);
}
}
return responseBody;
} catch (IOException | InterruptedException e) {
if (retryCount < DEFAULT_MAX_RETRIES) {
retryCount++;
System.err.printf("Exception occurred while retrieving suggestions (attempt %d of %d): %s. Retrying in %d seconds.%n", retryCount, DEFAULT_MAX_RETRIES, e.getMessage(), GENERAL_RETRY_DELAY_SECONDS);
Thread.sleep(GENERAL_RETRY_DELAY_SECONDS * 1000);
continue;
} else {
return "Exception: " + e.getMessage();
}
}
}
return "Error: Max suggestions retries exceeded.";
}
public static void testSuggestions(String shipmentReference, String solverEmail) throws IOException, InterruptedException {
if (shipmentReference == null || shipmentReference.isEmpty()) {
System.err.println("Shipment Reference is required!");
return;
}
if (solverEmail != null && !isValidEmail(solverEmail)) {
System.err.println("Invalid Solver Email format!");
return;
}
String token = jwtToken != null ? jwtToken : getJwtTokenWithRetries();
if (token == null) {
System.err.println("Token Error or Server Not Reachable.");
return;
}
Map<String, String> requestBody = new HashMap<>();
requestBody.put("reference", shipmentReference.replace("\"", "\"\"")); // Escape double quotes
requestBody.put("solverEmail", solverEmail.replace("\"", "\"\""));
String jsonBody = convertMapToJson(requestBody);
String strResponse = getLoadViewerSuggestionsCsvWithRetries(jsonBody);
if (strResponse.startsWith("Error") || strResponse.startsWith("Exception")) {
System.err.println("Error retrieving data from LoadViewer: " + strResponse);
return;
}
System.out.println("Raw Suggestions Response:\n" + strResponse);
String[] arrLines = strResponse.split("\\r?\\n");
Map<String, Object> results = new HashMap<>();
Map<String, String> statusInfo = new HashMap<>();
List<Map<String, Object>> recommendations = new ArrayList<>();
results.put("status", statusInfo);
results.put("recommendations", recommendations);
if (arrLines.length > 0) {
for (String line : arrLines) {
String[] arrFields = line.split(",");
if (arrFields.length >= 1) {
String recordType = arrFields[0].trim();
if (recordType.equals("S")) { // Status Record
if (arrFields.length == 9 && arrFields[1].trim().equalsIgnoreCase("d")) {
statusInfo.put("status", arrFields[2].trim());
statusInfo.put("message", arrFields[3].trim());
statusInfo.put("reference", arrFields[4].trim());
statusInfo.put("solverEmail", arrFields[5].trim());
statusInfo.put("multipleContainers", arrFields[6].trim());
statusInfo.put("suggestExtra", arrFields[7].trim());
} else if (arrFields.length == 9 && arrFields[1].trim().equalsIgnoreCase("h")) {
// Header row, can be ignored
} else {
System.err.println("Invalid Status record format: " + line);
}
} else if (recordType.equals("R")) { // Recommendation Record
if (arrFields.length == 16 && arrFields[1].trim().equalsIgnoreCase("d")) {
Map<String, Object> recommendation = new HashMap<>();
recommendation.put("size", arrFields[2].trim());
recommendation.put("cbm", arrFields[3].trim());
recommendation.put("weight", arrFields[4].trim());
recommendation.put("noOfPackages", Integer.parseInt(arrFields[5].trim()));
recommendation.put("isPacked", arrFields[6].trim());
recommendation.put("packedPackages", Integer.parseInt(arrFields[7].trim()));
recommendation.put("suggestedIncrease", Integer.parseInt(arrFields[8].trim()));
recommendation.put("suggestedDecrease", Integer.parseInt(arrFields[9].trim()));
recommendation.put("erpId", arrFields[10].trim());
recommendation.put("skuNumber", arrFields[11].trim());
recommendation.put("description", arrFields[12].trim());
recommendation.put("po", arrFields[13].trim());
recommendation.put("shipper", arrFields[14].trim());
recommendations.add(recommendation);
} else if (arrFields.length == 16 && arrFields[1].trim().equalsIgnoreCase("h")) {
// Header row, can be ignored.
}
else {
System.err.println("Invalid Recommendation record format: " + line);
}
} else if (recordType != null && !recordType.trim().isEmpty()) {
System.err.println("Unknown record type: " + recordType + " in line: " + line);
}
}
}
} else {
System.out.println("Empty suggestions response.");
}
if (results != null && results.containsKey("status")) {
Map<String, String> status = (Map<String, String>) results.get("status");
System.out.printf("Status: %s - %s%n", status.get("status"), status.get("message"));
System.out.printf("Reference: %s%n", status.get("reference"));
System.out.printf("Solver Email: %s%n", status.get("solverEmail"));
System.out.printf("Multiple Containers: %s%n", status.get("multipleContainers"));
System.out.printf("Extra Suggested: %s%n", status.get("suggestExtra"));
List<Map<String, Object>> recs = (List<Map<String, Object>>) results.get("recommendations");
if (recs != null && recs.size() > 0) {
System.out.println("--- Suggestions ---");
for (Map<String, Object> recommendation : recs) {
System.out.printf(
"Is Packed: %s, Size LxWxH: %s, CBM: %s, Weight Kg: %s, Packages: %s, Packed: %s, Increase: %s, Decrease: %s, ErpId: %s, SkuNumber: %s, Description: %s, PO#: %s, Division: %s%n",
recommendation.get("isPacked"), recommendation.get("size"), recommendation.get("cbm"), recommendation.get("weight"),
recommendation.get("noOfPackages"), recommendation.get("packedPackages"), recommendation.get("suggestedIncrease"),
recommendation.get("suggestedDecrease"), recommendation.get("erpId"), recommendation.get("skuNumber"),
recommendation.get("description"), recommendation.get("po"), recommendation.get("shipper")
);
// --- Your ERP Processing Logic for Suggestions Here ---
}
} else {
System.out.printf("No suggestions found for reference: %s%n", shipmentReference);
}
} else {
System.err.printf("Error retrieving or parsing suggestions for reference: %s%n", shipmentReference);
}
}
private static String removeComma(String value) {
return (value != null) ? value.replace(",", "").replace("\r\n", " ") : "";
}
private static String formatDecimal3(double value) {
return String.format("%.3f", value).replaceAll("0*$", "").replaceAll("\\.$", "");
}
private static String generateLoadViewerCsv() {
List<Map<String, Object>> rsShipment = new ArrayList<>();
Map<String, Object> shipmentData = new HashMap<>();
shipmentData.put("reference", "SANDBOX-01");
shipmentData.put("hangingAllowed", true);
shipmentData.put("useMultipleContainers", false);
shipmentData.put("uniqueErpId", false);
shipmentData.put("suggestExtra", false);
rsShipment.add(shipmentData);
List<Map<String, Object>> rsContainer = new ArrayList<>();
Map<String, Object> containerData = new HashMap<>();
containerData.put("lengthMm", 5867);
containerData.put("widthMm", 2352);
containerData.put("heightMm", 2393);
containerData.put("maxWeightKg", 27200.0);
containerData.put("backMarginMm", 0);
containerData.put("isRefrigerated", false);
containerData.put("maxUseOne", false);
containerData.put("lclBreakEvenCBM", 0.0);
rsContainer.add(containerData);
List<Map<String, Object>> rsCargo = new ArrayList<>();
Map<String, Object> cargoData = new HashMap<>();
cargoData.put("color", "#008000");
cargoData.put("lengthMm", 700);
cargoData.put("widthMm", 500);
cargoData.put("heightMm", 300);
cargoData.put("weightKg", 91.5);
cargoData.put("cartons", 300);
cargoData.put("additionalCartons", 0);
cargoData.put("placement", 0);
cargoData.put("verticalRotationAllowed", true);
cargoData.put("skuPerCarton", 1);
cargoData.put("emptyPalletHeightMm", 0);
cargoData.put("trayCapMm", 0);
cargoData.put("trayHeightMm", 0);
cargoData.put("marginLength", 0);
cargoData.put("marginWidth", 0);
cargoData.put("marginHeight", 0);
cargoData.put("nameOfSet", "");
cargoData.put("cartonRatioInSet", 0);
cargoData.put("erpId", "");
cargoData.put("skuNumber", "");
cargoData.put("description", "");
cargoData.put("destination", 0);
cargoData.put("po", "");
cargoData.put("shipper", "");
rsCargo.add(cargoData);
List<String> csvLines = new ArrayList<>();
csvLines.add("S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra");
csvLines.add(String.format("S,D,%s,%s,%s,%s,%s",
removeComma((String) rsShipment.get(0).get("reference")),
rsShipment.get(0).get("hangingAllowed"),
rsShipment.get(0).get("useMultipleContainers"),
rsShipment.get(0).get("uniqueErpId"),
rsShipment.get(0).get("suggestExtra")));
csvLines.add("B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM");
for (Map<String, Object> row : rsContainer) {
csvLines.add(String.format("B,D,%s,%s,%s,%s,%s,%s,%s,%s",
row.get("lengthMm"), row.get("widthMm"), row.get("heightMm"),
formatDecimal3((Double) row.get("maxWeightKg")), row.get("backMarginMm"),
row.get("isRefrigerated"), row.get("maxUseOne"),
formatDecimal3((Double) row.get("lclBreakEvenCBM"))));
}
csvLines.add("C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper");
for (Map<String, Object> row : rsCargo) {
csvLines.add(String.format("C,D,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s",
removeComma((String) row.get("color")), row.get("lengthMm"), row.get("widthMm"), row.get("heightMm"),
formatDecimal3((Double) row.get("weightKg")), row.get("cartons"), row.get("additionalCartons"),
row.get("placement"), row.get("verticalRotationAllowed"), row.get("skuPerCarton"),
row.get("emptyPalletHeightMm"), row.get("trayCapMm"), row.get("trayHeightMm"),
row.get("marginLength"), row.get("marginWidth"), row.get("marginHeight"),
removeComma((String) row.get("nameOfSet")), row.get("cartonRatioInSet"),
removeComma((String) row.get("erpId")), removeComma((String) row.get("skuNumber")),
removeComma((String) row.get("description")), row.get("destination"),
removeComma((String) row.get("po")), removeComma((String) row.get("shipper"))));
}
return String.join(System.lineSeparator(), csvLines);
}
private static boolean isValidEmail(String email) {
// Basic email validation regex
String regex = "^[^\s@]+@[^\s@]+\\.[^\s@]+$";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(email);
return matcher.matches();
}
// Helper function to convert a Map to a JSON string
private static String convertMapToJson(Map<String, String> map) {
return map.entrySet().stream()
.map(entry -> String.format("\"%s\":\"%s\"", entry.getKey(), entry.getValue()))
.collect(Collectors.joining(",", "{", "}"));
}
// Helper function to convert a JSON string to a Map
private static Map<String, String> convertJsonToMap(String json) {
Map<String, String> map = new HashMap<>();
json = json.substring(1, json.length() - 1); // Remove curly braces
String[] pairs = json.split(",");
for (String pair : pairs) {
String[] keyValue = pair.split(":");
if (keyValue.length == 2) {
String key = keyValue[0].replaceAll("\"", "").trim();
String value = keyValue[1].replaceAll("\"", "").trim();
map.put(key, value);
}
}
return map;
}
public static void main(String[] args) {
try {
// Example of uploading data
String csvData = generateLoadViewerCsv();
String uploadResult = uploadToLoadViewerWithRetries(csvData);
System.out.println("Upload Result: " + uploadResult);
// Example of getting suggestions
testSuggestions("SANDBOX-01", "test@example.com");
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
open System
open System.Collections.Generic
open System.Net.Http
open System.Net.Http.Headers
open System.Text
open System.Text.RegularExpressions
open System.Threading.Tasks
// Configuration
let tokenUrl = "https://www.loadviewer.com/api/auth/jwt/token"
let uploadUrl = "https://www.loadviewer.com/api/integration/v1/upload"
let downloadUrl = "https://www.loadviewer.com/api/integration/v1/download"
let apiEmail = "YOUR_Email" // Replace with your actual email
let apiKey = "YOUR_API_KEY_HERE" // Replace with your actual API key
let mutable jwtToken : string option = None
let defaultMaxRetries = 3
let defaultRateLimitWaitSeconds = 65
let generalRetryDelaySeconds = 5
let httpClient = new HttpClient()
// Helper function to convert a Dictionary<string, string> to a JSON string
let convertMapToJson (map : Dictionary<string, string>) =
map
|> Seq.map (fun (key, value) -> sprintf """"%s":"%s\"""" key value)
|> String.concat ","
|> sprintf "{%s}"
// Helper function to convert a JSON string to a Dictionary<string, string>
let convertJsonToMap (json : string) : Dictionary<string, string> =
let map = new Dictionary<string, string>()
let trimmedJson = json.Substring(1, json.Length - 2) // Remove curly braces
let pairs = trimmedJson.Split(',')
for pair in pairs do
let keyValue = pair.Split(':')
if keyValue.Length = 2 then
let key = keyValue.[0].Replace("\"", "").Trim()
let value = keyValue.[1].Replace("\"", "").Trim()
map.Add(key, value)
map
// Asynchronous function to get the JWT token with retries
let rec getJwtTokenWithRetries (retryCount : int) : Async<string option> =
async {
if jwtToken.IsSome then
return jwtToken.Value
if retryCount > defaultMaxRetries then
eprintln "Failed to retrieve token after multiple retries."
return None
try
let requestBody = new Dictionary<string, string>()
requestBody.Add("email", apiEmail)
requestBody.Add("apikey", apiKey)
let jsonBody = convertMapToJson requestBody
let content = new StringContent(jsonBody, Encoding.UTF8, "application/json")
use response = await httpClient.PostAsync(Uri(tokenUrl), content)
if response.StatusCode = System.Net.HttpStatusCode.TooManyRequests then
if retryCount < defaultMaxRetries then
eprintfn "Rate limit hit on token endpoint (attempt %d of %d). Waiting %d seconds before retrying." retryCount defaultMaxRetries defaultRateLimitWaitSeconds
do! Async.Sleep(defaultRateLimitWaitSeconds * 1000)
return! getJwtTokenWithRetries (retryCount + 1)
else
eprintln "Failed to retrieve token after multiple rate limits."
return None
elif not response.IsSuccessStatusCode then
if retryCount < defaultMaxRetries then
let errorBody = await response.Content.ReadAsStringAsync()
eprintfn "Error retrieving token (attempt %d of %d): %i - %s. Retrying in %d seconds." retryCount defaultMaxRetries (int response.StatusCode) errorBody generalRetryDelaySeconds
do! Async.Sleep(generalRetryDelaySeconds * 1000)
return! getJwtTokenWithRetries (retryCount + 1)
else
let errorBody = await response.Content.ReadAsStringAsync()
eprintfn "Failed to retrieve token after %d retries: %i - %s" defaultMaxRetries (int response.StatusCode) errorBody
return None
else
let responseBody = await response.Content.ReadAsStringAsync()
let jsonResponse = convertJsonToMap responseBody
if jsonResponse.ContainsKey("token") then
jwtToken <- Some(jsonResponse.["token"])
return jwtToken.Value
else
eprintfn "Failed to extract token from response: %s" responseBody
return None
with
| ex ->
if retryCount < defaultMaxRetries then
eprintfn "Exception occurred while retrieving token (attempt %d of %d): %s. Retrying in %d seconds." retryCount defaultMaxRetries ex.Message generalRetryDelaySeconds
do! Async.Sleep(generalRetryDelaySeconds * 1000)
return! getJwtTokenWithRetries (retryCount + 1)
else
eprintfn "An error occurred while retrieving token: %s" ex.Message
return None
} |> Async.StartAsTask
// Asynchronous function to upload data to LoadViewer with retries
let rec uploadToLoadViewerWithRetries (csvData : string) (retryCount : int) : Async<string> =
async {
let currentToken =
match jwtToken with
| Some token -> token
| None ->
let tokenResult = await getJwtTokenWithRetries 1
match tokenResult with
| Some token -> token
| None -> return "ERROR: Unable to retrieve JWT token for upload."
jwtToken <- Some(currentToken)
if retryCount > defaultMaxRetries then
return "ERROR: Max upload retries exceeded."
try
let content = new StringContent(csvData, Encoding.UTF8, "text/csv")
content.Headers.ContentType <- MediaTypeHeaderValue("text/csv")
httpClient.DefaultRequestHeaders.Authorization <- new AuthenticationHeaderValue("Bearer", currentToken)
use response = await httpClient.PostAsync(Uri(uploadUrl), content)
if response.StatusCode = System.Net.HttpStatusCode.Unauthorized || response.StatusCode = System.Net.HttpStatusCode.Gone then
eprintln "JWT token expired or invalid. Attempting to retrieve a new token."
jwtToken <- None
let newTokenResult = await getJwtTokenWithRetries 1
match newTokenResult with
| Some newToken ->
httpClient.DefaultRequestHeaders.Authorization <- new AuthenticationHeaderValue("Bearer", newToken)
return! uploadToLoadViewerWithRetries csvData (retryCount + 1)
| None -> return "ERROR: Unable to retrieve a new JWT token after token expiration."
elif response.StatusCode = System.Net.HttpStatusCode.TooManyRequests then
if retryCount < defaultMaxRetries then
eprintfn "Rate limit hit on upload endpoint (attempt %d of %d). Waiting %d seconds before retrying." retryCount defaultMaxRetries defaultRateLimitWaitSeconds
do! Async.Sleep(defaultRateLimitWaitSeconds * 1000)
return! uploadToLoadViewerWithRetries csvData (retryCount + 1)
else
return "ERROR: Upload failed after multiple rate limits."
elif not response.IsSuccessStatusCode && response.StatusCode <> System.Net.HttpStatusCode.Conflict then
if retryCount < defaultMaxRetries then
let errorBody = await response.Content.ReadAsStringAsync()
eprintfn "Error uploading data (attempt %d of %d): %i - %s. Retrying in %d seconds." retryCount defaultMaxRetries (int response.StatusCode) errorBody generalRetryDelaySeconds
do! Async.Sleep(generalRetryDelaySeconds * 1000)
return! uploadToLoadViewerWithRetries csvData (retryCount + 1)
else
let errorBody = await response.Content.ReadAsStringAsync()
return sprintf "ERROR: Upload failed after %d attempts. Status: %i - %s" defaultMaxRetries (int response.StatusCode) errorBody
else
let responseBody = await response.Content.ReadAsStringAsync()
return sprintf "%i - %s" (int response.StatusCode) responseBody
with
| ex ->
if retryCount < defaultMaxRetries then
eprintfn "Exception occurred during upload (attempt %d of %d): %s. Retrying in %d seconds." retryCount defaultMaxRetries ex.Message generalRetryDelaySeconds
do! Async.Sleep(generalRetryDelaySeconds * 1000)
return! uploadToLoadViewerWithRetries csvData (retryCount + 1)
else
return sprintf "Exception: %s" ex.Message
} |> Async.StartAsTask |> Async.AwaitTask
// Asynchronous function to get LoadViewer suggestions CSV with retries
let rec getLoadViewerSuggestionsCsvWithRetries (requestBodyJson : string) (retryCount : int) : Async<string> =
async {
let currentToken =
match jwtToken with
| Some token -> token
| None ->
let tokenResult = await getJwtTokenWithRetries 1
match tokenResult with
| Some token -> token
| None -> return "ERROR: Unable to retrieve JWT token for getting suggestions."
jwtToken <- Some(currentToken)
if retryCount > defaultMaxRetries then
return "ERROR: Max suggestions retries exceeded."
try
let content = new StringContent(requestBodyJson, Encoding.UTF8, "application/json")
content.Headers.ContentType <- MediaTypeHeaderValue("application/json")
httpClient.DefaultRequestHeaders.Authorization <- new AuthenticationHeaderValue("Bearer", currentToken)
use response = await httpClient.PostAsync(Uri(downloadUrl), content)
let responseBody = await response.Content.ReadAsStringAsync()
printfn "POST Request to LoadViewer Suggestions URL: %s\nRequest Body:\n%s\nHTTP Status Code: %i\nResponse Text:\n%s" downloadUrl requestBodyJson (int response.StatusCode) responseBody
if response.StatusCode = System.Net.HttpStatusCode.Unauthorized || response.StatusCode = System.Net.HttpStatusCode.Gone then
eprintln "JWT token expired or invalid. Attempting to retrieve a new token."
jwtToken <- None
let newTokenResult = await getJwtTokenWithRetries 1
match newTokenResult with
| Some newToken ->
httpClient.DefaultRequestHeaders.Authorization <- new AuthenticationHeaderValue("Bearer", newToken)
return! getLoadViewerSuggestionsCsvWithRetries requestBodyJson (retryCount + 1)
| None -> return "ERROR: Unable to retrieve a new JWT token after token expiration."
elif response.StatusCode = System.Net.HttpStatusCode.TooManyRequests then
if retryCount < defaultMaxRetries then
eprintfn "Rate limit hit on suggestions endpoint (attempt %d of %d). Waiting %d seconds before retrying." retryCount defaultMaxRetries defaultRateLimitWaitSeconds
do! Async.Sleep(defaultRateLimitWaitSeconds * 1000)
return! getLoadViewerSuggestionsCsvWithRetries requestBodyJson (retryCount + 1)
else
return "ERROR: Failed to retrieve suggestions after multiple rate limits."
elif not response.IsSuccessStatusCode then
if retryCount < defaultMaxRetries then
eprintfn "Error retrieving suggestions (attempt %d of %d): %i - %s. Retrying in %d seconds." retryCount defaultMaxRetries (int response.StatusCode) responseBody generalRetryDelaySeconds
do! Async.Sleep(generalRetryDelaySeconds * 1000)
return! getLoadViewerSuggestionsCsvWithRetries requestBodyJson (retryCount + 1)
else
return sprintf "ERROR: Failed to retrieve suggestions after %d attempts. Status: %i - %s" defaultMaxRetries (int response.StatusCode) responseBody
else
return responseBody
with
| ex ->
if retryCount < defaultMaxRetries then
eprintfn "Exception occurred while retrieving suggestions (attempt %d of %d): %s. Retrying in %d seconds." retryCount defaultMaxRetries ex.Message generalRetryDelaySeconds
do! Async.Sleep(generalRetryDelaySeconds * 1000)
return! getLoadViewerSuggestionsCsvWithRetries requestBodyJson (retryCount + 1)
else
return sprintf "Exception: %s" ex.Message
} |> Async.StartAsTask |> Async.AwaitTask
// Function to test getting suggestions
let testSuggestions (shipmentReference : string) (solverEmail : string) : Async<unit> =
async {
if String.IsNullOrWhiteSpace(shipmentReference) then
eprintln "Shipment Reference is required!"
return ()
if not (String.IsNullOrWhiteSpace(solverEmail) || Regex.IsMatch(solverEmail, "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")) then
eprintln "Invalid Solver Email format!"
return ()
let currentToken =
match jwtToken with
| Some token -> token
| None ->
let tokenResult = await getJwtTokenWithRetries 1
match tokenResult with
| Some token -> token
| None ->
eprintln "Token Error or Server Not Reachable."
return ()
jwtToken <- Some(currentToken)
let requestBody = new Dictionary<string, string>()
requestBody.Add("reference", shipmentReference.Replace("\"", "\"\"")) // Escape double quotes
requestBody.Add("solverEmail", solverEmail.Replace("\"", "\"\""))
let jsonBody = convertMapToJson requestBody
let strResponse = await getLoadViewerSuggestionsCsvWithRetries jsonBody 1
if strResponse.StartsWith("ERROR") || strResponse.StartsWith("Exception") then
eprintfn "Error retrieving data from LoadViewer: %s" strResponse
return ()
printfn "Raw Suggestions Response:\n%s" strResponse
let arrLines = strResponse.Split([|"\r\n"; "\n"|], StringSplitOptions.RemoveEmptyEntries)
let results = new Dictionary<string, obj>()
let statusInfo = new Dictionary<string, string>() :> obj
let recommendations = new List<Dictionary<string, obj>>() :> obj
results.Add("status", statusInfo)
results.Add("recommendations", recommendations)
if arrLines.Length > 0 then
for line in arrLines do
let arrFields = line.Split(',')
if arrFields.Length >= 1 then
let recordType = arrFields.[0].Trim()
if recordType = "S" then // Status Record
if arrFields.Length = 9 && arrFields.[1].Trim().ToLower() = "d" then
statusInfo.["status"] <- arrFields.[2].Trim() :> obj
statusInfo.["message"] <- arrFields.[3].Trim() :> obj
statusInfo.["reference"] <- arrFields.[4].Trim() :> obj
statusInfo.["solverEmail"] <- arrFields.[5].Trim() :> obj
statusInfo.["multipleContainers"] <- arrFields.[6].Trim() :> obj
statusInfo.["suggestExtra"] <- arrFields.[7].Trim() :> obj
elif arrFields.Length = 9 && arrFields.[1].Trim().ToLower() = "h" then
// Header row, can be ignored
()
else
eprintln $"Invalid Status record format: {line}"
elif recordType = "R" then // Recommendation Record
if arrFields.Length = 16 && arrFields.[1].Trim().ToLower() = "d" then
let recommendation = new Dictionary<string, obj>()
recommendation.["size"] <- arrFields.[2].Trim() :> obj
recommendation.["cbm"] <- arrFields.[3].Trim() :> obj
recommendation.["weight"] <- arrFields.[4].Trim() :> obj
recommendation.["noOfPackages"] <- int arrFields.[5].Trim() :> obj
recommendation.["isPacked"] <- arrFields.[6].Trim() :> obj
recommendation.["packedPackages"] <- int arrFields.[7].Trim() :> obj
recommendation.["suggestedIncrease"] <- int arrFields.[8].Trim() :> obj
recommendation.["suggestedDecrease"] <- int arrFields.[9].Trim() :> obj
recommendation.["erpId"] <- arrFields.[10].Trim() :> obj
recommendation.["skuNumber"] <- arrFields.[11].Trim() :> obj
recommendation.["description"] <- arrFields.[12].Trim() :> obj
recommendation.["po"] <- arrFields.[13].Trim() :> obj
recommendation.["shipper"] <- arrFields.[14].Trim() :> obj
(recommendations :?> List<Dictionary<string, obj>>).Add(recommendation)
elif arrFields.Length = 16 && arrFields.[1].Trim().ToLower() = "h" then
// Header row, can be ignored.
()
else
eprintln $"Invalid Recommendation record format: {line}"
elif not (String.IsNullOrWhiteSpace recordType) then
eprintln $"Unknown record type: {recordType} in line: {line}"
else
printfn "Empty suggestions response."
if results.ContainsKey("status") then
let status = results.["status"] :?> Dictionary<string, string>
printfn "Status: %s - %s" status.["status"] status.["message"]
printfn "Reference: %s" status.["reference"]
printfn "Solver Email: %s" status.["solverEmail"]
printfn "Multiple Containers: %s" status.["multipleContainers"]
printfn "Extra Suggested: %s" status.["suggestExtra"]
if results.ContainsKey("recommendations") then
let recs = results.["recommendations"] :?> List<Dictionary<string, obj>>
if recs.Count > 0 then
printfn "--- Suggestions ---"
for recommendation in recs do
printfn "Is Packed: %s, Size LxWxH: %s, CBM: %s, Weight Kg: %s, Packages: %i, Packed: %i, Increase: %i, Decrease: %i, ErpId: %s, SkuNumber: %s, Description: %s, PO#: %s, Division: %s"
(recommendation.["isPacked"] :?> string)
(recommendation.["size"] :?> string)
(recommendation.["cbm"] :?> string)
(recommendation.["weight"] :?> string)
(recommendation.["noOfPackages"] :?> int)
(recommendation.["packedPackages"] :?> int)
(recommendation.["suggestedIncrease"] :?> int)
(recommendation.["suggestedDecrease"] :?> int)
(recommendation.["erpId"] :?> string)
(recommendation.["skuNumber"] :?> string)
(recommendation.["description"] :?> string)
(recommendation.["po"] :?> string)
(recommendation.["shipper"] :?> string)
// --- Your ERP Processing Logic for Suggestions Here ---
else
printfn $"No suggestions found for reference: {shipmentReference}"
else
eprintln $"Error parsing recommendations for reference: {shipmentReference}"
else
eprintln $"Error retrieving or parsing suggestions for reference: {shipmentReference}"
}
let removeComma (value : string) : string =
if value <> null then value.Replace(",", "").Replace("\r\n", " ") else ""
let formatDecimal3 (value : float) : string =
sprintf "%.3f" value |> (fun s -> s.TrimEnd('0').TrimEnd('.'))
let generateLoadViewerCsv () : string =
let rsShipment = ResizeArray()
let shipmentData = new Dictionary<string, obj>()
shipmentData.Add("reference", "SANDBOX-01" :> obj)
shipmentData.Add("hangingAllowed", true :> obj)
shipmentData.Add("useMultipleContainers", false :> obj)
shipmentData.Add("uniqueErpId", false :> obj)
shipmentData.Add("suggestExtra", false :> obj)
rsShipment.Add(shipmentData)
let rsContainer = ResizeArray()
let containerData = new Dictionary<string, obj>()
containerData.Add("lengthMm", 5867 :> obj)
containerData.Add("widthMm", 2352 :> obj)
containerData.Add("heightMm", 2393 :> obj)
containerData.Add("maxWeightKg", 27200.0 :> obj)
containerData.Add("backMarginMm", 0 :> obj)
containerData.Add("isRefrigerated", false :> obj)
containerData.Add("maxUseOne", false :> obj)
containerData.Add("lclBreakEvenCBM", 0.0 :> obj)
rsContainer.Add(containerData)
let rsCargo = ResizeArray()
let cargoData = new Dictionary<string, obj>()
cargoData.Add("color", "#008000" :> obj)
cargoData.Add("lengthMm", 700 :> obj)
cargoData.Add("widthMm", 500 :> obj)
cargoData.Add("heightMm", 300 :> obj)
cargoData.Add("weightKg", 91.5 :> obj)
cargoData.Add("cartons", 300 :> obj)
cargoData.Add("additionalCartons", 0 :> obj)
cargoData.Add("placement", 0 :> obj)
cargoData.Add("verticalRotationAllowed", true :> obj)
cargoData.Add("skuPerCarton", 1 :> obj)
cargoData.Add("emptyPalletHeightMm", 0 :> obj)
cargoData.Add("trayCapMm", 0 :> obj)
cargoData.Add("trayHeightMm", 0 :> obj)
cargoData.Add("marginLength", 0 :> obj)
cargoData.Add("marginWidth", 0 :> obj)
cargoData.Add("marginHeight", 0 :> obj)
cargoData.Add("nameOfSet", "" :> obj)
cargoData.Add("cartonRatioInSet", 0 :> obj)
cargoData.Add("erpId", "" :> obj)
cargoData.Add("skuNumber", "" :> obj)
cargoData.Add("description", "" :> obj)
cargoData.Add("destination", 0 :> obj)
cargoData.Add("po", "" :> obj)
cargoData.Add("shipper", "" :> obj)
rsCargo.Add(cargoData)
let csvLines = ResizeArray()
csvLines.Add("S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra")
csvLines.Add(sprintf "S,D,%s,%b,%b,%b,%b"
(removeComma (rsShipment.[0].["reference"] :> string))
(rsShipment.[0].["hangingAllowed"] :> bool)
(rsShipment.[0].["useMultipleContainers"] :> bool)
(rsShipment.[0].["uniqueErpId"] :> bool)
(rsShipment.[0].["suggestExtra"] :> bool))
csvLines.Add("B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM")
for row in rsContainer do
csvLines.Add(sprintf "B,D,%i,%i,%i,%s,%i,%b,%b,%s"
(row.["lengthMm"] :> int)
(row.["widthMm"] :> int)
(row.["heightMm"] :> int)
(formatDecimal3 (row.["maxWeightKg"] :> float))
(row.["backMarginMm"] :> int)
(row.["isRefrigerated"] :> bool)
(row.["maxUseOne"] :> bool)
(formatDecimal3 (row.["lclBreakEvenCBM"] :> float)))
csvLines.Add("C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper")
for row in rsCargo do
csvLines.Add(sprintf "C,D,%s,%i,%i,%i,%s,%i,%i,%i,%b,%i,%i,%i,%i,%i,%i,%i,%s,%i,%s,%s,%s,%i,%s,%s"
(removeComma (row.["color"] :> string))
(row.["lengthMm"] :> int)
(row.["widthMm"] :> int)
(row.["heightMm"] :> int)
(formatDecimal3 (row.["weightKg"] :> float))
(row.["cartons"] :> int)
(row.["additionalCartons"] :> int)
(row.["placement"] :> int)
(row.["verticalRotationAllowed"] :> bool)
(row.["skuPerCarton"] :> int)
(row.["emptyPalletHeightMm"] :> int)
(row.["trayCapMm"] :> int)
(row.["trayHeightMm"] :> int)
(row.["marginLength"] :> int)
(row.["marginWidth"] :> int)
(row.["marginHeight"] :> int)
(removeComma (row.["nameOfSet"] :> string))
(row.["cartonRatioInSet"] :> int)
(removeComma (row.["erpId"] :> string))
(removeComma (row.["skuNumber"] :> string))
(removeComma (row.["description"] :> string))
(row.["destination"] :> int)
(removeComma (row.["po"] :> string))
(removeComma (row.["shipper"] :> string)))
String.Join(Environment.NewLine, csvLines)
let isValidEmail (email : string) : bool =
// Basic email validation regex
let regex = "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"
Regex.IsMatch(email, regex)
[<EntryPoint>]
let main argv =
task {
// Example of uploading data
let csvData = generateLoadViewerCsv ()
let uploadResult = await uploadToLoadViewerWithRetries csvData 1
printfn "Upload Result: %s" uploadResult
// Example of getting suggestions
do! testSuggestions "SANDBOX-01" "test@example.com"
} |> Async.AwaitTask
0
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"time"
)
const (
tokenURL = "https://www.loadviewer.com/api/auth/jwt/token"
uploadURL = "https://www.loadviewer.com/api/integration/v1/upload"
downloadURL = "https://www.loadviewer.com/api/integration/v1/download"
apiEmail = "YOUR_Email" // Replace with your actual email
apiKey = "YOUR_API_KEY_HERE" // Replace with your actual API key
defaultMaxRetries = 3
defaultRateLimitWaitSeconds = 65
generalRetryDelaySeconds = 5
)
var jwtToken string
func getJWTTokenWithRetries(retryCount int) (string, error) {
if jwtToken != "" {
return jwtToken, nil
}
if retryCount > defaultMaxRetries {
return "", fmt.Errorf("ERROR: Failed to retrieve token after multiple retries")
}
requestBody := map[string]string{
"email": apiEmail,
"apikey": apiKey,
}
jsonBody, err := json.Marshal(requestBody)
if err != nil {
return "", fmt.Errorf("ERROR: Failed to marshal request body: %w", err)
}
resp, err := http.Post(tokenURL, "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
time.Sleep(time.Duration(generalRetryDelaySeconds) * time.Second)
return getJWTTokenWithRetries(retryCount + 1)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
if retryCount < defaultMaxRetries {
fmt.Printf("Rate limit hit on token endpoint (attempt %d of %d). Waiting %d seconds before retrying.\n", retryCount+1, defaultMaxRetries, defaultRateLimitWaitSeconds)
time.Sleep(time.Duration(defaultRateLimitWaitSeconds) * time.Second)
return getJWTTokenWithRetries(retryCount + 1)
} else {
return "", fmt.Errorf("ERROR: Failed to retrieve token after multiple rate limits")
}
} else if resp.StatusCode >= 300 {
bodyBytes, _ := io.ReadAll(resp.Body)
bodyString := string(bodyBytes)
if retryCount < defaultMaxRetries {
fmt.Printf("Error retrieving token (attempt %d of %d): %d - %s. Retrying in %d seconds.\n", retryCount+1, defaultMaxRetries, resp.StatusCode, bodyString, generalRetryDelaySeconds)
time.Sleep(time.Duration(generalRetryDelaySeconds) * time.Second)
return getJWTTokenWithRetries(retryCount + 1)
} else {
return "", fmt.Errorf("ERROR: Failed to retrieve token after %d retries: %d - %s", defaultMaxRetries, resp.StatusCode, bodyString)
}
}
bodyBytes, err = io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("ERROR: Failed to read response body: %w", err)
}
var result map[string]interface{}
err = json.Unmarshal(bodyBytes, &result)
if err != nil {
return "", fmt.Errorf("ERROR: Failed to unmarshal response body: %w", err)
}
if token, ok := result["token"].(string); ok {
jwtToken = token
return jwtToken, nil
} else {
return "", fmt.Errorf("ERROR: Failed to extract token from response: %s", string(bodyBytes))
}
}
func uploadToLoadViewerWithRetries(csvData string, retryCount int) (string, error) {
if jwtToken == "" {
token, err := getJWTTokenWithRetries(1)
if err != nil {
return "", fmt.Errorf("ERROR: Unable to retrieve JWT token for upload: %w", err)
}
jwtToken = token
}
if retryCount > defaultMaxRetries {
return "", fmt.Errorf("ERROR: Max upload retries exceeded")
}
req, err := http.NewRequest("POST", uploadURL, bytes.NewBufferString(csvData))
if err != nil {
return "", fmt.Errorf("ERROR: Failed to create upload request: %w", err)
}
req.Header.Set("Content-Type", "text/csv")
req.Header.Set("Authorization", "Bearer "+jwtToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
time.Sleep(time.Duration(generalRetryDelaySeconds) * time.Second)
return uploadToLoadViewerWithRetries(csvData, retryCount+1)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusGone {
fmt.Println("JWT token expired or invalid. Attempting to retrieve a new token.")
jwtToken = ""
token, err := getJWTTokenWithRetries(1)
if err != nil {
return "", fmt.Errorf("ERROR: Unable to retrieve a new JWT token after token expiration: %w", err)
}
jwtToken = token
return uploadToLoadViewerWithRetries(csvData, retryCount) // Retry with the new token
} else if resp.StatusCode == http.StatusTooManyRequests {
if retryCount < defaultMaxRetries {
fmt.Printf("Rate limit hit on upload endpoint (attempt %d of %d). Waiting %d seconds before retrying.\n", retryCount+1, defaultMaxRetries, defaultRateLimitWaitSeconds)
time.Sleep(time.Duration(defaultRateLimitWaitSeconds) * time.Second)
return uploadToLoadViewerWithRetries(csvData, retryCount+1)
} else {
return "", fmt.Errorf("ERROR: Upload failed after multiple rate limits")
}
} else if resp.StatusCode >= 300 && resp.StatusCode != http.StatusConflict {
bodyBytes, _ := io.ReadAll(resp.Body)
bodyString := string(bodyBytes)
if retryCount < defaultMaxRetries {
fmt.Printf("Error uploading data (attempt %d of %d): %d - %s. Retrying in %d seconds.\n", retryCount+1, defaultMaxRetries, resp.StatusCode, bodyString, generalRetryDelaySeconds)
time.Sleep(time.Duration(generalRetryDelaySeconds) * time.Second)
return uploadToLoadViewerWithRetries(csvData, retryCount+1)
} else {
return "", fmt.Errorf("ERROR: Upload failed after %d attempts. Status: %d - %s", defaultMaxRetries, resp.StatusCode, bodyString)
}
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("ERROR: Failed to read upload response body: %w", err)
}
return fmt.Sprintf("%d - %s", resp.StatusCode, string(bodyBytes)), nil
}
func getLoadViewerSuggestionsCsvWithRetries(requestBodyJSON string, retryCount int) (string, error) {
if jwtToken == "" {
token, err := getJWTTokenWithRetries(1)
if err != nil {
return "", fmt.Errorf("ERROR: Unable to retrieve JWT token for getting suggestions: %w", err)
}
jwtToken = token
}
if retryCount > defaultMaxRetries {
return "", fmt.Errorf("ERROR: Max suggestions retries exceeded")
}
req, err := http.NewRequest("POST", downloadURL, bytes.NewBufferString(requestBodyJSON))
if err != nil {
return "", fmt.Errorf("ERROR: Failed to create suggestions request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+jwtToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
time.Sleep(time.Duration(generalRetryDelaySeconds) * time.Second)
return getLoadViewerSuggestionsCsvWithRetries(requestBodyJSON, retryCount+1)
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
bodyString := string(bodyBytes)
fmt.Printf("POST Request to LoadViewer Suggestions URL: %s\nRequest Body:\n%s\nHTTP Status Code: %d\nResponse Text:\n%s\n", downloadURL, requestBodyJSON, resp.StatusCode, bodyString)
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusGone {
fmt.Println("JWT token expired or invalid. Attempting to retrieve a new token.")
jwtToken = ""
token, err := getJWTTokenWithRetries(1)
if err != nil {
return "", fmt.Errorf("ERROR: Unable to retrieve a new JWT token after token expiration: %w", err)
}
jwtToken = token
return getLoadViewerSuggestionsCsvWithRetries(requestBodyJSON, retryCount) // Retry with the new token
} else if resp.StatusCode == http.StatusTooManyRequests {
if retryCount < defaultMaxRetries {
fmt.Printf("Rate limit hit on suggestions endpoint (attempt %d of %d). Waiting %d seconds before retrying.\n", retryCount+1, defaultMaxRetries, defaultRateLimitWaitSeconds)
time.Sleep(time.Duration(defaultRateLimitWaitSeconds) * time.Second)
return getLoadViewerSuggestionsCsvWithRetries(requestBodyJSON, retryCount+1)
} else {
return "", fmt.Errorf("ERROR: Failed to retrieve suggestions after multiple rate limits")
}
} else if resp.StatusCode >= 300 {
if retryCount < defaultMaxRetries {
fmt.Printf("Error retrieving suggestions (attempt %d of %d): %d - %s. Retrying in %d seconds.\n", retryCount+1, defaultMaxRetries, resp.StatusCode, bodyString, generalRetryDelaySeconds)
time.Sleep(time.Duration(generalRetryDelaySeconds) * time.Second)
return getLoadViewerSuggestionsCsvWithRetries(requestBodyJSON, retryCount+1)
} else {
return "", fmt.Errorf("ERROR: Failed to retrieve suggestions after %d attempts. Status: %d - %s", defaultMaxRetries, resp.StatusCode, bodyString)
}
}
return bodyString, nil
}
func testSuggestions(shipmentReference string, solverEmail string) error {
if shipmentReference == "" {
fmt.Println("ERROR: Shipment Reference is required!")
return nil
}
emailRegex := regexp.MustCompile(`^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$`)
if solverEmail != "" && !emailRegex.MatchString(solverEmail) {
fmt.Println("ERROR: Invalid Solver Email format!")
return nil
}
if jwtToken == "" {
token, err := getJWTTokenWithRetries(1)
if err != nil {
fmt.Println("ERROR: Token Error or Server Not Reachable:", err)
return err
}
jwtToken = token
}
requestBody := map[string]string{
"reference": strings.ReplaceAll(shipmentReference, "\"", "\"\""),
"solverEmail": strings.ReplaceAll(solverEmail, "\"", "\"\""),
}
jsonBody, err := json.Marshal(requestBody)
if err != nil {
return fmt.Errorf("ERROR: Failed to marshal suggestions request body: %w", err)
}
strResponse, err := getLoadViewerSuggestionsCsvWithRetries(string(jsonBody), 1)
if err != nil {
fmt.Println("ERROR: retrieving data from LoadViewer:", err)
return err
}
fmt.Printf("Raw Suggestions Response:\n%s\n", strResponse)
arrLines := strings.Split(strResponse, "\n")
results := map[string]interface{}{
"status": map[string]string{},
"recommendations": []map[string]interface{}{},
}
if len(arrLines) > 0 {
for _, line := range arrLines {
arrFields := strings.Split(line, ",")
if len(arrFields) >= 1 {
recordType := strings.TrimSpace(arrFields[0])
switch recordType {
case "S": // Status Record
if len(arrFields) == 9 && strings.ToLower(strings.TrimSpace(arrFields[1])) == "d" {
statusInfo := results["status"].(map[string]string)
statusInfo["status"] = strings.TrimSpace(arrFields[2])
statusInfo["message"] = strings.TrimSpace(arrFields[3])
statusInfo["reference"] = strings.TrimSpace(arrFields[4])
statusInfo["solverEmail"] = strings.TrimSpace(arrFields[5])
statusInfo["multipleContainers"] = strings.TrimSpace(arrFields[6])
statusInfo["suggestExtra"] = strings.TrimSpace(arrFields[7])
} else if len(arrFields) == 9 && strings.ToLower(strings.TrimSpace(arrFields[1])) == "h" {
// Header row, can be ignored
} else if len(strings.TrimSpace(line)) > 0 {
fmt.Println("ERROR: Invalid Status record format:", line)
}
case "R": // Recommendation Record
if len(arrFields) == 16 && strings.ToLower(strings.TrimSpace(arrFields[1])) == "d" {
recommendation := map[string]interface{}{
"size": strings.TrimSpace(arrFields[2]),
"cbm": strings.TrimSpace(arrFields[3]),
"weight": strings.TrimSpace(arrFields[4]),
"noOfPackages": parseInt(strings.TrimSpace(arrFields[5])),
"isPacked": strings.TrimSpace(arrFields[6]),
"packedPackages": parseInt(strings.TrimSpace(arrFields[7])),
"suggestedIncrease": parseInt(strings.TrimSpace(arrFields[8])),
"suggestedDecrease": parseInt(strings.TrimSpace(arrFields[9])),
"erpId": strings.TrimSpace(arrFields[10]),
"skuNumber": strings.TrimSpace(arrFields[11]),
"description": strings.TrimSpace(arrFields[12]),
"po": strings.TrimSpace(arrFields[13]),
"shipper": strings.TrimSpace(arrFields[14]),
}
recommendations := results["recommendations"].([]map[string]interface{})
results["recommendations"] = append(recommendations, recommendation)
} else if len(arrFields) == 16 && strings.ToLower(strings.TrimSpace(arrFields[1])) == "h" {
// Header row, can be ignored
} else if len(strings.TrimSpace(line)) > 0 {
fmt.Println("ERROR: Invalid Recommendation record format:", line)
}
default:
if strings.TrimSpace(line) != "" {
fmt.Println("ERROR: Unknown record type:", recordType, "in line:", line)
}
}
}
}
} else {
fmt.Println("Empty suggestions response.")
}
if statusInfo, ok := results["status"].(map[string]string); ok {
fmt.Printf("Status: %s - %s\n", statusInfo["status"], statusInfo["message"])
fmt.Printf("Reference: %s\n", statusInfo["reference"])
fmt.Printf("Solver Email: %s\n", statusInfo["solverEmail"])
fmt.Printf("Multiple Containers: %s\n", statusInfo["multipleContainers"])
fmt.Printf("Extra Suggested: %s\n", statusInfo["suggestExtra"])
}
if recs, ok := results["recommendations"].([]map[string]interface{}); ok {
if len(recs) > 0 {
fmt.Println("--- Suggestions ---")
for _, recommendation := range recs {
fmt.Printf("Is Packed: %s, Size LxWxH: %s, CBM: %s, Weight Kg: %s, Packages: %d, Packed: %d, Increase: %d, Decrease: %d, ErpId: %s, SkuNumber: %s, Description: %s, PO#:%s, Division: %s\n",
recommendation["isPacked"],
recommendation["size"],
recommendation["cbm"],
recommendation["weight"],
recommendation["noOfPackages"],
recommendation["packedPackages"],
recommendation["suggestedIncrease"],
recommendation["suggestedDecrease"],
recommendation["erpId"],
recommendation["skuNumber"],
recommendation["description"],
recommendation["po"],
recommendation["shipper"],
)
// --- Your ERP Processing Logic for Suggestions Here ---
}
} else {
fmt.Printf("No suggestions found for reference: %s\n", shipmentReference)
}
} else {
fmt.Println("Error parsing recommendations.")
}
return nil
}
func removeComma(value string) string {
return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(value, ",", ""), "\r\n", " "), "\n", " ")
}
func formatDecimal3(value float64) string {
return strings.TrimSuffix(strings.TrimSuffix(fmt.Sprintf("%.3f", value), "0"), ".")
}
func generateLoadViewerCsv() string {
var csvLines []string
// Shipment Data
shipmentData := map[string]interface{}{
"reference": "SANDBOX-01",
"hangingAllowed": true,
"useMultipleContainers": false,
"uniqueErpId": false,
"suggestExtra": false,
}
csvLines = append(csvLines, "S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra")
csvLines = append(csvLines, fmt.Sprintf("S,D,%s,%t,%t,%t,%t",
removeComma(shipmentData["reference"].(string)),
shipmentData["hangingAllowed"].(bool),
shipmentData["useMultipleContainers"].(bool),
shipmentData["uniqueErpId"].(bool),
shipmentData["suggestExtra"].(bool),
))
// Container Data
containerData := map[string]interface{}{
"lengthMm": 5867,
"widthMm": 2352,
"heightMm": 2393,
"maxWeightKg": 27200.0,
"backMarginMm": 0,
"isRefrigerated": false,
"maxUseOne": false,
"lclBreakEvenCBM": 0.0,
}
csvLines = append(csvLines, "B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM")
csvLines = append(csvLines, fmt.Sprintf("B,D,%d,%d,%d,%s,%d,%t,%t,%s",
containerData["lengthMm"].(int),
containerData["widthMm"].(int),
containerData["heightMm"].(int),
formatDecimal3(containerData["maxWeightKg"].(float64)),
containerData["backMarginMm"].(int),
containerData["isRefrigerated"].(bool),
containerData["maxUseOne"].(bool),
formatDecimal3(containerData["lclBreakEvenCBM"].(float64)),
))
// Cargo Data
cargoData := map[string]interface{}{
"color": "#008000",
"lengthMm": 700,
"widthMm": 500,
"heightMm": 300,
"weightKg": 91.5,
"cartons": 300,
"additionalCartons": 0,
"placement": 0,
"verticalRotationAllowed": true,
"skuPerCarton": 1,
"emptyPalletHeightMm": 0,
"trayCapMm": 0,
"trayHeightMm": 0,
"marginLength": 0,
"marginWidth": 0,
"marginHeight": 0,
"nameOfSet": "",
"cartonRatioInSet": 0,
"erpId": "",
"skuNumber": "",
"description": "",
"destination": 0,
"po": "",
"shipper": "",
}
csvLines = append(csvLines, "C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper")
csvLines = append(csvLines, fmt.Sprintf("C,D,%s,%d,%d,%d,%s,%d,%d,%d,%t,%d,%d,%d,%d,%d,%d,%d,%s,%d,%s,%s,%s,%d,%s,%s",
removeComma(cargoData["color"].(string)),
cargoData["lengthMm"].(int),
cargoData["widthMm"].(int),
cargoData["heightMm"].(int),
formatDecimal3(cargoData["weightKg"].(float64)),
cargoData["cartons"].(int),
cargoData["additionalCartons"].(int),
cargoData["placement"].(int),
cargoData["verticalRotationAllowed"].(bool),
cargoData["skuPerCarton"].(int),
cargoData["emptyPalletHeightMm"].(int),
cargoData["trayCapMm"].(int),
cargoData["trayHeightMm"].(int),
cargoData["marginLength"].(int),
cargoData["marginWidth"].(int),
cargoData["marginHeight"].(int),
removeComma(cargoData["nameOfSet"].(string)),
cargoData["cartonRatioInSet"].(int),
removeComma(cargoData["erpId"].(string)),
removeComma(cargoData["skuNumber"].(string)),
removeComma(cargoData["description"].(string)),
cargoData["destination"].(int),
removeComma(cargoData["po"].(string)),
removeComma(cargoData["shipper"].(string)),
))
return strings.Join(csvLines, "\n")
}
func parseInt(s string) int {
i, _ := strconv.Atoi(s) // Ignoring error for simplicity, assuming valid integer
return i
}
func main() {
csvData := generateLoadViewerCsv()
uploadResult, err := uploadToLoadViewerWithRetries(csvData, 1)
if err != nil {
fmt.Println("Upload Result Error:", err)
} else {
fmt.Println("Upload Result:", uploadResult)
}
err = testSuggestions("SANDBOX-01", "test@example.com")
if err != nil {
fmt.Println("Test Suggestions Error:", err)
}
}
# Define constants
token_url <- "https://www.loadviewer.com/api/auth/jwt/token"
upload_url <- "https://www.loadviewer.com/api/integration/v1/upload"
download_url <- "https://www.loadviewer.com/api/integration/v1/download"
api_email <- "YOUR_Email" # Replace with your actual email
api_key <- "YOUR_API_KEY_HERE" # Replace with your actual API key
jwt_token <- NULL
default_max_retries <- 3
default_rate_limit_wait_seconds <- 65
general_retry_delay_seconds <- 5
# Function to get JWT token with retries
get_jwt_token_with_retries <- function(retry_count = 0) {
if (!is.null(jwt_token)) {
return(jwt_token)
}
if (retry_count > default_max_retries) {
stop("ERROR: Failed to retrieve token after multiple retries.")
}
post_data <- list(email = api_email, apikey = api_key)
response <- tryCatch(
httr::POST(token_url, body = post_data, encode = "json"),
error = function(e) {
Sys.sleep(general_retry_delay_seconds)
get_jwt_token_with_retries(retry_count + 1)
}
)
if (httr::status_code(response) == 429) {
if (retry_count < default_max_retries) {
cat(sprintf("Rate limit hit on token endpoint (attempt %d < %d). Waiting %d seconds before retrying.\n",
retry_count + 1, default_max_retries, default_rate_limit_wait_seconds))
Sys.sleep(default_rate_limit_wait_seconds)
return(get_jwt_token_with_retries(retry_count + 1))
} else {
stop("ERROR: Failed to retrieve token after multiple rate limits.")
}
} else if (httr::status_code(response) >= 300) {
body <- httr::content(response, "text")
if (retry_count < default_max_retries) {
cat(sprintf("Error retrieving token (attempt %d < %d): %d - %s. Retrying in %d seconds.\n",
retry_count + 1, default_max_retries, httr::status_code(response), body, general_retry_delay_seconds))
Sys.sleep(general_retry_delay_seconds)
return(get_jwt_token_with_retries(retry_count + 1))
} else {
stop(sprintf("ERROR: Failed to retrieve token after %d retries: %d - %s", default_max_retries, httr::status_code(response), body))
}
}
result <- httr::content(response, "parsed")
if (!is.null(result$token)) {
jwt_token <<- result$token
return(jwt_token)
} else {
stop(sprintf("ERROR: Failed to extract token from response: %s", httr::content(response, "text")))
}
}
# Function to upload to LoadViewer with retries
upload_to_loadviewer_with_retries <- function(csv_data, retry_count = 0) {
if (is.null(jwt_token)) {
jwt_token <<- get_jwt_token_with_retries()
if (is.null(jwt_token)) {
stop("ERROR: Unable to retrieve JWT token for upload.")
}
}
if (retry_count > default_max_retries) {
stop("ERROR: Max upload retries exceeded.")
}
response <- tryCatch(
httr::POST(upload_url,
body = csv_data,
httr::add_headers("Content-Type" = "text/csv",
"Authorization" = paste("Bearer", jwt_token))),
error = function(e) {
Sys.sleep(general_retry_delay_seconds)
upload_to_loadviewer_with_retries(csv_data, retry_count + 1)
}
)
if (httr::status_code(response) %in% c(401, 410)) {
cat("JWT token expired or invalid. Attempting to retrieve a new token.\n")
jwt_token <<- NULL
jwt_token <<- get_jwt_token_with_retries()
if (is.null(jwt_token)) {
stop("ERROR: Unable to retrieve a new JWT token after token expiration.")
}
return(upload_to_loadviewer_with_retries(csv_data, retry_count)) # Retry with new token
} else if (httr::status_code(response) == 429) {
if (retry_count < default_max_retries) {
cat(sprintf("Rate limit hit on upload endpoint (attempt %d < %d). Waiting %d seconds before retrying.\n",
retry_count + 1, default_max_retries, default_rate_limit_wait_seconds))
Sys.sleep(default_rate_limit_wait_seconds)
return(upload_to_loadviewer_with_retries(csv_data, retry_count + 1))
} else {
stop("ERROR: Upload failed after multiple rate limits.")
}
} else if (httr::status_code(response) >= 300 && httr::status_code(response) != 409) {
body <- httr::content(response, "text")
if (retry_count < default_max_retries) {
cat(sprintf("Error uploading data (attempt %d < %d): %d - %s. Retrying in %d seconds.\n",
retry_count + 1, default_max_retries, httr::status_code(response), body, general_retry_delay_seconds))
Sys.sleep(general_retry_delay_seconds)
return(upload_to_loadviewer_with_retries(csv_data, retry_count + 1))
} else {
stop(sprintf("ERROR: Upload failed after %d attempts. Status: %d - %s", default_max_retries, httr::status_code(response), body))
}
}
return(sprintf("%d - %s", httr::status_code(response), httr::content(response, "text")))
}
# Function to get LoadViewer suggestions CSV with retries
get_loadviewer_suggestions_csv_with_retries <- function(request_body_json, retry_count = 0) {
if (is.null(jwt_token)) {
jwt_token <<- get_jwt_token_with_retries()
if (is.null(jwt_token)) {
stop("ERROR: Unable to retrieve JWT token for getting suggestions.")
}
}
if (retry_count > default_max_retries) {
stop("ERROR: Max suggestions retries exceeded.")
}
response <- tryCatch(
httr::POST(download_url,
body = request_body_json,
httr::add_headers("Content-Type" = "application/json",
"Authorization" = paste("Bearer", jwt_token))),
error = function(e) {
Sys.sleep(general_retry_delay_seconds)
get_loadviewer_suggestions_csv_with_retries(request_body_json, retry_count + 1)
}
)
cat(sprintf("POST Request to LoadViewer Suggestions URL: %s\nRequest Body:\n%s\nHTTP Status Code: %d\nResponse Text:\n%s\n",
download_url, request_body_json, httr::status_code(response), httr::content(response, "text")))
if (httr::status_code(response) %in% c(401, 410)) {
cat("JWT token expired or invalid. Attempting to retrieve a new token.\n")
jwt_token <<- NULL
jwt_token <<- get_jwt_token_with_retries()
if (is.null(jwt_token)) {
stop("ERROR: Unable to retrieve a new JWT token after token expiration.")
}
return(get_loadviewer_suggestions_csv_with_retries(request_body_json, retry_count)) # Retry with new token
} else if (httr::status_code(response) == 429) {
if (retry_count < default_max_retries) {
cat(sprintf("Rate limit hit on suggestions endpoint (attempt %d < %d). Waiting %d seconds before retrying.\n",
retry_count + 1, default_max_retries, default_rate_limit_wait_seconds))
Sys.sleep(default_rate_limit_wait_seconds)
return(get_loadviewer_suggestions_csv_with_retries(request_body_json, retry_count + 1))
} else {
stop("ERROR: Failed to retrieve suggestions after multiple rate limits.")
}
} else if (httr::status_code(response) >= 300) {
body <- httr::content(response, "text")
if (retry_count < default_max_retries) {
cat(sprintf("Error retrieving suggestions (attempt %d < %d): %d - %s. Retrying in %d seconds.\n",
retry_count + 1, default_max_retries, httr::status_code(response), body, general_retry_delay_seconds))
Sys.sleep(general_retry_delay_seconds)
return(get_loadviewer_suggestions_csv_with_retries(request_body_json, retry_count + 1))
} else {
stop(sprintf("ERROR: Failed to retrieve suggestions after %d attempts. Status: %d - %s", default_max_retries, httr::status_code(response), body))
}
}
return(httr::content(response, "text"))
}
# Function to test suggestions
test_suggestions <- function(shipment_reference, solver_email) {
if (nchar(shipment_reference) == 0) {
cat("ERROR: Shipment Reference is required!\n")
return()
}
if (nchar(solver_email) > 0 && !grepl("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$", solver_email)) {
cat("ERROR: Invalid Solver Email format!\n")
return()
}
if (is.null(jwt_token)) {
jwt_token <<- get_jwt_token_with_retries()
if (is.null(jwt_token)) {
cat("ERROR: Token Error or Server Not Reachable.\n")
return()
}
}
request_body <- jsonlite::toJSON(list(reference = gsub("\"", "\"\"", shipment_reference),
solverEmail = gsub("\"", "\"\"", solver_email)), auto_unbox = TRUE)
str_response <- get_loadviewer_suggestions_csv_with_retries(request_body)
if (startsWith(str_response, "ERROR:")) {
cat(sprintf("ERROR: retrieving data from LoadViewer: %s\n", str_response))
return()
}
cat(sprintf("Raw Suggestions Response:\n%s\n", str_response))
arr_lines <- strsplit(str_response, "\n")[[1]]
results <- list(status = list(), recommendations = list())
if (length(arr_lines) > 0) {
for (line in arr_lines) {
arr_fields <- strsplit(line, ",")[[1]]
if (length(arr_fields) >= 1) {
record_type <- trimws(arr_fields[1])
if (trimws(arr_fields[1]) == 'S') { # Status Record
if (length(arr_fields) == 9 && tolower(trimws(arr_fields[2])) == 'd') {
results$status$status <- trimws(arr_fields[3])
results$status$message <- trimws(arr_fields[4])
results$status$reference <- trimws(arr_fields[5])
results$status$solverEmail <- trimws(arr_fields[6])
results$status$multipleContainers <- trimws(arr_fields[7])
results$status$suggestExtra <- trimws(arr_fields[8])
} else if (length(arr_fields) == 9 && tolower(trimws(arr_fields[2])) == 'h') {
# Header row, can be ignored
} else if (nchar(trimws(line)) > 0) {
cat(sprintf("ERROR: Invalid Status record format: %s\n", line))
}
} else if (trimws(arr_fields[1]) == 'R') { # Recommendation Record
if (length(arr_fields) == 16 && tolower(trimws(arr_fields[2])) == 'd') {
recommendation <- list(
size = trimws(arr_fields[3]),
cbm = trimws(arr_fields[4]),
weight = trimws(arr_fields[5]),
noOfPackages = as.integer(trimws(arr_fields[6])),
isPacked = trimws(arr_fields[7]),
packedPackages = as.integer(trimws(arr_fields[8])),
suggestedIncrease = as.integer(trimws(arr_fields[9])),
suggestedDecrease = as.integer(trimws(arr_fields[10])),
erpId = trimws(arr_fields[11]),
skuNumber = trimws(arr_fields[12]),
description = trimws(arr_fields[13]),
po = trimws(arr_fields[14]),
shipper = trimws(arr_fields[15])
)
results$recommendations <- append(results$recommendations, list(recommendation))
} else if (length(arr_fields) == 16 && tolower(trimws(arr_fields[2])) == 'h') {
# Header row, can be ignored
} else if (nchar(trimws(line)) > 0) {
cat(sprintf("ERROR: Invalid Recommendation record format: %s\n", line))
}
} else if (nchar(trimws(line)) > 0) {
cat(sprintf("ERROR: Unknown record type: %s in line: %s\n", record_type, line))
}
}
}
} else {
cat("Empty suggestions response.\n")
}
if (length(results$status) > 0) {
cat(sprintf("Status: %s - %s\n", results$status$status, results$status$message))
cat(sprintf("Reference: %s\n", results$status$reference))
cat(sprintf("Solver Email: %s\n", results$status$solverEmail))
cat(sprintf("Multiple Containers: %s\n", results$status$multipleContainers))
cat(sprintf("Extra Suggested: %s\n", results$status$suggestExtra))
}
if (length(results$recommendations) > 0) {
cat("--- Suggestions ---\n")
for (recommendation in results$recommendations) {
cat(sprintf("Is Packed: %s, Size LxWxH: %s, CBM: %s, Weight Kg: %s, Packages: %d, Packed: %d, Increase: %d, Decrease: %d, ErpId: %s, SkuNumber: %s, Description: %s, PO#: %s, Division: %s \n",
recommendation$isPacked,
recommendation$size,
recommendation$cbm,
recommendation$weight,
recommendation$noOfPackages,
recommendation$packedPackages,
recommendation$suggestedIncrease,
recommendation$suggestedDecrease,
recommendation$erpId,
recommendation$skuNumber,
recommendation$description,
recommendation$po,
recommendation$shipper
))
# --- Your ERP Processing Logic for Suggestions Here ---
}
} else {
cat(sprintf("No suggestions found for reference: %s\n", shipment_reference))
}
}
# Function to remove comma
remove_comma <- function(value) {
gsub(",", "", gsub("[\r\n]", " ", value))
}
# Function to format decimal with 3 places
format_decimal_3 <- function(value) {
sprintf("%.3f", value)
}
# Function to generate LoadViewer CSV data (example)
generate_loadviewer_csv <- function() {
shipment_data <- data.frame(
record_type = "S",
header_detail = c("H", "D"),
reference = c(NA, remove_comma("SANDBOX-01")),
hangingAllowed = c(NA, TRUE),
useMultipleContainers = c(NA, FALSE),
uniqueErpId = c(NA, FALSE),
suggestExtra = c(NA, FALSE)
)
container_data <- data.frame(
record_type = "B",
header_detail = c("H", "D"),
lengthMm = c(NA, 5867),
widthMm = c(NA, 2352),
heightMm = c(NA, 2393),
maxWeightKg = c(NA, format_decimal_3(27200.0)),
backMarginMm = c(NA, 0),
isRefrigerated = c(NA, FALSE),
maxUseOne = c(NA, FALSE),
lclBreakEvenCBM = c(NA, format_decimal_3(0.0))
)
cargo_data <- data.frame(
record_type = "C",
header_detail = c("H", "D"),
color = c(NA, remove_comma("#008000")),
lengthMm = c(NA, 700),
widthMm = c(NA, 500),
heightMm = c(NA, 300),
weightKg = c(NA, format_decimal_3(91.5)),
cartons = c(NA, 300),
additionalCartons = c(NA, 0),
placement = c(NA, 0),
verticalRotationAllowed = c(NA, TRUE),
skuPerCarton = c(NA, 1),
emptyPalletHeightMm = c(NA, 0),
trayCapMm = c(NA, 0),
trayHeightMm = c(NA, 0),
marginLenghtMm = c(NA, 0),
marginWidthMm = c(NA, 0),
marginHeightMm = c(NA, 0),
nameOfSet = c(NA, remove_comma("")),
cartonRatioInSet = c(NA, 0),
erpId = c(NA, remove_comma("")),
skuNumber = c(NA, remove_comma("")),
description = c(NA, remove_comma("")),
destination = c(NA, 0),
po = c(NA, remove_comma("")),
shipper = c(NA, remove_comma(""))
)
shipment_csv <- paste0(apply(shipment_data, 1, paste, collapse = ","), collapse = "\n")
container_csv <- paste0(apply(container_data, 1, paste, collapse = ","), collapse = "\n")
cargo_csv <- paste0(apply(cargo_data, 1, paste, collapse = ","), collapse = "\n")
return(paste(shipment_csv, container_csv, cargo_csv, sep = "\n"))
}
# Main execution
if (interactive()) {
csv_data <- generate_loadviewer_csv()
upload_result <- upload_to_loadviewer_with_retries(csv_data)
cat(sprintf("Upload Result: %s\n", upload_result))
test_suggestions("SANDBOX-01", "test@example.com")
}
<?php
$token_url = "https://www.loadviewer.com/api/auth/jwt/token";
$upload_url = "https://www.loadviewer.com/api/integration/v1/upload";
$download_url = "https://www.loadviewer.com/api/integration/v1/download";
$api_email = "YOUR_Email"; // Replace with your actual email
$api_key = "YOUR_API_KEY_HERE"; // Replace with your actual API key
$jwt_token = null;
const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_RATE_LIMIT_WAIT_SECONDS = 65;
const GENERAL_RETRY_DELAY_SECONDS = 5;
function get_jwt_token_with_retries($retry_count = 0) {
global $jwt_token, $token_url, $api_email, $api_key;
if ($jwt_token !== null) {
return $jwt_token;
}
if ($retry_count > DEFAULT_MAX_RETRIES) {
error_log("ERROR: Failed to retrieve token after multiple retries.");
return null;
}
$post_data = json_encode(['email' => $api_email, 'apikey' => $api_key]);
$ch = curl_init($token_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($http_code === 429) {
if ($retry_count < DEFAULT_MAX_RETRIES) {
$retry_count++;
error_log("Rate limit hit on token endpoint (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "). Waiting " . DEFAULT_RATE_LIMIT_WAIT_SECONDS . " seconds before retrying.");
sleep(DEFAULT_RATE_LIMIT_WAIT_SECONDS);
curl_close($ch);
return get_jwt_token_with_retries($retry_count);
} else {
error_log("ERROR: Failed to retrieve token after multiple rate limits.");
curl_close($ch);
return null;
}
} elseif ($http_code >= 300) {
if ($retry_count < DEFAULT_MAX_RETRIES) {
$retry_count++;
error_log("Error retrieving token (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "): {$http_code} - {$response}. Retrying in " . GENERAL_RETRY_DELAY_SECONDS . " seconds.");
sleep(GENERAL_RETRY_DELAY_SECONDS);
curl_close($ch);
return get_jwt_token_with_retries($retry_count);
} else {
error_log("ERROR: Failed to retrieve token after " . DEFAULT_MAX_RETRIES . " retries: {$http_code} - {$response}");
curl_close($ch);
return null;
}
}
if ($response === false) {
if ($retry_count < DEFAULT_MAX_RETRIES) {
$retry_count++;
error_log("ERROR: cURL error while retrieving token (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "): " . curl_error($ch) . ". Retrying in " . GENERAL_RETRY_DELAY_SECONDS . " seconds.");
sleep(GENERAL_RETRY_DELAY_SECONDS);
curl_close($ch);
return get_jwt_token_with_retries($retry_count);
} else {
error_log("ERROR: cURL error while retrieving token after " . DEFAULT_MAX_RETRIES . " retries: " . curl_error($ch));
curl_close($ch);
return null;
}
}
curl_close($ch);
$result = json_decode($response, true);
if (isset($result['token'])) {
$jwt_token = $result['token'];
return $jwt_token;
} else {
error_log("ERROR: Failed to extract token from response: {$response}");
return null;
}
}
function upload_to_loadviewer_with_retries($csv_data, $retry_count = 0) {
global $jwt_token, $upload_url;
if ($jwt_token === null) {
$jwt_token = get_jwt_token_with_retries();
if ($jwt_token === null) {
return "ERROR: Unable to retrieve JWT token for upload.";
}
}
if ($retry_count > DEFAULT_MAX_RETRIES) {
return "ERROR: Max upload retries exceeded.";
}
$ch = curl_init($upload_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $csv_data);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: text/csv',
'Authorization: Bearer ' . $jwt_token,
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($http_code === 401 || $http_code === 410) {
error_log("JWT token expired or invalid. Attempting to retrieve a new token.");
$jwt_token = null;
$jwt_token = get_jwt_token_with_retries();
if ($jwt_token === null) {
curl_close($ch);
return "ERROR: Unable to retrieve a new JWT token after token expiration.";
}
curl_close($ch);
return upload_to_loadviewer_with_retries($csv_data, $retry_count); // Retry with the new token
} elseif ($http_code === 429) {
if ($retry_count < DEFAULT_MAX_RETRIES) {
$retry_count++;
error_log("Rate limit hit on upload endpoint (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "). Waiting " . DEFAULT_RATE_LIMIT_WAIT_SECONDS . " seconds before retrying.");
sleep(DEFAULT_RATE_LIMIT_WAIT_SECONDS);
curl_close($ch);
return upload_to_loadviewer_with_retries($csv_data, $retry_count);
} else {
curl_close($ch);
return "ERROR: Upload failed after multiple rate limits.";
}
} elseif ($http_code >= 300 && $http_code !== 409) {
if ($retry_count < DEFAULT_MAX_RETRIES) {
$retry_count++;
error_log("Error uploading data (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "): {$http_code} - {$response}. Retrying in " . GENERAL_RETRY_DELAY_SECONDS . " seconds.");
sleep(GENERAL_RETRY_DELAY_SECONDS);
curl_close($ch);
return upload_to_loadviewer_with_retries($csv_data, $retry_count);
} else {
curl_close($ch);
return "ERROR: Upload failed after " . DEFAULT_MAX_RETRIES . " attempts. Status: {$http_code} - {$response}";
}
}
if ($response === false) {
if ($retry_count < DEFAULT_MAX_RETRIES) {
$retry_count++;
error_log("ERROR: cURL error during upload (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "): " . curl_error($ch) . ". Retrying in " . GENERAL_RETRY_DELAY_SECONDS . " seconds.");
sleep(GENERAL_RETRY_DELAY_SECONDS);
curl_close($ch);
return upload_to_loadviewer_with_retries($csv_data, $retry_count);
} else {
curl_close($ch);
return "ERROR: cURL error during upload after " . DEFAULT_MAX_RETRIES . " retries: " . curl_error($ch);
}
}
curl_close($ch);
return "{$http_code} - {$response}";
}
function get_loadviewer_suggestions_csv_with_retries($request_body_json, $retry_count = 0) {
global $jwt_token, $download_url;
if ($jwt_token === null) {
$jwt_token = get_jwt_token_with_retries();
if ($jwt_token === null) {
return "ERROR: Unable to retrieve JWT token for getting suggestions.";
}
}
if ($retry_count > DEFAULT_MAX_RETRIES) {
return "ERROR: Max suggestions retries exceeded.";
}
$ch = curl_init($download_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $request_body_json);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $jwt_token,
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
error_log("POST Request to LoadViewer Suggestions URL: {$download_url}\nRequest Body:\n{$request_body_json}\nHTTP Status Code: {$http_code}\nResponse Text:\n{$response}");
if ($http_code === 401 || $http_code === 410) {
error_log("JWT token expired or invalid. Attempting to retrieve a new token.");
$jwt_token = null;
$jwt_token = get_jwt_token_with_retries();
if ($jwt_token === null) {
curl_close($ch);
return "ERROR: Unable to retrieve a new JWT token after token expiration.";
}
curl_close($ch);
return get_loadviewer_suggestions_csv_with_retries($request_body_json, $retry_count); // Retry with the new token
} elseif ($http_code === 429) {
if ($retry_count < DEFAULT_MAX_RETRIES) {
$retry_count++;
error_log("Rate limit hit on suggestions endpoint (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "). Waiting " . DEFAULT_RATE_LIMIT_WAIT_SECONDS . " seconds before retrying.");
sleep(DEFAULT_RATE_LIMIT_WAIT_SECONDS);
curl_close($ch);
return get_loadviewer_suggestions_csv_with_retries($request_body_json, $retry_count);
} else {
curl_close($ch);
return "ERROR: Failed to retrieve suggestions after multiple rate limits.";
}
} elseif ($http_code >= 300) {
if ($retry_count < DEFAULT_MAX_RETRIES) {
$retry_count++;
error_log("Error retrieving suggestions (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "): {$http_code} - {$response}. Retrying in " . GENERAL_RETRY_DELAY_SECONDS . " seconds.");
sleep(GENERAL_RETRY_DELAY_SECONDS);
curl_close($ch);
return get_loadviewer_suggestions_csv_with_retries($request_body_json, $retry_count);
} else {
curl_close($ch);
return "ERROR: Failed to retrieve suggestions after " . DEFAULT_MAX_RETRIES . " attempts. Status: {$http_code} - {$response}";
}
}
if ($response === false) {
if ($retry_count < DEFAULT_MAX_RETRIES) {
$retry_count++;
error_log("ERROR: cURL error while retrieving suggestions (attempt {$retry_count} of " . DEFAULT_MAX_RETRIES . "): " . curl_error($ch) . ". Retrying in " . GENERAL_RETRY_DELAY_SECONDS . " seconds.");
sleep(GENERAL_RETRY_DELAY_SECONDS);
curl_close($ch);
return get_loadviewer_suggestions_csv_with_retries($request_body_json, $retry_count);
} else {
curl_close($ch);
return "ERROR: cURL error while retrieving suggestions after " . DEFAULT_MAX_RETRIES . " retries: " . curl_error($ch);
}
}
curl_close($ch);
return $response;
}
function test_suggestions($shipment_reference, $solver_email) {
if (empty($shipment_reference)) {
error_log("ERROR: Shipment Reference is required!");
return;
}
if (!empty($solver_email) && !filter_var($solver_email, FILTER_VALIDATE_EMAIL)) {
error_log("ERROR: Invalid Solver Email format!");
return;
}
global $jwt_token;
if ($jwt_token === null) {
$jwt_token = get_jwt_token_with_retries();
if ($jwt_token === null) {
error_log("ERROR: Token Error or Server Not Reachable.");
return;
}
}
$request_body = json_encode(['reference' => str_replace('"', '""', $shipment_reference), 'solverEmail' => str_replace('"', '""', $solver_email)]);
$str_response = get_loadviewer_suggestions_csv_with_retries($request_body);
if (strpos($str_response, "ERROR:") === 0) {
error_log("ERROR: retrieving data from LoadViewer: " . $str_response);
return;
}
echo "Raw Suggestions Response:\n" . $str_response . "\n";
$arr_lines = preg_split('/\r\n|\r|\n/', $str_response);
$results = [];
$status_info = [];
$recommendations = [];
$results['status'] = &$status_info;
$results['recommendations'] = &$recommendations;
if (count($arr_lines) > 0) {
foreach ($arr_lines as $line) {
$arr_fields = explode(',', $line);
if (count($arr_fields) >= 1) {
$record_type = trim($arr_fields[0]);
if ($record_type === 'S') { // Status Record
if (count($arr_fields) === 9 && strtolower(trim($arr_fields[1])) === 'd') {
$status_info['status'] = trim($arr_fields[2]);
$status_info['message'] = trim($arr_fields[3]);
$status_info['reference'] = trim($arr_fields[4]);
$status_info['solverEmail'] = trim($arr_fields[5]);
$status_info['multipleContainers'] = trim($arr_fields[6]);
$status_info['suggestExtra'] = trim($arr_fields[7]);
} elseif (count($arr_fields) === 9 && strtolower(trim($arr_fields[1])) === 'h') {
// Header row, can be ignored
} else {
error_log("ERROR: Invalid Status record format: " . $line);
}
} elseif ($record_type === 'R') { // Recommendation Record
if (count($arr_fields) === 16 && strtolower(trim($arr_fields[1])) === 'd') {
$recommendation = [];
$recommendation['size'] = trim($arr_fields[2]);
$recommendation['cbm'] = trim($arr_fields[3]);
$recommendation['weight'] = trim($arr_fields[4]);
$recommendation['noOfPackages'] = intval(trim($arr_fields[5]));
$recommendation['isPacked'] = trim($arr_fields[6]);
$recommendation['packedPackages'] = intval(trim($arr_fields[7]));
$recommendation['suggestedIncrease'] = intval(trim($arr_fields[8]));
$recommendation['suggestedDecrease'] = intval(trim($arr_fields[9]));
$recommendation['erpId'] = trim($arr_fields[10]);
$recommendation['skuNumber'] = trim($arr_fields[11]);
$recommendation['description'] = trim($arr_fields[12]);
$recommendation['po'] = trim($arr_fields[13]);
$recommendation['shipper'] = trim($arr_fields[14]);
$recommendations[] = $recommendation;
} elseif (count($arr_fields) === 16 && strtolower(trim($arr_fields[1])) === 'h') {// Header row, can be ignored.
} else {
error_log("ERROR: Invalid Recommendation record format: " . $line);
}
} elseif (!empty($record_type)) {
error_log("ERROR: Unknown record type: " . $record_type . " in line: " . $line);
}
}
}
} else {
echo "Empty suggestions response.\n";
}
if (isset($results['status'])) {
$status = $results['status'];
echo "Status: " . (isset($status['status']) ? $status['status'] : '') . " - " . (isset($status['message']) ? $status['message'] : '') . "\n";
echo "Reference: " . (isset($status['reference']) ? $status['reference'] : '') . "\n";
echo "Solver Email: " . (isset($status['solverEmail']) ? $status['solverEmail'] : '') . "\n";
echo "Multiple Containers: " . (isset($status['multipleContainers']) ? $status['multipleContainers'] : '') . "\n";
echo "Extra Suggested: " . (isset($status['suggestExtra']) ? $status['suggestExtra'] : '') . "\n";
if (isset($results['recommendations']) && count($results['recommendations']) > 0) {
echo "--- Suggestions ---\n";
foreach ($results['recommendations'] as $recommendation) {
echo "Is Packed: " . (isset($recommendation['isPacked']) ? $recommendation['isPacked'] : '') . ", ";
echo "Size LxWxH: " . (isset($recommendation['size']) ? $recommendation['size'] : '') . ", ";
echo "CBM: " . (isset($recommendation['cbm']) ? $recommendation['cbm'] : '') . ", ";
echo "Weight Kg: " . (isset($recommendation['weight']) ? $recommendation['weight'] : '') . ", ";
echo "Packages: " . (isset($recommendation['noOfPackages']) ? $recommendation['noOfPackages'] : '') . ", ";
echo "Packed: " . (isset($recommendation['packedPackages']) ? $recommendation['packedPackages'] : '') . ", ";
echo "Increase: " . (isset($recommendation['suggestedIncrease']) ? $recommendation['suggestedIncrease'] : '') . ", ";
echo "Decrease: " . (isset($recommendation['suggestedDecrease']) ? $recommendation['suggestedDecrease'] : '') . ", ";
echo "ErpId: " . (isset($recommendation['erpId']) ? $recommendation['erpId'] : '') . ", ";
echo "SkuNumber: " . (isset($recommendation['skuNumber']) ? $recommendation['skuNumber'] : '') . ", ";
echo "Description: " . (isset($recommendation['description']) ? $recommendation['description'] : '') . ", ";
echo "PO#: " . (isset($recommendation['po']) ? $recommendation['po'] : '') . ", ";
echo "Division: " . (isset($recommendation['shipper']) ? $recommendation['shipper'] : '') . "\n";
// --- Your ERP Processing Logic for Suggestions Here ---
}
} else {
echo "No suggestions found for reference: " . $shipment_reference . "\n";
}
} else {
error_log("ERROR: retrieving or parsing suggestions for reference: " . $shipment_reference);
}
}
function remove_comma($value) {
return ($value !== null) ? str_replace(["\r\n", "\r", "\n", ","], [" ", " ", " ", ""], $value) : '';
}
function format_decimal_3($value) {
return rtrim(rtrim(sprintf('%.3f', $value), '0'), '.');
}
function generate_loadviewer_csv() {
$rs_shipment = [];
$shipment_data = [
'reference' => 'SANDBOX-01',
'hangingAllowed' => true,
'useMultipleContainers' => false,
'uniqueErpId' => false,
'suggestExtra' => false,
];
$rs_shipment[] = $shipment_data;
$rs_container = [];
$container_data = [
'lengthMm' => 5867,
'widthMm' => 2352,
'heightMm' => 2393,
'maxWeightKg' => 27200.0,
'backMarginMm' => 0,
'isRefrigerated' => false,
'maxUseOne' => false,
'lclBreakEvenCBM' => 0.0,
];
$rs_container[] = $container_data;
$rs_cargo = [];
$cargo_data = [
'color' => '#008000',
'lengthMm' => 700,
'widthMm' => 500,
'heightMm' => 300,
'weightKg' => 91.5,
'cartons' => 300,
'additionalCartons' => 0,
'placement' => 0,
'verticalRotationAllowed' => true,
'skuPerCarton' => 1,
'emptyPalletHeightMm' => 0,
'trayCapMm' => 0,
'trayHeightMm' => 0,
'marginLength' => 0,
'marginWidth' => 0,
'marginHeight' => 0,
'nameOfSet' => '',
'cartonRatioInSet' => 0,
'erpId' => '',
'skuNumber' => '',
'description' => '',
'destination' => 0,
'po' => '',
'shipper' => '',
];
$rs_cargo[] = $cargo_data;
$csv_lines = [];
$csv_lines[] = "S,H,reference,hangingAllowed,useMultipleContainers,uniqueErpId,suggestExtra";
$csv_lines[] = "S,D," . remove_comma($rs_shipment[0]['reference']) . "," . ($rs_shipment[0]['hangingAllowed'] ? 'true' : 'false') . "," . ($rs_shipment[0]['useMultipleContainers'] ? 'true' : 'false') . "," . ($rs_shipment[0]['uniqueErpId'] ? 'true' : 'false') . "," . ($rs_shipment[0]['suggestExtra'] ? 'true' : 'false');
$csv_lines[] = "B,H,lengthMm,widthMm,heightMm,maxWeightKg,backMarginMm,isRefrigerated,maxUseOne,lclBreakEvenCBM";
foreach ($rs_container as $row) {
$csv_lines[] = "B,D," . $row['lengthMm'] . "," . $row['widthMm'] . "," . $row['heightMm'] . "," . format_decimal_3($row['maxWeightKg']) . "," . $row['backMarginMm'] . "," . ($row['isRefrigerated'] ? 'true' : 'false') . "," . ($row['maxUseOne'] ? 'true' : 'false') . "," . format_decimal_3($row['lclBreakEvenCBM']);
}
$csv_lines[] = "C,H,color,lengthMm,widthMm,heightMm,weightKg,cartons,additionalCartons,placement,verticalRotationAllowed,skuPerCarton,emptyPalletHeightMm,trayCapMm,trayHeightMm,marginLenghtMm,marginWidthMm,marginHeightMm,nameOfSet,cartonRatioInSet,erpId,skuNumber,description,destination,po,shipper";
foreach ($rs_cargo as $row) {
$csv_lines[] = "C,D," . remove_comma($row['color']) . "," . $row['lengthMm'] . "," . $row['widthMm'] . "," . $row['heightMm'] . "," . format_decimal_3($row['weightKg']) . "," . $row['cartons'] . "," . $row['additionalCartons'] . "," . $row['placement'] . "," . ($row['verticalRotationAllowed'] ? 'true' : 'false') . "," . $row['skuPerCarton'] . "," . $row['emptyPalletHeightMm'] . "," . $row['trayCapMm'] . "," . $row['trayHeightMm'] . "," . $row['marginLength'] . "," . $row['marginWidth'] . "," . $row['marginHeight'] . "," . remove_comma($row['nameOfSet']) . "," . $row['cartonRatioInSet'] . "," . remove_comma($row['erpId']) . "," . remove_comma($row['skuNumber']) . "," . remove_comma($row['description']) . "," . $row['destination'] . "," . remove_comma($row['po']) . "," . remove_comma($row['shipper']);
}
return implode("\n", $csv_lines);
}
// Example usage:
$csv_data = generate_loadviewer_csv();
$upload_result = upload_to_loadviewer_with_retries($csv_data);
echo "Upload Result: " . htmlspecialchars($upload_result) . "\n";
test_suggestions("SANDBOX-01", "test@example.com");
?>
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart'; // Import the intl package
// Constants
const String tokenUrl = 'https://www.loadviewer.com/api/auth/jwt/token';
const String uploadUrl = 'https://www.loadviewer.com/api/integration/v1/upload';
const String downloadUrl = 'https://www.loadviewer.com/api/integration/v1/download';
const String apiEmail = 'YOUR_Email'; // Replace with your actual email
const String apiKey = 'YOUR_API_KEY_HERE'; // Replace with your actual API key
const int defaultMaxRetries = 3;
const int defaultRateLimitWaitSeconds = 65;
const int generalRetryDelaySeconds = 5;
String? jwtToken; // Keep it nullable
// Helper function for exponential backoff
Future<void> _delay(int retryCount) async {
await Future.delayed(Duration(seconds: generalRetryDelaySeconds * retryCount));
}
// Function to get JWT token with retries
Future<String?> getJwtTokenWithRetries({int retryCount = 0}) async {
if (jwtToken != null) {
return jwtToken;
}
if (retryCount > defaultMaxRetries) {
throw Exception('ERROR: Failed to retrieve token after multiple retries');
}
final Map<String, String> requestBody = {
'email': apiEmail,
'apikey': apiKey,
};
final String jsonBody = jsonEncode(requestBody);
try {
final response = await http.post(
Uri.parse(tokenUrl),
headers: {'Content-Type': 'application/json'},
body: jsonBody,
);
if (response.statusCode == 429) {
if (retryCount < defaultMaxRetries) {
print(
'Rate limit hit on token endpoint (attempt ${retryCount + 1} of $defaultMaxRetries). Waiting $defaultRateLimitWaitSeconds seconds before retrying.');
await Future.delayed(
const Duration(seconds: defaultRateLimitWaitSeconds));
return getJwtTokenWithRetries(retryCount: retryCount + 1);
} else {
throw Exception('ERROR: Failed to retrieve token after multiple rate limits');
}
} else if (response.statusCode >= 300) {
if (retryCount < defaultMaxRetries) {
print(
'Error retrieving token (attempt ${retryCount + 1} of $defaultMaxRetries): ${response.statusCode} - ${response.body}. Retrying in $generalRetryDelaySeconds seconds.');
await _delay(retryCount);
return getJwtTokenWithRetries(retryCount: retryCount + 1);
} else {
throw Exception(
'ERROR: Failed to retrieve token after $defaultMaxRetries attempts: ${response.statusCode} - ${response.body}');
}
}
final Map<String, dynamic> result = jsonDecode(response.body);
if (result.containsKey('token')) {
jwtToken = result['token'];
return jwtToken;
} else {
throw Exception(
'ERROR: Failed to extract token from response: ${response.body}');
}
} catch (e) {
await _delay(retryCount);
return getJwtTokenWithRetries(retryCount: retryCount + 1);
}
}
// Function to upload to LoadViewer with retries
Future<String> uploadToLoadViewerWithRetries(String csvData,
{int retryCount = 0}) async {
if (jwtToken == null) {
jwtToken = await getJwtTokenWithRetries();
if (jwtToken == null) {
throw Exception('ERROR: Unable to retrieve JWT token for upload');
}
}
if (retryCount > defaultMaxRetries) {
throw Exception('ERROR: Max upload retries exceeded');
}
try {
final response = await http.post(
Uri.parse(uploadUrl),
headers: {
'Content-Type': 'text/csv',
'Authorization': 'Bearer $jwtToken',
},
body: csvData,
);
if (response.statusCode == 401 || response.statusCode == 410) {
print('JWT token expired or invalid. Attempting to retrieve a new token.');
jwtToken = null;
jwtToken = await getJwtTokenWithRetries();
if (jwtToken == null) {
throw Exception(
'ERROR: Unable to retrieve a new JWT token after token expiration');
}
return uploadToLoadViewerWithRetries(csvData,
retryCount:
retryCount); // Retry with the same retryCount, not incremented
} else if (response.statusCode == 429) {
if (retryCount < defaultMaxRetries) {
print(
'Rate limit hit on upload endpoint (attempt ${retryCount + 1} of $defaultMaxRetries). Waiting $defaultRateLimitWaitSeconds seconds before retrying.');
await Future.delayed(
const Duration(seconds: defaultRateLimitWaitSeconds));
return uploadToLoadViewerWithRetries(csvData,
retryCount: retryCount + 1);
} else {
throw Exception('ERROR: Upload failed after multiple rate limits');
}
} else if (response.statusCode >= 300 &&
response.statusCode != 409) {
if (retryCount < defaultMaxRetries) {
print(
'Error uploading data (attempt ${retryCount + 1} of $defaultMaxRetries): ${response.statusCode} - ${response.body}. Retrying in $generalRetryDelaySeconds seconds.');
await _delay(retryCount);
return uploadToLoadViewerWithRetries(csvData,
retryCount: retryCount + 1);
} else {
throw Exception(
'ERROR: Upload failed after $defaultMaxRetries attempts. Status: ${response.statusCode} - ${response.body}');
}
}
return '${response.statusCode} - ${response.body}';
} catch (e) {
await _delay(retryCount);
return uploadToLoadViewerWithRetries(csvData,
retryCount: retryCount + 1);
}
}
// Function to get LoadViewer suggestions CSV with retries
Future<String> getLoadViewerSuggestionsCsvWithRetries(String requestBodyJson,
{int retryCount = 0}) async {
if (jwtToken == null) {
jwtToken = await getJwtTokenWithRetries();
if (jwtToken == null) {
throw Exception(
'ERROR: Unable to retrieve JWT token for getting suggestions');
}
}
if (retryCount > defaultMaxRetries) {
throw Exception('ERROR: Max suggestions retries exceeded');
}
try {
final response = await http.post(
Uri.parse(downloadUrl),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $jwtToken',
},
body: requestBodyJson,
);
print(
'POST Request to LoadViewer Suggestions URL: $downloadUrl\nRequest Body:\n$requestBodyJson\nHTTP Status Code: ${response.statusCode}\nResponse Text:\n${response.body}\n');
if (response.statusCode == 401 || response.statusCode == 410) {
print('JWT token expired or invalid. Attempting to retrieve a new token.');
jwtToken = null;
jwtToken = await getJwtTokenWithRetries();
if (jwtToken == null) {
throw Exception(
'ERROR: Unable to retrieve a new JWT token after token expiration');
}
return getLoadViewerSuggestionsCsvWithRetries(requestBodyJson,
retryCount:
retryCount); // Retry with same count, not incremented.
} else if (response.statusCode == 429) {
if (retryCount < defaultMaxRetries) {
print(
'Rate limit hit on suggestions endpoint (attempt ${retryCount + 1} of $defaultMaxRetries). Waiting $defaultRateLimitWaitSeconds seconds before retrying.');
await Future.delayed(
const Duration(seconds: defaultRateLimitWaitSeconds));
return getLoadViewerSuggestionsCsvWithRetries(requestBodyJson,
retryCount: retryCount + 1);
} else {
throw Exception('ERROR: Failed to retrieve suggestions after multiple rate limits');
}
} else if (response.statusCode >= 300) {
if (retryCount < defaultMaxRetries) {
print(
'Error retrieving suggestions (attempt ${retryCount + 1} of $defaultMaxRetries): ${response.statusCode} - ${response.body}. Retrying in $generalRetryDelaySeconds seconds.');
await _delay(retryCount);
return getLoadViewerSuggestionsCsvWithRetries(requestBodyJson,
retryCount: retryCount + 1);
} else {
throw Exception(
'ERROR: Failed to retrieve suggestions after $defaultMaxRetries attempts. Status: ${response.statusCode} - ${response.body}');
}
}
return response.body;
} catch (e) {
await _delay(retryCount);
return getLoadViewerSuggestionsCsvWithRetries(requestBodyJson,
retryCount: retryCount + 1);
}
}
// Function to test suggestions
Future<void> testSuggestions(String shipmentReference, String solverEmail) async {
if (shipmentReference.isEmpty) {
print('ERROR: Shipment Reference is required!');
return;
}
final emailRegex =
RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$'); // More robust email regex
if (solverEmail.isNotEmpty && !emailRegex.hasMatch(solverEmail)) {
print('ERROR: Invalid Solver Email format!');
return;
}
if (jwtToken == null) {
jwtToken = await getJwtTokenWithRetries();
if (jwtToken == null) {
print('ERROR: Token Error or Server Not Reachable.');
return;
}
}
final Map<String, String> requestBody = {
'reference': shipmentReference.replaceAll('"', '""'),
'solverEmail': solverEmail.replaceAll('"', '""'),
};
final String jsonBody = jsonEncode(requestBody);
final String strResponse =
await getLoadViewerSuggestionsCsvWithRetries(jsonBody);
print('Raw Suggestions Response:\n$strResponse\n');
final List<String> arrLines = strResponse.split('\n');
final Map<String, dynamic> results = {
'status': <String, String>{},
'recommendations': <Map<String, dynamic>>[],
};
if (arrLines.isNotEmpty) {
for (String line in arrLines) {
final List<String> arrFields = line.split(',');
if (arrFields.isNotEmpty) {
final String recordType = arrFields[0].trim();
switch (recordType) {
case 'S': // Status Record
if (arrFields.length == 9 &&
arrFields[1].trim().toLowerCase() == 'd') {
final Map<String, String> statusInfo =
results['status'].cast<String, String>();
statusInfo['status'] = arrFields[2].trim();
statusInfo['message'] = arrFields[3].trim();
statusInfo['reference'] = arrFields[4].trim();
statusInfo['solverEmail'] = arrFields[5].trim();
statusInfo['multipleContainers'] = arrFields[6].trim();
statusInfo['suggestExtra'] = arrFields[7].trim();
} else if (arrFields.length == 9 &&
arrFields[1].trim().toLowerCase() == 'h') {
// Header row, can be ignored
} else if (line.trim().isNotEmpty) {
print('ERROR: Invalid Status record format: $line');
}
break;
case 'R': // Recommendation Record
if (arrFields.length == 16 &&
arrFields[1].trim().toLowerCase() == 'd') {
final Map<String, dynamic> recommendation = {
'size': arrFields[2].trim(),
'cbm': arrFields[3].trim(),
'weight': arrFields[4].trim(),
'noOfPackages': int.tryParse(arrFields[5].trim()) ?? 0,
'isPacked': arrFields[6].trim(),
'packedPackages': int.tryParse(arrFields[7].trim()) ?? 0,
'suggestedIncrease': int.tryParse(arrFields[8].trim()) ?? 0,
'suggestedDecrease': int.tryParse(arrFields[9].trim()) ?? 0,
'erpId': arrFields[10].trim(),
'skuNumber': arrFields[11].trim(),
'description': arrFields[12].trim(),
'po': arrFields[13].trim(),
'shipper': arrFields[14].trim(),
};
List<Map<String, dynamic>> recommendations =
List<Map<String, dynamic>>.from(results['recommendations']);
recommendations.add(recommendation);
results['recommendations'] = recommendations;
} else if (arrFields.length == 16 &&
arrFields[1].trim().toLowerCase() == 'h') {
// Header row, can be ignored.
} else if (line.trim().isNotEmpty) {
print('ERROR: Invalid Recommendation record format: $line');
}
break;
default:
if (line.trim().isNotEmpty) {
print('ERROR: Unknown record type: $recordType in line: $line');
}
}
}
}
} else {
print('Empty suggestions response.');
}
final Map<String, String>? statusInfo =
results['status'] as Map<String, String>?;
if (statusInfo != null) {
print('Status: ${statusInfo['status']} - ${statusInfo['message']}');
print('Reference: ${statusInfo['reference']}');
print('Solver Email: ${statusInfo['solverEmail']}');
print('Multiple Containers: ${statusInfo['multipleContainers']}');
print('Extra Suggested: ${statusInfo['suggestExtra']}');
}
final List<Map<String, dynamic>>? recommendations =
results['recommendations'] as List<Map<String, dynamic>>?;
if (recommendations != null && recommendations.isNotEmpty) {
print('--- Suggestions ---');
for (var recommendation in recommendations) {
print(
'Is Packed: ${recommendation['isPacked']}, Size LxWxH: ${recommendation['size']}, CBM: ${recommendation['cbm']}, Weight Kg: ${recommendation['weight']}, Packages: ${recommendation['noOfPackages']}, Packed: ${recommendation['packedPackages']}, Increase: ${recommendation['suggestedIncrease']}, Decrease: ${recommendation['suggestedDecrease']}, ErpId: ${recommendation['erpId']}, SkuNumber: ${recommendation['skuNumber']}, Description: ${recommendation['description']}, PO#: ${recommendation['po']}, Division: ${recommendation['shipper']} ');
// --- Your ERP Processing Logic for Suggestions Here ---
}
} else {
print('No suggestions found for reference: $shipmentReference');
}
}
// Function to remove commas and newlines
String removeComma(String value) {
return value.replaceAll(',', '').replaceAll('\r\n', ' ').replaceAll('\n', ' ');
}
// Function to format decimal with 3 places
String formatDecimal3(double value) {
final formatter = NumberFormat('#.###', 'en_US'); // Use en_US for consistent formatting
String formatted = formatter.format(value);
if (formatted.endsWith('.000')) {
formatted = formatted.substring(0, formatted.length - 4);
} else if (formatted.endsWith('00')) {
formatted = formatted.substring(0, formatted.length - 2);
} else if (formatted.endsWith('0')) {
formatted = formatted.substring(0, formatted.length - 1);
}
return formatted;
}
// Function to generate LoadViewer CSV data (example)
String generateLoadViewerCsv() {
final List<List<String>> csvData = [];
// Shipment Data
csvData.add([
'S',
'H',
'reference',
'hangingAllowed',
'useMultipleContainers',
'uniqueErpId',
'suggestExtra'
]);
csvData.add([
'S',
'D',
removeComma('SANDBOX-01'),
'true',
'false',
'false',
'false'
]);
// Container Data
csvData.add([
'B',
'H',
'lengthMm',
'widthMm',
'heightMm',
'maxWeightKg',
'backMarginMm',
'isRefrigerated',
'maxUseOne',
'lclBreakEvenCBM'
]);
csvData.add([
'B',
'D',
'5867',
'2352',
'2393',
formatDecimal3(27200.0),
'0',
'false',
'false',
formatDecimal3(0.0)
]);
// Cargo Data
csvData.add([
'C',
'H',
'color',
'lengthMm',
'widthMm',
'heightMm',
'weightKg',
'cartons',
'additionalCartons',
'placement',
'verticalRotationAllowed',
'skuPerCarton',
'emptyPalletHeightMm',
'trayCapMm',
'trayHeightMm',
'marginLenghtMm',
'marginWidthMm',
'marginHeightMm',
'nameOfSet',
'cartonRatioInSet',
'erpId',
'skuNumber',
'description',
'destination',
'po',
'shipper'
]);
csvData.add([
'C',
'D',
removeComma('#008000'),
'700',
'500',
'300',
formatDecimal3(91.5),
'300',
'0',
'0',
'true',
'1',
'0',
'0',
'0',
'0',
'0',
'0',
removeComma(''),
'0',
removeComma(''),
removeComma(''),
removeComma(''),
'0',
removeComma(''),
removeComma('')
]);
// Join the rows with newlines
return csvData.map((row) => row.join(',')).join('\n');
}
// Main function
Future<void> main() async {
try {
String csvData = generateLoadViewerCsv();
String uploadResult = await uploadToLoadViewerWithRetries(csvData);
print('Upload Result: $uploadResult');
await testSuggestions('SANDBOX-01', 'test@example.com');
} catch (e) {
print('Error: $e'); // Catch and print any exceptions during the process.
}
}
Important Reminders
- Requests without API Key will return HTTP
400
or401
- Requests with expired taken shall return HTTP
410
you should replace your cached token with new one from LoadViewer. - Use
SANDBOX-*
Shipment References to avoid real charges, these shipments are not published. - Accepted Shipment always retrun Success with code 202 with Draft status. SANDBOX shipments and other valid shipment that cannot be published are Saved with Draft Status.
- Accepted and Published Shipment always retrun Success with code 200 with Coins Charges and Coin Balance after charge.
- Use the manual file upload method as your first validation step
- Use the Shipments page to delete the SHIPMENTS to try again.
- Always verify
success
and noerror
structure in responses