WooCommerce API nodes for Node-RED

This commit is contained in:
2026-10-14 23:18:22 +02:00
commit e3b89fc74c
17 changed files with 1049 additions and 0 deletions
+37
View File
@@ -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
+9
View File
@@ -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"]
+182
View File
@@ -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
+50
View File
@@ -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"
+12
View File
@@ -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:
+34
View File
@@ -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
+57
View File
@@ -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": [[]]
}
]
+138
View File
@@ -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/"
+22
View File
@@ -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"
}
}
+54
View File
@@ -0,0 +1,54 @@
<script type="text/javascript">
RED.nodes.registerType("woocommerce-config", {
category: "config",
defaults: {
name: { value: "" },
baseUrl: { value: "https://shop.hanssonit.se/wp-json/wc/v3", required: true }
},
credentials: {
consumerKey: { type: "text" },
consumerSecret: { type: "password" }
},
label: function () {
return this.name || this.baseUrl || "WooCommerce";
},
labelStyle: function () {
return this.name ? "node_label_italic" : "";
}
});
</script>
<script type="text/x-red" data-template-name="woocommerce-config">
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-config-input-name" placeholder="WooCommerce Config">
</div>
<div class="form-row">
<label for="node-config-input-baseUrl"><i class="fa fa-server"></i> Base URL</label>
<input type="text" id="node-config-input-baseUrl" placeholder="https://shop.hanssonit.se/wp-json/wc/v3">
</div>
<div class="form-row">
<label for="node-config-input-consumerKey"><i class="fa fa-key"></i> Consumer Key</label>
<input type="text" id="node-config-input-consumerKey" autocomplete="off">
</div>
<div class="form-row">
<label for="node-config-input-consumerSecret"><i class="fa fa-lock"></i> Consumer Secret</label>
<input type="password" id="node-config-input-consumerSecret" autocomplete="off">
<div class="form-tips" style="color:#888; font-size:11px; margin-top:4px;">
Create API key at: WooCommerce → Settings → Advanced → REST API.<br>
Key must have <b>Read</b> permission (preferably Read/Write).<br>
User bound to key must be <b>Administrator</b> or equivalent.
</div>
</div>
</script>
<script type="text/x-red" data-help-name="woocommerce-config">
<p>WooCommerce REST API configuration node.</p>
<h3>Details</h3>
<p>Configure your WooCommerce store connection.</p>
<ul>
<li><b>Base URL</b> – Must be <code>https://shop.hanssonit.se/wp-json/wc/v3</code></li>
<li><b>Consumer Key</b> – From WooCommerce → Settings → Advanced → REST API</li>
<li><b>Consumer Secret</b> – Paired with Consumer Key</li>
</ul>
</script>
+52
View File
@@ -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);
};
+54
View File
@@ -0,0 +1,54 @@
<script type="text/javascript">
RED.nodes.registerType("woocommerce-customer", {
category: "WooCommerce",
color: "#7f54b3",
defaults: {
name: { value: "" },
config: { type: "woocommerce-config", required: true },
operation: { value: "search" }
},
inputs: 1,
outputs: 1,
icon: "font-awesome/fa-users",
label: function () {
return this.name || ("WC Customer " + this.operation);
},
labelStyle: function () {
return this.name ? "node_label_italic" : "";
},
oneditprepare: function () {
$("#node-input-config").val(this.config ? this.config.id : "");
},
oneditsave: function () {
this.operation = $("#node-select-operation").val();
}
});
</script>
<script type="text/x-red" data-template-name="woocommerce-customer">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="WooCommerce Customer">
</div>
<div class="form-row">
<label for="node-input-config"><i class="fa fa-server"></i> Config</label>
<input type="text" id="node-input-config">
</div>
<div class="form-row">
<label for="node-select-operation"><i class="fa fa-wrench"></i> Operation</label>
<select id="node-select-operation">
<option value="search">Search by Email</option>
<option value="get">Get Customer by ID</option>
<option value="create">Create Customer</option>
</select>
</div>
<div class="form-tips" style="color:#888; font-size:11px; margin-top:10px;">
<b>Search</b>: msg.email – exact match on customer email<br>
<b>Get</b>: msg.customerId – WooCommerce user ID<br>
<b>Create</b>: msg.email, msg.firstname, msg.lastname, msg.billing, msg.shipping
</div>
</script>
<script type="text/x-red" data-help-name="woocommerce-customer">
<p>WooCommerce customer operations: search (by email), get, create.</p>
</script>
+76
View File
@@ -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);
};
+70
View File
@@ -0,0 +1,70 @@
<script type="text/javascript">
RED.nodes.registerType("woocommerce-order", {
category: "WooCommerce",
color: "#7f54b3",
defaults: {
name: { value: "" },
config: { type: "woocommerce-config", required: true },
operation: { value: "list" }
},
inputs: 1,
outputs: 1,
icon: "font-awesome/fa-shopping-cart",
label: function () {
return this.name || ("WC Order " + this.operation);
},
labelStyle: function () {
return this.name ? "node_label_italic" : "";
},
oneditprepare: function () {
$("#node-input-config").val(this.config ? this.config.id : "");
},
oneditsave: function () {
this.operation = $("#node-select-operation").val();
}
});
</script>
<script type="text/x-red" data-template-name="woocommerce-order">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="WooCommerce Order">
</div>
<div class="form-row">
<label for="node-input-config"><i class="fa fa-server"></i> Config</label>
<input type="text" id="node-input-config">
</div>
<div class="form-row">
<label for="node-select-operation"><i class="fa fa-wrench"></i> Operation</label>
<select id="node-select-operation">
<option value="list">List Orders</option>
<option value="get">Get Order</option>
<option value="updateStatus">Update Status</option>
<option value="addNote">Add Note</option>
<option value="getNotes">Get Notes</option>
</select>
</div>
<div class="form-tips" style="color:#888; font-size:11px; margin-top:10px;">
<b>List filters</b>: msg.status (processing, completed, on-hold, etc.),
msg.after (ISO 8601 date), msg.before, msg.customer (ID), msg.product (ID),
msg.limit (per_page), msg.page<br>
<b>Update Status</b>: msg.orderId + msg.status<br>
<b>Add Note</b>: msg.orderId + msg.note (+ optional msg.customerNote=true)
</div>
</script>
<script type="text/x-red" data-help-name="woocommerce-order">
<p>WooCommerce order operations: list, get, updateStatus, addNote, getNotes.</p>
<h3>Operations</h3>
<ul>
<li><b>list</b> – List orders with filters (status, after, before, customer, product)</li>
<li><b>get</b> – Get single order by msg.orderId</li>
<li><b>updateStatus</b> – Update order status (msg.orderId + msg.status)</li>
<li><b>addNote</b> – Add order note (msg.orderId + msg.note)</li>
<li><b>getNotes</b> – Get order notes (msg.orderId)</li>
</ul>
<h3>Order Object Fields</h3>
<p><code>billing</code> (first_name, last_name, email, company, address, phone),
<code>line_items</code>, <code>total</code>, <code>currency</code>,
<code>number</code>, <code>date_created</code>, <code>meta_data</code></p>
</script>
+83
View File
@@ -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);
};
+54
View File
@@ -0,0 +1,54 @@
<script type="text/javascript">
RED.nodes.registerType("woocommerce-product", {
category: "WooCommerce",
color: "#7f54b3",
defaults: {
name: { value: "" },
config: { type: "woocommerce-config", required: true },
operation: { value: "list" }
},
inputs: 1,
outputs: 1,
icon: "font-awesome/fa-cube",
label: function () {
return this.name || ("WC Product " + this.operation);
},
labelStyle: function () {
return this.name ? "node_label_italic" : "";
},
oneditprepare: function () {
$("#node-input-config").val(this.config ? this.config.id : "");
},
oneditsave: function () {
this.operation = $("#node-select-operation").val();
}
});
</script>
<script type="text/x-red" data-template-name="woocommerce-product">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="WooCommerce Product">
</div>
<div class="form-row">
<label for="node-input-config"><i class="fa fa-server"></i> Config</label>
<input type="text" id="node-input-config">
</div>
<div class="form-row">
<label for="node-select-operation"><i class="fa fa-wrench"></i> Operation</label>
<select id="node-select-operation">
<option value="list">List Products</option>
<option value="get">Get Product by ID</option>
</select>
</div>
<div class="form-tips" style="color:#888; font-size:11px; margin-top:10px;">
<b>List</b>: msg.limit, msg.page, msg.category (ID), msg.search<br>
<b>Get</b>: msg.productId – WooCommerce product ID<br>
Useful for enriching order data with product details.
</div>
</script>
<script type="text/x-red" data-help-name="woocommerce-product">
<p>WooCommerce product operations: list, get.</p>
<p>Useful for enriching order line_items with product details (name, SKU, image).</p>
</script>
+65
View File
@@ -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);
};