# production-orders-sync Standalone worker that syncs Tiny ERP production orders directly into the existing Graphs Postgres tables used by `GET /api/production-orders`. This is intentionally separate from `api-tiny-n8n`; it does not call n8n and does not depend on the Tiny -> n8n bridge. ## Sync Design Tiny/Olist API 2.0 public webhook docs do not list an official production-order webhook/event. The documented webhook/event surfaces found are for stock updates, product sending, tracking code sending, invoice sending, product price sending, sales order status updates, and freight quote lookup. The production-order API page found is an action endpoint, `gerar.ordem.producao.pedido.php`, which generates production orders from a sales order. That is not a live production-order webhook. Docs checked: - https://tiny.com.br/api-docs/api2-webhooks - https://tiny.com.br/api-docs/api2-webhooks-tiny - https://tiny.com.br/api-docs/api2-webhooks-atualizacao-situacao-pedido - https://tiny.com.br/api-docs/api2-pedidos-gerar-ordem-producao For that reason, this worker is designed around: - backfill mode for historical date ranges - recent polling mode for periodic syncs, usually every 15 or 30 minutes from an external scheduler - idempotent upserts by `tiny_id`, so repeated polling updates existing rows without duplicates An optional future optimization is for a sales-order webhook in the existing middleware to trigger a recent production-order sync. Polling should still remain the source of truth because production-order status can change later without a sales-order webhook. ## What It Writes The worker upserts `production_orders` by `tiny_id`, stores the full Tiny production-order object in `tiny_payload`, updates `updated_at` on every conflict update, and replaces `production_order_markers` for each synced order. It preserves the existing frontend/backend contract: - `GET /api/production-orders?start=YYYY-MM-DD&end=YYYY-MM-DD&search=...` - Response shape: `{ orders, counts }` - Status values: `open`, `in_progress`, `finished`, `canceled` ## Configure Copy `.env.example` to `.env` and fill in: ```sh TINY_API_TOKEN=... DATABASE_URL=postgres://... SYNC_MODE=backfill SYNC_START_DATE=2026-07-02 SYNC_END_DATE=2026-07-02 DRY_RUN=true TINY_REQUEST_DELAY_MS=5000 ``` Tiny production-order endpoint names vary by API/account documentation. I could not confirm public documentation for these exact production-order endpoint names, so treat the defaults as inferred and account-specific until dry-run logs prove the payload shape: ```sh TINY_LIST_ENDPOINT=ordens.producao.pesquisa.php TINY_DETAIL_ENDPOINT=ordem.producao.obter.php TINY_FETCH_DETAILS=false ``` If Tiny returns only summary rows from the list endpoint, set `TINY_FETCH_DETAILS=true` after confirming the detail endpoint for the account. The worker keeps `SYNC_START_DATE` and `SYNC_END_DATE` in `YYYY-MM-DD` format. Tiny request dates default to `DD/MM/YYYY`; override with `TINY_DATE_FORMAT=YYYY-MM-DD` if the endpoint expects ISO dates. Backfill behavior: - `SYNC_MODE=backfill` - `SYNC_START_DATE` is required. - `SYNC_END_DATE` is optional; when empty, the worker syncs through today in `SYNC_TIME_ZONE`, default `America/Sao_Paulo`. Recent polling behavior: - `SYNC_MODE=recent` - `RECENT_SYNC_DAYS=2` syncs today plus yesterday. - Run the same manual command from cron, Swarm, Kubernetes, or another scheduler every 15 or 30 minutes. - Upserts by `tiny_id` keep repeated runs idempotent. This worker does not include its own scheduler yet; deployment should run it manually first, then schedule the same command externally. ## Manual Run ```sh npm install npm run sync ``` For a no-write validation run: ```sh DRY_RUN=true \ SYNC_MODE=backfill \ SYNC_START_DATE=2026-07-02 \ SYNC_END_DATE=2026-07-02 \ TINY_REQUEST_DELAY_MS=5000 \ npm run sync ``` Inspect the dry-run logs before enabling writes. Dry-run logs include the Tiny response shape, discovered array paths, the first extracted raw order preview, and the exact `production_orders` / `production_order_markers` values that would be written. ## Docker Build and run the worker once: ```sh docker build -t production-orders-sync:local . PRODUCTION_ORDERS_SYNC_IMAGE=production-orders-sync:local docker compose run --rm production-orders-sync ``` The optional local Postgres example is in `docker-compose.local-db.yml`. In normal deployment, set `DATABASE_URL` to the existing Graphs Postgres database. ```sh docker compose -f docker-compose.local-db.yml up -d postgres ``` ## Gitea Actions + Portainer CI/CD is defined in `.gitea/workflows/ci-cd.yml`: - run `npm test` - run `npm run check` - build and push the Docker image - trigger the Portainer stack webhook when `PORTAINER_WEBHOOK_URL` is configured Portainer stack templates live in `deploy/`: - `deploy/portainer-one-shot.yml` for first dry-run checks and backfills - `deploy/portainer-stack.yml` for the long-running recent polling stack See `deploy/README.md` for required Gitea secrets and Portainer environment variables. ## Logs The worker logs: - start/end config without secrets - `[Production Orders Sync]` prefix for easy separation from `api-tiny-n8n` - current page, endpoint, date range, and Tiny-formatted request dates - orders found per page - mapped rows and dry-run rows that would be upserted - rows upserted - skipped/invalid rows with raw payload preview - Tiny block/rate-limit messages and retries ## Tiny Field Mapping The mapper accepts several common Tiny/PT-BR field variants. Required fields are: - Tiny production order ID, mapped to `tiny_id` - product description, mapped to `product_description` Status normalization accepts: - `em_aberto`, `em aberto`, `aberta` -> `open` - `andamento`, `em andamento` -> `in_progress` - `finalizada`, `finalizado` -> `finished` - `cancelada`, `cancelado`, `cancelled` -> `canceled`