# TWIN Sandbox Scripts

Scripts to simulate the consignment lifecycle on a TWIN node (local or staging), so each party can integrate against TWIN without repository access. Each actor runs only the scripts relevant to their role.

## Who does what

| Role                  | Publishes                                             | Receives                                         | Reads                                     |
| --------------------- | ----------------------------------------------------- | ------------------------------------------------ | ----------------------------------------- |
| **Freight Forwarder** | the consignment, its documents and its journey events | —                                                | its own consignments                      |
| **Border Agency**     | —                                                     | ISN signals, on pre-notification and on despatch | consignments with a leg in its country    |
| **Location Operator** | a `twin:locationOperatorMatch` event back             | a notification on `twin:carrierBooking`          | consignments calling at a port it manages |

Two things follow from this table, and they are the reason the lifecycle looks the way it does:

- **Location Operators are notified on events, not on consignment or document changes.** Specifically on `twin:carrierBooking`, and only when the consignment's port of exit or port of entry is one they manage. A lifecycle of creates, updates and documents alone would never reach them, so the lifecycle publishes journey events too.
- **Border Agencies are scoped by country** and **Location Operators by port**, both derived from a leg's UN/LOCODE. Change the ports on the route and who is entitled to read changes with them.

Who may read what is governed by data sharing policies, which are managed in the TWIN web app rather than by these scripts. Set them up there before publishing, or what you publish reaches nobody.

## Structure

```
./
├── fetch-consignment.sh           # read a consignment and download its documents
├── bin/                           # bundled jq, used automatically if none is installed
├── shared/
│   └── common.sh                  # shared helpers (login, inbox)
├── data/
│   └── consignment-lifecycle.template.ndjson
├── freight-forwarder/
│   └── publish-consignment.sh
├── location-operator/
│   └── send-match.sh
└── README.md
```

## Prerequisites

- `bash`
- `curl`

## Configuration

Every script reads `config.env`, in this folder. A `SANDBOX_*` variable set on the command line wins over the file — `SANDBOX_ACTIVITY_DELAY=2 bash …` — so a one-off run needs no edit. This applies to the `SANDBOX_` namespace only: `SUPPLY_CHAIN_*` is shared with the node, and a stray value left in a shell should not quietly point a script at a different node or account.

The download bundle already includes a `config.env` ready to fill in (in the repo it's `config.env.example` — copy it). It looks like this:

```env
# The TWIN node to talk to
SUPPLY_CHAIN_NODE_URL=https://supply-chain.rc.twinnodes.com

# Account credentials provided by the TWIN team
SUPPLY_CHAIN_USER_NAME=<your email>
SUPPLY_CHAIN_USER_PASSWORD=<your password>
SUPPLY_CHAIN_ORGANIZATION_IDENTITY=<your organisation DID>

# Seconds between published activities (default 120)
SANDBOX_ACTIVITY_DELAY=120

# Route of the published consignment (defaults shown)
SANDBOX_PORT_OF_EXIT_NAME=Calais
SANDBOX_PORT_OF_EXIT_CODE=FRCQF
SANDBOX_PORT_OF_ENTRY_NAME=Felixstowe
SANDBOX_PORT_OF_ENTRY_CODE=GBFXT
```

## Before you start: data sharing

Nothing you publish reaches anyone until your organisation says who may read it. That is set up **in the TWIN web app**, not by these scripts — there is nothing to run here for it.

What you set there has to match the route you publish, because entitlement is derived from the legs:

| Route                                   | Border Agencies entitled | Location Operators entitled |
| --------------------------------------- | ------------------------ | --------------------------- |
| Calais (`FRCQF`) → Felixstowe (`GBFXT`) | `FR`, `GB`               | `FRCQF`, `GBFXT`            |
| Rotterdam (`NLRTM`) → Antwerp (`BEANR`) | `NL`, `BE`               | `NLRTM`, `BEANR`            |
| Dover (`GBDVR`) → Felixstowe (`GBFXT`)  | `GB`                     | `GBDVR`, `GBFXT`            |

A Border Agency may read a consignment when one of its legs is in its country, and a leg's country is its UN/LOCODE's first two letters. A Location Operator may read one calling at a port it manages.

So if you change the route below, check that the sharing set up in the web app still covers the new ports — otherwise you will publish successfully and nobody will see it.

---

## Freight Forwarder

Publishes the full consignment lifecycle to the TWIN dataspace inbox: 23 activities covering the consignment itself, its 9 documents and its 9 journey events. The script prints the consignment's `globalId`.

The carrier is named by the `twin:carrierBooking` event, which the node applies to the consignment when it lands, and the activities before that event carry no carrier at all — nothing has been booked yet. From there on the consignment activities do carry it, because each of them re-sends the whole consignment and replaces its stored form: an update that omitted the carrier would silently undo the booking. Override the name with `SANDBOX_CARRIER_NAME`.

Two notifications leave TWIN while this runs, both automatically:

- **ISN signals to the Border Agencies**, on the pre-notification and on the despatch.
- **A signal to the Location Operator** on the `twin:carrierBooking` event — but only if the operator manages the port of exit or the port of entry you configured. Publishing a route no operator manages is a valid run that notifies nobody.

```bash
bash freight-forwarder/publish-consignment.sh
```

The script publishes one activity every `SANDBOX_ACTIVITY_DELAY` seconds (default 120 — the full lifecycle takes ~46 minutes). For quick testing:

```bash
SANDBOX_ACTIVITY_DELAY=2 bash freight-forwarder/publish-consignment.sh
```

### Publishing more than one consignment

Every run derives its own consignment reference from the current UTC time, so the same account can run the script as often as needed and get a separate consignment each time:

```
Consignment:    TESTFF-20260806-171432
Global ID:      6-GB-GB000000000001-20260806-171432
```

The identifiers are built as `<prefix>-<reference>`, all three parts overridable in `config.env`:

| Variable                        | Default               | Controls                         |
| ------------------------------- | --------------------- | -------------------------------- |
| `SANDBOX_CONSIGNMENT_PREFIX`    | `TESTFF`              | the consignment `identifier`     |
| `SANDBOX_GLOBAL_ID_PREFIX`      | `6-GB-GB000000000001` | the consignment `globalId`       |
| `SANDBOX_CONSIGNMENT_REFERENCE` | current UTC timestamp | the run reference shared by both |

Reuse a reference to publish onto a consignment you published earlier instead of creating another one:

```bash
SANDBOX_CONSIGNMENT_REFERENCE=20260806-171432 bash freight-forwarder/publish-consignment.sh
```

The run republishes the same 23 activities against that consignment, so it stays a single consignment with one set of events rather than gaining duplicates. It is not a way to produce new document revisions: the sample documents carry fixed version ids, and a document republished under a version it already has is the one it already had.

Note that `SANDBOX_CONSIGNMENT_GLOBAL_ID` is unrelated — it tells `fetch-consignment.sh` and `send-match.sh` which consignment to act on, and is ignored here.

### Changing the route

The lifecycle data is a template — the port of exit (departure) and port of entry (arrival) are `{{PLACEHOLDER}}` values filled in when each activity is published, so the same file can be published as any route:

```bash
SANDBOX_PORT_OF_EXIT_NAME=Rotterdam SANDBOX_PORT_OF_EXIT_CODE=NLRTM \
  SANDBOX_PORT_OF_ENTRY_NAME=Dover SANDBOX_PORT_OF_ENTRY_CODE=GBDVR \
  bash freight-forwarder/publish-consignment.sh
```

Both names and codes must be set as a pair — the code is the port's UN/LOCODE and is what Border Agencies and Location Operators match on. Codes are validated before anything is published, so a malformed one fails the run rather than leaving a half-published consignment.

A route change also changes who is entitled to read the result, so check the data sharing set up in the web app still covers the new ports.

### Documents

The lifecycle data embeds generated sample PDFs as the document content. Each PDF is a fixed placeholder file — its printed identifier is the one the sample data shipped with, not the reference generated for your run, so treat the PDF text as illustrative and the activity metadata as authoritative.

---

## Location Operator

TWIN signals your configured endpoint when a consignment books a carrier — the `twin:carrierBooking` event — and only for consignments whose port of exit or port of entry is one of your managed locations. That signal carries the consignment's `globalId`, which is your entry point into everything below.

Read the consignment and download its documents to verify it against your records:

```bash
bash fetch-consignment.sh <globalId>
```

This saves the consignment JSON and every attached document (the real files, not just metadata) to `downloads/<globalId>/`.

Then send your match back to confirm the consignment matches your records:

```bash
bash location-operator/send-match.sh <globalId>
```

This publishes a **`twin:locationOperatorMatch`** journey event: it joins the consignment's `events` list and advances its status.

Both scripts also accept `SANDBOX_CONSIGNMENT_GLOBAL_ID` in your `config.env` instead of passing it as an argument every time.

---

## Border Agency

TWIN automatically sends ISN signals to your configured endpoint when:

- A consignment pre-notification is published
- A consignment is despatched

Verify that your endpoint is receiving the signals from TWIN. The signal contains the consignment `globalId` — use it to retrieve the full consignment data and download the attached documents:

```bash
bash fetch-consignment.sh <globalId>
```

This saves the consignment JSON and every attached document to `downloads/<globalId>/`.

---

## Running against a local node

Start the supply-chain node, plus the ISN and MCP receivers if you want to watch the
signals arrive:

```bash
pnpm --filter=@twin.org/supply-chain-node start
pnpm sim:isn
pnpm sim:mcp
```

Point `SUPPLY_CHAIN_NODE_URL` at your local node in `config.env`, then run the same
steps above.

## Questions

Contact the TWIN team if you have any issues running the scripts or need credentials.
