Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions shipbob_to_gorgias/java/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
target/
.env
metadata.json
59 changes: 59 additions & 0 deletions shipbob_to_gorgias/java/PANDIUM.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
version: 1.0
base: Java:25
build: mvn clean package
run: java -jar target/sb2gorgias-1.0-jar-with-dependencies.jar

# Supplies the PAN_SEC_* credentials the clients read: SHIPBOB_ACCESS_TOKEN and
# SHIPBOB_WEBHOOK_SECRET; GORGIAS-OAUTH_ACCESS_TOKEN and GORGIAS-OAUTH_ACCOUNT.
# Both are OAuth2 connectors: Pandium runs the authorization flow when a tenant
# connects and refreshes the tokens, so the integration only reads access tokens.
connectors:
- shipbob
- gorgias-oauth

configs:
schema:
properties:
order_start_date:
type: string
format: date
newest_order_first:
type: boolean
default: false
type: object
uischema:
elements:
- type: Section
label: Sync Orders Since
subtitle:
Set how far back the first sync reaches. Later syncs will pick up where the previous sync left off. Limited to the last 30 days.
elements:
- label: Sync orders since
scope: '#/properties/order_start_date'
type: Control
options:
trim: true
- type: Divider
- type: Section
label: Newest orders first?
subtitle: Check this box to order each customer's shipbob_orders sidebar list newest-first rather than oldest-first.
elements:
- scope: '#/properties/newest_order_first'
type: Control
type: VerticalLayout

metadata_schema:
schema:
name: metadata_schema
properties:
# Cron-flow cursors: the point each order query resumes from next run.
new_order_start_date:
type: string
updated_order_start_date:
type: string
# Webhook-flow dedupe: "shipment_id:status" -> ISO timestamp it was
# ticketed, pruned to a 30-minute window on every run.
processed_events:
type: object
additionalProperties:
type: string
168 changes: 168 additions & 0 deletions shipbob_to_gorgias/java/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# ShipBob to Gorgias in Java

The Java implementation of the [ShipBob to Gorgias sample](../README.md). Read that first
for what the integration does and which parts of the Pandium platform it exercises; this page
covers the code, and how to build, run, and test it.

Java 25, Maven, `unirest-java`, `org.json`, JUnit 5. No framework, no web server.

## Layout

```
java/
├── PANDIUM.yaml manifest: runtime, configs, metadata schema
├── pom.xml dependencies (unirest-java, org.json, JUnit)
├── src/main/java/sb2gorgias/
│ ├── Main.java entry point; dispatches on run mode
│ ├── Lib.java the Pandium runtime contract: config, secrets, context, metadata
│ ├── HttpClient.java hand-rolled retry client (Unirest has no built-in backoff)
│ ├── Cron.java Flow A — resumable order sync
│ ├── Webhook.java Flow B — shipment status webhook -> ticket, with dedupe
│ ├── ShipBobClient.java / ShipBobApi.java ShipBob client
│ └── GorgiasClient.java / GorgiasApi.java Gorgias client
└── src/test/java/sb2gorgias/ both flows covered end to end; no network
```

`Lib.java` is the file to read first — the whole platform contract in one file: `PAN_CFG_*`/
`PAN_SEC_*` as plain maps, `PAN_CTX_*` as named methods, the metadata file read, and the
single stdout write that hands metadata back to Pandium. It also defines `newLogger`, which
every other file gets its own named `java.util.logging.Logger` instance from.

`ShipBobClient`/`GorgiasClient` exist because Java has no runtime monkey-patching — `Cron`
and `Webhook` depend on the interfaces, production wiring uses the real `*Api` classes, and
tests use hand-written fakes implementing the same interfaces.

## Implementation notes

**The run-limit deadline** is a `ScheduledExecutorService`-based watchdog in `Cron.java`,
injectable via `Cron.Deps` so tests can trigger it deterministically without waiting 9 real
minutes. Because it runs on a real background thread in production, the cursor state it
shares with the paging loop (`Cron.CursorState`) only exposes `synchronized` accessors —
every read, including log lines, goes through them.

**No hand-rolled numeric-id formatting is needed.** `org.json` decodes JSON integers as
`Integer`/`Long`, never `Double`, so there's no scientific-notation risk when an id gets
embedded in a URL or a dedupe key — a class of bug some other ports here had to guard against
explicitly.

**Update-date comparison and sorting use real `java.time.OffsetDateTime`** instead of string
comparison, since ShipBob's per-shipment `last_update_at` needs to be compared and sorted
correctly regardless of exact string format. The cursor values written back into tenant
metadata are still formatted to match every other port's shape exactly (6-digit microseconds,
no offset suffix) — that's a wire format other tenants' stored metadata already relies on,
not a place to diverge.

**Date formatting for the customer sidebar** works on the raw ISO string with a regex instead
of a full parse, in `GorgiasApi.java` — ShipBob timestamps are UTC-only and this is a
display-only value, so a full `java.time` parse would be more work for no behavioral gain.

**HTTP retry** is hand-rolled in `HttpClient.java` (exponential backoff, a small set of
retryable status codes) because `unirest-java` has no retry support of its own — the same gap
Go's port had to fill for its own HTTP stack.

## Prerequisites

- Java 25 (e.g. `brew install openjdk@25`)
- Maven (e.g. `brew install maven`)

## Building

```bash
cd java
mvn clean package
```

## Running the tests

The tests cover both flows end to end — including the timeout flush and the webhook dedupe —
with no network access and no credentials:

```bash
mvn test
```

`src/test/java/sb2gorgias/Helpers.java` builds a real `Pandium` directly instead of from the
environment, and `FakeShipBobClient`/`FakeGorgiasClient` implement the client interfaces
in-memory, so client helper logic (customer key resolution, payload building) still runs
under test.

## Running it locally

Both flows talk to the live ShipBob and Gorgias APIs, so use sandbox credentials either way.

### With the Pandium CLI

The CLI runs the integration in the current folder using the environment of a real tenant, so
the tenant's provisioned connector secrets are never copied onto your machine. Download it
from the Admin Dashboard under **Settings → Developer Resources**.

```bash
pandium login # defaults to sandbox
pandium get integrations # find your integration id
pandium get tenants -i <integration_id> # find the tenant id to borrow
```

Then, from this directory:

```bash
pandium local build # runs the manifest's build command (mvn clean package)
pandium local run <tenant_id> # cron flow
pandium local run <tenant_id> -m webhook # webhook flow — see below
```

`--mode` takes `init`, `normal`, or `webhook`, and `--path` points at a directory other than
the current one. A local `.env` overrides anything pulled from Pandium — that is both how you
change one config without touching the tenant, and how you hand the CLI a webhook payload to
run against.

### With environment variables directly

Pandium hands every value over as a plain environment variable in production — no `.env`
loader is built into this port, so export them directly:

```bash
export PAN_SEC_SHIPBOB_ACCESS_TOKEN=eyJ...
export PAN_SEC_GORGIAS_OAUTH_ACCESS_TOKEN=...
export PAN_SEC_GORGIAS_OAUTH_ACCOUNT=your-store

export PAN_CFG_ORDER_START_DATE=2026-07-01
export PAN_CFG_NEWEST_ORDER_FIRST=false

export PAN_CTX_RUN_MODE=normal
export PAN_CTX_TENANT_METADATA_FILE=./metadata.json
```

Seed the metadata file, build, then run the cron flow:

```bash
echo '{}' > metadata.json
mvn -q clean package -DskipTests
java -jar target/sb2gorgias-1.0-jar-with-dependencies.jar
```

Logs stream to stderr; the last line on stdout is the JSON that Pandium would merge into
tenant metadata. To simulate resuming, paste that line into `metadata.json` and run again.

### Exercising the webhook flow

Neither route can invent a delivery, so both read the run triggers from the environment — an
array whose `payload.file` points at a body on disk, exactly as Pandium would supply it.
Write one by hand using the same shape the tests use:

```bash
mkdir -p /tmp/wh
cat > /tmp/wh/event.json <<'EOF'
{"id": 456789, "order_id": 289012345, "reference_id": "MERCHANT-ORDER-1001",
"status": "Delivered", "status_details": [],
"tracking": {"carrier": "USPS", "tracking_number": "9400100000000000000000"},
"delivery_date": "2026-07-09T18:22:00Z",
"products": [{"name": "Pinnacle Shampoo", "sku": "PIN-100",
"inventory_items": [{"name": "Pinnacle Shampoo", "quantity": 4}]}],
"recipient": {"name": "Jane Buyer", "email": "jane@example.com",
"address": {"address1": "100 Nowhere Blvd", "city": "Gotham City", "country": "US"}}}
EOF
export PAN_CTX_RUN_TRIGGERS='[{"id":"t1","source":"webhook","payload":{"file":"/tmp/wh/event.json"}}]'
export PAN_CTX_RUN_MODE=webhook
```

Then run it the same way as above (via the CLI or the built jar directly).
98 changes: 98 additions & 0 deletions shipbob_to_gorgias/java/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>com.pandium</groupId>
<artifactId>sb2gorgias</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>sb2gorgias</name>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>25</maven.compiler.release>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<enableRulesSummary>false</enableRulesSummary>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>sb2gorgias.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
<descriptorRef>bin</descriptorRef>
<descriptorRef>src</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>sb2gorgias.Main</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>build-jar-with-dependencies</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>com.konghq</groupId>
<artifactId>unirest-java</artifactId>
<version>3.14.5</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20250517</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Loading