WooCommerce API nodes for Node-RED
This commit is contained in:
+37
@@ -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
|
||||||
@@ -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"]
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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:
|
||||||
@@ -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
|
||||||
@@ -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": [[]]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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/"
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user