Skill: cjboy007/ssa-order-tracker (version 1.0.0)

--- name: order-tracker description: "Track and manage sales orders with status updates, notifications, and dashboard reporting. Supports order creation, status transitions (pending/confirmed/shipped/delivered), email/Discord notifications, and order history visualization. Use when you need to monitor order fulfillment, send shipping updates to customers, or generate order analytics." --- # Order Tracker Skill ## Description 本地订单跟踪系统，提供手动订单状态管理、客户邮件通知和命令行看板功能。适用于 Farreach Electronic 外贸订单生命周期管理（从生产到交付），无需对接工厂 ERP 或物流 API。 ## When to Use - 查询某个订单的当前状态 - 更新订单状态（生产中 → 待发货 → 已发货 → 已完成） - 向客户发送订单状态变更邮件通知 - 查看所有在途订单的看板汇总 - 检测逾期或紧急（≤7 天）订单 ## Prerequisites - Node.js v16+ - 已配置 `imap-smtp-email` skill 的 SMTP `.env` 文件（用于发送通知邮件） - 依赖安装：`npm install`（在 skill 目录下执行） ## Skills Directory `skills/order-tracker/` ## How to Invoke All scripts are run from the `scripts/` subdirectory. ### 1. View Order Dashboard ```bash cd skills/order-tracker/scripts # 查看所有订单（按状态分组） node order-dashboard.js # 按状态过滤 node order-dashboard.js --status in_production node order-dashboard.js --status shipped # 查看单个订单详情 node order-dashboard.js --order-id ORD-20260324-001 # 输出格式（table / compact / json） node order-dashboard.js --format json ``` ### 2. Update Order Status ```bash cd skills/order-tracker/scripts # 预览（dry-run，不写入） node update-order-status.js --order-id ORD-20260324-001 --status ready_to_ship --dry-run # 更新状态 node update-order-status.js \ --order-id ORD-20260324-001 \ --status shipped \ --notes "DHL Express, 单号: 1234567890, ETA: 4月25日" # 更新状态 + 标记需要发通知 node update-order-status.js \ --order-id ORD-20260324-001 \ --status in_production \ --notes "工厂已排期" \ --trigger-notification ``` ### 3. Send Customer Notification Email ```bash cd skills/order-tracker/scripts # 预览邮件（不发送） node send-order-notification.js --order-id ORD-20260324-001 --dry-run # 发送通知（使用订单当前状态的邮件模板） node send-order-notification.js --order-id ORD-20260324-001 # 指定状态模板发送 node send-order-notification.js --order-id ORD-20260324-001 --status shipped ``` ## Order Status Model 6 状态机： ``` pending_production → in_production → ready_to_ship → shipped → completed ↘ cancelled（任意阶段可取消） ``` | 状态 | 中文 | 说明 | |------|------|------| | `pending_production` | 待生产 | 订单确认，等待生产 | | `in_production` | 生产中 | 工厂生产中 | | `ready_to_ship` | 待发货 | 生产完成，等待发货 | | `shipped` | 已发货 | 货物已发出 | | `completed` | 已完成 | 客户确认收货 | | `cancelled` | 已取消 | 订单取消 | ## Email Templates 通知脚本支持 5 种双语（EN/ZH）邮件模板： - `in_production` — 生产进度通知 - `ready_to_ship` — 准备发货通知 - `shipped` — 发货确认（含物流单号） - `completed` — 订单完成确认 - `cancelled` — 取消通知 ## Typical Workflow Example ```bash BASE=skills/order-tracker/scripts ORDER=ORD-20260324-001 # 开始生产 + 通知客户 node $BASE/update-order-status.js --order-id $ORDER --status in_production --notes "工厂已排期" node $BASE/send-order-notification.js --order-id $ORDER # 发货 + 通知客户 node $BASE/update-order-status.js --order-id $ORDER --status shipped --notes "DHL 单号: 1234567890" node $BASE/send-order-notification.js --order-id $ORDER # 查看看板 node $BASE/order-dashboard.js ``` ## Data Files | 文件 | 路径 | 说明 | |------|------|------| | 订单数据 | `data/orders.json` | 所有订单（手动维护） | | 订单 Schema | `config/order-schema.json` | JSON Schema 验证定义 | | 状态变更日志 | `logs/status-changes.log` | 每次状态更新记录 | | 通知日志 | `logs/notifications.log` | 邮件发送记录 | ## Constraints - 不对接工厂 ERP/MES 系统 - 不抓取物流 API - 状态手动更新 - 数据存储本地 JSON 文件 ## Source - Task: task-005（Phase 3） - Implemented by: IRON (bailian/qwen3.5-plus) - Reviewed by: WILSON (aiberm/claude-sonnet-4-6) - Completed: 2026-03-24

path: README.md
content: # Order Tracker Skill

本地订单跟踪系统 — 手动状态管理 + 客户邮件通知 + 命令行看板

> 适用于 Farreach Electronic，task-005 产出物，由 IRON (qwen3.5-plus) 实现，WILSON 审阅通过。

---

## 功能概览

| 功能模块 | 脚本 | 说明 |
|----------|------|------|
| 订单状态更新 | `scripts/update-order-status.js` | 手动更新订单状态，含验证、日志、备份 |
| 客户邮件通知 | `scripts/send-order-notification.js` | 状态变更时发送双语通知邮件 |
| 订单看板 | `scripts/order-dashboard.js` | 命令行汇总视图，支持多种输出格式 |

---

## 目录结构

```
order-tracker/
├── README.md                        # 本文件
├── SKILL.md                         # AgentSkills 规范
├── package.json                     # Node.js 依赖
├── config/
│   └── order-schema.json            # 订单 JSON Schema（数据验证）
├── data/
│   └── orders.json                  # 订单数据文件（本地存储）
├── logs/
│   ├── status-changes.log           # 状态变更日志
│   └── notifications.log            # 邮件通知日志
└── scripts/
    ├── update-order-status.js       # 订单状态更新脚本
    ├── send-order-notification.js   # 客户邮件通知脚本
    └── order-dashboard.js           # 订单看板脚本
```

---

## 订单状态模型

订单生命周期共 6 个状态：

```
pending_production → in_production → ready_to_ship → shipped → completed
                                                             ↘ cancelled (任意阶段可取消)
```

| 状态 | 中文 | 说明 |
|------|------|------|
| `pending_production` | 待生产 | 订单已确认，等待生产排期 |
| `in_production` | 生产中 | 工厂已开始生产 |
| `ready_to_ship` | 待发货 | 生产完成，等待发货安排 |
| `shipped` | 已发货 | 货物已发出，提供跟踪信息 |
| `completed` | 已完成 | 客户确认收货，订单关闭 |
| `cancelled` | 已取消 | 订单取消 |

### 合法状态流转

| 当前状态 | 可流转到 |
|----------|---------|
| `pending_production` | `in_production`, `cancelled` |
| `in_production` | `ready_to_ship`, `cancelled` |
| `ready_to_ship` | `shipped`, `cancelled` |
| `shipped` | `completed`, `cancelled` |
| `completed` | _(终态)_ |
| `cancelled` | _(终态)_ |

---

## 快速开始

### 环境要求

- Node.js v16+
- 网易企业邮 SMTP 配置（`imap-smtp-email` skill 的 `.env` 文件）

### 安装依赖

```bash
cd /Users/wilson/.openclaw/workspace/skills/order-tracker
npm install
```

### 添加新订单

直接编辑 `data/orders.json`，参考现有订单格式，或使用 `config/order-schema.json` 验证结构。

---

## 使用说明

### 1. 订单状态更新

```bash
cd scripts/

# 查看帮助
node update-order-status.js --help

# 预览变更（不写入）
node update-order-status.js --order-id ORD-20260324-001 --status ready_to_ship --dry-run

# 更新状态
node update-order-status.js --order-id ORD-20260324-001 --status ready_to_ship --notes "生产完成，等待发货"

# 更新状态并标记需要发送通知
node update-order-status.js --order-id ORD-20260324-001 --status shipped \
  --notes "已通过 DHL 发出，单号 1234567890" \
  --trigger-notification

# 使用自定义数据文件路径
node update-order-status.js --order-id ORD-xxx --status completed \
  --orders-file /path/to/orders.json
```

**选项说明：**

| 参数 | 必需 | 说明 |
|------|------|------|
| `--order-id` | ✅ | 订单 ID |
| `--status` | ✅ | 新状态（见状态模型） |
| `--notes` | ❌ | 变更备注 |
| `--dry-run` | ❌ | 预览模式，不写入文件 |
| `--trigger-notification` | ❌ | 标记需要发送客户通知 |
| `--orders-file` | ❌ | 自定义 orders.json 路径 |
| `--schema-file` | ❌ | 自定义 schema 路径 |

---

### 2. 发送客户邮件通知

> **依赖：** 需要 `imap-smtp-email` skill 的 `.env` 文件，路径为  
> `/Users/wilson/.openclaw/workspace/skills/imap-smtp-email/.env`

```bash
cd scripts/

# 预览邮件（不发送）
node send-order-notification.js --order-id ORD-20260324-001 --dry-run

# 发送通知
node send-order-notification.js --order-id ORD-20260324-001

# 覆盖状态（发送指定状态的通知模板）
node send-order-notification.js --order-id ORD-20260324-001 --status shipped

# 使用自定义数据文件
node send-order-notification.js --order-id ORD-xxx --orders-file /path/to/orders.json
```

**选项说明：**

| 参数 | 必需 | 说明 |
|------|------|------|
| `--order-id` | ✅ | 订单 ID |
| `--status` | ❌ | 覆盖状态（默认用订单当前状态） |
| `--dry-run` | ❌ | 预览邮件内容，不实际发送 |
| `--orders-file` | ❌ | 自定义 orders.json 路径 |

**支持的邮件模板（5 种状态）：**
- `in_production` — 生产进度通知
- `ready_to_ship` — 准备发货通知
- `shipped` — 发货确认（含物流信息）
- `completed` — 订单完成确认
- `cancelled` — 取消通知

---

### 3. 订单看板

```bash
cd scripts/

# 显示所有订单（按状态分组）
node order-dashboard.js

# 按状态过滤
node order-dashboard.js --status in_production
node order-dashboard.js --status shipped

# 查看单个订单详情
node order-dashboard.js --order-id ORD-20260324-001

# 输出格式
node order-dashboard.js --format table    # 完整表格（默认）
node order-dashboard.js --format compact  # 紧凑表格
node order-dashboard.js --format json     # JSON 导出

# 组合使用
node order-dashboard.js --status shipped --format json
```

**看板功能：**
- 📊 订单汇总统计（总订单数、总金额、各状态分布）
- 🚨 逾期订单检测（交期已过且未完成）
- ⚡ 紧急订单检测（≤7 天交期且未完成）
- 🎨 ANSI 颜色区分状态

---

## 典型工作流

### 订单从生产到发货的完整流程

```bash
# Step 1: 生产开始，更新状态并通知客户
node update-order-status.js \
  --order-id ORD-20260324-001 \
  --status in_production \
  --notes "工厂已排期，预计4月15日完成"
node send-order-notification.js --order-id ORD-20260324-001

# Step 2: 生产完成，准备发货
node update-order-status.js \
  --order-id ORD-20260324-001 \
  --status ready_to_ship \
  --notes "生产完成，QC 通过，等待装箱"

# Step 3: 发货，更新物流信息
node update-order-status.js \
  --order-id ORD-20260324-001 \
  --status shipped \
  --notes "DHL Express, 单号: 1234567890, ETA: 4月25日"
node send-order-notification.js --order-id ORD-20260324-001

# Step 4: 确认收货
node update-order-status.js \
  --order-id ORD-20260324-001 \
  --status completed \
  --notes "客户确认收货"
node send-order-notification.js --order-id ORD-20260324-001

# 随时查看看板
node order-dashboard.js
```

---

## 数据格式

### orders.json 结构

```json
{
  "orders": [
    {
      "order_id": "ORD-20260324-001",
      "customer_name": "Michael Chen",
      "customer_email": "[REDACTED_SECRET]",
      "customer_company": "TechSource Electronics Ltd",
      "product_list": [
        {
          "sku": "SKW-HDMI-2.1-2M",
          "name": "HDMI 2.1 Cable 2M",
          "quantity": 500,
          "unit_price": 8.5,
          "currency": "USD"
        }
      ],
      "quantity": 500,
      "unit_price": 8.5,
      "total_amount": 4250,
      "currency": "USD",
      "delivery_date": "2026-04-20",
      "status": "in_production",
      "status_history": [
        {
          "status": "pending_production",
          "changed_at": "2026-03-24T10:30:00+08:00",
          "changed_by": "jaden",
          "notes": "订单确认",
          "notification_sent": true
        }
      ],
      "shipping_address": "...",
      "notes": "...",
      "created_at": "2026-03-24T10:30:00+08:00",
      "updated_at": "2026-03-24T15:00:00+08:00"
    }
  ]
}
```

完整字段定义见 `config/order-schema.json`（JSON Schema draft-07）。

---

## SMTP 配置

通知脚本复用 `imap-smtp-email` skill 的 SMTP 配置，无需单独配置：

```
依赖文件：/Users/wilson/.openclaw/workspace/skills/imap-smtp-email/.env
```

`.env` 需包含：
```env
SMTP_HOST=smtphz.qiye.163.com
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=[REDACTED_SECRET]
SMTP_PASS=<password>
```

---

## 日志文件

| 日志 | 路径 | 内容 |
|------|------|------|
| 状态变更 | `logs/status-changes.log` | 每次状态更新记录 |
| 邮件通知 | `logs/notifications.log` | 发送/失败的通知记录 |

---

## 约束与扩展说明

### 当前约束（task-005 设计决策）

- ❌ 不对接工厂生产系统（ERP/MES）
- ❌ 不抓取物流 API（17TRACK/顺丰等）
- ✅ 状态手动更新（CLI 脚本）
- ✅ 数据存储本地 JSON 文件

### 未来可扩展方向

- 接入工厂 ERP 系统自动同步生产状态
- 接入 17TRACK API 自动更新物流轨迹
- 添加 Web UI 界面
- 接入 OKKI CRM 同步订单数据

---

## 依赖

| 包 | 版本 | 用途 |
|----|------|------|
| `nodemailer` | ^7.0 | SMTP 邮件发送 |
| `dotenv` | ^16.0 | 环境变量加载 |

---

## 开发历史

- **2026-03-24** — task-005 完成，所有 5 个子任务通过审阅
- **2026-03-24** — WILSON 修复 `order-dashboard.js` 中 `COLORS.black` 未定义 bug
- 执行方：IRON (bailian/qwen3.5-plus)
- 审阅方：WILSON (aiberm/claude-sonnet-4-6)


path: config/order-schema.json
content: {
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://farreach.local/schemas/order.json",
  "title": "订单数据模型 (Order Data Schema)",
  "description": "本地订单跟踪系统的数据结构定义，用于 task-005 订单管理模块",
  "type": "object",
  "required": [
    "order_id",
    "customer_name",
    "customer_email",
    "product_list",
    "quantity",
    "unit_price",
    "currency",
    "delivery_date",
    "status",
    "status_history",
    "created_at",
    "updated_at"
  ],
  "properties": {
    "order_id": {
      "type": "string",
      "description": "订单唯一标识（UUID 或自定义编号）",
      "examples": ["ORD-20260324-001", "550e8400-e29b-41d4-a716-446655440000"]
    },
    "customer_name": {
      "type": "string",
      "description": "客户名称",
      "minLength": 1,
      "maxLength": 200
    },
    "customer_email": {
      "type": "string",
      "format": "email",
      "description": "客户邮箱（用于通知）",
      "maxLength": 255
    },
    "customer_company": {
      "type": "string",
      "description": "客户公司名称（可选）",
      "maxLength": 200
    },
    "product_list": {
      "type": "array",
      "description": "产品清单",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": ["sku", "name", "quantity", "unit_price"],
        "properties": {
          "sku": {
            "type": "string",
            "description": "产品 SKU",
            "minLength": 1
          },
          "name": {
            "type": "string",
            "description": "产品名称",
            "minLength": 1
          },
          "description": {
            "type": "string",
            "description": "产品描述（可选）"
          },
          "quantity": {
            "type": "integer",
            "description": "产品数量",
            "minimum": 1
          },
          "unit_price": {
            "type": "number",
            "description": "产品单价",
            "minimum": 0
          },
          "currency": {
            "type": "string",
            "description": "货币单位（可选，默认使用订单级 currency）",
            "pattern": "^[A-Z]{3}$"
          }
        }
      }
    },
    "quantity": {
      "type": "integer",
      "description": "订单总数量",
      "minimum": 1
    },
    "unit_price": {
      "type": "number",
      "description": "订单单价（或平均单价）",
      "minimum": 0
    },
    "total_amount": {
      "type": "number",
      "description": "订单总金额（可选，可自动计算）",
      "minimum": 0
    },
    "currency": {
      "type": "string",
      "description": "货币单位",
      "pattern": "^[A-Z]{3}$",
      "examples": ["USD", "CNY", "EUR", "GBP"]
    },
    "delivery_date": {
      "type": "string",
      "format": "date",
      "description": "预计交期（ISO 8601 日期）",
      "examples": ["2026-04-15"]
    },
    "status": {
      "type": "string",
      "description": "当前订单状态",
      "enum": [
        "pending_production",
        "in_production",
        "ready_to_ship",
        "shipped",
        "completed",
        "cancelled"
      ]
    },
    "status_history": {
      "type": "array",
      "description": "状态变更历史记录",
      "items": {
        "type": "object",
        "required": ["status", "changed_at", "changed_by"],
        "properties": {
          "status": {
            "type": "string",
            "description": "变更后的状态",
            "enum": [
              "pending_production",
              "in_production",
              "ready_to_ship",
              "shipped",
              "completed",
              "cancelled"
            ]
          },
          "changed_at": {
            "type": "string",
            "format": "date-time",
            "description": "变更时间（ISO 8601）"
          },
          "changed_by": {
            "type": "string",
            "description": "变更操作者（manual/system/用户名）",
            "examples": ["manual", "system", "jaden", "wilson"]
          },
          "notes": {
            "type": "string",
            "description": "变更备注（可选）"
          },
          "notification_sent": {
            "type": "boolean",
            "description": "是否已发送客户通知",
            "default": false
          }
        }
      }
    },
    "created_at": {
      "type": "string",
      "format": "date-time",
      "description": "订单创建时间"
    },
    "updated_at": {
      "type": "string",
      "format": "date-time",
      "description": "最后更新时间"
    },
    "notes": {
      "type": "string",
      "description": "备注信息（可选）",
      "maxLength": 2000
    },
    "shipping_address": {
      "type": "object",
      "description": "收货地址（可选）",
      "properties": {
        "country": {
          "type": "string"
        },
        "state": {
          "type": "string"
        },
        "city": {
          "type": "string"
        },
        "address_line1": {
          "type": "string"
        },
        "address_line2": {
          "type": "string"
        },
        "postal_code": {
          "type": "string"
        }
      }
    },
    "tracking_number": {
      "type": "string",
      "description": "物流运单号（发货后填写）",
      "maxLength": 100
    },
    "carrier": {
      "type": "string",
      "description": "物流承运商（可选）",
      "maxLength": 100
    }
  },
  "additionalProperties": false
}


path: data/orders.json
content: {
  "orders": [
    {
      "order_id": "ORD-20260324-001",
      "customer_name": "John Smith",
      "customer_email": "[REDACTED_SECRET]",
      "customer_company": "Acme Corp",
      "product_list": [
        {
          "sku": "SKW-HDMI-2.1-2M",
          "name": "HDMI 2.1 Cable 2M",
          "description": "High-speed HDMI 2.1 cable, 48Gbps, 8K support",
          "quantity": 500,
          "unit_price": 8.5,
          "currency": "USD"
        },
        {
          "sku": "SKW-DP-1.4-3M",
          "name": "DisplayPort 1.4 Cable 3M",
          "description": "DP 1.4 cable, 32.4Gbps, 8K@60Hz",
          "quantity": 300,
          "unit_price": 12,
          "currency": "USD"
        }
      ],
      "quantity": 800,
      "unit_price": 10,
      "total_amount": 7850,
      "currency": "USD",
      "delivery_date": "2026-04-20",
      "status": "in_production",
      "status_history": [
        {
          "status": "pending_production",
          "changed_at": "2026-03-24T10:30:00+08:00",
          "changed_by": "jaden",
          "notes": "订单确认，等待生产排期",
          "notification_sent": true
        },
        {
          "status": "in_production",
          "changed_at": "2026-03-24T09:11:30.699Z",
          "changed_by": "manual",
          "notes": "生产计划确认，开始生产",
          "notification_sent": true,
          "notification_template": "order_production_started"
        }
      ],
      "created_at": "2026-03-24T10:30:00+08:00",
      "updated_at": "2026-03-24T09:11:30.699Z",
      "notes": "客户为英国分销商，首次下单，需重点关注质量",
      "shipping_address": {
        "country": "United Kingdom",
        "city": "London",
        "address_line1": "123 Tech Park Road",
        "postal_code": "SW1A 1AA"
      }
    },
    {
      "order_id": "ORD-20260320-002",
      "customer_name": "Emily Davis",
      "customer_email": "[REDACTED_SECRET]",
      "customer_company": "GlobalTech Inc",
      "product_list": [
        {
          "sku": "SKW-USB-C-1M",
          "name": "USB-C to USB-C Cable 1M",
          "description": "USB-C 3.2 Gen2, 10Gbps, 100W PD",
          "quantity": 1000,
          "unit_price": 6.8,
          "currency": "USD"
        },
        {
          "sku": "SKW-USB-C-2M",
          "name": "USB-C to USB-C Cable 2M",
          "description": "USB-C 3.2 Gen2, 10Gbps, 100W PD",
          "quantity": 800,
          "unit_price": 8.5,
          "currency": "USD"
        }
      ],
      "quantity": 1800,
      "unit_price": 7.56,
      "total_amount": 13600,
      "currency": "USD",
      "delivery_date": "2026-04-10",
      "status": "in_production",
      "status_history": [
        {
          "status": "pending_production",
          "changed_at": "2026-03-20T14:00:00+08:00",
          "changed_by": "wilson",
          "notes": "订单确认",
          "notification_sent": true
        },
        {
          "status": "in_production",
          "changed_at": "2026-03-22T09:15:00+08:00",
          "changed_by": "system",
          "notes": "生产排期完成，已开始生产",
          "notification_sent": true
        }
      ],
      "created_at": "2026-03-20T14:00:00+08:00",
      "updated_at": "2026-03-22T09:15:00+08:00",
      "notes": "美国老客户，常规订单，交期紧张",
      "shipping_address": {
        "country": "United States",
        "state": "California",
        "city": "San Francisco",
        "address_line1": "456 Market Street",
        "postal_code": "94102"
      }
    },
    {
      "order_id": "ORD-20260315-003",
      "customer_name": "Robert Wilson",
      "customer_email": "[REDACTED_SECRET]",
      "customer_company": "TechSolutions GmbH",
      "product_list": [
        {
          "sku": "SKW-HDMI-2.0-5M",
          "name": "HDMI 2.0 Cable 5M",
          "description": "HDMI 2.0 cable, 18Gbps, 4K@60Hz",
          "quantity": 200,
          "unit_price": 15,
          "currency": "EUR"
        }
      ],
      "quantity": 200,
      "unit_price": 15,
      "total_amount": 3000,
      "currency": "EUR",
      "delivery_date": "2026-03-28",
      "status": "shipped",
      "status_history": [
        {
          "status": "pending_production",
          "changed_at": "2026-03-15T11:00:00+08:00",
          "changed_by": "jaden",
          "notes": "订单确认",
          "notification_sent": true
        },
        {
          "status": "in_production",
          "changed_at": "2026-03-17T10:00:00+08:00",
          "changed_by": "system",
          "notes": "开始生产",
          "notification_sent": true
        },
        {
          "status": "ready_to_ship",
          "changed_at": "2026-03-21T16:30:00+08:00",
          "changed_by": "wilson",
          "notes": "生产完成，待发货",
          "notification_sent": true
        },
        {
          "status": "shipped",
          "changed_at": "2026-03-23T14:00:00+08:00",
          "changed_by": "wilson",
          "notes": "已发货，DHL 快递",
          "notification_sent": true
        }
      ],
      "created_at": "2026-03-15T11:00:00+08:00",
      "updated_at": "2026-03-23T14:00:00+08:00",
      "notes": "德国客户，要求 CE 认证，已提供",
      "shipping_address": {
        "country": "Germany",
        "city": "Munich",
        "address_line1": "789 Industriestrasse",
        "postal_code": "80331"
      },
      "tracking_number": "DHL1234567890",
      "carrier": "DHL Express"
    }
  ],
  "metadata": {
    "schema_version": "1.0",
    "schema_ref": "order-schema.json",
    "created_at": "2026-03-24T16:55:00+08:00",
    "created_by": "iron",
    "total_orders": 3,
    "status_summary": {
      "pending_production": 0,
      "in_production": 2,
      "ready_to_ship": 0,
      "shipped": 2,
      "completed": 0,
      "cancelled": 0
    },
    "updated_at": "2026-03-25T01:59:53.138Z",
    "updated_by": "manual"
  }
}

path: package-lock.json
content: {
  "name": "order-tracker-skill",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "order-tracker-skill",
      "version": "1.0.0",
      "license": "MIT",
      "dependencies": {
        "dotenv": "^16.6.1",
        "nodemailer": "^7.0.13"
      }
    },
    "node_modules/dotenv": {
      "version": "16.6.1",
      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://dotenvx.com"
      }
    },
    "node_modules/nodemailer": {
      "version": "7.0.13",
      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT-0",
      "engines": {
        "node": ">=6.0.0"
      }
    }
  }
}


path: package.json
content: {
  "name": "order-tracker-skill",
  "version": "1.0.0",
  "description": "Order tracking and management skill for Farreach Electronic",
  "main": "scripts/update-order-status.js",
  "scripts": {
    "update-status": "node scripts/update-order-status.js",
    "notify": "node scripts/send-order-notification.js"
  },
  "dependencies": {
    "dotenv": "^16.6.1",
    "nodemailer": "^7.0.13"
  },
  "keywords": [
    "order",
    "tracking",
    "management",
    "skill"
  ],
  "author": "Farreach Electronic",
  "license": "MIT"
}


path: scripts/order-dashboard.js
content: #!/usr/bin/env node

/**
 * Order Dashboard - Command-line order status summary view
 * 
 * Usage:
 *   node order-dashboard.js                           # Show all orders grouped by status
 *   node order-dashboard.js --status in_production    # Filter by status
 *   node order-dashboard.js --order-id ORD-20260324-001  # View single order details
 *   node order-dashboard.js --format json             # Output as JSON
 *   node order-dashboard.js --format compact          # Compact table view
 *   node order-dashboard.js --format table            # Full table view (default)
 */

const fs = require('fs');
const path = require('path');

// Configuration
const DATA_DIR = path.join(__dirname, '..', 'data');
const ORDERS_FILE = path.join(DATA_DIR, 'orders.json');

// ANSI color codes
const COLORS = {
  reset: '\x1b[0m',
  bold: '\x1b[1m',
  dim: '\x1b[2m',
  red: '\x1b[31m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  magenta: '\x1b[35m',
  cyan: '\x1b[36m',
  white: '\x1b[37m',
  bgRed: '\x1b[41m',
  bgYellow: '\x1b[43m',
  bgGreen: '\x1b[42m',
  black: '\x1b[30m',
};

// Status color mapping
const STATUS_COLORS = {
  pending_production: COLORS.yellow,
  in_production: COLORS.blue,
  ready_to_ship: COLORS.cyan,
  shipped: COLORS.magenta,
  completed: COLORS.green,
  cancelled: COLORS.red,
};

// Parse command line arguments
function parseArgs(args) {
  const result = {
    status: null,
    orderId: null,
    format: 'table',
  };

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--status' && args[i + 1]) {
      result.status = args[++i];
    } else if (args[i] === '--order-id' && args[i + 1]) {
      result.orderId = args[++i];
    } else if (args[i] === '--format' && args[i + 1]) {
      result.format = args[++i];
    } else if (args[i] === '--help' || args[i] === '-h') {
      printHelp();
      process.exit(0);
    }
  }

  return result;
}

function printHelp() {
  console.log(`
${COLORS.bold}Order Dashboard${COLORS.reset} - Command-line order status summary view

${COLORS.bold}Usage:${COLORS.reset}
  node order-dashboard.js [options]

${COLORS.bold}Options:${COLORS.reset}
  --status <status>      Filter orders by status
                         Values: pending_production, in_production, ready_to_ship, shipped, completed, cancelled
  --order-id <id>        View details of a single order
  --format <format>      Output format: table (default), json, compact
  --help, -h             Show this help message

${COLORS.bold}Examples:${COLORS.reset}
  node order-dashboard.js                              # Show all orders grouped by status
  node order-dashboard.js --status in_production       # Show only in-production orders
  node order-dashboard.js --order-id ORD-20260324-001  # View single order details
  node order-dashboard.js --format json                # Output as JSON
`);
}

// Load orders data
function loadOrders() {
  if (!fs.existsSync(ORDERS_FILE)) {
    console.error(`${COLORS.red}Error: Orders file not found at ${ORDERS_FILE}${COLORS.reset}`);
    process.exit(1);
  }

  try {
    const data = JSON.parse(fs.readFileSync(ORDERS_FILE, 'utf8'));
    return data;
  } catch (error) {
    console.error(`${COLORS.red}Error reading orders file: ${error.message}${COLORS.reset}`);
    process.exit(1);
  }
}

// Calculate days until delivery
function daysUntilDelivery(deliveryDate) {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const delivery = new Date(deliveryDate);
  delivery.setHours(0, 0, 0, 0);
  const diffTime = delivery - today;
  const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
  return diffDays;
}

// Check if order is overdue
function isOverdue(order) {
  const completedStatuses = ['completed', 'cancelled'];
  if (completedStatuses.includes(order.status)) {
    return false;
  }
  return daysUntilDelivery(order.delivery_date) < 0;
}

// Check if order is urgent (delivery ≤ 7 days)
function isUrgent(order) {
  const completedStatuses = ['completed', 'cancelled'];
  if (completedStatuses.includes(order.status)) {
    return false;
  }
  const days = daysUntilDelivery(order.delivery_date);
  return days >= 0 && days <= 7;
}

// Format currency
function formatCurrency(amount, currency) {
  const symbols = { USD: '$', EUR: '€', GBP: '£', CNY: '¥' };
  const symbol = symbols[currency] || currency;
  return `${symbol}${amount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}

// Format date
function formatDate(dateStr) {
  const date = new Date(dateStr);
  return date.toISOString().split('T')[0];
}

// Colorize status
function colorizeStatus(status) {
  const color = STATUS_COLORS[status] || COLORS.white;
  return `${color}${status}${COLORS.reset}`;
}

// Get status label with emoji
function getStatusLabel(status) {
  const labels = {
    pending_production: '⏳ Pending Production',
    in_production: '🏭 In Production',
    ready_to_ship: '📦 Ready to Ship',
    shipped: '🚚 Shipped',
    completed: '✅ Completed',
    cancelled: '❌ Cancelled',
  };
  return labels[status] || status;
}

// Print table header
function printTableHeader() {
  const header = [
    'Order ID',
    'Customer',
    'Products',
    'Qty',
    'Amount',
    'Status',
    'Delivery',
    'Days',
  ];
  
  console.log(`\n${COLORS.bold}${COLORS.cyan}${header.join(' │ ')}${COLORS.reset}`);
  console.log(`${COLORS.dim}${'─'.repeat(120)}${COLORS.reset}`);
}

// Print order row (table format)
function printOrderRow(order) {
  const days = daysUntilDelivery(order.delivery_date);
  let daysLabel = days.toString();
  
  if (isOverdue(order)) {
    daysLabel = `${COLORS.bgRed}${COLORS.white} OVERDUE ${COLORS.reset}`;
  } else if (isUrgent(order)) {
    daysLabel = `${COLORS.bgYellow}${COLORS.black} ${days}d ⚠️ ${COLORS.reset}`;
  } else if (days < 0) {
    daysLabel = `${COLORS.dim}${days}d${COLORS.reset}`;
  } else {
    daysLabel = `${COLORS.green}${days}d${COLORS.reset}`;
  }

  const productSummary = order.product_list.length > 1
    ? `${order.product_list.length} items`
    : order.product_list[0]?.sku || 'N/A';

  const row = [
    order.order_id,
    order.customer_company?.substring(0, 20) || order.customer_name,
    productSummary,
    order.quantity.toString(),
    formatCurrency(order.total_amount, order.currency),
    colorizeStatus(order.status),
    formatDate(order.delivery_date),
    daysLabel,
  ];

  console.log(row.join(' │ '));
}

// Print compact order row
function printCompactRow(order) {
  const days = daysUntilDelivery(order.delivery_date);
  let statusIcon = '';
  
  if (isOverdue(order)) {
    statusIcon = '🔴';
  } else if (isUrgent(order)) {
    statusIcon = '🟡';
  } else {
    statusIcon = '🟢';
  }

  console.log(`  ${statusIcon} ${order.order_id} │ ${order.customer_company} │ ${colorizeStatus(order.status)} │ ${formatDate(order.delivery_date)} (${days}d) │ ${formatCurrency(order.total_amount, order.currency)}`);
}

// Print single order details
function printOrderDetails(order) {
  const days = daysUntilDelivery(order.delivery_date);
  
  console.log(`\n${COLORS.bold}${COLORS.cyan}═══════════════════════════════════════════════════════════${COLORS.reset}`);
  console.log(`${COLORS.bold}${COLORS.cyan}  ORDER DETAILS: ${order.order_id}${COLORS.reset}`);
  console.log(`${COLORS.bold}${COLORS.cyan}═══════════════════════════════════════════════════════════${COLORS.reset}\n`);

  console.log(`${COLORS.bold}Customer:${COLORS.reset}`);
  console.log(`  Name: ${order.customer_name}`);
  console.log(`  Company: ${order.customer_company}`);
  console.log(`  Email: ${order.customer_email}`);
  console.log(`  Shipping: ${order.shipping_address?.city}, ${order.shipping_address?.country}`);

  console.log(`\n${COLORS.bold}Order Summary:${COLORS.reset}`);
  console.log(`  Status: ${colorizeStatus(order.status)}`);
  console.log(`  Total Amount: ${formatCurrency(order.total_amount, order.currency)}`);
  console.log(`  Delivery Date: ${formatDate(order.delivery_date)} (${days} days)`);
  
  if (isOverdue(order)) {
    console.log(`  ${COLORS.bgRed}${COLORS.white} ⚠️  OVERDUE ${COLORS.reset}`);
  } else if (isUrgent(order)) {
    console.log(`  ${COLORS.bgYellow}${COLORS.black} ⚠️  URGENT (≤7 days) ${COLORS.reset}`);
  }

  console.log(`\n${COLORS.bold}Products:${COLORS.reset}`);
  order.product_list.forEach((product, idx) => {
    console.log(`  ${idx + 1}. ${product.name} (${product.sku})`);
    console.log(`     Qty: ${product.quantity} × ${formatCurrency(product.unit_price, product.currency)} = ${formatCurrency(product.quantity * product.unit_price, product.currency)}`);
  });

  console.log(`\n${COLORS.bold}Status History:${COLORS.reset}`);
  order.status_history?.forEach((entry, idx) => {
    const date = new Date(entry.changed_at).toISOString().split('T')[0];
    console.log(`  ${idx + 1}. ${colorizeStatus(entry.status)} - ${date} by ${entry.changed_by}`);
    if (entry.notes) {
      console.log(`     "${entry.notes}"`);
    }
  });

  if (order.tracking_number) {
    console.log(`\n${COLORS.bold}Shipping:${COLORS.reset}`);
    console.log(`  Carrier: ${order.carrier || 'N/A'}`);
    console.log(`  Tracking: ${order.tracking_number}`);
  }

  if (order.notes) {
    console.log(`\n${COLORS.bold}Notes:${COLORS.reset}`);
    console.log(`  ${order.notes}`);
  }

  console.log(`\n${COLORS.bold}${COLORS.cyan}═══════════════════════════════════════════════════════════${COLORS.reset}\n`);
}

// Print summary statistics
function printSummary(orders) {
  const statusCounts = {};
  let totalAmount = 0;
  let overdueCount = 0;
  let urgentCount = 0;

  orders.forEach(order => {
    statusCounts[order.status] = (statusCounts[order.status] || 0) + 1;
    totalAmount += order.total_amount;
    if (isOverdue(order)) overdueCount++;
    if (isUrgent(order)) urgentCount++;
  });

  console.log(`\n${COLORS.bold}${COLORS.cyan}═══════════════════════════════════════════════════════════${COLORS.reset}`);
  console.log(`${COLORS.bold}${COLORS.cyan}  ORDER SUMMARY${COLORS.reset}`);
  console.log(`${COLORS.bold}${COLORS.cyan}═══════════════════════════════════════════════════════════${COLORS.reset}\n`);

  console.log(`${COLORS.bold}Total Orders:${COLORS.reset} ${orders.length}`);
  console.log(`${COLORS.bold}Total Value:${COLORS.reset} ${formatCurrency(totalAmount, 'USD')} (mixed currencies)\n`);

  console.log(`${COLORS.bold}Status Breakdown:${COLORS.reset}`);
  Object.entries(statusCounts).forEach(([status, count]) => {
    const label = getStatusLabel(status);
    console.log(`  ${colorizeStatus(status)}: ${count}`);
  });

  if (overdueCount > 0) {
    console.log(`\n${COLORS.bgRed}${COLORS.white} ⚠️  OVERDUE ORDERS: ${overdueCount} ${COLORS.reset}`);
  }
  if (urgentCount > 0) {
    console.log(`${COLORS.bgYellow}${COLORS.black} ⚠️  URGENT ORDERS (≤7 days): ${urgentCount} ${COLORS.reset}`);
  }

  console.log(`\n${COLORS.bold}${COLORS.cyan}═══════════════════════════════════════════════════════════${COLORS.reset}\n`);
}

// Main dashboard view (grouped by status)
function printDashboard(orders, format) {
  if (format === 'json') {
    const output = {
      generated_at: new Date().toISOString(),
      total_orders: orders.length,
      status_groups: {},
      summary: {
        total_value: orders.reduce((sum, o) => sum + o.total_amount, 0),
        overdue_count: orders.filter(isOverdue).length,
        urgent_count: orders.filter(isUrgent).length,
      },
    };

    orders.forEach(order => {
      if (!output.status_groups[order.status]) {
        output.status_groups[order.status] = [];
      }
      output.status_groups[order.status].push({
        order_id: order.order_id,
        customer: order.customer_company,
        amount: order.total_amount,
        currency: order.currency,
        delivery_date: order.delivery_date,
        days_until_delivery: daysUntilDelivery(order.delivery_date),
        is_overdue: isOverdue(order),
        is_urgent: isUrgent(order),
      });
    });

    console.log(JSON.stringify(output, null, 2));
    return;
  }

  // Print summary
  printSummary(orders);

  // Group orders by status
  const grouped = {};
  orders.forEach(order => {
    if (!grouped[order.status]) {
      grouped[order.status] = [];
    }
    grouped[order.status].push(order);
  });

  // Print each status group
  const statusOrder = ['pending_production', 'in_production', 'ready_to_ship', 'shipped', 'completed', 'cancelled'];
  
  statusOrder.forEach(status => {
    const statusOrders = grouped[status];
    if (!statusOrders || statusOrders.length === 0) return;

    console.log(`\n${COLORS.bold}${getStatusLabel(status)} (${statusOrders.length} orders)${COLORS.reset}`);
    console.log(`${COLORS.dim}${'─'.repeat(80)}${COLORS.reset}`);

    if (format === 'compact') {
      statusOrders.forEach(order => printCompactRow(order));
    } else {
      printTableHeader();
      statusOrders.forEach(order => printOrderRow(order));
    }
    console.log();
  });

  // Print overdue orders section if any
  const overdueOrders = orders.filter(isOverdue);
  if (overdueOrders.length > 0) {
    console.log(`\n${COLORS.bold}${COLORS.red}🚨 OVERDUE ORDERS - IMMEDIATE ACTION REQUIRED${COLORS.reset}`);
    console.log(`${COLORS.dim}${'─'.repeat(80)}${COLORS.reset}`);
    overdueOrders.forEach(order => {
      const days = Math.abs(daysUntilDelivery(order.delivery_date));
      console.log(`  ${COLORS.red}${order.order_id}${COLORS.reset} - ${order.customer_company} - ${days} days overdue`);
    });
  }

  // Print urgent orders section if any
  const urgentOrders = orders.filter(o => isUrgent(o) && !isOverdue(o));
  if (urgentOrders.length > 0) {
    console.log(`\n${COLORS.bold}${COLORS.yellow}⚠️  URGENT ORDERS - Delivery within 7 days${COLORS.reset}`);
    console.log(`${COLORS.dim}${'─'.repeat(80)}${COLORS.reset}`);
    urgentOrders.forEach(order => {
      const days = daysUntilDelivery(order.delivery_date);
      console.log(`  ${COLORS.yellow}${order.order_id}${COLORS.reset} - ${order.customer_company} - ${days} days remaining`);
    });
  }
}

// Main function
function main() {
  const args = parseArgs(process.argv.slice(2));
  const data = loadOrders();
  let orders = data.orders || [];

  // Filter by status if specified
  if (args.status) {
    orders = orders.filter(o => o.status === args.status);
  }

  // View single order if order-id specified
  if (args.orderId) {
    const order = orders.find(o => o.order_id === args.orderId);
    if (!order) {
      console.error(`${COLORS.red}Error: Order ${args.orderId} not found${COLORS.reset}`);
      process.exit(1);
    }
    
    if (args.format === 'json') {
      console.log(JSON.stringify(order, null, 2));
    } else {
      printOrderDetails(order);
    }
    return;
  }

  // Print dashboard
  if (orders.length === 0) {
    console.log(`${COLORS.dim}No orders found.${COLORS.reset}`);
    return;
  }

  printDashboard(orders, args.format);
}

// Run
main();


path: scripts/send-order-notification.js
content: #!/usr/bin/env node

/**
 * Order Notification Script
 * 
 * Sends customer notification emails when order status changes.
 * Reuses SMTP configuration from imap-smtp-email skill.
 * 
 * Usage:
 *   node send-order-notification.js --order-id ORD-xxx [--status new_status] [--dry-run]
 * 
 * Options:
 *   --order-id    Order ID to notify (required)
 *   --status      Override status (optional, defaults to order's current status)
 *   --dry-run     Preview email without sending
 *   --orders-file Path to orders.json (default: ../data/orders.json)
 */

const fs = require('fs');
const path = require('path');
const nodemailer = require('nodemailer');
require('dotenv').config({ path: path.resolve(__dirname, '../../imap-smtp-email/.env') });

// Parse command line arguments
function parseArgs(args) {
  const parsed = {
    orderId: null,
    status: null,
    dryRun: false,
    ordersFile: null
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '--order-id' && args[i + 1]) {
      parsed.orderId = args[++i];
    } else if (arg === '--status' && args[i + 1]) {
      parsed.status = args[++i];
    } else if (arg === '--dry-run') {
      parsed.dryRun = true;
    } else if (arg === '--orders-file' && args[i + 1]) {
      parsed.ordersFile = args[++i];
    }
  }

  return parsed;
}

// Email templates by status
const EMAIL_TEMPLATES = {
  in_production: {
    subject: {
      en: (order) => `Your Order ${order.order_id} Has Started Production`,
      zh: (order) => `您的订单 ${order.order_id} 已开始生产`
    },
    body: {
      en: (order) => `
Dear ${order.customer_name},

We are pleased to inform you that your order ${order.order_id} has started production.

Order Summary:
- Order ID: ${order.order_id}
- Current Status: In Production
- Estimated Delivery Date: ${order.delivery_date}
- Total Quantity: ${order.quantity} units
- Total Amount: ${order.total_amount?.toFixed(2) || 'N/A'} ${order.currency}

Product List:
${order.product_list.map(p => `  - ${p.name} (SKU: ${p.sku}): ${p.quantity} units @ ${p.unit_price.toFixed(2)} ${p.currency || order.currency}`).join('\n')}

Our team is now working on your order with priority. We will keep you updated on the progress.

If you have any questions, please don't hesitate to contact us.

Best regards,
Farreach Electronic Co., Ltd
[REDACTED_SECRET]
      `.trim(),
      zh: (order) => `
尊敬的 ${order.customer_name}：

我们很高兴地通知您，您的订单 ${order.order_id} 已开始生产。

订单摘要：
- 订单号：${order.order_id}
- 当前状态：生产中
- 预计交期：${order.delivery_date}
- 总数量：${order.quantity} 件
- 总金额：${order.total_amount?.toFixed(2) || 'N/A'} ${order.currency}

产品清单：
${order.product_list.map(p => `  - ${p.name} (SKU: ${p.sku}): ${p.quantity} 件 @ ${p.unit_price.toFixed(2)} ${p.currency || order.currency}`).join('\n')}

我们的团队正在优先处理您的订单。我们将随时向您更新生产进度。

如有任何疑问，请随时与我们联系。

此致
敬礼
远达电子有限公司
[REDACTED_SECRET]
      `.trim()
    }
  },
  ready_to_ship: {
    subject: {
      en: (order) => `Your Order ${order.order_id} Is Ready to Ship`,
      zh: (order) => `您的订单 ${order.order_id} 已备货完成，即将发货`
    },
    body: {
      en: (order) => `
Dear ${order.customer_name},

Great news! Your order ${order.order_id} has completed production and is ready to ship.

Order Summary:
- Order ID: ${order.order_id}
- Current Status: Ready to Ship
- Estimated Delivery Date: ${order.delivery_date}
- Total Quantity: ${order.quantity} units

Product List:
${order.product_list.map(p => `  - ${p.name} (SKU: ${p.sku}): ${p.quantity} units`).join('\n')}

We are arranging the shipment and will provide you with the tracking information shortly.

Thank you for your patience and trust in Farreach Electronic.

Best regards,
Farreach Electronic Co., Ltd
[REDACTED_SECRET]
      `.trim(),
      zh: (order) => `
尊敬的 ${order.customer_name}：

好消息！您的订单 ${order.order_id} 已完成生产，即将发货。

订单摘要：
- 订单号：${order.order_id}
- 当前状态：待发货
- 预计交期：${order.delivery_date}
- 总数量：${order.quantity} 件

产品清单：
${order.product_list.map(p => `  - ${p.name} (SKU: ${p.sku}): ${p.quantity} 件`).join('\n')}

我们正在安排发货，稍后将为您提供物流跟踪信息。

感谢您对远达电子的信任与耐心。

此致
敬礼
远达电子有限公司
[REDACTED_SECRET]
      `.trim()
    }
  },
  shipped: {
    subject: {
      en: (order) => `Your Order ${order.order_id} Has Been Shipped`,
      zh: (order) => `您的订单 ${order.order_id} 已发货`
    },
    body: {
      en: (order) => `
Dear ${order.customer_name},

Your order ${order.order_id} has been shipped!

Order Summary:
- Order ID: ${order.order_id}
- Current Status: Shipped
- Carrier: ${order.carrier || 'To be confirmed'}
- Tracking Number: ${order.tracking_number || 'To be provided'}
- Estimated Delivery Date: ${order.delivery_date}

Shipping Address:
${order.shipping_address ? [
  order.shipping_address.address_line1,
  order.shipping_address.address_line2,
  order.shipping_address.city,
  order.shipping_address.state,
  order.shipping_address.postal_code,
  order.shipping_address.country
].filter(Boolean).join('\n') : 'N/A'}

You can track your shipment using the tracking number above. Please allow 1-2 business days for the tracking information to be updated.

Thank you for choosing Farreach Electronic!

Best regards,
Farreach Electronic Co., Ltd
[REDACTED_SECRET]
      `.trim(),
      zh: (order) => `
尊敬的 ${order.customer_name}：

您的订单 ${order.order_id} 已发货！

订单摘要：
- 订单号：${order.order_id}
- 当前状态：已发货
- 物流公司：${order.carrier || '待确认'}
- 运单号：${order.tracking_number || '待提供'}
- 预计交期：${order.delivery_date}

收货地址：
${order.shipping_address ? [
  order.shipping_address.address_line1,
  order.shipping_address.address_line2,
  order.shipping_address.city,
  order.shipping_address.state,
  order.shipping_address.postal_code,
  order.shipping_address.country
].filter(Boolean).join('\n') : 'N/A'}

您可以使用上述运单号跟踪您的货物。物流信息可能需要 1-2 个工作日更新。

感谢您选择远达电子！

此致
敬礼
远达电子有限公司
[REDACTED_SECRET]
      `.trim()
    }
  },
  completed: {
    subject: {
      en: (order) => `Your Order ${order.order_id} Is Complete`,
      zh: (order) => `您的订单 ${order.order_id} 已完成`
    },
    body: {
      en: (order) => `
Dear ${order.customer_name},

Your order ${order.order_id} has been completed successfully!

Order Summary:
- Order ID: ${order.order_id}
- Status: Completed
- Total Amount: ${order.total_amount?.toFixed(2) || 'N/A'} ${order.currency}

We hope you are satisfied with our products and service. Your feedback is valuable to us.

If you have any questions or need further assistance, please don't hesitate to contact us.

We look forward to serving you again!

Best regards,
Farreach Electronic Co., Ltd
[REDACTED_SECRET]
      `.trim(),
      zh: (order) => `
尊敬的 ${order.customer_name}：

您的订单 ${order.order_id} 已顺利完成！

订单摘要：
- 订单号：${order.order_id}
- 状态：已完成
- 总金额：${order.total_amount?.toFixed(2) || 'N/A'} ${order.currency}

希望您对我们的产品和服务满意。您的反馈对我们非常重要。

如有任何疑问或需要进一步协助，请随时与我们联系。

期待再次为您服务！

此致
敬礼
远达电子有限公司
[REDACTED_SECRET]
      `.trim()
    }
  },
  cancelled: {
    subject: {
      en: (order) => `Your Order ${order.order_id} Has Been Cancelled`,
      zh: (order) => `您的订单 ${order.order_id} 已取消`
    },
    body: {
      en: (order) => `
Dear ${order.customer_name},

Your order ${order.order_id} has been cancelled.

Order Summary:
- Order ID: ${order.order_id}
- Status: Cancelled
- Original Delivery Date: ${order.delivery_date}

If you have any questions about this cancellation or would like to place a new order, please contact us.

We apologize for any inconvenience caused.

Best regards,
Farreach Electronic Co., Ltd
[REDACTED_SECRET]
      `.trim(),
      zh: (order) => `
尊敬的 ${order.customer_name}：

您的订单 ${order.order_id} 已取消。

订单摘要：
- 订单号：${order.order_id}
- 状态：已取消
- 原定交期：${order.delivery_date}

如果您对此次取消有任何疑问，或希望重新下单，请与我们联系。

对给您带来的不便，我们深表歉意。

此致
敬礼
远达电子有限公司
[REDACTED_SECRET]
      `.trim()
    }
  }
};

// Detect customer language preference (simple heuristic based on email domain or name)
function detectLanguage(order) {
  const email = order.customer_email?.toLowerCase() || '';
  const name = order.customer_name?.toLowerCase() || '';
  const company = order.customer_company?.toLowerCase() || '';

  // Check for Chinese indicators
  const chineseIndicators = ['.cn', 'china', '中文', 'cn'];
  if (chineseIndicators.some(ind => email.includes(ind) || company.includes(ind))) {
    return 'zh';
  }

  // Default to English for international customers
  return 'en';
}

// Log function
function log(message, level = 'INFO') {
  const timestamp = new Date().toISOString();
  const logLine = `[${timestamp}] [${level}] ${message}`;
  console.log(logLine);
  return logLine;
}

// Create SMTP transporter
function createTransporter() {
  const config = {
    host: process.env.SMTP_HOST,
    port: parseInt(process.env.SMTP_PORT) || 587,
    secure: process.env.SMTP_SECURE === 'true',
    auth: {
      user: process.env.SMTP_USER,
      pass: process.env.SMTP_PASS,
    },
    tls: {
      rejectUnauthorized: process.env.SMTP_REJECT_UNAUTHORIZED !== 'false',
    },
  };

  if (!config.host || !config.auth.user || !config.auth.pass) {
    throw new Error('Missing SMTP configuration. Please check imap-smtp-email/.env');
  }

  return nodemailer.createTransport(config);
}

// Send email
async function sendEmail(to, subject, text) {
  const transporter = createTransporter();

  try {
    await transporter.verify();
  } catch (err) {
    throw new Error(`SMTP connection failed: ${err.message}`);
  }

  const mailOptions = {
    from: process.env.SMTP_FROM || process.env.SMTP_USER,
    to: to,
    subject: subject,
    text: text,
  };

  const info = await transporter.sendMail(mailOptions);

  return {
    success: true,
    messageId: info.messageId,
    response: info.response,
  };
}

// Main function
async function main() {
  const args = parseArgs(process.argv.slice(2));
  const logLines = [];

  logLines.push(log('=== Order Notification Script ==='));

  // Validate required arguments
  if (!args.orderId) {
    console.error('Error: --order-id is required');
    console.error('Usage: node send-order-notification.js --order-id ORD-xxx [--status new_status] [--dry-run]');
    process.exit(1);
  }

  logLines.push(log(`Order ID: ${args.orderId}`));
  logLines.push(log(`Dry Run: ${args.dryRun}`));

  // Determine file paths
  const scriptDir = __dirname;
  const ordersFile = args.ordersFile || path.join(scriptDir, '..', 'data', 'orders.json');
  const logFile = path.join(scriptDir, '..', 'logs', 'notifications.log');

  logLines.push(log(`Orders file: ${ordersFile}`));
  logLines.push(log(`Log file: ${logFile}`));

  // Check if orders file exists
  if (!fs.existsSync(ordersFile)) {
    const errorMsg = `Orders file not found: ${ordersFile}`;
    logLines.push(log(errorMsg, 'ERROR'));
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    console.error(errorMsg);
    process.exit(1);
  }

  // Read orders file
  let ordersData;
  try {
    const ordersContent = fs.readFileSync(ordersFile, 'utf8');
    ordersData = JSON.parse(ordersContent);
    logLines.push(log(`Loaded ${ordersData.orders?.length || 0} orders`));
  } catch (err) {
    const errorMsg = `Failed to read orders file: ${err.message}`;
    logLines.push(log(errorMsg, 'ERROR'));
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    console.error(errorMsg);
    process.exit(1);
  }

  // Find the order
  const orderIndex = ordersData.orders?.findIndex(o => o.order_id === args.orderId);
  if (orderIndex === undefined || orderIndex === -1) {
    const errorMsg = `Order not found: ${args.orderId}`;
    logLines.push(log(errorMsg, 'ERROR'));
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    console.error(errorMsg);
    process.exit(1);
  }

  const order = ordersData.orders[orderIndex];
  const currentStatus = args.status || order.status;
  logLines.push(log(`Found order: ${order.order_id} (status: ${currentStatus})`));

  // Check if template exists for this status
  if (!EMAIL_TEMPLATES[currentStatus]) {
    const errorMsg = `No email template for status: ${currentStatus}`;
    logLines.push(log(errorMsg, 'ERROR'));
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    console.error(errorMsg);
    console.error(`Available statuses: ${Object.keys(EMAIL_TEMPLATES).join(', ')}`);
    process.exit(1);
  }

  // Detect language
  const lang = detectLanguage(order);
  logLines.push(log(`Detected language: ${lang}`));

  // Generate email content
  const template = EMAIL_TEMPLATES[currentStatus];
  const subject = template.subject[lang](order);
  const body = template.body[lang](order);

  logLines.push(log(`Email subject: ${subject}`));
  logLines.push(log(`Email recipient: ${order.customer_email}`));

  if (args.dryRun) {
    logLines.push(log('DRY RUN: Email preview (not sending)'));
    console.log('\n=== EMAIL PREVIEW ===');
    console.log(`To: ${order.customer_email}`);
    console.log(`Subject: ${subject}`);
    console.log('\n--- Body ---');
    console.log(body);
    console.log('--- End Body ---\n');
    console.log(logLines.join('\n'));
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    process.exit(0);
  }

  // Send email
  logLines.push(log('Sending email...'));
  try {
    const result = await sendEmail(order.customer_email, subject, body);
    logLines.push(log(`Email sent successfully! Message ID: ${result.messageId}`));
    console.log('\n=== NOTIFICATION SENT ===');
    console.log(`Order: ${order.order_id}`);
    console.log(`To: ${order.customer_email}`);
    console.log(`Subject: ${subject}`);
    console.log(`Message ID: ${result.messageId}`);
  } catch (err) {
    const errorMsg = `Failed to send email: ${err.message}`;
    logLines.push(log(errorMsg, 'ERROR'));
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    console.error(errorMsg);
    process.exit(1);
  }

  // Update orders.json to mark notification as sent
  const now = new Date().toISOString();
  const latestHistoryEntry = order.status_history[order.status_history.length - 1];
  if (latestHistoryEntry) {
    latestHistoryEntry.notification_sent = true;
    latestHistoryEntry.notification_sent_at = now;
    latestHistoryEntry.notification_message_id = logLines.find(l => l.includes('Message ID:'))?.split('Message ID: ')[1] || null;
  }
  order.updated_at = now;

  // Update metadata if exists
  if (ordersData.metadata) {
    ordersData.metadata.updated_at = now;
    ordersData.metadata.updated_by = 'notification-script';
  }

  // Write updated orders file
  try {
    const backupFile = ordersFile + '.bak';
    fs.copyFileSync(ordersFile, backupFile);
    logLines.push(log(`Backup created: ${backupFile}`));

    const updatedContent = JSON.stringify(ordersData, null, 2);
    fs.writeFileSync(ordersFile, updatedContent, 'utf8');
    logLines.push(log(`Orders file updated (notification_sent marked)`));
  } catch (err) {
    const errorMsg = `Failed to update orders file: ${err.message}`;
    logLines.push(log(errorMsg, 'ERROR'));
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    console.error(errorMsg);
    process.exit(1);
  }

  // Write log
  logLines.push(log('=== Notification Complete ==='));
  fs.appendFileSync(logFile, logLines.join('\n') + '\n');

  console.log(`\nLog written to: ${logFile}`);
  console.log(`Backup saved to: ${ordersFile}.bak`);
}

// Run
main().catch(err => {
  console.error('Fatal error:', err.message);
  process.exit(1);
});


path: scripts/update-order-status.js
content: #!/usr/bin/env node

/**
 * Order Status Update Script
 * 
 * Updates order status with validation, logging, and optional notification trigger.
 * 
 * Usage:
 *   node update-order-status.js --order-id ORD-xxx --status new_status [--notes "note"] [--dry-run] [--trigger-notification]
 * 
 * Options:
 *   --order-id          Order ID to update (required)
 *   --status            New status (required)
 *   --notes             Optional notes for the status change
 *   --dry-run           Preview changes without writing
 *   --trigger-notification  Mark that customer notification should be sent
 *   --orders-file       Path to orders.json (default: ../data/orders.json)
 *   --schema-file       Path to order-schema.json (default: ../config/order-schema.json)
 */

const fs = require('fs');
const path = require('path');
const { argv } = require('process');

// Parse command line arguments
function parseArgs(args) {
  const parsed = {
    orderId: null,
    status: null,
    notes: null,
    dryRun: false,
    triggerNotification: false,
    ordersFile: null,
    schemaFile: null
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '--order-id' && args[i + 1]) {
      parsed.orderId = args[++i];
    } else if (arg === '--status' && args[i + 1]) {
      parsed.status = args[++i];
    } else if (arg === '--notes' && args[i + 1]) {
      parsed.notes = args[++i];
    } else if (arg === '--dry-run') {
      parsed.dryRun = true;
    } else if (arg === '--trigger-notification') {
      parsed.triggerNotification = true;
    } else if (arg === '--orders-file' && args[i + 1]) {
      parsed.ordersFile = args[++i];
    } else if (arg === '--schema-file' && args[i + 1]) {
      parsed.schemaFile = args[++i];
    }
  }

  return parsed;
}

// Valid order statuses
const VALID_STATUSES = [
  'pending_production',
  'in_production',
  'ready_to_ship',
  'shipped',
  'completed',
  'cancelled'
];

// Status transition rules (from the order status model)
const ALLOWED_TRANSITIONS = {
  'pending_production': ['in_production', 'cancelled'],
  'in_production': ['ready_to_ship', 'cancelled'],
  'ready_to_ship': ['shipped', 'cancelled'],
  'shipped': ['completed', 'cancelled'],
  'completed': [],  // Terminal state
  'cancelled': []   // Terminal state
};

// Status change notification triggers
const NOTIFICATION_TRIGGERS = {
  'pending_production→in_production': 'order_production_started',
  'in_production→ready_to_ship': 'order_ready_to_ship',
  'ready_to_ship→shipped': 'order_shipped',
  'shipped→completed': 'order_completed',
  '*→cancelled': 'order_cancelled'
};

// Log function
function log(message, level = 'INFO') {
  const timestamp = new Date().toISOString();
  const logLine = `[${timestamp}] [${level}] ${message}`;
  console.log(logLine);
  return logLine;
}

// Validate status transition
function validateTransition(currentStatus, newStatus) {
  if (!VALID_STATUSES.includes(newStatus)) {
    return {
      valid: false,
      error: `Invalid status "${newStatus}". Valid statuses: ${VALID_STATUSES.join(', ')}`
    };
  }

  const allowed = ALLOWED_TRANSITIONS[currentStatus];
  if (!allowed) {
    return {
      valid: false,
      error: `Unknown current status "${currentStatus}"`
    };
  }

  if (!allowed.includes(newStatus)) {
    return {
      valid: false,
      error: `Invalid transition from "${currentStatus}" to "${newStatus}". Allowed transitions: ${allowed.join(', ') || 'none (terminal state)'}`
    };
  }

  return { valid: true };
}

// Get notification template for transition
function getNotificationTemplate(fromStatus, toStatus) {
  const key = `${fromStatus}→${toStatus}`;
  if (NOTIFICATION_TRIGGERS[key]) {
    return NOTIFICATION_TRIGGERS[key];
  }
  if (toStatus === 'cancelled') {
    return NOTIFICATION_TRIGGERS['*→cancelled'];
  }
  return null;
}

// Main function
function main() {
  const args = parseArgs(argv.slice(2));
  const logLines = [];

  logLines.push(log('=== Order Status Update Script ==='));

  // Validate required arguments
  if (!args.orderId) {
    console.error('Error: --order-id is required');
    console.error('Usage: node update-order-status.js --order-id ORD-xxx --status new_status [--notes "note"] [--dry-run] [--trigger-notification]');
    process.exit(1);
  }

  if (!args.status) {
    console.error('Error: --status is required');
    console.error('Usage: node update-order-status.js --order-id ORD-xxx --status new_status [--notes "note"] [--dry-run] [--trigger-notification]');
    process.exit(1);
  }

  logLines.push(log(`Order ID: ${args.orderId}`));
  logLines.push(log(`New Status: ${args.status}`));
  logLines.push(log(`Dry Run: ${args.dryRun}`));
  logLines.push(log(`Trigger Notification: ${args.triggerNotification}`));
  if (args.notes) {
    logLines.push(log(`Notes: ${args.notes}`));
  }

  // Determine file paths
  const scriptDir = __dirname;
  const ordersFile = args.ordersFile || path.join(scriptDir, '..', 'data', 'orders.json');
  const schemaFile = args.schemaFile || path.join(scriptDir, '..', 'config', 'order-schema.json');
  const logFile = path.join(scriptDir, '..', 'logs', 'status-changes.log');
  const backupFile = ordersFile + '.bak';

  logLines.push(log(`Orders file: ${ordersFile}`));
  logLines.push(log(`Log file: ${logFile}`));

  // Check if orders file exists
  if (!fs.existsSync(ordersFile)) {
    const errorMsg = `Orders file not found: ${ordersFile}`;
    logLines.push(log(errorMsg, 'ERROR'));
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    console.error(errorMsg);
    process.exit(1);
  }

  // Read orders file
  let ordersData;
  try {
    const ordersContent = fs.readFileSync(ordersFile, 'utf8');
    ordersData = JSON.parse(ordersContent);
    logLines.push(log(`Loaded ${ordersData.orders?.length || 0} orders`));
  } catch (err) {
    const errorMsg = `Failed to read orders file: ${err.message}`;
    logLines.push(log(errorMsg, 'ERROR'));
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    console.error(errorMsg);
    process.exit(1);
  }

  // Find the order
  const orderIndex = ordersData.orders?.findIndex(o => o.order_id === args.orderId);
  if (orderIndex === undefined || orderIndex === -1) {
    const errorMsg = `Order not found: ${args.orderId}`;
    logLines.push(log(errorMsg, 'ERROR'));
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    console.error(errorMsg);
    process.exit(1);
  }

  const order = ordersData.orders[orderIndex];
  logLines.push(log(`Found order: ${order.order_id} (current status: ${order.status})`));

  // Validate transition
  const transition = validateTransition(order.status, args.status);
  if (!transition.valid) {
    const errorMsg = `Validation failed: ${transition.error}`;
    logLines.push(log(errorMsg, 'ERROR'));
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    console.error(errorMsg);
    process.exit(1);
  }

  logLines.push(log(`Transition validated: ${order.status} → ${args.status}`));

  // Check notification template
  const notificationTemplate = getNotificationTemplate(order.status, args.status);
  if (notificationTemplate) {
    logLines.push(log(`Notification template: ${notificationTemplate}`));
  }

  // Prepare the update
  const now = new Date().toISOString();
  const statusHistoryEntry = {
    status: args.status,
    changed_at: now,
    changed_by: 'manual',
    notes: args.notes || null,
    notification_sent: false
  };

  if (args.triggerNotification && notificationTemplate) {
    statusHistoryEntry.notification_sent = true;
    statusHistoryEntry.notification_template = notificationTemplate;
  }

  // Show what will be updated
  logLines.push(log('--- Changes to apply ---'));
  logLines.push(log(`  status: ${order.status} → ${args.status}`));
  logLines.push(log(`  updated_at: ${order.updated_at} → ${now}`));
  logLines.push(log(`  status_history: adding 1 entry`));
  if (args.triggerNotification) {
    logLines.push(log(`  notification: will be triggered (template: ${notificationTemplate})`));
  }

  if (args.dryRun) {
    logLines.push(log('DRY RUN: No changes written'));
    console.log('\n=== DRY RUN COMPLETE ===');
    console.log(logLines.join('\n'));
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    process.exit(0);
  }

  // Create backup
  try {
    fs.copyFileSync(ordersFile, backupFile);
    logLines.push(log(`Backup created: ${backupFile}`));
  } catch (err) {
    const errorMsg = `Failed to create backup: ${err.message}`;
    logLines.push(log(errorMsg, 'ERROR'));
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    console.error(errorMsg);
    process.exit(1);
  }

  // Apply the update
  order.status = args.status;
  order.updated_at = now;
  order.status_history.push(statusHistoryEntry);

  // Update metadata if exists
  if (ordersData.metadata) {
    ordersData.metadata.updated_at = now;
    ordersData.metadata.updated_by = 'manual';
    
    // Update status summary
    const oldStatus = ordersData.orders[orderIndex].status === args.status ? args.status : 
      (ordersData.orders.find(o => o.order_id === args.orderId)?.status || order.status);
    
    // Recalculate status summary
    const statusSummary = {};
    VALID_STATUSES.forEach(s => statusSummary[s] = 0);
    ordersData.orders.forEach(o => {
      if (statusSummary[o.status] !== undefined) {
        statusSummary[o.status]++;
      }
    });
    ordersData.metadata.status_summary = statusSummary;
  }

  // Write the updated file
  try {
    const updatedContent = JSON.stringify(ordersData, null, 2);
    fs.writeFileSync(ordersFile, updatedContent, 'utf8');
    logLines.push(log(`Orders file updated successfully`));
  } catch (err) {
    const errorMsg = `Failed to write orders file: ${err.message}`;
    logLines.push(log(errorMsg, 'ERROR'));
    // Restore backup
    try {
      fs.copyFileSync(backupFile, ordersFile);
      logLines.push(log(`Backup restored due to write failure`));
    } catch (restoreErr) {
      logLines.push(log(`Failed to restore backup: ${restoreErr.message}`, 'ERROR'));
    }
    fs.appendFileSync(logFile, logLines.join('\n') + '\n');
    console.error(errorMsg);
    process.exit(1);
  }

  // Write log
  logLines.push(log('=== Update Complete ==='));
  fs.appendFileSync(logFile, logLines.join('\n') + '\n');

  console.log('\n=== UPDATE COMPLETE ===');
  console.log(`Order ${args.orderId} status updated: ${order.status} → ${args.status}`);
  if (args.triggerNotification && notificationTemplate) {
    console.log(`Notification triggered: ${notificationTemplate}`);
  }
  console.log(`Log written to: ${logFile}`);
  console.log(`Backup saved to: ${backupFile}`);
}

// Run
main();


path: test/smoke-test.sh
content: #!/bin/bash
# smoke-test.sh - Basic smoke test for order-tracker skill
# Run from the order-tracker/ root directory

set -e

BASE="$(cd "$(dirname "$0")/.." && pwd)"
SCRIPTS="$BASE/scripts"

echo "=== Order Tracker Smoke Test ==="
echo "Base: $BASE"
echo ""

echo "[1/3] Testing order-dashboard.js (compact view)..."
node "$SCRIPTS/order-dashboard.js" --format compact
echo "✅ Dashboard OK"
echo ""

echo "[2/3] Testing update-order-status.js (dry-run)..."
node "$SCRIPTS/update-order-status.js" \
  --order-id ORD-20260324-001 \
  --status ready_to_ship \
  --dry-run
echo "✅ Status update dry-run OK"
echo ""

echo "[3/3] Testing send-order-notification.js (dry-run)..."
node "$SCRIPTS/send-order-notification.js" \
  --order-id ORD-20260324-001 \
  --dry-run
echo "✅ Notification dry-run OK"
echo ""

echo "=== All smoke tests passed ✅ ==="


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)