From e3b89fc74cd328ed00824710dabba5228d92a52d Mon Sep 17 00:00:00 2001 From: n3wk Date: Wed, 14 Oct 2026 23:18:22 +0200 Subject: [PATCH] WooCommerce API nodes for Node-RED --- .gitignore | 37 +++++++ Dockerfile | 9 ++ README.md | 182 +++++++++++++++++++++++++++++++ bootstrap-install.sh | 50 +++++++++ docker-compose.yml | 12 ++ entrypoint.sh | 34 ++++++ flows/woocommerce-to-zammad.json | 57 ++++++++++ integrate-into-nodered.sh | 138 +++++++++++++++++++++++ nodes/package.json | 22 ++++ nodes/woocommerce-config.html | 54 +++++++++ nodes/woocommerce-config.js | 52 +++++++++ nodes/woocommerce-customer.html | 54 +++++++++ nodes/woocommerce-customer.js | 76 +++++++++++++ nodes/woocommerce-order.html | 70 ++++++++++++ nodes/woocommerce-order.js | 83 ++++++++++++++ nodes/woocommerce-product.html | 54 +++++++++ nodes/woocommerce-product.js | 65 +++++++++++ 17 files changed, 1049 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 bootstrap-install.sh create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 flows/woocommerce-to-zammad.json create mode 100644 integrate-into-nodered.sh create mode 100644 nodes/package.json create mode 100644 nodes/woocommerce-config.html create mode 100644 nodes/woocommerce-config.js create mode 100644 nodes/woocommerce-customer.html create mode 100644 nodes/woocommerce-customer.js create mode 100644 nodes/woocommerce-order.html create mode 100644 nodes/woocommerce-order.js create mode 100644 nodes/woocommerce-product.html create mode 100644 nodes/woocommerce-product.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fed5784 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Secrets & credentials +.env +.env.* +*.pem +*.key +credentials* +credentials.json +consumer*secret* +consumer*key* +secrets* + +# Dependencies +node_modules/ + +# Build artifacts +dist/ +build/ +*.log + +# OS files +.DS_Store +Thumbs.db +desktop.ini + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Docker +.dockerignore + +# Temp +tmp/ +temp/ +*.tmp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fe4cf08 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM nodered/node-red:latest + +USER root + +COPY nodes/ /opt/nextcloud-nodes/node-red-contrib-nextcloud-woocommerce +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..8819279 --- /dev/null +++ b/README.md @@ -0,0 +1,182 @@ +# nextcloud-nodered-woocommerce + +Node-RED nodes for the [WooCommerce](https://woocommerce.com/) REST API. Part of the +[nextcloud-nodered](https://kode.hannesahlin.se/n3wk/) suite. + +## Package + +`node-red-contrib-nextcloud-woocommerce` + +## Nodes + +| Node | Type | Purpose | +|------|------|---------| +| `woocommerce-config` | config | Base URL + Consumer Key + Consumer Secret | +| `woocommerce-order` | function | list (with filters), get, updateStatus, addNote, getNotes | +| `woocommerce-customer` | function | search (by email), get, create | +| `woocommerce-product` | function | list, get (for enriching order data) | + +## Installation + +### Prerequisites + +- An existing [nextcloud-node-red](https://kode.hannesahlin.se/n3wk/nextcloud-nodered-ocs-api) installation. +- WooCommerce API key with Read permission (preferably Read/Write), bound to an Administrator user. + +### Quick install (recommended) + +```bash +git clone https://github.com/kode-tjenester/nextcloud-nodered-woocommerce.git +cd nextcloud-nodered-woocommerce +export NODERED_DIR=/path/to/your/nextcloud-node-red +bash bootstrap-install.sh +cd $NODERED_DIR +docker compose build --no-cache +docker compose up -d +``` + +### Manual install + +```bash +export NODERED_DIR=/path/to/your/nextcloud-node-red +bash integrate-into-nodered.sh +``` + +The integrate script: +1. Copies all `.js` and `.html` files into `$NODERED_DIR/nodes/`. +2. Copies the package to `$NODERED_DIR/opt/nextcloud-nodes/node-red-contrib-nextcloud-woocommerce/`. +3. Patches `$NODERED_DIR/entrypoint.sh` to install the nodes on container start. +4. Is idempotent — safe to run multiple times. + +## Authentication + +| Method | Details | +|--------|---------| +| Consumer Key | From WooCommerce → Settings → Advanced → REST API | +| Consumer Secret | Paired with Consumer Key | + +**Important:** Base URL must be `https://shop.hanssonit.se/wp-json/wc/v3/`. +The API key must have at least Read permission. The user the key is bound to +must be an Administrator or have equivalent capabilities. + +## Node Details + +### woocommerce-order + +Operations: `list`, `get`, `updateStatus`, `addNote`, `getNotes`. + +**List with filters:** +``` +msg.status = "processing" | "completed" | "on-hold" | ... +msg.after = "2026-10-01T00:00:00" (ISO 8601) +msg.before = "2026-10-31T23:59:59" +msg.customer = customer ID +msg.product = product ID +msg.limit = per_page (default 10) +msg.page = page number +``` + +**Update status:** +``` +msg.orderId = 31921 +msg.status = "completed" +``` + +**Add note:** +``` +msg.orderId = 31921 +msg.note = "Customer contacted via phone" +msg.customerNote = true (optional — visible to customer) +``` + +### woocommerce-customer + +Operations: `search` (by email), `get`, `create`. + +**Search by email:** +``` +msg.email = "lars@lrginvest.se" +``` + +### woocommerce-product + +Operations: `list`, `get`. + +Useful for enriching order `line_items` with product name, SKU, images. + +## Vertical Integration Flow: WooCommerce → Zammad → Nextcloud + +This is the trigger half of the vertical flow. Combined with +[nextcloud-nodered-zammad](https://github.com/kode-tjenester/nextcloud-nodered-zammad) +and the existing Nextcloud OCS nodes: + +### Step 1: Poll for new orders (or webhook) + +Use `woocommerce-order` with operation `list` and `msg.status = "processing"` +on a schedule (inject node with cron), or trigger via a WooCommerce webhook. + +### Step 2: Extract order info in a Function node + +```javascript +msg.title = "Order #" + msg.payload.number + " – " + + msg.payload.billing.first_name + " " + msg.payload.billing.last_name + + (msg.payload.billing.company ? " (" + msg.payload.billing.company + ")" : ""); + +msg.customer = msg.payload.billing.email; + +msg.body = "Products:\n"; +for (let item of msg.payload.line_items) { + msg.body += "- " + item.name + " x" + item.quantity + + " (" + msg.payload.currency + " " + item.total + ")\n"; +} +msg.body += "\nTotal: " + msg.payload.currency + " " + msg.payload.total + "\n"; +msg.body += "Address: " + msg.payload.billing.address_1 + ", " + + msg.payload.billing.city + " " + msg.payload.billing.postcode + "\n"; +msg.body += "Phone: " + msg.payload.billing.phone + "\n"; +msg.body += "Email: " + msg.payload.billing.email; + +msg.ticketId = msg.payload.id; +msg.tag = "woocommerce-order"; + +return msg; +``` + +### Step 3: Create Zammad ticket + +`zammad-ticket` node (operation: create) receives the Function output. +Title: `Order #31921 – Lars Gardelin (LRG Invest AB)` +Customer: billing email +Body: product list + amount + address + phone + +### Step 4: Tag the ticket + +`zammad-ticket` (operation: addTag) with `msg.tag = "woocommerce-order"`. + +### Step 5: Organize in Nextcloud + +Existing Nextcloud OCS nodes: +- Create/update customer folder +- Create note or Deck card with order details +- Optional: calendar event for follow-up +- Link back to Zammad ticket in note/card + +### Example Flow JSON + +See `flows/woocommerce-to-zammad.json` for a complete example flow +(no real credentials included). + +## Integration with Other Packages + +- **nextcloud-nodered-zammad**: Creates and manages Zammad tickets from WooCommerce orders. +- **nextcloud-nodered-ocs-api**: Provides Nextcloud OCS nodes for creating folders, notes, deck cards, etc. +- **nextcloud-nodered-agent**: Agent-based nodes for complex orchestration. + +## Credentials Security + +- Consumer Key and Secret are stored **only** in the Node-RED config node (encrypted at rest). +- Never commit credentials to git (`.gitignore` excludes them). +- The config node uses Node-RED's built-in credential encryption. + +## License + +MIT diff --git a/bootstrap-install.sh b/bootstrap-install.sh new file mode 100644 index 0000000..bf4ac44 --- /dev/null +++ b/bootstrap-install.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Bootstrap installer for node-red-contrib-nextcloud-woocommerce +# Clones repo and integrates into existing nextcloud-node-red + +REPO_URL="https://kode.hannesahlin.se/n3wk/nextcloud-nodered-woocommerce.git" +INSTALL_DIR="${INSTALL_DIR:-/opt/node-red-contrib-nextcloud-woocommerce}" +NODERED_DIR="${NODERED_DIR:-}" + +echo "=== Nextcloud WooCommerce Node-RED Bootstrap Installer ===" + +# Auto-detect nextcloud-node-red directory if not set +if [[ -z "$NODERED_DIR" ]]; then + for dir in \ + "/opt/nextcloud-node-red" \ + "/home/ncdeploy/nextcloud-node-red" \ + "/root/nextcloud-node-red" \ + "$(docker inspect -f '{{.Config.Labels}}' nextcloud-node-red 2>/dev/null | grep -o 'com.docker.compose.project.working_dir=[^ ]*' | cut -d= -f2)"; do + if [[ -n "$dir" && -d "$dir" && -f "$dir/Dockerfile" ]]; then + NODERED_DIR="$dir" + break + fi + done +fi + +if [[ -z "$NODERED_DIR" ]]; then + echo "ERROR: Could not auto-detect nextcloud-node-red directory." + echo "Set NODERED_DIR environment variable." + exit 1 +fi + +echo "Target Node-RED: $NODERED_DIR" + +# Clone or update repo +if [[ -d "$INSTALL_DIR/.git" ]]; then + echo "Updating existing repo..." + git -C "$INSTALL_DIR" pull --ff-only || echo "Pull failed, continuing with existing code..." +else + echo "Cloning repo..." + git clone --depth 1 "$REPO_URL" "$INSTALL_DIR" +fi + +# Run integration +export NODERED_DIR +"$INSTALL_DIR/integrate-into-nodered.sh" + +echo "" +echo "=== Bootstrap Complete ===" +echo "Rebuild with: cd $NODERED_DIR && docker compose build --no-cache && docker compose up -d" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..86fdf70 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + node-red: + build: . + image: nextcloud-node-red-woocommerce:latest + container_name: nextcloud-node-red-woocommerce + restart: unless-stopped + network_mode: host + volumes: + - node-red-data:/data + +volumes: + node-red-data: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..20b6eaf --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# Install woocommerce nodes into /data at runtime. +# /data is a Docker volume that masks build-time COPY. + +set -eu + +SRC="/opt/nextcloud-nodes/node-red-contrib-nextcloud-woocommerce" +DST_DIR="/data/node_modules" +DST="$DST_DIR/node-red-contrib-nextcloud-woocommerce" + +if [ ! -d "$SRC" ]; then + echo "[entrypoint] ERROR: source directory missing: $SRC" >&2 + ls -la /opt || true + ls -la /opt/nextcloud-nodes || true + exit 1 +fi + +mkdir -p "$DST_DIR" +rm -rf "$DST" +cp -a "$SRC" "$DST_DIR/" + +if [ ! -f "$DST/package.json" ]; then + echo "[entrypoint] ERROR: install did not produce $DST/package.json" >&2 + ls -la /data || true + ls -la "$DST_DIR" || true + exit 1 +fi + +chown -R node-red:node-red /data "$DST_DIR" "$DST" || true +chmod -R a+rX "$DST" || true + +export NODE_RED_USER_DIR=/data + +exec node-red --userDir /data diff --git a/flows/woocommerce-to-zammad.json b/flows/woocommerce-to-zammad.json new file mode 100644 index 0000000..14368d9 --- /dev/null +++ b/flows/woocommerce-to-zammad.json @@ -0,0 +1,57 @@ +[ + { + "id": "wc-poll-trigger", + "type": "inject", + "name": "Poll Processing Orders", + "topic": "woocommerce/poll", + "payload": "", + "payloadType": "date", + "repeat": "900", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "x": 150, + "y": 100, + "wires": [["wc-list-orders"]] + }, + { + "id": "wc-list-orders", + "type": "woocommerce-order", + "name": "List Processing Orders", + "config": "woocommerce-config-placeholder", + "operation": "list", + "x": 350, + "y": 100, + "wires": [["wc-extract-info"]] + }, + { + "id": "wc-extract-info", + "type": "function", + "name": "Extract Order Info", + "func": "var order = Array.isArray(msg.payload) ? msg.payload[0] : msg.payload;\nif (!order || !order.billing) return null;\nmsg.title = 'Order #' + order.number + ' – ' + order.billing.first_name + ' ' + order.billing.last_name;\nif (order.billing.company) msg.title += ' (' + order.billing.company + ')';\nmsg.customer = order.billing.email;\nmsg.body = 'Products:\\n';\nif (order.line_items) {\n for (var i = 0; i < order.line_items.length; i++) {\n var item = order.line_items[i];\n msg.body += '- ' + item.name + ' x' + item.quantity + ' (' + order.currency + ' ' + item.total + ')\\n';\n }\n}\nmsg.body += '\\nTotal: ' + order.currency + ' ' + order.total;\nmsg.body += '\\nAddress: ' + order.billing.address_1 + ', ' + order.billing.city + ' ' + order.billing.postcode;\nmsg.body += '\\nPhone: ' + order.billing.phone;\nmsg.body += '\\nEmail: ' + order.billing.email;\nmsg.ticketId = order.id;\nmsg.tag = 'woocommerce-order';\nreturn msg;", + "outputs": 1, + "x": 550, + "y": 100, + "wires": [["zammad-create-ticket"]] + }, + { + "id": "zammad-create-ticket", + "type": "zammad-ticket", + "name": "Create Ticket", + "config": "zammad-config-placeholder", + "operation": "create", + "x": 750, + "y": 100, + "wires": [["zammad-add-tag"]] + }, + { + "id": "zammad-add-tag", + "type": "zammad-ticket", + "name": "Add WooCommerce Tag", + "config": "zammad-config-placeholder", + "operation": "addTag", + "x": 950, + "y": 100, + "wires": [[]] + } +] diff --git a/integrate-into-nodered.sh b/integrate-into-nodered.sh new file mode 100644 index 0000000..7b40bb8 --- /dev/null +++ b/integrate-into-nodered.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Integration script for node-red-contrib-nextcloud-woocommerce +# Patches woocommerce nodes into existing nextcloud-node-red Docker image + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGE_NAME="node-red-contrib-nextcloud-woocommerce" + +# Auto-detect Node-RED directory +if [[ -z "${NODERED_DIR:-}" ]]; then + for dir in \ + "/opt/nextcloud-node-red" \ + "/home/ncdeploy/nextcloud-node-red" \ + "/root/nextcloud-node-red" \ + "$(dirname "$SCRIPT_DIR")/nextcloud-node-red" \ + "$(docker inspect -f '{{.Config.Labels}}' nextcloud-node-red 2>/dev/null | grep -o 'com.docker.compose.project.working_dir=[^ ]*' | cut -d= -f2)"; do + if [[ -n "$dir" && -d "$dir" && -f "$dir/Dockerfile" ]]; then + NODERED_DIR="$dir" + break + fi + done +fi + +if [[ -z "${NODERED_DIR:-}" ]] || [[ ! -d "$NODERED_DIR" ]]; then + echo "ERROR: Could not auto-detect nextcloud-node-red directory." + echo "Set NODERED_DIR environment variable." + exit 1 +fi + +if [[ ! -f "$NODERED_DIR/Dockerfile" ]]; then + echo "ERROR: No Dockerfile found in $NODERED_DIR" + exit 1 +fi + +echo "=== Nextcloud WooCommerce Node-RED Integration ===" +echo "Source: $SCRIPT_DIR" +echo "Target: $NODERED_DIR" + +# 1. Copy nodes to nextcloud-node-red nodes subdirectory +NODES_TARGET="$NODERED_DIR/nodes/$PACKAGE_NAME" +echo "[integrate] Copying woocommerce nodes to $NODES_TARGET..." +mkdir -p "$NODES_TARGET" +cp "$SCRIPT_DIR/nodes"/*.js "$NODES_TARGET/" +cp "$SCRIPT_DIR/nodes"/*.html "$NODES_TARGET/" +cp "$SCRIPT_DIR/nodes/package.json" "$NODES_TARGET/" +echo " Nodes copied." + +# 2. Patch Dockerfile to COPY woocommerce nodes into /opt +DOCKERFILE="$NODERED_DIR/Dockerfile" +echo "[integrate] Patching Dockerfile..." + +if grep -q "$PACKAGE_NAME" "$DOCKERFILE"; then + echo " Dockerfile already contains $PACKAGE_NAME" +else + cp "$DOCKERFILE" "$DOCKERFILE.bak.$(date +%s)" + + sed -i '/COPY nodes\//a\ +COPY nodes/ '"$PACKAGE_NAME"' /opt/nextcloud-nodes/'"$PACKAGE_NAME"'\ +' "$DOCKERFILE" + + echo " Dockerfile patched with COPY for $PACKAGE_NAME." +fi + +# 3. Patch entrypoint.sh to install woocommerce nodes at container start +ENTRYPOINT="$NODERED_DIR/entrypoint.sh" +echo "[integrate] Patching entrypoint.sh..." + +if [[ -f "$ENTRYPOINT" ]]; then + cp "$ENTRYPOINT" "$ENTRYPOINT.bak.$(date +%s)" + + if ! grep -q "$PACKAGE_NAME" "$ENTRYPOINT"; then + cat >> "$ENTRYPOINT" << 'PATCH_EOF' + +# --- node-red-contrib-nextcloud-woocommerce --- +SRC_WC="/opt/nextcloud-nodes/node-red-contrib-nextcloud-woocommerce" +DST_WC="/data/node_modules/node-red-contrib-nextcloud-woocommerce" +if [ -d "$SRC_WC" ]; then + echo "[entrypoint] Installing woocommerce nodes into /data..." + mkdir -p /data/node_modules + rm -rf "$DST_WC" + cp -a "$SRC_WC" /data/node_modules/ + chown -R node-red:node-red "$DST_WC" 2>/dev/null || true + chmod -R a+rX "$DST_WC" 2>/dev/null || true + echo "[entrypoint] WooCommerce nodes installed." +fi +PATCH_EOF + echo " entrypoint.sh patched." + else + echo " entrypoint.sh already contains $PACKAGE_NAME." + fi +else + echo " WARNING: entrypoint.sh not found at $ENTRYPOINT" +fi + +# 4. Create woocommerce nodes package in /opt for entrypoint to copy +OPT_NODES_DIR="$NODERED_DIR/opt/nextcloud-nodes" +echo "[integrate] Creating woocommerce nodes package in $OPT_NODES_DIR..." +mkdir -p "$OPT_NODES_DIR/$PACKAGE_NAME/nodes" +cp "$SCRIPT_DIR/nodes"/*.js "$OPT_NODES_DIR/$PACKAGE_NAME/nodes/" +cp "$SCRIPT_DIR/nodes"/*.html "$OPT_NODES_DIR/$PACKAGE_NAME/nodes/" +cp "$SCRIPT_DIR/nodes/package.json" "$OPT_NODES_DIR/$PACKAGE_NAME/package.json" +echo " Package created." + +# 5. Update docker-compose.yml to mount /opt/nextcloud-nodes +COMPOSE="$NODERED_DIR/docker-compose.yml" +if [[ -f "$COMPOSE" ]]; then + echo "[integrate] Checking docker-compose.yml for woocommerce volume..." + if ! grep -q "nextcloud-nodes" "$COMPOSE"; then + cp "$COMPOSE" "$COMPOSE.bak.$(date +%s)" + + sed -i '/- node-red-data:\/data/a\ + - nextcloud-nodes:/opt/nextcloud-nodes' "$COMPOSE" + + sed -i '/^volumes:/a\ + nextcloud-nodes:' "$COMPOSE" + + echo " docker-compose.yml updated with nextcloud-nodes volume." + else + echo " docker-compose.yml already has nextcloud-nodes volume." + fi +else + echo " WARNING: docker-compose.yml not found at $COMPOSE" +fi + +echo "" +echo "=== Integration Complete ===" +echo "" +echo "Next steps:" +echo "1. Rebuild the Docker image:" +echo " cd $NODERED_DIR && docker compose build --no-cache" +echo "" +echo "2. Restart the container:" +echo " docker compose up -d" +echo "" +echo "3. In Node-RED, configure a 'woocommerce config' node with your Consumer Key + Secret." +echo "" +echo "4. Import example flows from: $SCRIPT_DIR/flows/" diff --git a/nodes/package.json b/nodes/package.json new file mode 100644 index 0000000..032a73a --- /dev/null +++ b/nodes/package.json @@ -0,0 +1,22 @@ +{ + "name": "node-red-contrib-nextcloud-woocommerce", + "version": "1.0.0", + "description": "Node-RED nodes for WooCommerce REST API integration", + "keywords": ["node-red", "woocommerce", "ecommerce", "orders"], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://kode.hannesahlin.se/n3wk/nextcloud-nodered-woocommerce" + }, + "node-red": { + "nodes": { + "woocommerce-config": "woocommerce-config.js", + "woocommerce-order": "woocommerce-order.js", + "woocommerce-customer": "woocommerce-customer.js", + "woocommerce-product": "woocommerce-product.js" + } + }, + "engines": { + "node": ">=14.0.0" + } +} diff --git a/nodes/woocommerce-config.html b/nodes/woocommerce-config.html new file mode 100644 index 0000000..a63e9ee --- /dev/null +++ b/nodes/woocommerce-config.html @@ -0,0 +1,54 @@ + + + + + diff --git a/nodes/woocommerce-config.js b/nodes/woocommerce-config.js new file mode 100644 index 0000000..7917d21 --- /dev/null +++ b/nodes/woocommerce-config.js @@ -0,0 +1,52 @@ +module.exports = function (RED) { + "use strict"; + + const https = require("https"); + const http = require("http"); + + function WooCommerceConfigNode(config) { + RED.nodes.createNode(this, config); + const node = this; + + node.baseUrl = (config.baseUrl || "https://shop.hanssonit.se/wp-json/wc/v3").replace(/\/+$/, ""); + node.consumerKey = config.consumerKey || ""; + node.consumerSecret = config.consumerSecret || ""; + + node.sendRequest = function (method, path, body, msgHeaders, callback) { + const url = new URL(node.baseUrl + path); + const parsedBody = body !== undefined ? (typeof body === "string" ? body : JSON.stringify(body)) : undefined; + + const headers = Object.assign({ "Accept": "application/json", "Content-Type": "application/json" }, msgHeaders || {}); + if (parsedBody !== undefined) headers["Content-Length"] = Buffer.byteLength(parsedBody); + headers["Authorization"] = "Basic " + Buffer.from(node.consumerKey + ":" + node.consumerSecret).toString("base64"); + + const options = { + method, hostname: url.hostname, + port: url.port || (url.protocol === "https:" ? 443 : 80), + path: url.pathname + url.search, headers, rejectUnauthorized: false + }; + + const req = (url.protocol === "https:" ? https : http).request(options, function (res) { + let data = ""; + res.on("data", chunk => data += chunk); + res.on("end", () => { + let parsed = null; + if (data) { try { parsed = JSON.parse(data); } catch (e) { parsed = data; } } + if (res.statusCode >= 200 && res.statusCode < 300) { + callback(null, { statusCode: res.statusCode, body: parsed, headers: res.headers }); + } else { + const err = new Error("WooCommerce API returned status " + res.statusCode + ": " + data); + err.statusCode = res.statusCode; + err.body = parsed; + callback(err); + } + }); + }); + req.on("error", callback); + if (parsedBody !== undefined) req.write(parsedBody); + req.end(); + }; + } + + RED.nodes.registerType("woocommerce-config", WooCommerceConfigNode); +}; diff --git a/nodes/woocommerce-customer.html b/nodes/woocommerce-customer.html new file mode 100644 index 0000000..c3586ba --- /dev/null +++ b/nodes/woocommerce-customer.html @@ -0,0 +1,54 @@ + + + + + diff --git a/nodes/woocommerce-customer.js b/nodes/woocommerce-customer.js new file mode 100644 index 0000000..95bb2a7 --- /dev/null +++ b/nodes/woocommerce-customer.js @@ -0,0 +1,76 @@ +module.exports = function (RED) { + "use strict"; + + function WooCommerceCustomerNode(config) { + RED.nodes.createNode(this, config); + const node = this; + + node.configNode = RED.nodes.getNode(config.config); + if (!node.configNode) { + node.error("No woocommerce-config node configured"); + return; + } + + node.operation = config.operation || "search"; + + node.status({ fill: "blue", shape: "ring", text: "ready" }); + + function doRequest(method, path, body, msg, done) { + node.status({ fill: "yellow", shape: "dot", text: method + " " + path }); + node.configNode.sendRequest(method, path, body, {}, function (err, result) { + if (err) { + node.status({ fill: "red", shape: "ring", text: "error" }); + if (done) done(err); + else node.error(err, msg); + return; + } + msg.payload = result.body; + msg.statusCode = result.statusCode; + msg.woocommerce = result; + node.status({ fill: "green", shape: "dot", text: result.statusCode }); + done(); + }); + } + + node.on("input", function (msg, send, done) { + const op = msg.operation || node.operation; + + switch (op) { + case "search": { + const params = new URLSearchParams(); + if (msg.email) params.append("email", msg.email); + if (msg.limit) params.append("per_page", msg.limit); + if (msg.page) params.append("page", msg.page); + const qs = params.toString(); + doRequest("GET", "/customers" + (qs ? "?" + qs : ""), undefined, msg, done); + break; + } + case "get": { + const id = msg.customerId || msg.payload; + if (!id) { node.error("Missing customerId"); if (done) done(new Error("Missing customerId")); return; } + doRequest("GET", "/customers/" + encodeURIComponent(id), undefined, msg, done); + break; + } + case "create": { + const body = msg.payload && typeof msg.payload === "object" ? msg.payload : { + email: msg.email || "", + first_name: msg.firstname || msg.firstName || "", + last_name: msg.lastname || msg.lastName || "", + username: msg.username || "", + billing: msg.billing || {}, + shipping: msg.shipping || {} + }; + doRequest("POST", "/customers", body, msg, done); + break; + } + default: { + const err = new Error("Unknown operation: " + op); + node.error(err); + if (done) done(err); + } + } + }); + } + + RED.nodes.registerType("woocommerce-customer", WooCommerceCustomerNode); +}; diff --git a/nodes/woocommerce-order.html b/nodes/woocommerce-order.html new file mode 100644 index 0000000..43752f6 --- /dev/null +++ b/nodes/woocommerce-order.html @@ -0,0 +1,70 @@ + + + + + diff --git a/nodes/woocommerce-order.js b/nodes/woocommerce-order.js new file mode 100644 index 0000000..7f4fe75 --- /dev/null +++ b/nodes/woocommerce-order.js @@ -0,0 +1,83 @@ +module.exports = function (RED) { + "use strict"; + + function WooCommerceOrderNode(config) { + RED.nodes.createNode(this, config); + const node = this; + + node.configNode = RED.nodes.getNode(config.config); + if (!node.configNode) { node.error("No woocommerce-config node configured"); return; } + + node.operation = config.operation || "list"; + node.status({ fill: "blue", shape: "ring", text: "ready" }); + + function doRequest(method, path, body, msg, done) { + node.status({ fill: "yellow", shape: "dot", text: method + " " + path }); + node.configNode.sendRequest(method, path, body, {}, function (err, result) { + if (err) { node.status({ fill: "red", shape: "ring", text: "error" }); done ? done(err) : node.error(err, msg); return; } + msg.payload = result.body; + msg.statusCode = result.statusCode; + msg.woocommerce = result; + node.status({ fill: "green", shape: "dot", text: result.statusCode }); + done(); + }); + } + + function buildListParams(msg) { + const p = new URLSearchParams(); + if (msg.status) p.append("status", msg.status); + if (msg.after) p.append("after", msg.after); + if (msg.before) p.append("before", msg.before); + if (msg.customer) p.append("customer", msg.customer); + if (msg.product) p.append("product", msg.product); + if (msg.limit) p.append("per_page", msg.limit); + if (msg.page) p.append("page", msg.page); + if (msg.order) p.append("order", msg.order); + if (msg.orderby) p.append("orderby", msg.orderby); + return p.toString() ? "?" + p.toString() : ""; + } + + node.on("input", function (msg, send, done) { + const op = msg.operation || node.operation; + switch (op) { + case "list": { + doRequest("GET", "/orders" + buildListParams(msg), undefined, msg, done); + break; + } + case "get": { + const id = msg.orderId || msg.payload; + if (!id) { node.error("Missing orderId"); done && done(new Error("Missing orderId")); return; } + doRequest("GET", "/orders/" + encodeURIComponent(id), undefined, msg, done); + break; + } + case "updateStatus": { + const id = msg.orderId || (msg.payload && msg.payload.id); + const status = msg.status || msg.newStatus; + if (!id || !status) { node.error("Missing orderId or status"); done && done(new Error("Missing orderId or status")); return; } + doRequest("PUT", "/orders/" + encodeURIComponent(id), { status }, msg, done); + break; + } + case "addNote": { + const id = msg.orderId || (msg.payload && msg.payload.id); + const note = msg.note || msg.noteText || (msg.payload && msg.payload.note); + if (!id || !note) { node.error("Missing orderId or note"); done && done(new Error("Missing orderId or note")); return; } + doRequest("POST", "/orders/" + encodeURIComponent(id) + "/notes", { note, customer_note: msg.customerNote || false }, msg, done); + break; + } + case "getNotes": { + const id = msg.orderId || (msg.payload && msg.payload.id); + if (!id) { node.error("Missing orderId"); done && done(new Error("Missing orderId")); return; } + doRequest("GET", "/orders/" + encodeURIComponent(id) + "/notes", undefined, msg, done); + break; + } + default: { + const err = new Error("Unknown operation: " + op); + node.error(err); + if (done) done(err); + } + } + }); + } + + RED.nodes.registerType("woocommerce-order", WooCommerceOrderNode); +}; diff --git a/nodes/woocommerce-product.html b/nodes/woocommerce-product.html new file mode 100644 index 0000000..2729387 --- /dev/null +++ b/nodes/woocommerce-product.html @@ -0,0 +1,54 @@ + + + + + diff --git a/nodes/woocommerce-product.js b/nodes/woocommerce-product.js new file mode 100644 index 0000000..ad6e0e6 --- /dev/null +++ b/nodes/woocommerce-product.js @@ -0,0 +1,65 @@ +module.exports = function (RED) { + "use strict"; + + function WooCommerceProductNode(config) { + RED.nodes.createNode(this, config); + const node = this; + + node.configNode = RED.nodes.getNode(config.config); + if (!node.configNode) { + node.error("No woocommerce-config node configured"); + return; + } + + node.operation = config.operation || "list"; + + node.status({ fill: "blue", shape: "ring", text: "ready" }); + + function doRequest(method, path, body, msg, done) { + node.status({ fill: "yellow", shape: "dot", text: method + " " + path }); + node.configNode.sendRequest(method, path, body, {}, function (err, result) { + if (err) { + node.status({ fill: "red", shape: "ring", text: "error" }); + if (done) done(err); + else node.error(err, msg); + return; + } + msg.payload = result.body; + msg.statusCode = result.statusCode; + msg.woocommerce = result; + node.status({ fill: "green", shape: "dot", text: result.statusCode }); + done(); + }); + } + + node.on("input", function (msg, send, done) { + const op = msg.operation || node.operation; + + switch (op) { + case "list": { + const params = new URLSearchParams(); + if (msg.limit) params.append("per_page", msg.limit); + if (msg.page) params.append("page", msg.page); + if (msg.category) params.append("category", msg.category); + if (msg.search) params.append("search", msg.search); + const qs = params.toString(); + doRequest("GET", "/products" + (qs ? "?" + qs : ""), undefined, msg, done); + break; + } + case "get": { + const id = msg.productId || msg.payload; + if (!id) { node.error("Missing productId"); if (done) done(new Error("Missing productId")); return; } + doRequest("GET", "/products/" + encodeURIComponent(id), undefined, msg, done); + break; + } + default: { + const err = new Error("Unknown operation: " + op); + node.error(err); + if (done) done(err); + } + } + }); + } + + RED.nodes.registerType("woocommerce-product", WooCommerceProductNode); +};