diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..8ec5d44 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,46 @@ +name: Bug report +description: Something in the SDK, docs, or examples doesn't work as documented. +labels: [bug] +body: + - type: textarea + id: what-happened + attributes: + label: What happened? + description: What you expected vs. what actually happened. + validations: + required: true + - type: textarea + id: repro + attributes: + label: Minimal reproduction + description: >- + A minimal code snippet. Do **not** include real `apiSid` / `apiToken` values or + real printer / device UIDs — use placeholders. + render: ts + validations: + required: true + - type: input + id: sdk-version + attributes: + label: expedy-sdk-node version + placeholder: "1.1.0" + validations: + required: true + - type: input + id: node-version + attributes: + label: Node.js version + placeholder: "node --version" + validations: + required: true + - type: dropdown + id: resource + attributes: + label: Affected resource + options: + - printers (cloud thermal printer) + - devices (Raspberry Pi / USB) + - Documentation only + - Not sure + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..6d2d4c1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,25 @@ +name: Feature request +description: Propose a new SDK method, type, doc page, or example. +labels: [enhancement] +body: + - type: textarea + id: problem + attributes: + label: What's missing? + description: What are you trying to do that the SDK / docs don't currently support? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: A method signature, a new doc page, an example — whatever fits. + validations: + required: false + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Any workaround you're currently using. + validations: + required: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..fb81868 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +## Summary + + + +## Checklist + +- [ ] `npm run typecheck && npm run build && npm run typecheck:examples` pass locally +- [ ] `npm test` passes locally +- [ ] If a request/response field changed: `src/types/*.ts`, `openapi.yaml`, and the + matching `docs/api/**/*.md` page were all updated together +- [ ] No real credentials, UIDs, or internal URLs were introduced (this is a public repo) + +## Test plan + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..056c47e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18, 20, 22] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm run typecheck + - run: npm run build + - run: npm run typecheck:examples + - run: node --test test/*.test.mjs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f79f221 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,72 @@ +# AGENTS.md + +Guidance for coding agents (Claude Code, Cursor, GitHub Copilot, …) working in or against +this repository, and a condensed reference for agents integrating `expedy-sdk-node` into +someone else's codebase. + +## What this is + +Official Node.js SDK + API documentation for the **Expedy Print API v2**. It sends print +jobs to two kinds of hardware: + +| Resource | Hardware | Print method | +| --- | --- | --- | +| `printers` | Expedy cloud thermal receipt printer (own internet connection) | `client.printers.createPrintJob(printerUid, { printer_msg, ... })` | +| `devices` | Raspberry Pi gateway + a third-party USB printer plugged into it | `client.devices.usb.createPrintJob(deviceUid, usbPort, { usb_msg, ... })` | + +Read [`docs/concepts/printers-vs-devices.md`](docs/concepts/printers-vs-devices.md) before +writing code against either endpoint — picking the wrong one is the most common mistake. + +`displays` and `medias` are **out of scope** for this repository. + +## Non-obvious things to get right + +- **Authentication is not Bearer.** The `Authorization` header is the raw + `:` value, colon-separated, **no prefix**. `ExpedyClient` builds this + automatically — never hand-construct the header. +- **`printer_han` for Chinese/Japanese/Korean.** Without this field, CJK characters are + silently replaced with `?` **before the job reaches the printer** — no error is raised. + If a user asks to print non-Latin text and the code doesn't set `printer_han`, that's a + bug. See [`docs/receipt-layout/asian-characters.md`](docs/receipt-layout/asian-characters.md). + Values: `"cn"` Chinese, `"kr"` Korean, `"jp"` Japanese. Omit for Latin scripts. +- **`200` means accepted, not printed.** Both print endpoints are asynchronous. Don't tell a + user "your ticket printed" based on the SDK call resolving — see + [`docs/concepts/delivery-and-idempotency.md`](docs/concepts/delivery-and-idempotency.md). +- **No de-duplication.** Retrying a print request after a network error can produce two + physical tickets. Track `request_uid` if you add retry logic. +- **`printer_msg` / `usb_msg` carries an XML-like tag language**, not HTML — ``, ``, + ``, ``, ``, ``, plus one-shot provisioning tags + (``, ``, ``, ``, ``). Full reference: + [`docs/receipt-layout/text-layout-tags.md`](docs/receipt-layout/text-layout-tags.md). +- **`printer_status` is an activation flag**, not connectivity. `"0"` means suspended by + ExpedyPRINT (usually billing), not "printer is offline". +- Errors are `ExpedyError` (network/config) or `ExpedyApiError` (`status`, `rawBody`, + `requestUid`), both exported from the package root. Always read `err.message` / + `rawBody.message` rather than branching on `status` alone. + +## Where to look + +- **Full API reference**: [`docs/README.md`](docs/README.md) — reading order included. +- **Machine-readable spec**: [`openapi.yaml`](openapi.yaml) — all 16 operations, request/ + response schemas, `printer_han` enum. +- **Runnable examples**: [`examples/`](examples/) — one file per feature, each a complete + standalone script (`node --experimental-strip-types examples/.ts`). +- **SDK source**: `src/client.ts` (HTTP layer, ~130 lines), `src/resources/*.ts` (one + method per endpoint), `src/types/*.ts` (request/response shapes with JSDoc). +- **Canonical docs site**: — same content as `docs/`, plus + hardware setup guides and ~190 integration guides out of this repo's scope (see + [`docs/integrations.md`](docs/integrations.md) for the index). + +## Working on this repository + +- `npm run typecheck` — type-check `src/` only. +- `npm run build` — compile to `dist/`. +- `npm run typecheck:examples` — type-check `examples/` against the compiled types. +- `npm test` — build, then run the test suite (`node --test`, no test framework + dependency). +- Touching a field in `src/types/*.ts`? Update the matching schema in `openapi.yaml` and the + matching page under `docs/api/` in the same change — see + [`CONTRIBUTING.md`](CONTRIBUTING.md). +- This is a **public** repository. Never commit real credentials, UIDs, or internal URLs — + use the placeholder values already used throughout `docs/` and `examples/` + (`WP0RGS1SEDZ`, `MMAAZ112PI`, `example.com`, …). diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9492dac --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,56 @@ +# Changelog + +All notable changes to this project are documented here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to +[Semantic Versioning](https://semver.org/). + +## [1.1.0] + +### Added + +- `printer_han` field on `CreatePrintJobRequest` and `CreateUsbPrintJobRequest` — required + to print Chinese, Japanese or Korean text. Without it, CJK characters are silently + replaced with `?` before the job reaches the printer. New `PrinterHan` / + `PrinterHanScript` exported types, new + [`docs/receipt-layout/asian-characters.md`](docs/receipt-layout/asian-characters.md) + reference page, and two new runnable examples + (`examples/receipt-asian-characters.ts`, `examples/device-rpi-usb-print-asian.ts`). +- `docs/getting-started/errors.md` — SDK error types, status codes by endpoint, retry + guidance. +- `docs/concepts/delivery-and-idempotency.md` — what a `200` response actually guarantees, + and how to avoid double prints. +- `docs/integrations.md` — index of no-code / e-commerce / delivery platforms that connect + to Expedy PRINT. +- `openapi.yaml` — OpenAPI 3.1 description of all 16 API operations. +- `AGENTS.md` and `llms.txt` for coding agents and LLM-based tools. +- `CONTRIBUTING.md` and `SECURITY.md`. +- Test suite (`test/client.test.mjs`, Node's built-in test runner, no dependencies) and a + `ci.yml` GitHub Actions workflow (Node 18 / 20 / 22). +- JSDoc across `src/types/*.ts` clarifying field semantics that were previously undocumented + in code (e.g. `printer_status` as an activation flag, not a connectivity check). + +### Changed + +- `CreatePrintJobResponse.request_timestamp` is now optional. The field is returned by the + API but is not part of the documented response contract. + +## [1.0.2] — 2026-06-10 + +### Fixed + +- Dropped `/fr/` from `expedy.io` links in the README (the site auto-localizes); fixed + Cloud Print Box and support URLs. + +## [1.0.1] — 2026-06-10 + +### Added + +- Supply-chain / provenance verification note in the README (`npm audit signatures`). + +## [1.0.0] — 2026-06-05 + +### Added + +- Initial public release: `ExpedyClient` with `printers` and `devices` resources + (`system`, `usb`, `wifi`), TypeScript types, and the full `docs/` reference. +- GitHub Actions publish workflow with npm provenance (OIDC trusted publishing). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..31bb258 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contributing + +Thanks for considering a contribution to `expedy-sdk-node`. + +## Development + +```bash +npm ci +npm run typecheck # type-check src/ +npm run build # compile to dist/ +npm run typecheck:examples # type-check examples/ against the compiled types +npm test # build, then run the test suite +``` + +The test suite (`test/*.test.mjs`) uses Node's built-in test runner against the compiled +`dist/` output — no test framework dependency. `ExpedyClient` accepts a `fetch` +implementation in its config, which the tests use to mock HTTP calls without a network +connection. + +## Keeping things in sync + +This repository carries three parallel descriptions of the same API surface: + +- `src/types/*.ts` — the TypeScript types the SDK actually returns/accepts. +- `openapi.yaml` — the machine-readable spec, used by tooling and by other-language clients. +- `docs/api/**/*.md` — the human-readable reference. + +**If you add, rename, or change the semantics of a request/response field, update all +three in the same change.** A mismatch between the SDK types and `openapi.yaml` is worse +than no spec at all. + +## Style + +- No comments explaining *what* code does — names should do that. JSDoc is for the *why* + or for behavior a reader could not otherwise guess (see the `printer_han` fields in + `src/types/*.ts` for the bar to meet). +- Match the existing resource/method shape in `src/resources/*.ts` when adding an endpoint: + one method per operation, `RequestOptions` as the last parameter, `encodeURIComponent` + around every path segment. +- Examples under `examples/` must be runnable as-is with + `node --experimental-strip-types examples/.ts` given the right environment + variables — keep them self-contained. + +## This is a public repository + +Never commit real credentials, UIDs, tokens, or internal URLs. Use the placeholder values +already used throughout the codebase (`WP0RGS1SEDZ`, `MMAAZ112PI`, `example.com`, …). + +## Reporting a security issue + +See [SECURITY.md](SECURITY.md) — please do not open a public issue for a vulnerability. diff --git a/README.md b/README.md index 2f772e8..fc0eaa3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # expedy-sdk-node [![npm version](https://img.shields.io/npm/v/expedy-sdk-node.svg)](https://www.npmjs.com/package/expedy-sdk-node) +[![npm downloads](https://img.shields.io/npm/dm/expedy-sdk-node.svg)](https://www.npmjs.com/package/expedy-sdk-node) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/LICENSE) [![types: TypeScript](https://img.shields.io/badge/types-TypeScript-3178c6.svg)](https://www.typescriptlang.org/) @@ -42,22 +43,43 @@ console.log(`Queued job ${request_uid}`); Full walkthrough: [docs/getting-started/quickstart.md](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/getting-started/quickstart.md). +## Chinese, Japanese, Korean + +CJK text needs the `printer_han` field or it prints as `?` — no single-byte code page +carries Hanzi, Kana or Hangul, so without it every such character is silently replaced +before the job reaches the printer. + +```ts +await client.printers.createPrintJob(printerUid, { + printer_msg: "주문 #1234
", + printer_han: "kr", // "cn" Chinese · "kr" Korean · "jp" Japanese +}); +``` + +Details, gotchas and examples: [docs/receipt-layout/asian-characters.md](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/receipt-layout/asian-characters.md). + ## Documentation -The complete reference lives under [`docs/`](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/README.md). Key entry points: +The complete reference lives under [`docs/`](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/README.md), and the same content is published at [docs.expedy.io](https://docs.expedy.io/). Key entry points: - [Printers vs. devices](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/concepts/printers-vs-devices.md) — which resource to use. - [Authentication](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/getting-started/authentication.md) — `Authorization: :`. - [Create a print job](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/api/printers/create-print-job.md) — flagship endpoint. - [Text layout tags](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/receipt-layout/text-layout-tags.md) — full tag reference. +- [Asian characters](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/receipt-layout/asian-characters.md) — `printer_han` for Chinese, Japanese, Korean. - [Device actions](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/device-actions/autocut.md) — ``, ``. - [Parameter tags](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/parameter-tags/wifi.md) — Wi-Fi, NTP, APN, keep-alive, audible beep. +- [Delivery and idempotency](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/concepts/delivery-and-idempotency.md) — what `200` means, and how to avoid double prints. +- [Errors](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/getting-started/errors.md) — status codes and the `ExpedyApiError` shape. +- [Integrations index](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/integrations.md) — no-code / e-commerce / delivery platforms (Zapier, Shopify, WooCommerce, Uber Eats, …). +- [`openapi.yaml`](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/openapi.yaml) — OpenAPI 3.1 spec for all 14 endpoints. +- [`AGENTS.md`](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/AGENTS.md) — condensed reference for coding agents (Claude Code, Cursor, Copilot…). ## SDK surface ```ts client.printers.list(); -client.printers.createPrintJob(printerUid, { printer_msg, origin? }); +client.printers.createPrintJob(printerUid, { printer_msg, origin?, printer_han? }); client.devices.list(); client.devices.get(deviceUid); @@ -70,7 +92,7 @@ client.devices.system.shutdown(deviceUid); client.devices.usb.getConfiguration(deviceUid); client.devices.usb.scan(deviceUid); client.devices.usb.readScan(deviceUid); -client.devices.usb.createPrintJob(deviceUid, usbPort, { usb_msg, notification_url?, origin? }); +client.devices.usb.createPrintJob(deviceUid, usbPort, { usb_msg, notification_url?, origin?, printer_han? }); client.devices.wifi.getConfiguration(deviceUid); client.devices.wifi.addSsid(deviceUid, { wifi_ssid, wifi_psk }); diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..f1f1e8e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,39 @@ +# Security Policy + +## Reporting a vulnerability + +Please **do not** open a public GitHub issue for a suspected security vulnerability. +Instead, report it through the +[Expedy support portal](https://help.expedy.io/support/tickets/new), or through GitHub's +[private vulnerability reporting](https://github.com/ExpedyDev/expedy-sdk-node/security/advisories/new) +if enabled on this repository. + +Include enough detail to reproduce the issue: affected version, environment, and a minimal +example. + +## Credentials + +`apiSid` and `apiToken` (the `Authorization: :` pair) are secrets: + +- Store them in a secrets manager or environment variable — never in a client bundle or + committed to source control. +- Rotate the token from the [Expedy console](https://www.expedy.fr/console/) if it has ever + been logged, committed, or shared by accident. +- This repository, its `docs/` and its `examples/` never contain real credentials — + everything is a placeholder (`WP0RGS1SEDZ`, `MMAAZ112PI`, environment variable + references). + +## Supply chain + +Releases are published from GitHub Actions with +[npm provenance](https://docs.npmjs.com/generating-provenance-statements) — a signed +attestation linking each published version to its source commit and build. Verify it with: + +```bash +npm audit signatures +``` + +## Supported versions + +Only the latest published `1.x` release is supported. Security fixes are released as a new +patch or minor version — please upgrade rather than pinning to an old version. diff --git a/docs/README.md b/docs/README.md index 7c57134..aead91b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,8 @@ Welcome. This documentation covers the **Expedy Print API v2**, the HTTP surface 2. [Concepts → Printer compatibility](./concepts/compatibility.md) — short, honest list of what's known to work today. 3. [Getting started → Authentication](./getting-started/authentication.md) — how the `Authorization` header is built. 4. [Getting started → Quickstart](./getting-started/quickstart.md) — send your first print job in ~10 lines of Node. +5. [Concepts → Delivery and idempotency](./concepts/delivery-and-idempotency.md) — what a `200` response actually guarantees, and how to avoid double prints. +6. [Getting started → Errors](./getting-started/errors.md) — status codes and the `ExpedyApiError` shape. ## API reference @@ -37,6 +39,7 @@ Everything below gets embedded **inside the `printer_msg` string** you POST to ` - [QR code](./receipt-layout/qr-code.md) - [EAN-13 barcode](./receipt-layout/ean13-barcode.md) - [PDF](./receipt-layout/print-pdf.md) +- [Asian characters (Chinese / Japanese / Korean) — `printer_han`](./receipt-layout/asian-characters.md) - [Remove the "Printed using Expedy.io" footer](./receipt-layout/expedy-mention.md) ### Device actions @@ -58,3 +61,15 @@ Everything below gets embedded **inside the `printer_msg` string** you POST to ` - [Generic notification](./samples/generic-notification.md) Business-specific samples (restaurant, photo booth, promo QR, etc.) live in separate repositories. + +## Integrations + +Not building a custom integration? [`docs/integrations.md`](./integrations.md) indexes the +no-code / e-commerce / delivery platforms that connect to Expedy PRINT without writing any +code (Zapier, Make, n8n, Shopify, WooCommerce, Uber Eats, and more). + +## For AI agents + +[`AGENTS.md`](../AGENTS.md) at the repository root is a condensed reference meant to be read +by coding agents (Claude Code, Cursor, Copilot…) — the two resources, the auth format, the +`printer_han` pitfall, and where to find the rest. diff --git a/docs/api/devices/usb/create-usb-print-job.md b/docs/api/devices/usb/create-usb-print-job.md index b3d0d25..f7586e6 100644 --- a/docs/api/devices/usb/create-usb-print-job.md +++ b/docs/api/devices/usb/create-usb-print-job.md @@ -26,8 +26,9 @@ Content-Type: `application/json` | Field | Type | Required | Description | | --- | --- | --- | --- | | `usb_msg` | string | yes | Payload sent to the printer. For ESC/POS thermal printers, this accepts the **same tag language** as [`printer_msg`](../../printers/create-print-job.md#building-the-printer_msg-payload). For label / PDF printers, send the **raw HTTPS URL** of the PDF (no tag wrapper — see [Print PDF](../../../receipt-layout/print-pdf.md)). | -| `notification_url` | string | no | Webhook URL that Expedy will hit to report the job's outcome. | +| `notification_url` | string | no | URL the print service calls once it has handed the job to the printer. | | `origin` | string | no | Free-form identifier echoed in the Expedy console. | +| `printer_han` | string | no | Script used to compose the receipt: `cn` Chinese, `kr` Korean, `jp` Japanese (`1` accepted as a synonym of `cn`). Omit for Latin scripts. Same rules as on the `printers` endpoint — see [Asian characters](../../../receipt-layout/asian-characters.md). | ```json { @@ -37,11 +38,27 @@ Content-Type: `application/json` } ``` +### Chinese, Japanese and Korean text + +CJK characters need the `printer_han` field or they print as `?` — see +[Asian characters](../../../receipt-layout/asian-characters.md) for the full explanation. + +```ts +await client.devices.usb.createPrintJob(deviceUid, 1, { + usb_msg: "注文 #1234
", + printer_han: "jp", +}); +``` + ## Response — 200 OK +A `200` confirms the job was **accepted and queued**, not that it printed — delivery is +asynchronous and the printer may be offline. Pass `notification_url` to know what actually +happened. See [delivery and idempotency](../../../concepts/delivery-and-idempotency.md). + | Field | Type | Description | | --- | --- | --- | -| `last_ping` | number | Unix timestamp (seconds) of the last device ping. | +| `last_ping` | number | Unix timestamp (seconds) of the device's last contact with the server. A value far in the past means the device was not online when you sent the job. | | `request_uid` | string | Unique identifier for the queued job. | ```json @@ -70,3 +87,9 @@ await client.devices.usb.createPrintJob("MMAAZ112PI", 1, { origin: "warehouse/outbound", }); ``` + +## See also + +- [Asian characters](../../../receipt-layout/asian-characters.md) — `printer_han` for Chinese, Japanese and Korean. +- [Delivery and idempotency](../../../concepts/delivery-and-idempotency.md) +- [Errors](../../../getting-started/errors.md) diff --git a/docs/api/printers/create-print-job.md b/docs/api/printers/create-print-job.md index 4827374..4093fac 100644 --- a/docs/api/printers/create-print-job.md +++ b/docs/api/printers/create-print-job.md @@ -24,6 +24,7 @@ Content-Type: `application/json` | --- | --- | --- | --- | | `printer_msg` | string | yes | The ticket content. Plain UTF-8 text mixed with XML-like tags that drive layout, actions and printer parameters. See [how to build a ticket](#building-the-printer_msg-payload). | | `origin` | string | no | Free-form identifier echoed in the Expedy console — useful to trace which system / feature / order emitted the job (e.g. `"pos/checkout"`, `"kitchen/prep-slip"`). | +| `printer_han` | string | no | Script used to compose the receipt: `cn` Chinese, `kr` Korean, `jp` Japanese (`1` accepted as a synonym of `cn`). Omit for Latin scripts. **Required for Chinese/Japanese/Korean text** — without it every such character prints as `?`. See [Asian characters](../../receipt-layout/asian-characters.md). | ### Building the `printer_msg` payload @@ -40,12 +41,27 @@ Full tag reference: - [Autocut](../../device-actions/autocut.md) — `` - Parameter tags: [Wi-Fi](../../parameter-tags/wifi.md), [audible beep](../../parameter-tags/audible-beep.md), [NTP](../../parameter-tags/ntp-clock.md), [APN](../../parameter-tags/apn-mobile-data.md), [keep-alive](../../parameter-tags/keep-alive.md) +### Chinese, Japanese and Korean text + +CJK characters need the `printer_han` field or they print as `?` — see +[Asian characters](../../receipt-layout/asian-characters.md) for the full explanation and examples. + +```ts +await client.printers.createPrintJob(printerUid, { + printer_msg: "주문 #1234
", + printer_han: "kr", +}); +``` + ## Response — 200 OK +A `200` confirms the job was **accepted and queued** — not that it printed. See +[delivery and idempotency](../../concepts/delivery-and-idempotency.md). + | Field | Type | Description | | --- | --- | --- | | `request_uid` | string | Unique identifier for the queued print job. | -| `request_timestamp` | string | Unix timestamp (seconds) when the platform accepted the job. | +| `request_timestamp` | string | Unix timestamp (seconds) when the platform accepted the job. Returned by the API but not part of the documented response contract — treat it as optional. | ```json { @@ -107,4 +123,7 @@ const response = await fetch( ## See also - [Printers vs. devices](../../concepts/printers-vs-devices.md) — when to use this endpoint instead of the USB variant. +- [Asian characters](../../receipt-layout/asian-characters.md) — `printer_han` for Chinese, Japanese and Korean. +- [Delivery and idempotency](../../concepts/delivery-and-idempotency.md) — what `200` actually means, and how to avoid double prints. +- [Errors](../../getting-started/errors.md) — status codes and the `ExpedyApiError` shape. - [Generic receipt sample](../../samples/generic-receipt.md) — a full `printer_msg` with logo, text, QR and cut. diff --git a/docs/api/printers/list-printers.md b/docs/api/printers/list-printers.md index 106a4df..aa9c840 100644 --- a/docs/api/printers/list-printers.md +++ b/docs/api/printers/list-printers.md @@ -18,11 +18,17 @@ Array of printer objects. | --- | --- | --- | | `printer_uid` | string | Unique ID used in [`POST /printers/{printer_uid}/print`](./create-print-job.md). | | `printer_name` | string | Human name configured in the Expedy console (e.g. `"Lobby"`, `"Kitchen"`). | -| `printer_status` | string | Numeric status flag. | +| `printer_status` | string | `"1"` active, `"0"` suspended. | | `printer_width` | string | Paper width in millimetres (`"58"`, `"80"`, `"104"`). | -| `printer_graphic_mode` | string | Selected graphic mode for image rendering. Set in the Expedy console. | +| `printer_graphic_mode` | string | `"0"` Graphics (default), `"1"` BitImageRaster, `"2"` BitImageColumn. Set in the Expedy console. | | `printer_print_mode` | string | Selected print mode. Set in the Expedy console. | +> ℹ️ **`printer_status` is an activation flag, not a live connectivity check.** It is an +> administrative flag controlled solely by ExpedyPRINT — `"0"` means the printer has been +> suspended (usually a billing issue) and will not print until reactivated. To verify that +> an active printer is physically reachable, send a test print rather than relying on this +> field. + ```json [ { "printer_uid": "UP3VS5JXRYA", "printer_name": "Lobby", diff --git a/docs/concepts/delivery-and-idempotency.md b/docs/concepts/delivery-and-idempotency.md new file mode 100644 index 0000000..19503cd --- /dev/null +++ b/docs/concepts/delivery-and-idempotency.md @@ -0,0 +1,61 @@ +# Delivery and idempotency + +## A `200` means "accepted", not "printed" + +Both print endpoints are asynchronous. A `200 OK` confirms the job was **received and +queued** by the Expedy platform — it does not guarantee paper output. + +- **`printers`** — the server hands the job to the cloud printer the next time it connects. + The printer may be offline, out of paper, powered off, or unreachable on its network / SIM + at that moment. +- **`devices`** (Raspberry Pi + USB printer) — same asynchronous handoff, plus the extra hop + from the Pi to the USB printer. Pass `notification_url` to be told once the job has + actually been handed to the printer, rather than assuming it printed. + +Use the returned `request_uid` to reference the job in your own logs and in support +requests — it is your correlation ID between "I sent this" and "what actually happened". + +## No de-duplication + +Every accepted request produces a print. **Neither endpoint de-duplicates.** If you retry a +request after a network error, and the first attempt actually reached the server, you will +get two tickets. + +Guard against this on your side: + +- Track `request_uid` per business event (order, ticket, label) and skip re-sending once + you have one. +- Mark the order "printed" only after a `200` — not optimistically before the call. +- Only retry on a network error or a `5xx` — see [Errors](../getting-started/errors.md). A + `4xx` will not become printable by resending the same body. + +## Signals to watch + +| Signal | Endpoint | What it tells you | +| --- | --- | --- | +| `request_uid` | both | Correlate the call with what shows up in the [console](https://www.expedy.fr/console/) print history. | +| `last_ping` | `devices` | Unix timestamp of the device's last contact with the server. Far in the past ⇒ the device was not online when you sent the job. | +| `notification_url` callback | `devices` | Fired once the job is actually handed to the USB printer — the closest thing to a delivery confirmation available today. | +| `printer_status` | `GET /printers/all` | An **administrative** activation flag (`"1"` active, `"0"` suspended), not live connectivity. Send a test print to check reachability. | + +## USB ports need prior configuration + +A USB port on a Raspberry Pi gateway only accepts jobs once a printer has been **detected +and configured** on it. If the port is empty, or was never configured, the job is rejected. +Use [`GET /devices/{device_uid}/usb/scan`](../api/devices/usb/scan-ports.md) followed by +[`GET /devices/{device_uid}/usb/scan/read`](../api/devices/usb/get-scan-result.md) to +discover what is plugged in, then [`GET /devices/{device_uid}/usb/conf`](../api/devices/usb/get-configuration.md) +to confirm the saved configuration before printing — see +[`examples/device-rpi-usb-print.ts`](../../examples/device-rpi-usb-print.ts) for the pattern. + +## Keep content within the paper width + +`printer_msg` / `usb_msg` is not wrapped for you: 32 characters per line at 58 mm, 48 at +80 mm (fewer for CJK text — see [Asian characters](../receipt-layout/asian-characters.md)). +See the [layout reference](../receipt-layout/text-layout-tags.md). + +## See also + +- [Errors](../getting-started/errors.md) +- [Create a print job](../api/printers/create-print-job.md) +- [Create a USB print job](../api/devices/usb/create-usb-print-job.md) diff --git a/docs/getting-started/errors.md b/docs/getting-started/errors.md new file mode 100644 index 0000000..45c65aa --- /dev/null +++ b/docs/getting-started/errors.md @@ -0,0 +1,96 @@ +# Errors + +## SDK error types + +`client.request()` throws one of two error classes, both exported from the package root. + +### `ExpedyError` + +Thrown for anything that happens **before** a response is available: a network failure, or +missing `apiSid` / `apiToken` at construction time. + +```ts +export class ExpedyError extends Error { + readonly cause?: unknown; +} +``` + +### `ExpedyApiError` + +Thrown for any **non-2xx** HTTP response. Extends `ExpedyError`. + +```ts +export class ExpedyApiError extends ExpedyError { + readonly status: number; // HTTP status code + readonly rawBody: unknown; // parsed JSON body, or the raw text if parsing failed + readonly requestUid?: string; // present when the response body included a request_uid +} +``` + +```ts +import { ExpedyApiError, ExpedyError } from "expedy-sdk-node"; + +try { + await client.printers.createPrintJob(printerUid, { printer_msg: "Hi" }); +} catch (err) { + if (err instanceof ExpedyApiError) { + console.error(`Expedy API ${err.status}: ${err.message}`); + } else if (err instanceof ExpedyError) { + console.error(`Network / config error: ${err.message}`, err.cause); + } else { + throw err; + } +} +``` + +**Always read the `message` field rather than relying on the status code alone.** The API +returns a JSON envelope on every non-2xx response: + +```json +{ "message": "Invalid printer" } +``` + +`err.message` (built by the SDK) already embeds this text; `err.rawBody` gives you the +parsed envelope if you need to branch on more than the message string. + +## Status codes by endpoint + +### `POST /printers/{printer_uid}/print` + +| Status | Meaning | +| --- | --- | +| `401` / `403` | Missing or invalid credentials (`SID` / `TOKEN`). | +| `422` | The request could not be processed — e.g. an unknown `printer_uid` or a malformed body. | + +### `GET /printers/all` + +| Status | Meaning | +| --- | --- | +| `401` / `403` | Missing or invalid credentials. | + +### `POST /devices/{device_uid}/usb/{usb_port}/print` + +| Status | Meaning | +| --- | --- | +| `403` | Missing or invalid credentials, or the device does not belong to this account. | +| `404` | Unknown `device_uid`, or no configured printer on that `usb_port`. | +| `405` | Wrong HTTP method — this endpoint is `POST` only. | +| `422` | Empty or malformed `usb_msg`. | +| `500` | The job could not be handed to the device. Retry, then contact [support](https://help.expedy.io/support/tickets/new) if it persists. | + +Other `devices` endpoints (`ping`, `reboot`, Wi-Fi configuration, …) share the same +`401` / `403` credential errors; consult each endpoint's page under +[`docs/api/devices/`](../api/devices/) for anything endpoint-specific. + +## Retry guidance + +Only retry on a network error (`ExpedyError` without a `status`) or a `5xx` +`ExpedyApiError`. A `4xx` means the request itself is invalid — retrying it unchanged will +fail again. See [delivery and idempotency](../concepts/delivery-and-idempotency.md) before +adding any retry logic, since the print endpoints do not de-duplicate. + +## See also + +- [Delivery and idempotency](../concepts/delivery-and-idempotency.md) +- [Create a print job](../api/printers/create-print-job.md) +- [Create a USB print job](../api/devices/usb/create-usb-print-job.md) diff --git a/docs/integrations.md b/docs/integrations.md new file mode 100644 index 0000000..a3d1033 --- /dev/null +++ b/docs/integrations.md @@ -0,0 +1,184 @@ +# Integrations + +Already using an e-commerce platform, a delivery aggregator, or a no-code automation tool? +There is a good chance you don't need to write any code at all — Expedy PRINT ships +first-party or Zapier/Make-based integrations for the platforms below. + +**Use this index instead of the SDK when** your order source is one of these platforms and +you just need "print the order automatically" — no custom logic, no non-standard ticket +layout. **Use the SDK (this repository) instead when** you're building your own backend, +need custom ticket formatting (the [tag language](./receipt-layout/text-layout-tags.md)), +need Chinese/Japanese/Korean support (`printer_han`), or need the [`devices`](./concepts/printers-vs-devices.md) +resource to drive a non-thermal printer over a Raspberry Pi gateway. + +> These pages live on the canonical docs site, not in this repository — `displays` and +> `medias` integrations included. Links point to `docs.expedy.io`; if a platform has since +> renamed or moved, search [docs.expedy.io](https://docs.expedy.io/) directly. + +### E-commerce & storefronts + +- [AquilaCMS](https://docs.expedy.io/en/integrations/aquilacms/connect-a-receipt-printer-to-aquilacms) — Print AquilaCMS orders on a receipt printer +- [BaseLinker](https://docs.expedy.io/en/integrations/baselinker/connect-a-receipt-printer-to-baselinker) — Print from BaseLinker with Expedy PRINT +- [Big Cartel](https://docs.expedy.io/en/integrations/big-cartel/connect-a-receipt-printer-to-big-cartel) — Print from Big Cartel with Expedy PRINT +- [BigCommerce](https://docs.expedy.io/en/integrations/bigcommerce/connect-a-receipt-printer-to-bigcommerce) — Print from BigCommerce with Expedy PRINT +- [CS-Cart](https://docs.expedy.io/en/integrations/cs-cart/connect-a-receipt-printer-to-cs-cart) — Print from CS-Cart with Expedy PRINT +- [Ecwid by Lightspeed](https://docs.expedy.io/en/integrations/ecwid/connect-a-receipt-printer-to-ecwid) — Print from Ecwid by Lightspeed with Expedy PRINT +- [GrazeCart](https://docs.expedy.io/en/integrations/grazecart/connect-a-receipt-printer-to-grazecart) — Print from GrazeCart with Expedy PRINT +- [Gumroad](https://docs.expedy.io/en/integrations/gumroad/connect-a-receipt-printer-to-gumroad) — Print from Gumroad with Expedy PRINT +- [HoneyCart](https://docs.expedy.io/en/integrations/honeycart/connect-a-receipt-printer-to-honeycart) — Print from HoneyCart with Expedy PRINT +- [Jumpseller](https://docs.expedy.io/en/integrations/jumpseller/connect-a-receipt-printer-to-jumpseller) — Print from Jumpseller with Expedy PRINT +- [Katana](https://docs.expedy.io/en/integrations/katana/connect-a-receipt-printer-to-katana) — Print from Katana with Expedy PRINT +- [Lemon Squeezy](https://docs.expedy.io/en/integrations/lemon-squeezy/connect-a-receipt-printer-to-lemon-squeezy) — Print from Lemon Squeezy with Expedy PRINT +- [Magento](https://docs.expedy.io/en/integrations/magento/connect-a-receipt-printer-to-magento) — Print from Magento with Expedy PRINT +- [Prestashop](https://docs.expedy.io/en/integrations/prestashop/prestashop-module-cloud-print-orders-automatically) — PrestaShop Module: Automatic Order Printing +- [Salla](https://docs.expedy.io/en/integrations/salla/connect-a-receipt-printer-to-salla) — Print from Salla with Expedy PRINT +- [SamCart](https://docs.expedy.io/en/integrations/samcart/connect-a-receipt-printer-to-samcart) — Print from SamCart with Expedy PRINT +- [Shift4Shop](https://docs.expedy.io/en/integrations/shift4shop/connect-a-receipt-printer-to-shift4shop) — Print from Shift4Shop with Expedy PRINT +- **Shopify** + - [Shopify plugins : Cloud Print orders automatically](https://docs.expedy.io/en/integrations/shopify/shopify-plugins-cloud-print-orders-automatically) + - [Order Print Settings : Receipt customisation](https://docs.expedy.io/en/integrations/shopify/order-print-settings-receipt-customisation) + - [Print a copy of the order](https://docs.expedy.io/en/integrations/shopify/print-a-copy-of-the-order) + - [Add a logo to the Shopify order form](https://docs.expedy.io/en/integrations/shopify/add-a-logo-to-the-shopify-order-form) + - [Shopify and Zapier, print only the orders you choose](https://docs.expedy.io/en/integrations/shopify/connect-shopify-to-expedy-print-with-zapier) +- [SHOPLINE](https://docs.expedy.io/en/integrations/shopline/connect-a-receipt-printer-to-shopline) — Print from SHOPLINE with Expedy PRINT +- [Squarespace Commerce](https://docs.expedy.io/en/integrations/squarespace-commerce/connect-a-receipt-printer-to-squarespace-commerce) — Print from Squarespace Commerce with Expedy PRINT +- [Store Factory](https://docs.expedy.io/en/integrations/store-factory/connect-a-receipt-printer-to-store-factory) — Print Store Factory orders on a receipt printer +- [Stripe](https://docs.expedy.io/en/integrations/stripe/connect-a-receipt-printer-to-stripe) — Print from Stripe with Expedy PRINT +- [ThriveCart](https://docs.expedy.io/en/integrations/thrivecart/connect-a-receipt-printer-to-thrivecart) — Print from ThriveCart with Expedy PRINT +- [Webflow](https://docs.expedy.io/en/integrations/webflow/connect-a-receipt-printer-to-webflow) — Print from Webflow with Expedy PRINT +- [Wix](https://docs.expedy.io/en/integrations/wix/connect-a-receipt-printer-to-wix) — Print from Wix with Expedy PRINT +- [WiziShop](https://docs.expedy.io/en/integrations/wizishop/connect-a-receipt-printer-to-wizishop) — Print from WiziShop with Expedy PRINT +- **Wordpress | Woocommerce** + - [WordPress/WooCommerce Extension Plugin : Print orders automatically](https://docs.expedy.io/en/integrations/wordpress-woocommerce/wordpress-woocommerce-extension-plugin-print-orders-automatically) + - [WCFM Plugin : Print orders to each vendor printer](https://docs.expedy.io/en/integrations/wordpress-woocommerce/wcfm-woocommerce-plugin-print-orders-to-each-vendor-printer) + - [WooCommerce and Zapier, print only the orders you choose](https://docs.expedy.io/en/integrations/wordpress-woocommerce/connect-woocommerce-to-expedy-print-with-zapier) +- [Zoho Inventory](https://docs.expedy.io/en/integrations/zoho-inventory/connect-a-receipt-printer-to-zoho-inventory) — Print from Zoho Inventory with Expedy PRINT + +### Food delivery & restaurant POS + +- [App4](https://docs.expedy.io/en/integrations/app4/connect-a-receipt-printer-to-app4) — Print App4 orders on a receipt printer +- [Barmade](https://docs.expedy.io/en/integrations/barmade/connect-a-receipt-printer-to-barmade) — Receipt printing with Barmade +- [Bowo](https://docs.expedy.io/en/integrations/bowo/connect-a-receipt-printer-to-bowo) — Receipt printing with Bowo +- [BWares](https://docs.expedy.io/en/integrations/bwares/connect-a-receipt-printer-to-bwares) — Print BWares orders on a receipt printer +- [Châtaigne](https://docs.expedy.io/en/integrations/chataigne/connect-a-receipt-printer-to-chataigne) — Receipt printing with Châtaigne +- [Delicity](https://docs.expedy.io/en/integrations/delicity/connect-a-receipt-printer-to-delicity) — Print Delicity orders on a receipt printer +- [Deliveroo](https://docs.expedy.io/en/integrations/deliveroo/connect-a-receipt-printer-to-deliveroo) — Print Deliveroo orders on a receipt printer +- [Delivery Hero](https://docs.expedy.io/en/integrations/delivery-hero/connect-a-receipt-printer-to-delivery-hero) — Print Delivery Hero orders on a receipt printer +- [Dishop](https://docs.expedy.io/en/integrations/dishop/connect-a-receipt-printer-to-dishop) — Print Dishop orders on a receipt printer +- [DOOD](https://docs.expedy.io/en/integrations/dood/connect-a-receipt-printer-to-dood) — Print DOOD orders on a receipt printer +- [DoorDash](https://docs.expedy.io/en/integrations/doordash/connect-a-receipt-printer-to-doordash) — Print DoorDash orders on a receipt printer +- [eddress](https://docs.expedy.io/en/integrations/eddress/connect-a-receipt-printer-to-eddress) — Print eddress orders on a receipt printer +- [eEatself](https://docs.expedy.io/en/integrations/eeatself/connect-a-receipt-printer-to-eeatself) — Print eEatself orders on a receipt printer +- [Flex Catering](https://docs.expedy.io/en/integrations/flex-catering/connect-a-receipt-printer-to-flex-catering) — Print from Flex Catering with Expedy PRINT +- [Flipdish](https://docs.expedy.io/en/integrations/flipdish/connect-a-receipt-printer-to-flipdish) — Print from Flipdish with Expedy PRINT +- [Foodpanda](https://docs.expedy.io/en/integrations/foodpanda/connect-a-receipt-printer-to-foodpanda) — Print Foodpanda orders on a receipt printer +- [Formitable](https://docs.expedy.io/en/integrations/formitable/connect-a-receipt-printer-to-formitable) — Print from Formitable with Expedy PRINT +- [Fresh KDS](https://docs.expedy.io/en/integrations/fresh-kds/connect-a-receipt-printer-to-fresh-kds) — Print from Fresh KDS with Expedy PRINT +- [Glovo](https://docs.expedy.io/en/integrations/glovo/connect-a-receipt-printer-to-glovo) — Print Glovo orders on a receipt printer +- [GonnaOrder](https://docs.expedy.io/en/integrations/gonnaorder/connect-a-receipt-printer-to-gonnaorder) — Print GonnaOrder orders on a receipt printer +- [GoodBarber](https://docs.expedy.io/en/integrations/goodbarber/print-goodbarber-orders-on-a-receipt-printer) — Print GoodBarber eCommerce orders via Zapier +- [Hop Delivery](https://docs.expedy.io/en/integrations/hop-delivery/connect-a-receipt-printer-to-hop-delivery) — Print Hop Delivery orders on a receipt printer +- [HubRise](https://docs.expedy.io/en/integrations/hubrise/connecting-a-receipt-printer-to-hubrise) — Connect a receipt printer to HubRise +- [Just Eat](https://docs.expedy.io/en/integrations/just-eat/connect-a-receipt-printer-to-just-eat) — Print Just Eat orders on a receipt printer +- [Just Eat Takeaway](https://docs.expedy.io/en/integrations/just-eat-takeaway/connect-a-receipt-printer-to-just-eat-takeaway) — Print Just Eat Takeaway orders on a receipt printer +- [Kurve Kiosks](https://docs.expedy.io/en/integrations/kurve-kiosks/connect-a-receipt-printer-to-kurve-kiosks) — Print Kurve Kiosks orders on a receipt printer +- [La Toque Magique](https://docs.expedy.io/en/integrations/la-toque-magique/connect-a-receipt-printer-to-la-toque-magique) — Receipt printing with La Toque Magique +- [Lieferando](https://docs.expedy.io/en/integrations/lieferando/connect-a-receipt-printer-to-lieferando) — Print Lieferando orders on a receipt printer +- [LightKitch](https://docs.expedy.io/en/integrations/lightkitch/connect-a-receipt-printer-to-lightkitch) — Receipt printing with LightKitch +- [LivePepper](https://docs.expedy.io/en/integrations/livepepper/connect-a-receipt-printer-to-livepepper) — Print LivePepper orders on a receipt printer +- [Loca'Touch](https://docs.expedy.io/en/integrations/locatouch/connect-a-receipt-printer-to-locatouch) — Print Loca'Touch orders on a receipt printer +- [Love2Food](https://docs.expedy.io/en/integrations/love2food/connect-a-receipt-printer-to-love2food) — Receipt printing with Love2Food +- [Menulog](https://docs.expedy.io/en/integrations/menulog/connect-a-receipt-printer-to-menulog) — Print Menulog orders on a receipt printer +- [MynOber](https://docs.expedy.io/en/integrations/mynober/connect-a-receipt-printer-to-mynober) — Print MynOber orders on a receipt printer +- [Obypay](https://docs.expedy.io/en/integrations/obypay/connect-a-receipt-printer-to-obypay) — Print Obypay orders on a receipt printer +- [OrderLemon](https://docs.expedy.io/en/integrations/orderlemon/connect-a-receipt-printer-to-orderlemon) — Receipt printing with OrderLemon +- [Ordermate](https://docs.expedy.io/en/integrations/ordermate/connect-a-receipt-printer-to-ordermate) — Print from Ordermate with Expedy PRINT +- [OrderOut](https://docs.expedy.io/en/integrations/orderout/connect-a-receipt-printer-to-orderout) — Print from OrderOut with Expedy PRINT +- [Pyszne](https://docs.expedy.io/en/integrations/pyszne/connect-a-receipt-printer-to-pyszne) — Print Pyszne orders on a receipt printer +- [RestaJet](https://docs.expedy.io/en/integrations/restajet/connect-a-receipt-printer-to-restajet) — Print RestaJet orders on a receipt printer +- [Restaur'App](https://docs.expedy.io/en/integrations/restaur-app/connect-a-receipt-printer-to-restaur-app) — Print Restaur'App orders on a receipt printer +- [Restaurant-internet](https://docs.expedy.io/en/integrations/restaurant-internet/connect-a-receipt-printer-to-restaurant-internet) — Print Restaurant-internet orders on a receipt printer +- [Servier.bar](https://docs.expedy.io/en/integrations/servier-bar/connect-a-receipt-printer-to-servier-bar) — Print Servier.bar orders on a receipt printer +- [Skip The Dishes](https://docs.expedy.io/en/integrations/skip-the-dishes/connect-a-receipt-printer-to-skip-the-dishes) — Print Skip The Dishes orders on a receipt printer +- [SmartResto.Net](https://docs.expedy.io/en/integrations/smartresto/connect-a-receipt-printer-to-smartresto) — Print SmartResto.Net orders on a receipt printer +- [smilein](https://docs.expedy.io/en/integrations/smilein/connect-a-receipt-printer-to-smilein) — Print smilein orders on a receipt printer +- [Smood](https://docs.expedy.io/en/integrations/smood/connect-a-receipt-printer-to-smood) — Print Smood orders on a receipt printer +- [Tablati](https://docs.expedy.io/en/integrations/tablati/connect-a-receipt-printer-to-tablati) — Print Tablati orders on a receipt printer +- [Talabat](https://docs.expedy.io/en/integrations/talabat/connect-a-receipt-printer-to-talabat) — Print Talabat orders on a receipt printer +- [TastyCloud](https://docs.expedy.io/en/integrations/tastycloud/connect-a-receipt-printer-to-tastycloud) — Print TastyCloud orders on a receipt printer +- [Thuisbezorgd](https://docs.expedy.io/en/integrations/thuisbezorgd/connect-a-receipt-printer-to-thuisbezorgd) — Print Thuisbezorgd orders on a receipt printer +- [Uber Eats](https://docs.expedy.io/en/integrations/uber-eats/connect-a-receipt-printer-to-uber-eats) — Print Uber Eats orders on a receipt printer +- [WeDely](https://docs.expedy.io/en/integrations/wedely/connect-a-receipt-printer-to-wedely) — Print WeDely orders on a receipt printer +- [Wolt](https://docs.expedy.io/en/integrations/wolt/connect-a-receipt-printer-to-wolt) — Print Wolt orders on a receipt printer +- [Zenorder](https://docs.expedy.io/en/integrations/zenorder/connect-a-receipt-printer-to-zenorder) — Receipt printing with Zenorder +- [Zuplyit](https://docs.expedy.io/en/integrations/zuplyit/connect-a-receipt-printer-to-zuplyit) — Print Zuplyit orders on a receipt printer + +### No-code / iPaaS automation + +- [Activepieces](https://docs.expedy.io/en/integrations/activepieces/connect-a-receipt-printer-to-activepieces) — Print from Activepieces with Expedy PRINT +- [Airtable](https://docs.expedy.io/en/integrations/airtable/connect-a-receipt-printer-to-airtable) — Print from Airtable with Expedy PRINT +- [Albato](https://docs.expedy.io/en/integrations/albato/connect-a-receipt-printer-to-albato) — Print from Albato with Expedy PRINT +- [Bubble](https://docs.expedy.io/en/integrations/bubble/connect-a-receipt-printer-to-bubble) — Print from Bubble with Expedy PRINT +- [Google Sheets](https://docs.expedy.io/en/integrations/google-sheets/connect-a-receipt-printer-to-google-sheets) — Print from Google Sheets with Expedy PRINT +- [HubSpot](https://docs.expedy.io/en/integrations/hubspot/connect-a-receipt-printer-to-hubspot) — Print from HubSpot with Expedy PRINT +- [IFTTT](https://docs.expedy.io/en/integrations/ifttt/connect-a-receipt-printer-to-ifttt) — Print from IFTTT with Expedy PRINT +- [Integrately](https://docs.expedy.io/en/integrations/integrately/connect-a-receipt-printer-to-integrately) — Print from Integrately with Expedy PRINT +- [Jotform](https://docs.expedy.io/en/integrations/jotform/connect-a-receipt-printer-to-jotform) — Print from Jotform with Expedy PRINT +- [Latenode](https://docs.expedy.io/en/integrations/latenode/connect-a-receipt-printer-to-latenode) — Print from Latenode with Expedy PRINT +- [Make](https://docs.expedy.io/en/integrations/make/connect-a-receipt-printer-to-make) — Print from Make with Expedy PRINT +- [Microsoft Power Automate](https://docs.expedy.io/en/integrations/power-automate/connect-a-receipt-printer-to-power-automate) — Print from Microsoft Power Automate with Expedy PRINT +- [n8n](https://docs.expedy.io/en/integrations/n8n/connect-a-receipt-printer-to-n8n) — Print from n8n with Expedy PRINT +- [Pabbly Connect](https://docs.expedy.io/en/integrations/pabbly-connect/connect-a-receipt-printer-to-pabbly-connect) — Print from Pabbly Connect with Expedy PRINT +- [Pipedream](https://docs.expedy.io/en/integrations/pipedream/connect-a-receipt-printer-to-pipedream) — Print from Pipedream with Expedy PRINT +- [Pipedrive](https://docs.expedy.io/en/integrations/pipedrive/connect-a-receipt-printer-to-pipedrive) — Print from Pipedrive with Expedy PRINT +- [Relay.app](https://docs.expedy.io/en/integrations/relay-app/connect-a-receipt-printer-to-relay-app) — Print from Relay.app with Expedy PRINT +- [Retool](https://docs.expedy.io/en/integrations/retool/connect-a-receipt-printer-to-retool) — Print from Retool with Expedy PRINT +- [Softr](https://docs.expedy.io/en/integrations/softr/connect-a-receipt-printer-to-softr) — Print from Softr with Expedy PRINT +- [Tally](https://docs.expedy.io/en/integrations/tally/connect-a-receipt-printer-to-tally) — Print from Tally with Expedy PRINT +- [Typeform](https://docs.expedy.io/en/integrations/typeform/connect-a-receipt-printer-to-typeform) — Print from Typeform with Expedy PRINT +- [WeWeb](https://docs.expedy.io/en/integrations/weweb/connect-a-receipt-printer-to-weweb) — Print from WeWeb with Expedy PRINT +- [Xano](https://docs.expedy.io/en/integrations/xano/connect-a-receipt-printer-to-xano) — Print from Xano with Expedy PRINT +- [Zapier](https://docs.expedy.io/en/integrations/zapier/how-to-integrate-expedy-print-with-zapier) — How to integrate Expedy Print with Zapier + +### Shipping, labels & fulfillment + +- [Easyship](https://docs.expedy.io/en/integrations/easyship/connect-a-receipt-printer-to-easyship) — Print your Easyship shipping labels automatically +- [inFlow Inventory](https://docs.expedy.io/en/integrations/inflow-inventory/connect-a-receipt-printer-to-inflow-inventory) — Print from inFlow Inventory with Expedy PRINT +- [Order Desk](https://docs.expedy.io/en/integrations/order-desk/connect-a-receipt-printer-to-order-desk) — Print from Order Desk with Expedy PRINT +- [Packlink PRO](https://docs.expedy.io/en/integrations/packlink-pro/connect-a-receipt-printer-to-packlink-pro) — Print your Packlink PRO shipping labels automatically +- [shipcloud](https://docs.expedy.io/en/integrations/shipcloud/connect-a-receipt-printer-to-shipcloud) — Print your shipcloud shipping labels automatically +- [Shipmondo](https://docs.expedy.io/en/integrations/shipmondo/connect-a-receipt-printer-to-shipmondo) — Print your Shipmondo shipping labels automatically +- [Shippo](https://docs.expedy.io/en/integrations/shippo/connect-a-receipt-printer-to-shippo) — Print your Shippo shipping labels automatically +- [ShippyPro](https://docs.expedy.io/en/integrations/shippypro/connect-a-receipt-printer-to-shippypro) — Print your ShippyPro shipping labels automatically +- [ShipStation](https://docs.expedy.io/en/integrations/shipstation/connect-a-receipt-printer-to-shipstation) — Print your ShipStation shipping labels automatically +- [Starshipit](https://docs.expedy.io/en/integrations/starshipit/connect-a-receipt-printer-to-starshipit) — Print your Starshipit shipping labels automatically +- [Veeqo](https://docs.expedy.io/en/integrations/veeqo/connect-a-receipt-printer-to-veeqo) — Print from Veeqo with Expedy PRINT + +### POS, retail & inventory + +- [Amazon Seller Central](https://docs.expedy.io/en/integrations/amazon-seller-central/connect-a-receipt-printer-to-amazon-seller-central) — Print from Amazon Seller Central with Expedy PRINT +- [eBay](https://docs.expedy.io/en/integrations/ebay/connect-a-receipt-printer-to-ebay) — Print from eBay with Expedy PRINT +- [EKM](https://docs.expedy.io/en/integrations/ekm/connect-a-receipt-printer-to-ekm) — Print from EKM with Expedy PRINT +- [Lightspeed Retail POS](https://docs.expedy.io/en/integrations/lightspeed-retail/connect-a-receipt-printer-to-lightspeed-retail) — Print from Lightspeed Retail POS with Expedy PRINT +- [Square](https://docs.expedy.io/en/integrations/square/connect-a-receipt-printer-to-square) — Print from Square with Expedy PRINT + +### Bookings, forms & CRM + +- [Acuity Scheduling](https://docs.expedy.io/en/integrations/acuity-scheduling/connect-a-receipt-printer-to-acuity-scheduling) — Print from Acuity Scheduling with Expedy PRINT +- [Calendly](https://docs.expedy.io/en/integrations/calendly/connect-a-receipt-printer-to-calendly) — Print from Calendly with Expedy PRINT +- [Eventbrite](https://docs.expedy.io/en/integrations/eventbrite/connect-a-receipt-printer-to-eventbrite) — Print from Eventbrite with Expedy PRINT + +### Photo & cloud storage + +- [Dropbox](https://docs.expedy.io/en/integrations/dropbox/connect-a-receipt-printer-to-dropbox) — Print your Dropbox photos automatically +- [Flickr](https://docs.expedy.io/en/integrations/flickr/connect-a-receipt-printer-to-flickr) — Print your Flickr photos automatically +- [Google Drive](https://docs.expedy.io/en/integrations/google-drive/connect-a-receipt-printer-to-google-drive) — Print your Google Drive photos automatically +- [Instagram](https://docs.expedy.io/en/integrations/instagram/connect-a-receipt-printer-to-instagram) — Print your Instagram photos automatically +- [Pinterest](https://docs.expedy.io/en/integrations/pinterest/connect-a-receipt-printer-to-pinterest) — Print your Pinterest photos automatically +- [Tumblr](https://docs.expedy.io/en/integrations/tumblr/connect-a-receipt-printer-to-tumblr) — Print your Tumblr photos automatically + +## See also + +- [Printers vs. devices](./concepts/printers-vs-devices.md) — when a custom SDK + integration beats a no-code one. +- [Quickstart](./getting-started/quickstart.md) — build your own integration in ~10 lines. diff --git a/docs/receipt-layout/asian-characters.md b/docs/receipt-layout/asian-characters.md new file mode 100644 index 0000000..f0d5685 --- /dev/null +++ b/docs/receipt-layout/asian-characters.md @@ -0,0 +1,149 @@ +# Asian characters — `printer_han` + +Chinese, Japanese and Korean need the `printer_han` field, set to the script you are +printing. It is accepted on **both** print endpoints: + +- [`POST /printers/{printer_uid}/print`](../api/printers/create-print-job.md) — cloud thermal printer +- [`POST /devices/{device_uid}/usb/{usb_port}/print`](../api/devices/usb/create-usb-print-job.md) — printer plugged into a Raspberry Pi gateway + +| Value | Script | +| --- | --- | +| `cn` | Chinese (Hanzi) | +| `kr` | Korean (Hangul) | +| `jp` | Japanese (Kana / Kanji) | + +`1` is still accepted as a synonym of `cn`, for older integrations. + +## Why your CJK text prints as `?` + +By default the receipt is composed in **single-byte mode**: each character is mapped +through one of the printer's code pages. No single-byte code page contains Hanzi, Kana or +Hangul, so **without this field every such character is replaced with a `?` before the job +even reaches the device**. + +This is the single most common cause of a ticket full of `????` — the printer is fine, the +job was already mangled server-side. + +```ts +await client.printers.createPrintJob(printerUid, { + printer_msg: "주문 #1234
", + printer_han: "kr", +}); +``` + +## Three rules + +### 1. The value has to match the script + +Each value selects a different encoding and they do not overlap. Korean sent as `cn` comes +out as `?`, exactly as if the field had been left out. + +```ts +// ❌ Korean text, Chinese encoding → ? +{ printer_msg: "주문 #1234", printer_han: "cn" } + +// ✅ +{ printer_msg: "주문 #1234", printer_han: "kr" } +``` + +If a single receipt mixes scripts, only the one matching `printer_han` renders — split the +job or keep the other script out of the ticket. + +### 2. The printer has to carry the matching font + +`printer_han` switches the data stream to multi-byte mode; the glyphs themselves come from +the **printer's font ROM**. A model shipped without that font will not print the characters +even with the right value — and a printer sold for the Chinese market carries Hanzi, which +does not mean it carries Hangul or Kana. + +Test the exact script you need on the exact model you deploy. If the result is not +readable, [contact support](https://help.expedy.io/support/tickets/new). + +### 3. Latin text does not need it + +Leave `printer_han` out for European languages — accented characters (`é`, `ü`, `ñ`, `ç`) +are handled in the default single-byte mode. + +If accents specifically come out wrong on an Expedy cloud printer, that is a **code page** +problem, not a `printer_han` one: set the printer's code page to `CP437` with the +PrinterSetting software. See +[Text encoding settings](https://docs.expedy.io/en/expedy-print/installation/text-encoding-settings). + +## Always send UTF-8 + +Send your content as UTF-8 in every mode. The SDK serializes the request body with +`JSON.stringify` and `fetch` encodes it as UTF-8, so a JavaScript string containing CJK +needs no special handling on your side. + +The API stores and returns exactly what it receives, so the **print history in the +[console](https://www.expedy.fr/console/) shows the text as it arrived** — the quickest way +to tell a data problem from a printer one: + +- History shows `????` → the problem is upstream of Expedy (your encoding, your database). +- History shows `주문 #1234` but the paper shows `????` → `printer_han` is missing or wrong. +- History and paper both show `주문 #1234`, paper shows blanks or garbage glyphs → the + printer is missing the font. + +## Line width + +The 32 / 48 characters-per-line limits documented in +[text layout tags](./text-layout-tags.md) describe single-byte Latin text. In multi-byte +mode a CJK glyph typically occupies the width of two Latin characters, and the exact +result depends on the printer's font. Lay out CJK receipts conservatively and confirm on +the target model. + +## Full examples + +### Cloud thermal printer + +```ts +import { ExpedyClient } from "expedy-sdk-node"; + +const client = new ExpedyClient({ + apiSid: process.env.EXPEDY_API_SID!, + apiToken: process.env.EXPEDY_API_TOKEN!, +}); + +await client.printers.createPrintJob(process.env.EXPEDY_PRINTER_UID!, { + printer_msg: [ + "주문 #1234", + "
", + "테이블 7", + "
", + "비빔밥 x1", + "
", + "김치찌개 x2", + "
", + "", + ].join(""), + printer_han: "kr", + origin: "pos/kitchen", +}); +``` + +### ESC/POS printer on a Raspberry Pi gateway + +```ts +await client.devices.usb.createPrintJob(deviceUid, 1, { + usb_msg: "注文 #1234
", + printer_han: "jp", + origin: "pos/kitchen", +}); +``` + +### Typed values + +`printer_han` is typed, so a typo is caught at compile time: + +```ts +import type { PrinterHanScript } from "expedy-sdk-node"; + +const script: PrinterHanScript = "kr"; // "cn" | "kr" | "jp" +``` + +## See also + +- [Create a print job](../api/printers/create-print-job.md) +- [Create a USB print job](../api/devices/usb/create-usb-print-job.md) +- [Text layout tags](./text-layout-tags.md) +- [Delivery and idempotency](../concepts/delivery-and-idempotency.md) diff --git a/docs/receipt-layout/text-layout-tags.md b/docs/receipt-layout/text-layout-tags.md index b7d3d94..3b36ec7 100644 --- a/docs/receipt-layout/text-layout-tags.md +++ b/docs/receipt-layout/text-layout-tags.md @@ -14,6 +14,7 @@ Receipts are built using XML-like tags embedded in the `printer_msg` (or `usb_ms - A space counts as **1 character**. - A line break is automatically treated as a new line. - Some ESC/POS printers may have different limits depending on font size and encoding set in their firmware. +- These limits describe single-byte Latin text. Printing Chinese, Japanese or Korean requires the `printer_han` field and typically halves the characters that fit per line — see [Asian characters](./asian-characters.md). ## Tags @@ -89,6 +90,12 @@ Opens the cash drawer connected to the printer. See the [open cash drawer](../device-actions/open-cash-drawer.md) page. +### `printer_han` — Chinese, Japanese, Korean + +Not a layout tag — a request body field. Required whenever `printer_msg` / `usb_msg` +contains CJK text, or those characters print as `?`. See the dedicated +[Asian characters](./asian-characters.md) page. + ## Full example ```ts diff --git a/examples/device-rpi-usb-print-asian.ts b/examples/device-rpi-usb-print-asian.ts new file mode 100644 index 0000000..3072feb --- /dev/null +++ b/examples/device-rpi-usb-print-asian.ts @@ -0,0 +1,33 @@ +import { ExpedyClient } from "expedy-sdk-node"; + +const client = new ExpedyClient({ + apiSid: process.env.EXPEDY_API_SID!, + apiToken: process.env.EXPEDY_API_TOKEN!, +}); + +const deviceUid = process.env.EXPEDY_DEVICE_UID!; +const usbPort = Number(process.env.EXPEDY_USB_PORT ?? 1); + +// Discover what is attached first, if needed. +const conf = await client.devices.usb.getConfiguration(deviceUid); +const port = conf.usb_conf.find((p) => p.usb_port === usbPort); +if (!port || port.usb_status !== 1) { + throw new Error(`USB port ${usbPort} is not ready on device ${deviceUid}.`); +} + +// `printer_han` works the same way on ESC/POS printers attached over USB as +// it does on Expedy cloud printers — required for Chinese, Japanese and +// Korean text, and the printer has to carry the matching font in ROM. +// See: docs/receipt-layout/asian-characters.md +const { request_uid } = await client.devices.usb.createPrintJob( + deviceUid, + usbPort, + { + usb_msg: "注文 #1234
コーヒー x1
", + printer_han: "jp", // "cn" Chinese · "kr" Korean · "jp" Japanese + notification_url: "https://api.example.com/webhooks/expedy", + origin: "example/device-rpi-usb-print-asian", + }, +); + +console.log(`Queued USB print job ${request_uid}`); diff --git a/examples/receipt-asian-characters.ts b/examples/receipt-asian-characters.ts new file mode 100644 index 0000000..9e02c26 --- /dev/null +++ b/examples/receipt-asian-characters.ts @@ -0,0 +1,48 @@ +import { ExpedyClient } from "expedy-sdk-node"; + +const client = new ExpedyClient({ + apiSid: process.env.EXPEDY_API_SID!, + apiToken: process.env.EXPEDY_API_TOKEN!, +}); + +// Chinese, Japanese and Korean text needs `printer_han`, or every such +// character is replaced with `?` before the job reaches the printer. +// See: docs/receipt-layout/asian-characters.md + +const printer_msg = [ + "주문 #1234", + "
", + "테이블 7", + "
", + "비빔밥 x1", + "
", + "김치찌개 x2", + "
", + "", +].join(""); + +const { request_uid } = await client.printers.createPrintJob( + process.env.EXPEDY_PRINTER_UID!, + { + printer_msg, + printer_han: "kr", // "cn" Chinese · "kr" Korean · "jp" Japanese + origin: "example/receipt-asian-characters", + }, +); + +console.log(`Queued print job ${request_uid}`); + +// Japanese: +// await client.printers.createPrintJob(printerUid, { +// printer_msg: "注文 #1234
", +// printer_han: "jp", +// }); + +// Chinese: +// await client.printers.createPrintJob(printerUid, { +// printer_msg: "订单 #1234
", +// printer_han: "cn", +// }); + +// Latin text (accented characters included) never needs `printer_han` — +// leave the field out entirely. diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..3c99a7b --- /dev/null +++ b/llms.txt @@ -0,0 +1,64 @@ +# expedy-sdk-node + +> Official Node.js SDK + API documentation for the Expedy Print API v2. Send print jobs +> (including Chinese, Japanese and Korean text) to Expedy cloud thermal receipt printers +> and to Raspberry Pi gateways driving third-party USB printers. Alternative to Google +> Cloud Print. + +Two hardware resources: `printers` (Expedy cloud thermal receipt printers) and `devices` +(Raspberry Pi gateways driving USB printers). `displays` and `medias` are out of scope. + +## Start here + +- [README](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/README.md): install, + quickstart, SDK surface. +- [AGENTS.md](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/AGENTS.md): condensed + reference for coding agents — the non-obvious gotchas (auth format, `printer_han`, `200` + ≠ printed). +- [docs/README.md](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/README.md): + full documentation, with a recommended reading order. +- [openapi.yaml](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/openapi.yaml): + OpenAPI 3.1 description of all 16 operations. + +## Core concepts + +- [Printers vs. devices](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/concepts/printers-vs-devices.md): + which resource to use. +- [Authentication](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/getting-started/authentication.md): + `Authorization: :`, no Bearer/Basic prefix. +- [Delivery and idempotency](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/concepts/delivery-and-idempotency.md): + a `200` means accepted and queued, not printed; no de-duplication. +- [Errors](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/getting-started/errors.md): + `ExpedyError` / `ExpedyApiError`, status codes by endpoint. + +## Building a receipt payload + +- [Text layout tags](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/receipt-layout/text-layout-tags.md): + `
`, ``, ``, ``, ``, ``, ``, ``. +- [Images and logos](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/receipt-layout/image-and-logo-printing.md): + ``. +- [QR code](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/receipt-layout/qr-code.md): + ``. +- [EAN-13 barcode](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/receipt-layout/ean13-barcode.md): + ``. +- [PDF](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/receipt-layout/print-pdf.md): + ``. +- [Asian characters](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/receipt-layout/asian-characters.md): + `printer_han` for Chinese, Japanese and Korean — without it, CJK text prints as `?`. +- [Parameter tags](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/parameter-tags/wifi.md): + Wi-Fi, NTP, APN, keep-alive, audible beep. + +## Examples + +- [examples/](https://github.com/ExpedyDev/expedy-sdk-node/tree/main/examples): one + runnable TypeScript file per feature — quickstart, receipt layout tags, images, QR, + barcodes, Asian characters, device actions, Wi-Fi provisioning, Raspberry Pi USB + printing. + +## Optional + +- [Integrations index](https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/integrations.md): + no-code / e-commerce / delivery platforms that connect to Expedy PRINT without writing + code. +- [Canonical documentation site](https://docs.expedy.io/): same content, plus hardware + setup and maintenance guides out of this repository's scope. diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..b7c83ea --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,739 @@ +openapi: 3.1.0 +info: + title: Expedy Print API + version: "2.0.0" + summary: >- + Send print jobs to Expedy cloud thermal receipt printers and to Raspberry + Pi gateways driving third-party USB printers. + description: >- + Unofficial-but-maintained OpenAPI description of the Expedy Print API v2, + published alongside the official Node.js SDK + (https://github.com/ExpedyDev/expedy-sdk-node). Covers the two hardware + resources exposed by the API: `printers` (Expedy cloud thermal receipt + printers) and `devices` (Raspberry Pi gateways driving USB printers). + `displays` and `medias` are out of scope. + license: + name: MIT + url: https://github.com/ExpedyDev/expedy-sdk-node/blob/main/LICENSE + contact: + name: Expedy + url: https://www.expedy.io +externalDocs: + description: Full documentation + url: https://docs.expedy.io/ +servers: + - url: https://www.expedy.fr/api/v2 + description: Production + +security: + - expedyAuth: [] + +tags: + - name: printers + description: Expedy cloud thermal receipt printers. + - name: devices + description: Raspberry Pi gateways. + - name: system + description: Remote device control (ping, reboot, shutdown, firmware update). + - name: usb + description: USB ports on a Raspberry Pi gateway and the printers attached to them. + - name: wifi + description: Wi-Fi configuration of a Raspberry Pi gateway. + +paths: + /printers/all: + get: + operationId: listPrinters + summary: List printers + description: Return every Expedy cloud thermal printer owned by the authenticated account. + tags: [printers] + responses: + "200": + description: Array of printer objects. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Printer" + example: + - printer_uid: UP3VS5JXRYA + printer_name: Lobby + printer_status: "1" + printer_width: "58" + printer_graphic_mode: "0" + printer_print_mode: "0" + - printer_uid: MMAAZ112PI + printer_name: Kitchen + printer_status: "1" + printer_width: "80" + printer_graphic_mode: "0" + printer_print_mode: "0" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /printers/{printer_uid}/print: + post: + operationId: createPrintJob + summary: Create a print job + description: >- + Queue a ticket on an Expedy cloud thermal receipt printer. A `200` + confirms the job was accepted and queued — not that it printed. + Delivery is asynchronous. + tags: [printers] + parameters: + - $ref: "#/components/parameters/PrinterUid" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePrintJobRequest" + example: + printer_msg: "ORDER #1234
" + origin: pos-kitchen-01 + responses: + "200": + description: Job accepted and queued. + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePrintJobResponse" + example: + request_uid: 2XVXPN95E7RHYFJZCGMK8DSB634 + request_timestamp: "1776680582" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "422": + $ref: "#/components/responses/UnprocessableEntity" + + /devices/all: + get: + operationId: listDevices + summary: List devices + description: Return every Raspberry Pi gateway owned by the authenticated account. + tags: [devices] + responses: + "200": + description: Array of device objects. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Device" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /devices/{device_uid}: + get: + operationId: getDevice + summary: Get a device + description: Return the summary and status of a single Raspberry Pi gateway. + tags: [devices] + parameters: + - $ref: "#/components/parameters/DeviceUid" + responses: + "200": + description: Device summary. + content: + application/json: + schema: + $ref: "#/components/schemas/Device" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /devices/{device_uid}/ping: + get: + operationId: pingDevice + summary: Ping a device + description: >- + Ask the device to ping back the platform. Combined with a subsequent + read of `last_ping`, this is a reliable way to confirm the device is + online. + tags: [system] + parameters: + - $ref: "#/components/parameters/DeviceUid" + responses: + "200": + $ref: "#/components/responses/LastPingOk" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /devices/{device_uid}/update: + get: + operationId: updateDeviceFirmware + summary: Update device firmware + description: Initiate a software update on the device if any is available. + tags: [system] + parameters: + - $ref: "#/components/parameters/DeviceUid" + responses: + "200": + $ref: "#/components/responses/LastPingOk" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /devices/{device_uid}/reboot: + get: + operationId: rebootDevice + summary: Reboot a device + description: Trigger a remote reboot of the Raspberry Pi. + tags: [system] + parameters: + - $ref: "#/components/parameters/DeviceUid" + responses: + "200": + $ref: "#/components/responses/LastPingOk" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /devices/{device_uid}/shutdown: + get: + operationId: shutdownDevice + summary: Shut a device down + description: >- + Shut the Raspberry Pi down remotely. Use with care: once shut down, + the device cannot be started again without physical access. + tags: [system] + parameters: + - $ref: "#/components/parameters/DeviceUid" + responses: + "200": + $ref: "#/components/responses/LastPingOk" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /devices/{device_uid}/usb/conf: + get: + operationId: getUsbConfiguration + summary: Get USB configuration + description: Return the last known USB port configuration of the device. + tags: [usb] + parameters: + - $ref: "#/components/parameters/DeviceUid" + responses: + "200": + description: Last known configuration of every USB port. + content: + application/json: + schema: + $ref: "#/components/schemas/UsbConfigurationResponse" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /devices/{device_uid}/usb/scan: + get: + operationId: scanUsbPorts + summary: Scan USB ports + description: >- + Trigger a fresh scan of the device's USB ports for any compatible + printer connected. Use `GET /usb/scan/read` afterwards to read the + result. + tags: [usb] + parameters: + - $ref: "#/components/parameters/DeviceUid" + responses: + "200": + $ref: "#/components/responses/LastPingOk" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /devices/{device_uid}/usb/scan/read: + get: + operationId: getUsbScanResult + summary: Get USB scan result + description: Return the device's last known USB scan result. + tags: [usb] + parameters: + - $ref: "#/components/parameters/DeviceUid" + responses: + "200": + description: Same shape as `GET /usb/conf`. + content: + application/json: + schema: + $ref: "#/components/schemas/UsbConfigurationResponse" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /devices/{device_uid}/usb/{usb_port}/print: + post: + operationId: createUsbPrintJob + summary: Create a USB print job + description: >- + Queue a print job on a printer attached to one of the device's USB + ports. A `200` confirms the job was accepted and queued — not that it + printed. A port only accepts jobs once a printer has been detected and + configured on it. + tags: [usb] + parameters: + - $ref: "#/components/parameters/DeviceUid" + - name: usb_port + in: path + required: true + description: USB port number (typically `1` to `4`). + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateUsbPrintJobRequest" + example: + usb_msg: "ORDER #1234
" + notification_url: https://www.example.com/print-callback + origin: pos-kitchen-01 + responses: + "200": + description: Job accepted and queued. + content: + application/json: + schema: + $ref: "#/components/schemas/CreateUsbPrintJobResponse" + example: + last_ping: 1641509604 + request_uid: 1X5ERXL94BYVWHP92DK3MCASUGJ + "403": + $ref: "#/components/responses/Forbidden" + "404": + description: Unknown `device_uid`, or no configured printer on that `usb_port`. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" + "405": + description: Wrong HTTP method — this endpoint is `POST` only. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" + "422": + description: Empty or malformed `usb_msg`. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" + "500": + description: The job could not be handed to the device. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" + + /devices/{device_uid}/wifi/conf: + get: + operationId: getWifiConfiguration + summary: Get Wi-Fi configuration + description: Return the Wi-Fi SSIDs currently stored on the device. PSKs are masked. + tags: [wifi] + parameters: + - $ref: "#/components/parameters/DeviceUid" + responses: + "200": + description: Stored Wi-Fi configuration. + content: + application/json: + schema: + $ref: "#/components/schemas/WifiConfigurationResponse" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /devices/{device_uid}/wifi/add: + put: + operationId: addWifiSsid + summary: Add an SSID + description: >- + Add a new SSID to the device's Wi-Fi configuration. The change is not + applied until `GET /wifi/update` is called. + tags: [wifi] + parameters: + - $ref: "#/components/parameters/DeviceUid" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AddSsidRequest" + responses: + "200": + description: Same shape as `GET /wifi/conf`. + content: + application/json: + schema: + $ref: "#/components/schemas/WifiConfigurationResponse" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /devices/{device_uid}/wifi/{wifi_id}/del: + patch: + operationId: deleteWifiSsid + summary: Delete an SSID + description: >- + Remove an SSID from the device's Wi-Fi configuration. The change is + not applied until `GET /wifi/update` is called. + tags: [wifi] + parameters: + - $ref: "#/components/parameters/DeviceUid" + - name: wifi_id + in: path + required: true + description: ID of the SSID entry to remove, as returned by `GET /wifi/conf`. + schema: + type: integer + responses: + "200": + description: Same shape as `GET /wifi/conf`. + content: + application/json: + schema: + $ref: "#/components/schemas/WifiConfigurationResponse" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + /devices/{device_uid}/wifi/update: + get: + operationId: applyWifiUpdate + summary: Apply Wi-Fi update + description: Push any pending Wi-Fi configuration change (add / delete SSID) to the device. + tags: [wifi] + parameters: + - $ref: "#/components/parameters/DeviceUid" + responses: + "200": + $ref: "#/components/responses/LastPingOk" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + +components: + securitySchemes: + expedyAuth: + type: apiKey + in: header + name: Authorization + description: >- + Raw `:` value, colon-separated, with **no** + `Bearer` or `Basic` prefix. Both values are issued in the Expedy + console under **API**. + + parameters: + PrinterUid: + name: printer_uid + in: path + required: true + description: >- + Unique ID of the target printer. Obtain it from `GET /printers/all` + or the Expedy console. + schema: + type: string + example: WP0RGS1SEDZ + DeviceUid: + name: device_uid + in: path + required: true + description: The device's unique ID, as found in your Expedy account. + schema: + type: string + example: MMAAZ112PI + + responses: + Unauthorized: + description: Missing or invalid credentials (`SID` / `TOKEN`). + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" + Forbidden: + description: Missing or invalid credentials, or the resource does not belong to this account. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" + UnprocessableEntity: + description: The request could not be processed — e.g. an unknown UID or a malformed body. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" + LastPingOk: + description: Command accepted. + content: + application/json: + schema: + $ref: "#/components/schemas/DeviceLastPing" + example: + last_ping: 1641509604 + + schemas: + ErrorEnvelope: + type: object + description: Returned on every non-2xx response. Always read `message` rather than relying on the status code alone. + required: [message] + properties: + message: + type: string + example: Invalid printer + + PrinterHan: + type: string + description: >- + Multi-byte script used to compose the job. Required for Chinese, + Japanese and Korean text — without it every such character is + replaced with `?` before the job reaches the printer. Omit for Latin + scripts. `1` is accepted as a legacy synonym of `cn`. + enum: [cn, kr, jp, "1"] + + Printer: + type: object + required: + - printer_uid + - printer_name + - printer_status + - printer_width + - printer_graphic_mode + - printer_print_mode + properties: + printer_uid: + type: string + description: Unique ID used in `POST /printers/{printer_uid}/print`. + example: UP3VS5JXRYA + printer_name: + type: string + description: Human name configured in the Expedy console. + example: Lobby + printer_status: + type: string + description: >- + `"1"` active, `"0"` suspended. An administrative flag controlled + solely by ExpedyPRINT — not a live connectivity indicator. + example: "1" + printer_width: + type: string + description: Paper width in millimetres. + enum: ["58", "80", "104"] + printer_graphic_mode: + type: string + description: "`\"0\"` Graphics (default), `\"1\"` BitImageRaster, `\"2\"` BitImageColumn." + enum: ["0", "1", "2"] + printer_print_mode: + type: string + description: Numeric code for the printer's print mode (device-specific configuration). + + CreatePrintJobRequest: + type: object + required: [printer_msg] + properties: + printer_msg: + type: string + description: >- + The content to print, built with the receipt layout tags (``, + ``, ``, ``, ``, …). + example: "ORDER #1234
" + origin: + type: string + description: Free-form label to tag the source of the job. + printer_han: + $ref: "#/components/schemas/PrinterHan" + + CreatePrintJobResponse: + type: object + required: [request_uid] + properties: + request_uid: + type: string + description: Unique identifier of the accepted print job. + example: 2XVXPN95E7RHYFJZCGMK8DSB634 + request_timestamp: + type: string + description: >- + Unix timestamp (seconds) when the platform accepted the job. + Returned by the API but not part of the documented response + contract — treat as optional. + + Device: + type: object + required: + - device_uid + - version + - last_ping + - rpi_nickname + - rpi_disk_size + - rpi_vid_list + properties: + device_uid: + type: string + description: Unique ID used in all `/devices/{device_uid}/…` endpoints. + example: MMAAZ112PI + version: + type: string + description: Firmware version currently running on the Pi. + last_ping: + type: string + description: Unix timestamp (seconds) of the last successful ping. + rpi_nickname: + type: string + description: Human name configured in the Expedy console. + rpi_disk_size: + type: string + description: Total disk size of the device, in GB. + rpi_vid_list: + type: string + description: >- + Semicolon-separated list of media/display IDs bound to this + device. Belongs to the `displays` / `medias` resources, + documented separately. + + DeviceLastPing: + type: object + required: [last_ping] + properties: + last_ping: + type: integer + description: >- + Unix timestamp (seconds) of the device's last contact with the + server. A value far in the past means the device was not online. + example: 1641509604 + + UsbPortEntry: + type: object + required: [usb_port, usb_status] + properties: + usb_port: + type: integer + description: Port number (1–4). + usb_status: + type: integer + description: "`1` if a printer is detected on this port, `0` otherwise." + device_manufacturer: + type: string + description: Manufacturer string reported by the attached printer. + device_model: + type: string + description: Model string reported by the attached printer. + device_width: + type: integer + description: Printer paper width in millimetres, when detected. + + UsbConfigurationResponse: + type: object + required: [last_ping] + properties: + last_ping: + type: integer + description: Unix timestamp (seconds) of the last device ping. + usb_conf: + type: array + description: One entry per USB port (up to 4, depending on the device model). + items: + $ref: "#/components/schemas/UsbPortEntry" + usb_scan: + type: array + description: Present on `GET /usb/scan/read` responses on some firmware versions. + items: + $ref: "#/components/schemas/UsbPortEntry" + + CreateUsbPrintJobRequest: + type: object + required: [usb_msg] + properties: + usb_msg: + type: string + description: >- + Payload sent to the printer. For ESC/POS thermal printers, the + same tag language as `printer_msg`. For label / PDF printers, the + raw HTTPS URL of the PDF (no tag wrapper). + example: "ORDER #1234
" + notification_url: + type: string + format: uri + description: URL the print service calls once it has handed the job to the printer. + origin: + type: string + description: Free-form label to tag the source of the job. + printer_han: + $ref: "#/components/schemas/PrinterHan" + + CreateUsbPrintJobResponse: + type: object + required: [last_ping, request_uid] + properties: + last_ping: + type: integer + description: >- + Unix timestamp (seconds) of the device's last contact with the + server. A value far in the past means the device was not online + when the job was sent. + request_uid: + type: string + description: Unique identifier of the accepted print job. + example: 1X5ERXL94BYVWHP92DK3MCASUGJ + + WifiEntry: + type: object + required: [wifi_id, wifi_ssid, wifi_psk] + properties: + wifi_id: + type: integer + description: Unique ID used by "delete SSID". + wifi_ssid: + type: string + description: Network name. + wifi_psk: + type: string + description: Pre-shared key (masked with `*` in the response). + + WifiConfigurationResponse: + type: object + required: [last_ping, wifi_conf] + properties: + status: + type: string + last_ping: + type: integer + description: Unix timestamp (seconds) of the last device ping. + wifi_conf: + type: array + items: + $ref: "#/components/schemas/WifiEntry" + + AddSsidRequest: + type: object + required: [wifi_ssid, wifi_psk] + properties: + wifi_ssid: + type: string + description: Network name. + wifi_psk: + type: string + description: Pre-shared key. diff --git a/package-lock.json b/package-lock.json index 4b704f3..9cd172e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "expedy-sdk-node", - "version": "1.0.2", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "expedy-sdk-node", - "version": "1.0.2", + "version": "1.1.0", "license": "MIT", "devDependencies": { "@types/node": "^20.11.0", diff --git a/package.json b/package.json index cb43646..121c198 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "expedy-sdk-node", - "version": "1.0.2", - "description": "Official Node.js SDK for the Expedy Print API v2 — send print jobs to cloud thermal receipt printers and Raspberry Pi gateways.", + "version": "1.1.0", + "description": "Official Node.js SDK for the Expedy Print API v2 — send print jobs (including Chinese/Japanese/Korean text) to cloud thermal receipt printers and Raspberry Pi gateways.", "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", @@ -15,7 +15,9 @@ "files": [ "dist", "README.md", - "LICENSE" + "LICENSE", + "CHANGELOG.md", + "openapi.yaml" ], "sideEffects": false, "engines": { @@ -25,6 +27,7 @@ "build": "tsc -p tsconfig.json", "typecheck": "tsc --noEmit", "typecheck:examples": "tsc --noEmit -p tsconfig.examples.json", + "test": "npm run build && node --test test/*.test.mjs", "clean": "rm -rf dist", "prepack": "npm run clean && npm run build" }, @@ -48,9 +51,17 @@ "raspberry-pi", "raspberry-pi-printer", "label-printer", + "label-printing", "sdk", "node-sdk", - "typescript" + "typescript", + "openapi", + "cjk", + "chinese", + "japanese", + "korean", + "unicode", + "kitchen-printer" ], "license": "MIT", "author": { diff --git a/src/index.ts b/src/index.ts index 4fc73e6..a79cd95 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,11 @@ export type { WifiConfigurationResponse, AddSsidRequest, } from "./types/device.js"; -export type { RequestOptions } from "./types/common.js"; +export type { + RequestOptions, + PrinterHan, + PrinterHanScript, +} from "./types/common.js"; export { PrintersResource, } from "./resources/printers.js"; diff --git a/src/types/common.ts b/src/types/common.ts index cb30a93..8cd4087 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,3 +1,24 @@ export interface RequestOptions { signal?: AbortSignal; } + +/** + * Multi-byte script used to compose a print job — the `printer_han` field. + * + * - `"cn"` — Chinese (Hanzi) + * - `"kr"` — Korean (Hangul) + * - `"jp"` — Japanese (Kana / Kanji) + * + * Omit the field entirely for Latin scripts: accented characters are handled in + * the default single-byte mode. `"1"` is still accepted as a legacy synonym of + * `"cn"`. + * + * @see https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/receipt-layout/asian-characters.md + */ +export type PrinterHanScript = "cn" | "kr" | "jp"; + +/** + * Accepted values of `printer_han`, including the legacy `"1"` synonym of + * `"cn"` kept for backwards compatibility with older integrations. + */ +export type PrinterHan = PrinterHanScript | "1"; diff --git a/src/types/device.ts b/src/types/device.ts index f84e24c..4f5a213 100644 --- a/src/types/device.ts +++ b/src/types/device.ts @@ -1,21 +1,39 @@ +import type { PrinterHan } from "./common.js"; + export interface Device { + /** Unique ID of the device, as shown in the console under **Machines**. */ device_uid: string; + /** Firmware version running on the device. */ version: string; + /** Unix timestamp (seconds) of the device's last contact with the server. */ last_ping: string; + /** Human-readable label set in the Expedy console. */ rpi_nickname: string; + /** Disk size of the device, in gigabytes. */ rpi_disk_size: string; + /** Semicolon-separated list of media/display IDs attached to the device. */ rpi_vid_list: string; } export interface DeviceLastPing { + /** + * Unix timestamp (seconds) of the device's last contact with the server. + * + * A value far in the past means the device was not online. It is the cheapest + * signal available that a device has gone silent — worth reading on every + * call. + */ last_ping: number; } export interface UsbPortEntry { + /** USB port number, `1` to `4`, matching the ports shown in the console. */ usb_port: number; + /** `1` when a printer is detected and configured on the port, `0` otherwise. */ usb_status: number; device_manufacturer?: string; device_model?: string; + /** Printer width in millimetres, when detected. */ device_width?: number; } @@ -31,13 +49,50 @@ export interface UsbScanResponse { } export interface CreateUsbPrintJobRequest { + /** + * Payload sent to the printer. + * + * For ESC/POS thermal printers this accepts the same tag language as + * `printer_msg`. For label / PDF printers, send the raw HTTPS URL of the PDF + * with no tag wrapper. + */ usb_msg: string; + /** + * URL the print service calls once it has handed the job to the printer. Use + * it to close the loop in your own system instead of assuming the job + * printed. + */ notification_url?: string; + /** + * Free-form label identifying the source of the job (a URI, an app name, a + * department…). Echoed in the Expedy console. + */ origin?: string; + /** + * Script to compose the receipt in — required for Chinese, Japanese and + * Korean text. + * + * By default the receipt is composed in single-byte mode, and no single-byte + * code page contains Hanzi, Kana or Hangul: **without this field every such + * character is replaced with a `?` before the job even reaches the printer**. + * + * The value has to match the script — Korean sent as `"cn"` still comes out + * as `?` — and the printer has to carry the matching font in ROM. Leave it + * out for Latin scripts. + * + * @see https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/receipt-layout/asian-characters.md + */ + printer_han?: PrinterHan; } export interface CreateUsbPrintJobResponse { + /** + * Unix timestamp (seconds) of the device's last contact with the server. A + * value far in the past means the device was not online when you sent the + * job. + */ last_ping: number; + /** Unique identifier of the accepted print job. */ request_uid: string; } diff --git a/src/types/printer.ts b/src/types/printer.ts index 70d9f9d..c60a90d 100644 --- a/src/types/printer.ts +++ b/src/types/printer.ts @@ -1,18 +1,67 @@ +import type { PrinterHan } from "./common.js"; + export interface Printer { + /** Unique ID of the printer — pass it as `printer_uid` to `createPrintJob`. */ printer_uid: string; + /** Human-readable label set in the Expedy console (e.g. `"Lobby"`, `"Kitchen"`). */ printer_name: string; + /** + * Activation flag: `"1"` active, `"0"` suspended. + * + * This is an **administrative flag controlled solely by ExpedyPRINT** — not a + * live connectivity indicator. `"0"` means the printer has been suspended and + * will not print until reactivated. To verify that an active printer is + * physically reachable, send a test print instead of reading this field. + */ printer_status: string; - printer_width: string; - printer_graphic_mode: string; + /** Paper width in millimetres: `"58"` (32 chars/line), `"80"` (48), `"104"`. */ + printer_width: "58" | "80" | "104" | (string & {}); + /** + * Image rendering mode configured in the console: + * `"0"` Graphics (default) · `"1"` BitImageRaster · `"2"` BitImageColumn. + */ + printer_graphic_mode: "0" | "1" | "2" | (string & {}); + /** Numeric code for the printer's print mode (device-specific configuration). */ printer_print_mode: string; } export interface CreatePrintJobRequest { + /** + * The ticket content: plain UTF-8 text mixed with the Expedy layout tags + * (``, ``, ``, ``, ``, …). + */ printer_msg: string; + /** + * Free-form label identifying the source of the job (a URI, an app name, a + * department…). Echoed in the Expedy console — useful for filtering and + * debugging. + */ origin?: string; + /** + * Script to compose the receipt in — required for Chinese, Japanese and + * Korean text. + * + * By default the receipt is composed in single-byte mode, and no single-byte + * code page contains Hanzi, Kana or Hangul: **without this field every such + * character is replaced with a `?` before the job even reaches the printer**. + * + * The value has to match the script — Korean sent as `"cn"` still comes out + * as `?` — and the printer has to carry the matching font in ROM. Leave it + * out for Latin scripts. + * + * @see https://github.com/ExpedyDev/expedy-sdk-node/blob/main/docs/receipt-layout/asian-characters.md + */ + printer_han?: PrinterHan; } export interface CreatePrintJobResponse { + /** Unique identifier of the accepted print job. */ request_uid: string; - request_timestamp: string; + /** + * Unix timestamp (seconds) at which the platform accepted the job. + * + * Returned by the API but not part of the documented response contract — + * treat it as optional and do not depend on its presence. + */ + request_timestamp?: string; } diff --git a/test/client.test.mjs b/test/client.test.mjs new file mode 100644 index 0000000..3d8f182 --- /dev/null +++ b/test/client.test.mjs @@ -0,0 +1,219 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + ExpedyClient, + ExpedyApiError, + ExpedyError, + DEFAULT_BASE_URL, +} from "../dist/index.js"; + +function makeClient({ fetchImpl, baseUrl } = {}) { + return new ExpedyClient({ + apiSid: "SID123", + apiToken: "TOKEN456", + ...(baseUrl !== undefined ? { baseUrl } : {}), + fetch: fetchImpl, + }); +} + +function jsonResponse(status, body) { + return new Response(body === undefined ? "" : JSON.stringify(body), { + status, + headers: { "content-type": "application/json" }, + }); +} + +test("constructor throws ExpedyError when credentials are missing", () => { + assert.throws( + () => new ExpedyClient({ apiSid: "", apiToken: "" }), + (err) => err instanceof ExpedyError, + ); +}); + +test("Authorization header is the raw SID:TOKEN value, no prefix", async () => { + let capturedHeaders; + const client = makeClient({ + fetchImpl: async (_url, init) => { + capturedHeaders = init.headers; + return jsonResponse(200, { request_uid: "ABC" }); + }, + }); + + await client.printers.createPrintJob("PRT1", { printer_msg: "Hi" }); + + assert.equal(capturedHeaders.Authorization, "SID123:TOKEN456"); + assert.ok(!capturedHeaders.Authorization.startsWith("Bearer")); + assert.ok(!capturedHeaders.Authorization.startsWith("Basic")); +}); + +test("Content-Type is set only when a body is present", async () => { + const seen = []; + const client = makeClient({ + fetchImpl: async (_url, init) => { + seen.push(init.headers["Content-Type"]); + return jsonResponse(200, { last_ping: 1 }); + }, + }); + + await client.devices.system.ping("DEV1"); // GET, no body + await client.printers.createPrintJob("PRT1", { printer_msg: "Hi" }); // POST, body + + assert.equal(seen[0], undefined); + assert.equal(seen[1], "application/json"); +}); + +test("printer_han travels through the JSON body on printers.createPrintJob", async () => { + let capturedBody; + const client = makeClient({ + fetchImpl: async (_url, init) => { + capturedBody = JSON.parse(init.body); + return jsonResponse(200, { request_uid: "ABC" }); + }, + }); + + await client.printers.createPrintJob("PRT1", { + printer_msg: "주문 #1234", + printer_han: "kr", + }); + + assert.equal(capturedBody.printer_han, "kr"); + assert.equal(capturedBody.printer_msg, "주문 #1234"); +}); + +test("printer_han travels through the JSON body on devices.usb.createPrintJob", async () => { + let capturedBody; + const client = makeClient({ + fetchImpl: async (_url, init) => { + capturedBody = JSON.parse(init.body); + return jsonResponse(200, { last_ping: 1, request_uid: "ABC" }); + }, + }); + + await client.devices.usb.createPrintJob("DEV1", 1, { + usb_msg: "注文 #1234", + printer_han: "jp", + }); + + assert.equal(capturedBody.printer_han, "jp"); +}); + +test("CJK text survives the JSON round-trip as UTF-8", async () => { + let capturedBody; + const client = makeClient({ + fetchImpl: async (_url, init) => { + capturedBody = init.body; + return jsonResponse(200, { request_uid: "ABC" }); + }, + }); + + const text = "주문 #1234 — 김치찌개"; + await client.printers.createPrintJob("PRT1", { + printer_msg: text, + printer_han: "kr", + }); + + const roundTripped = JSON.parse(capturedBody).printer_msg; + assert.equal(roundTripped, text); + assert.deepEqual( + Array.from(new TextEncoder().encode(roundTripped)), + Array.from(new TextEncoder().encode(text)), + ); +}); + +test("UIDs and usb_port are percent-encoded in the URL", async () => { + let capturedUrl; + const client = makeClient({ + fetchImpl: async (url) => { + capturedUrl = url; + return jsonResponse(200, { last_ping: 1, request_uid: "ABC" }); + }, + }); + + await client.devices.usb.createPrintJob("dev/weird uid", "1", { + usb_msg: "Hi", + }); + + assert.ok(capturedUrl.includes(encodeURIComponent("dev/weird uid"))); + assert.ok(!capturedUrl.includes("dev/weird uid/usb")); +}); + +test("trailing slashes are stripped from a custom baseUrl", async () => { + let capturedUrl; + const client = makeClient({ + baseUrl: "https://example.test/api///", + fetchImpl: async (url) => { + capturedUrl = url; + return jsonResponse(200, []); + }, + }); + + await client.printers.list(); + + assert.equal(capturedUrl, "https://example.test/api/printers/all"); +}); + +test("DEFAULT_BASE_URL is used when no baseUrl is provided", async () => { + let capturedUrl; + const client = makeClient({ + fetchImpl: async (url) => { + capturedUrl = url; + return jsonResponse(200, []); + }, + }); + + await client.printers.list(); + + assert.ok(capturedUrl.startsWith(DEFAULT_BASE_URL)); +}); + +test("a non-2xx response throws ExpedyApiError with status, message and rawBody", async () => { + const client = makeClient({ + fetchImpl: async () => jsonResponse(422, { message: "Invalid printer" }), + }); + + await assert.rejects( + () => client.printers.createPrintJob("PRT1", { printer_msg: "Hi" }), + (err) => { + assert.ok(err instanceof ExpedyApiError); + assert.ok(err instanceof ExpedyError); + assert.equal(err.status, 422); + assert.match(err.message, /Invalid printer/); + assert.deepEqual(err.rawBody, { message: "Invalid printer" }); + return true; + }, + ); +}); + +test("requestUid is extracted from the error body when present", async () => { + const client = makeClient({ + fetchImpl: async () => + jsonResponse(500, { message: "boom", request_uid: "XYZ" }), + }); + + await assert.rejects( + () => client.devices.usb.createPrintJob("DEV1", 1, { usb_msg: "Hi" }), + (err) => { + assert.equal(err.requestUid, "XYZ"); + return true; + }, + ); +}); + +test("a network failure throws ExpedyError with the underlying cause", async () => { + const networkError = new Error("getaddrinfo ENOTFOUND"); + const client = makeClient({ + fetchImpl: async () => { + throw networkError; + }, + }); + + await assert.rejects( + () => client.printers.list(), + (err) => { + assert.ok(err instanceof ExpedyError); + assert.ok(!(err instanceof ExpedyApiError)); + assert.equal(err.cause, networkError); + return true; + }, + ); +});