# cXML File Move — Technical Reference

## SAP Ariba Network Integration

SAP Ariba Network is a cloud-based B2B marketplace. Sandvik (buyer) sends purchase orders via Ariba to Tekpal (supplier). The protocol is **cXML** (Commerce eXtensible Markup Language) v1.2.x over HTTPS POST.

### How SAP Ariba Sends Documents

Ariba uses **Supplier Managed Gateway** (SMG): Ariba pushes cXML documents to a supplier-hosted HTTPS endpoint. Ariba retries on failure with exponential backoff. The supplier must return a synchronous cXML `<Response>` acknowledgment — either `200 OK` (received) or an error code. Ariba considers the delivery failed only after all retries are exhausted.

### Ariba Network IPs

Ariba source IP ranges for allowlisting:

- `155.56.128.0/17`
- `130.214.160.64/28`
- `130.214.160.80/29`

### cXML Authentication Layers

| Layer | Mechanism | Where |
|-------|-----------|-------|
| Network | IP allowlisting (Caddy) | `/etc/caddy/Caddyfile` |
| Transport | HTTPS/TLS | Caddy terminates TLS |
| Application | `<Sender><Credential><SharedSecret>` | cXML `<Header>` element |
| Service | nc-server skips `parseBody()` + `jsonValue()` auth | `lib/rest.lua` |

The `SharedSecret` in the cXML `<Header>` is the application-level credential. Ariba sends it with every request. It is configured in the Ariba Network supplier account and validated on the receiving side.

## Architecture

```mermaid
graph TB
    subgraph "SAP Ariba Network"
        AR[Ariba SMG]
    end

    subgraph "Caddy Reverse Proxy :443"
        IP{IP allowlist}
    end

    subgraph "nc-server :5959"
        RL[rest.lua]
        CW[cxml-worker.lua]
    end

    subgraph "File System"
        UF[upload_file_path]
        DF[download_file_path]
        ST[send_test_path]
    end

    AR -->|HTTPS POST cXML| IP
    IP -->|allowed| RL
    RL -->|raw sock body| CW
    CW -->|save .xml| UF

    subgraph "Server-to-Server Download"
        CW2[cxml-worker.download]
        REMOTE[Remote nc-server]
    end

    CW2 -->|callRest.postUrl| REMOTE
    REMOTE -->|cXML Response| CW2
    CW2 -.->|reads test files| ST
```

## Endpoint: `/rest/nc/upload/cxml`

| Property | Value |
|----------|-------|
| Method | `POST` |
| Content-Type | `text/xml` or `application/xml` |
| Body | Raw cXML document |
| Response Content-Type | `application/xml` |
| Response Format | cXML `<Response>` |
| Auth | Caddy IP + HTTPS + SharedSecret |

### Request Flow

```mermaid
sequenceDiagram
    participant A as Ariba / Client
    participant C as Caddy :443
    participant R as rest.lua
    participant W as cxml-worker
    participant F as File System

    A->>C: HTTPS POST /rest/nc/upload/cxml
    C->>C: Check source IP
    alt IP not allowed
        C-->>A: 403 Forbidden
    else IP allowed
        C->>R: Forward to :5959
        R->>R: Skip parseBody() for /rest/nc/upload/cxml
        R->>R: Skip jsonValue() auth, pass raw sock
        R->>W: upload(sock)
        W->>W: Read sock.body as XML
        W->>W: Extract payloadID from cXML
        W->>F: createPath(upload_file_path)
        W->>F: writeFile(payloadID-timestamp.xml)
        W-->>R: {mime="application/xml", data=cXML Response}
        R-->>C: HTTP 200 application/xml
        C-->>A: cXML Response 200 OK
    end
```

### cXML Response Format

Success:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<cXML payloadID="uuid" timestamp="2026-04-16T15:05:38Z">
  <Response>
    <Status code="200" text="OK">Received</Status>
  </Response>
</cXML>
```

Error responses:

| Status Code | Text | When |
|-------------|------|------|
| 200 | OK | Document saved successfully |
| 400 | Bad Request | Empty body |
| 500 | Internal Server Error | File save failed |

## File Naming

Received cXML documents are saved with a unique filename that prevents overwrites:

```
{payloadID}-{YYYY-MM-DD_HH.MM.SS}.{ms}.xml
```

| Part | Source | Example |
|------|--------|---------|
| `payloadID` | Extracted from cXML `<cXML payloadID="...">` using PEG | `45C2325184CD1FE0AAB37CE40F84CC52` |
| `-` | Separator | |
| timestamp | `dt.currentFileStringMs()` — server local time with ms | `2026-04-16_18.07.16.12345` |
| `.xml` | Extension | |

The `payloadID` is the Ariba-assigned unique document identifier. Ariba recommends `datetime.processid.random@hostname` format. The same `payloadID` is reused on retries — the timestamp suffix ensures each save is unique even for retries.

## Folder Structure

```
/Volumes/nc/work/tekpal/Sandvik-SAP-edi/import/sales-order/
├── upload/                    ← received via /rest/nc/upload/cxml (upload_file_path)
│   ├── 45C2325184-2026-04-16_18.07.16.xml
│   └── 45C2325185-2026-04-16_18.07.17.xml
├── download/                  ← received via download() from remote (download_file_path)
│   └── ...
├── sap-test/                  ← test XML files (send_test_path)
│   ├── 4500036247.xml
│   ├── 4500036813.xml
│   └── ...
├── processed/                 ← moved after import (import_move_folder)
├── error/                     ← failed imports
└── *.xml                      ← files ready for import (import_path)
```

## Configuration (`rec.json`)

All paths and URLs are in `form/nc/erp-sync/cxml/rec.json`:

| Field | Purpose | Example |
|-------|---------|---------|
| `import_path` | Main import folder for cXML-to-order processing | `.../import/sales-order/` |
| `upload_file_path` | Where `/rest/nc/upload/cxml` saves received files | `.../import/sales-order/upload/` |
| `download_file_path` | Where `download()` saves files from remote | `.../import/sales-order/download/` |
| `send_test_path` | Test XML files for `sendExample()` and `download()` | `.../import/sales-order/sap-test/` |
| `call_url` | URL used by `sendExample()` (curl-based) | `http://localhost:5959/rest/nc/upload/cxml` |
| `download_url` | URL used by `download()` (callRest-based) | `http://localhost:5959/rest/nc/upload/cxml` |
| `import_move_folder` | Move files to `processed/` after import | `true` |
| `export_move_folder` | Move files to `processed/` after export | `true` |

## Worker Functions (`nc/worker/cxml-worker.lua`)

### `upload(sock)` — Receive cXML

Triggered by `POST /rest/nc/upload/cxml`. Receives raw XML, saves to `upload_file_path`. Returns cXML acknowledgment.

### `download()` — Pull from Remote Server

Reads all `.xml` files from `send_test_path`, sends each via `callRest.postUrl()` to `download_url`. Uses nc-server internal REST client (socket-based, no external dependencies).

```mermaid
sequenceDiagram
    participant W as cxml-worker.download()
    participant FS as send_test_path
    participant CR as callRest.postUrl()
    participant R as Remote /rest/nc/upload/cxml

    loop for each .xml file
        W->>FS: readFile()
        FS-->>W: xmlContent
        W->>CR: {url, post_data=xmlContent, header={content-type=text/xml}}
        CR->>R: HTTP POST
        R-->>CR: cXML Response
        CR-->>W: body
        W->>W: check code="200"
    end
    W-->>W: return {sent, errors, file_array}
```

### `sendExample()` — Push via curl

Reads all `.xml` files from `send_test_path`, sends each via `curl` to `call_url`. Used for external testing.

## Testing

### Test File

`test/convert/cxml-worker-test.lua`

### Running Tests

```bash
cd /Volumes/nc/nc-server/test/convert && lj cxml-worker-test.lua
```

### Test Cases

| # | Test | What It Verifies |
|---|------|-----------------|
| 1 | `cxml.require` | Module loads, exports `calls`, `sendExample`, `download` |
| 2 | `cxml.uploadEmptyBody` | Empty body returns 400 cXML response |
| 3 | `cxml.uploadNilBody` | Nil body returns 400 cXML response |
| 4 | `cxml.uploadValidXml` | Valid cXML saved to disk, returns 200, file content matches |
| 5 | `cxml.sendExample` | All `sap-test/*.xml` sent via curl, all return 200 |
| 6 | `cxml.download` | All `sap-test/*.xml` sent via `callRest.postUrl()`, all return 200 |

Test order matters: `sendExample` runs before `download` to verify both transport methods.

### Test Result Example

```
[  PASSED OSX arm64 ] 6 test(s), total time: 0.69941 seconds
  Functions tested: 2/2 (100%)
```

## Caddy Configuration

```caddyfile
handle /rest/nc/upload/cxml {
    @allowed remote_ip 155.56.128.0/17 130.214.160.64/28 130.214.160.80/29
    handle @allowed {
        reverse_proxy localhost:5959 {
            flush_interval -1
            fail_duration 1s
            unhealthy_request_count 1000
            stream_timeout 5s
        }
    }
    respond "Forbidden" 403
}
```

This block must appear **before** the `handle /rest/nc/*` catch-all in the Caddyfile. Caddy evaluates `handle` blocks top-down, first match wins. The IP restriction ensures only Ariba Network can reach the cXML endpoint.

Restart after changes: `brew services restart caddy`

## Files Changed for This Feature

| File | Change |
|------|--------|
| `/etc/caddy/Caddyfile` | IP-restricted handler for `/rest/nc/upload/cxml` |
| `nc/worker/cxml-worker.lua` | New: upload, download, sendExample functions |
| `nc/worker/query-worker.lua` | Removed old upload stub |
| `nc/nc-server.lua` | Registered `nc/worker/cxml-worker` plugin |
| `lib/rest.lua` | Skip `parseBody()` + raw sock passthrough for `/rest/nc/upload/cxml` |
| `form/nc/erp-sync/cxml/rec.json` | Added `call_url`, `upload_file_path`, `download_url`, `download_file_path`, `send_test_path` |
| `test/convert/cxml-worker-test.lua` | New: 6 test cases |
| `test/lib/dt-test.lua` | Added `currentFileStringMs` test |
