@simonfestl/husky-cli 1.12.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Husky Biz Wattiz Command
3
+ *
4
+ * Manages wattiz.fr (B2B e-mobility parts supplier) via web scraping
5
+ * PrestaShop platform integration
6
+ * - Parts search and catalog browsing
7
+ * - Order history and details
8
+ * - Invoice download
9
+ */
10
+ import { Command } from "commander";
11
+ import { WattizPlaywrightClient } from "../../lib/biz/wattiz-playwright.js";
12
+ import { getConfig, saveConfig } from "../config.js";
13
+ import * as path from "path";
14
+ import { homedir } from "os";
15
+ export const wattizCommand = new Command("wattiz")
16
+ .description("Manage wattiz.fr (e-mobility parts supplier - PrestaShop)");
17
+ // ============================================================================
18
+ // husky biz wattiz login
19
+ // ============================================================================
20
+ wattizCommand
21
+ .command("login")
22
+ .description("Setup Wattiz credentials")
23
+ .requiredOption("-u, --username <username>", "Login username or email")
24
+ .requiredOption("-p, --password <password>", "Login password")
25
+ .option("--base-url <url>", "Base URL", "https://www.wattiz.fr")
26
+ .option("--language <lang>", "Language (gb, fr, de, es)", "gb")
27
+ .action(async (options) => {
28
+ try {
29
+ // Save to config
30
+ const config = getConfig();
31
+ config.wattizUsername = options.username;
32
+ config.wattizPassword = options.password;
33
+ if (options.baseUrl) {
34
+ config.wattizBaseUrl = options.baseUrl;
35
+ }
36
+ if (options.language) {
37
+ config.wattizLanguage = options.language;
38
+ }
39
+ saveConfig(config);
40
+ console.log("Testing authentication...");
41
+ // Test login
42
+ const client = WattizPlaywrightClient.fromConfig();
43
+ try {
44
+ const result = await client.login();
45
+ if (result.success) {
46
+ console.log("✓ Successfully authenticated with wattiz.fr");
47
+ console.log("\nYou can now use:");
48
+ console.log(" husky biz wattiz orders list");
49
+ console.log(" husky biz wattiz orders get <id>");
50
+ console.log(" husky biz wattiz invoice <order-id>");
51
+ console.log(" husky biz wattiz products <query>");
52
+ }
53
+ else {
54
+ console.error("✗ Login failed:", result.error);
55
+ process.exit(1);
56
+ }
57
+ }
58
+ finally {
59
+ await client.close();
60
+ }
61
+ }
62
+ catch (error) {
63
+ console.error("✗ Login failed:", error.message);
64
+ process.exit(1);
65
+ }
66
+ });
67
+ // ============================================================================
68
+ // husky biz wattiz orders
69
+ // ============================================================================
70
+ const ordersSubcommand = new Command("orders")
71
+ .description("View supplier orders");
72
+ ordersSubcommand
73
+ .command("list")
74
+ .description("List all orders from Wattiz")
75
+ .option("--json", "Output as JSON")
76
+ .action(async (options) => {
77
+ const client = WattizPlaywrightClient.fromConfig();
78
+ try {
79
+ const orders = await client.listOrders();
80
+ if (options.json) {
81
+ console.log(JSON.stringify(orders, null, 2));
82
+ return;
83
+ }
84
+ if (orders.length === 0) {
85
+ console.log("\n No orders found.\n");
86
+ return;
87
+ }
88
+ console.log(`\n 📦 Wattiz Orders (${orders.length} found)\n`);
89
+ // Header
90
+ console.log(` ${"Order #".padEnd(15)} │ ` +
91
+ `${"Date".padEnd(20)} │ ` +
92
+ `${"Status".padEnd(15)} │ ` +
93
+ `${"Total".padEnd(12)}`);
94
+ console.log(" " + "─".repeat(75));
95
+ // Orders
96
+ for (const order of orders) {
97
+ console.log(` ${order.orderNumber.padEnd(15)} │ ` +
98
+ `${order.date.padEnd(20)} │ ` +
99
+ `${order.status.padEnd(15)} │ ` +
100
+ `${order.total}`);
101
+ }
102
+ console.log("");
103
+ }
104
+ catch (error) {
105
+ console.error("Error:", error.message);
106
+ process.exit(1);
107
+ }
108
+ finally {
109
+ await client.close();
110
+ }
111
+ });
112
+ ordersSubcommand
113
+ .command("get <id>")
114
+ .description("Get order details")
115
+ .option("--json", "Output as JSON")
116
+ .action(async (id, options) => {
117
+ const client = WattizPlaywrightClient.fromConfig();
118
+ try {
119
+ const order = await client.getOrder(id);
120
+ if (options.json) {
121
+ console.log(JSON.stringify(order, null, 2));
122
+ return;
123
+ }
124
+ console.log(`\n Order ${order.orderNumber}`);
125
+ console.log(" " + "─".repeat(60));
126
+ console.log(` Status: ${order.status}`);
127
+ console.log(` Date: ${order.date}`);
128
+ console.log(` Total: ${order.total}`);
129
+ if (order.trackingNumber) {
130
+ console.log(` Tracking: ${order.trackingNumber}`);
131
+ }
132
+ if (order.paymentMethod) {
133
+ console.log(` Payment: ${order.paymentMethod}`);
134
+ }
135
+ if (order.customer && order.customer.name) {
136
+ console.log(`\n Shipping To:`);
137
+ console.log(` ${order.customer.name}`);
138
+ if (order.customer.company) {
139
+ console.log(` ${order.customer.company}`);
140
+ }
141
+ if (order.customer.address) {
142
+ console.log(` ${order.customer.address}`);
143
+ }
144
+ if (order.customer.postcode && order.customer.city) {
145
+ console.log(` ${order.customer.postcode} ${order.customer.city}`);
146
+ }
147
+ if (order.customer.email) {
148
+ console.log(` ${order.customer.email}`);
149
+ }
150
+ }
151
+ if (order.items.length > 0) {
152
+ console.log(`\n Items:`);
153
+ for (const item of order.items) {
154
+ const sku = item.sku ? `[${item.sku}]`.padEnd(18) : "".padEnd(18);
155
+ const name = item.name.slice(0, 35).padEnd(35);
156
+ console.log(` ${item.quantity}x ${sku} ${name} (${item.total})`);
157
+ }
158
+ }
159
+ if (order.subtotal || order.shipping || order.tax) {
160
+ console.log(`\n Totals:`);
161
+ if (order.subtotal)
162
+ console.log(` Subtotal: ${order.subtotal}`);
163
+ if (order.shipping)
164
+ console.log(` Shipping: ${order.shipping}`);
165
+ if (order.tax)
166
+ console.log(` Tax: ${order.tax}`);
167
+ console.log(` Total: ${order.total}`);
168
+ }
169
+ if (order.invoiceUrl) {
170
+ console.log(`\n 📄 Invoice available`);
171
+ console.log(` Download with: husky biz wattiz invoice ${order.id}`);
172
+ }
173
+ console.log("");
174
+ }
175
+ catch (error) {
176
+ console.error("Error:", error.message);
177
+ process.exit(1);
178
+ }
179
+ finally {
180
+ await client.close();
181
+ }
182
+ });
183
+ wattizCommand.addCommand(ordersSubcommand);
184
+ // ============================================================================
185
+ // husky biz wattiz invoice
186
+ // ============================================================================
187
+ wattizCommand
188
+ .command("invoice <order-id>")
189
+ .description("Download invoice PDF for order")
190
+ .option("-o, --output <path>", "Save path (default: ~/Downloads/wattiz-invoice-{id}.pdf)")
191
+ .action(async (orderId, options) => {
192
+ const client = WattizPlaywrightClient.fromConfig();
193
+ try {
194
+ const savePath = options.output ||
195
+ path.join(homedir(), 'Downloads', `wattiz-invoice-${orderId}.pdf`);
196
+ console.log(`Downloading invoice for order #${orderId}...`);
197
+ const success = await client.downloadInvoice(orderId, savePath);
198
+ if (success) {
199
+ console.log(`✓ Invoice saved to: ${savePath}`);
200
+ }
201
+ else {
202
+ console.error("✗ Invoice not available for this order");
203
+ process.exit(1);
204
+ }
205
+ }
206
+ catch (error) {
207
+ console.error("Error:", error.message);
208
+ process.exit(1);
209
+ }
210
+ finally {
211
+ await client.close();
212
+ }
213
+ });
214
+ // ============================================================================
215
+ // husky biz wattiz products
216
+ // ============================================================================
217
+ wattizCommand
218
+ .command("products <query>")
219
+ .description("Search for parts in catalog")
220
+ .option("--json", "Output as JSON")
221
+ .action(async (query, options) => {
222
+ const client = WattizPlaywrightClient.fromConfig();
223
+ try {
224
+ const products = await client.searchProducts(query);
225
+ if (options.json) {
226
+ console.log(JSON.stringify(products, null, 2));
227
+ return;
228
+ }
229
+ if (products.length === 0) {
230
+ console.log(`\n No products found for: "${query}"\n`);
231
+ return;
232
+ }
233
+ console.log(`\n 🔍 Search results for: "${query}" (${products.length} found)\n`);
234
+ for (const product of products) {
235
+ const sku = product.sku ? `[${product.sku}]` : '';
236
+ const price = product.price || 'Price hidden (login required)';
237
+ const stock = product.stockStatus || '';
238
+ console.log(` ${product.name}`);
239
+ console.log(` ${sku} ${price}${stock ? ' - ' + stock : ''}`);
240
+ console.log(` ${product.url}`);
241
+ console.log("");
242
+ }
243
+ }
244
+ catch (error) {
245
+ console.error("Error:", error.message);
246
+ process.exit(1);
247
+ }
248
+ finally {
249
+ await client.close();
250
+ }
251
+ });
252
+ export default wattizCommand;
@@ -12,6 +12,9 @@ import { customersCommand } from "./biz/customers.js";
12
12
  import { seatableCommand } from "./biz/seatable.js";
13
13
  import { qdrantCommand } from "./biz/qdrant.js";
14
14
  import { gotessCommand } from "./biz/gotess.js";
15
+ import { skuterzoneCommand } from "./biz/skuterzone.js";
16
+ import { emoveCommand } from "./biz/emove.js";
17
+ import { wattizCommand } from "./biz/wattiz.js";
15
18
  import { guards } from "../lib/permissions.js";
16
19
  export const bizCommand = new Command("biz")
17
20
  .description("Business operations for autonomous agents")
@@ -22,5 +25,8 @@ export const bizCommand = new Command("biz")
22
25
  .addCommand(customersCommand)
23
26
  .addCommand(seatableCommand)
24
27
  .addCommand(qdrantCommand)
25
- .addCommand(gotessCommand);
28
+ .addCommand(gotessCommand)
29
+ .addCommand(skuterzoneCommand)
30
+ .addCommand(emoveCommand)
31
+ .addCommand(wattizCommand);
26
32
  export default bizCommand;
@@ -35,6 +35,13 @@ interface Config {
35
35
  skuterzoneUsername?: string;
36
36
  skuterzonePassword?: string;
37
37
  skuterzoneBaseUrl?: string;
38
+ emoveUsername?: string;
39
+ emovePassword?: string;
40
+ emoveBaseUrl?: string;
41
+ wattizUsername?: string;
42
+ wattizPassword?: string;
43
+ wattizBaseUrl?: string;
44
+ wattizLanguage?: string;
38
45
  }
39
46
  export declare function getConfig(): Config;
40
47
  export declare function saveConfig(config: Config): void;
@@ -197,6 +197,15 @@ configCommand
197
197
  "skuterzone-username": "skuterzoneUsername",
198
198
  "skuterzone-password": "skuterzonePassword",
199
199
  "skuterzone-base-url": "skuterzoneBaseUrl",
200
+ // Emove Distribution
201
+ "emove-username": "emoveUsername",
202
+ "emove-password": "emovePassword",
203
+ "emove-base-url": "emoveBaseUrl",
204
+ // Wattiz
205
+ "wattiz-username": "wattizUsername",
206
+ "wattiz-password": "wattizPassword",
207
+ "wattiz-base-url": "wattizBaseUrl",
208
+ "wattiz-language": "wattizLanguage",
200
209
  };
201
210
  const configKey = keyMappings[key];
202
211
  if (!configKey) {
@@ -212,6 +221,8 @@ configCommand
212
221
  console.log(" Gemini: gemini-api-key");
213
222
  console.log(" NocoDB: nocodb-api-token, nocodb-base-url, nocodb-workspace-id");
214
223
  console.log(" Skuterzone: skuterzone-username, skuterzone-password, skuterzone-base-url");
224
+ console.log(" Emove: emove-username, emove-password, emove-base-url");
225
+ console.log(" Wattiz: wattiz-username, wattiz-password, wattiz-base-url, wattiz-language");
215
226
  console.log(" Brain: agent-type");
216
227
  console.error("\n💡 For configuration help: husky explain config");
217
228
  process.exit(1);
@@ -233,7 +244,7 @@ configCommand
233
244
  config[configKey] = value;
234
245
  saveConfig(config);
235
246
  // Mask sensitive values in output
236
- const sensitiveKeys = ["api-key", "billbee-api-key", "billbee-password", "zendesk-api-token", "seatable-api-token", "gotess-token", "gemini-api-key", "nocodb-api-token", "skuterzone-username", "skuterzone-password"];
247
+ const sensitiveKeys = ["api-key", "billbee-api-key", "billbee-password", "zendesk-api-token", "seatable-api-token", "gotess-token", "gemini-api-key", "nocodb-api-token", "skuterzone-username", "skuterzone-password", "emove-username", "emove-password", "wattiz-username", "wattiz-password"];
237
248
  const displayValue = sensitiveKeys.includes(key) ? "***" : value;
238
249
  console.log(`✓ Set ${key} = ${displayValue}`);
239
250
  });
package/dist/index.js CHANGED
@@ -89,3 +89,4 @@ if (process.argv.length <= 2) {
89
89
  else {
90
90
  program.parse();
91
91
  }
92
+ // trigger CI
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Emove Distribution Client with Playwright
3
+ *
4
+ * Uses real browser automation for emovedistribution.com
5
+ * WooCommerce platform with Spanish locale
6
+ */
7
+ import { EmoveConfig, EmoveOrder, EmoveOrderDetails, EmoveProduct, EmoveLoginResult } from './emove-types.js';
8
+ export declare class EmovePlaywrightClient {
9
+ private config;
10
+ private browser?;
11
+ private page?;
12
+ constructor(config: EmoveConfig);
13
+ static fromConfig(): EmovePlaywrightClient;
14
+ private ensureBrowser;
15
+ login(): Promise<EmoveLoginResult>;
16
+ listOrders(): Promise<EmoveOrder[]>;
17
+ getOrder(orderId: string): Promise<EmoveOrderDetails>;
18
+ downloadInvoice(orderId: string, savePath: string): Promise<boolean>;
19
+ searchProducts(query: string): Promise<EmoveProduct[]>;
20
+ close(): Promise<void>;
21
+ }
22
+ export default EmovePlaywrightClient;
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Emove Distribution Client with Playwright
3
+ *
4
+ * Uses real browser automation for emovedistribution.com
5
+ * WooCommerce platform with Spanish locale
6
+ */
7
+ import { chromium } from 'playwright';
8
+ import { getConfig } from '../../commands/config.js';
9
+ export class EmovePlaywrightClient {
10
+ config;
11
+ browser;
12
+ page;
13
+ constructor(config) {
14
+ this.config = config;
15
+ }
16
+ static fromConfig() {
17
+ const config = getConfig();
18
+ const env = process.env.HUSKY_ENV || 'PROD';
19
+ const emoveConfig = {
20
+ username: process.env[`${env}_EMOVE_USERNAME`] ||
21
+ process.env.EMOVE_USERNAME ||
22
+ config.emoveUsername || '',
23
+ password: process.env[`${env}_EMOVE_PASSWORD`] ||
24
+ process.env.EMOVE_PASSWORD ||
25
+ config.emovePassword || '',
26
+ baseUrl: process.env[`${env}_EMOVE_BASE_URL`] ||
27
+ process.env.EMOVE_BASE_URL ||
28
+ config.emoveBaseUrl ||
29
+ 'https://emovedistribution.com',
30
+ };
31
+ if (!emoveConfig.username || !emoveConfig.password) {
32
+ throw new Error('Missing Emove credentials. Configure with:\n' +
33
+ ' husky config set emove-username <username>\n' +
34
+ ' husky config set emove-password <password>');
35
+ }
36
+ return new EmovePlaywrightClient(emoveConfig);
37
+ }
38
+ async ensureBrowser() {
39
+ if (!this.browser) {
40
+ this.browser = await chromium.launch({ headless: true });
41
+ this.page = await this.browser.newPage();
42
+ }
43
+ return this.page;
44
+ }
45
+ async login() {
46
+ try {
47
+ const page = await this.ensureBrowser();
48
+ // Navigate to login page (Spanish WooCommerce uses /mi-cuenta/)
49
+ await page.goto(`${this.config.baseUrl}/mi-cuenta/`);
50
+ await page.waitForLoadState('networkidle');
51
+ // Wait for the WooCommerce login form
52
+ await page.waitForSelector('input#username', { state: 'visible', timeout: 10000 });
53
+ // Fill in login form
54
+ await page.fill('input#username', this.config.username);
55
+ await page.fill('input#password', this.config.password);
56
+ // Submit form
57
+ await page.click('button[name="login"]');
58
+ // Wait for navigation
59
+ await page.waitForLoadState('networkidle');
60
+ // Check if logged in
61
+ const isLoggedIn = await page.locator('body.logged-in').count() > 0;
62
+ if (!isLoggedIn) {
63
+ return {
64
+ success: false,
65
+ cookies: '',
66
+ error: 'Login failed - check credentials'
67
+ };
68
+ }
69
+ // Extract cookies
70
+ const cookies = await page.context().cookies();
71
+ const cookieString = cookies.map(c => `${c.name}=${c.value}`).join('; ');
72
+ return {
73
+ success: true,
74
+ cookies: cookieString
75
+ };
76
+ }
77
+ catch (error) {
78
+ return {
79
+ success: false,
80
+ cookies: '',
81
+ error: `Login error: ${error.message}`
82
+ };
83
+ }
84
+ }
85
+ async listOrders() {
86
+ const page = await this.ensureBrowser();
87
+ // Navigate to orders page (Spanish: pedidos instead of orders)
88
+ await page.goto(`${this.config.baseUrl}/mi-cuenta/pedidos/`);
89
+ await page.waitForLoadState('networkidle');
90
+ // Check if we need to login
91
+ const needsLogin = await page.locator('form.woocommerce-form-login').count() > 0;
92
+ if (needsLogin) {
93
+ await this.login();
94
+ await page.goto(`${this.config.baseUrl}/mi-cuenta/pedidos/`);
95
+ await page.waitForLoadState('networkidle');
96
+ }
97
+ const orders = [];
98
+ // Find all order rows
99
+ const orderRows = page.locator('table.woocommerce-orders-table tr.woocommerce-orders-table__row');
100
+ const count = await orderRows.count();
101
+ for (let i = 0; i < count; i++) {
102
+ const row = orderRows.nth(i);
103
+ const orderNumberEl = row.locator('.woocommerce-orders-table__cell-order-number a');
104
+ const orderNumber = await orderNumberEl.textContent() || '';
105
+ const orderLink = await orderNumberEl.getAttribute('href') || '';
106
+ const orderId = orderLink.match(/ver-pedido\/(\d+)|view-order\/(\d+)/)?.[1] ||
107
+ orderLink.match(/ver-pedido\/(\d+)|view-order\/(\d+)/)?.[2] || '';
108
+ const date = await row.locator('.woocommerce-orders-table__cell-order-date').textContent() || '';
109
+ const status = await row.locator('.woocommerce-orders-table__cell-order-status').textContent() || '';
110
+ const total = await row.locator('.woocommerce-orders-table__cell-order-total').textContent() || '';
111
+ if (orderId) {
112
+ orders.push({
113
+ id: orderId,
114
+ orderNumber: orderNumber.trim(),
115
+ date: date.trim(),
116
+ status: status.trim(),
117
+ total: total.trim(),
118
+ itemCount: 0,
119
+ });
120
+ }
121
+ }
122
+ return orders;
123
+ }
124
+ async getOrder(orderId) {
125
+ const page = await this.ensureBrowser();
126
+ // Try Spanish and English URLs
127
+ const urls = [
128
+ `${this.config.baseUrl}/mi-cuenta/ver-pedido/${orderId}/`,
129
+ `${this.config.baseUrl}/mi-cuenta/view-order/${orderId}/`
130
+ ];
131
+ let loaded = false;
132
+ for (const url of urls) {
133
+ try {
134
+ await page.goto(url);
135
+ await page.waitForLoadState('networkidle');
136
+ loaded = true;
137
+ break;
138
+ }
139
+ catch (error) {
140
+ continue;
141
+ }
142
+ }
143
+ if (!loaded) {
144
+ throw new Error(`Could not load order ${orderId}`);
145
+ }
146
+ // Check if we need to login
147
+ const needsLogin = await page.locator('form.woocommerce-form-login').count() > 0;
148
+ if (needsLogin) {
149
+ await this.login();
150
+ await page.goto(urls[0]);
151
+ await page.waitForLoadState('networkidle');
152
+ }
153
+ // Extract order details
154
+ const orderNumber = await page.locator('mark.order-number').textContent().catch(() => orderId) || orderId;
155
+ const orderDate = await page.locator('mark.order-date').textContent().catch(() => '') || '';
156
+ const orderStatus = await page.locator('mark.order-status').textContent().catch(() => '') || '';
157
+ // Extract customer info
158
+ let customerName = '';
159
+ let customerAddress = '';
160
+ let customerEmail = '';
161
+ let customerPhone = '';
162
+ try {
163
+ const addressBlock = page.locator('.woocommerce-customer-details address').first();
164
+ const addressText = await addressBlock.textContent() || '';
165
+ const lines = addressText.split('\n').map(l => l.trim()).filter(l => l);
166
+ if (lines.length > 0)
167
+ customerName = lines[0];
168
+ if (lines.length > 1)
169
+ customerAddress = lines.slice(1).filter(l => !l.includes('@') && !l.startsWith('+')).join(', ');
170
+ customerEmail = await page.locator('.woocommerce-customer-details--email').first().textContent().catch(() => '') || '';
171
+ customerPhone = await page.locator('.woocommerce-customer-details--phone').first().textContent().catch(() => '') || '';
172
+ }
173
+ catch (error) {
174
+ // Customer details not available
175
+ }
176
+ // Extract line items
177
+ const items = [];
178
+ const itemRows = page.locator('table.woocommerce-table--order-details tbody tr.woocommerce-table__line-item');
179
+ const itemCount = await itemRows.count();
180
+ for (let i = 0; i < itemCount; i++) {
181
+ const itemRow = itemRows.nth(i);
182
+ const name = await itemRow.locator('.woocommerce-table__product-name').textContent() || '';
183
+ const qty = await itemRow.locator('.product-quantity').textContent() || '1';
184
+ const total = await itemRow.locator('.woocommerce-table__product-total').textContent() || '';
185
+ items.push({
186
+ sku: '',
187
+ name: name.trim(),
188
+ quantity: parseInt(qty.replace(/\D/g, ''), 10) || 1,
189
+ price: '',
190
+ total: total.trim(),
191
+ });
192
+ }
193
+ // Look for invoice link
194
+ const invoiceLink = await page.locator('a.print-invoice').first().getAttribute('href').catch(() => '');
195
+ // Extract total
196
+ const totalText = await page.locator('table.woocommerce-table--order-details tfoot tr:last-child .woocommerce-Price-amount').textContent().catch(() => '');
197
+ return {
198
+ id: orderId,
199
+ orderNumber: orderNumber?.trim() || orderId,
200
+ date: orderDate?.trim() || '',
201
+ status: orderStatus?.trim() || '',
202
+ total: totalText?.trim() || '',
203
+ itemCount: items.length,
204
+ invoiceUrl: invoiceLink || undefined,
205
+ customer: {
206
+ name: customerName.trim(),
207
+ address: customerAddress.trim(),
208
+ city: '',
209
+ postcode: '',
210
+ email: customerEmail.trim(),
211
+ phone: customerPhone.trim(),
212
+ },
213
+ items,
214
+ subtotal: '',
215
+ shipping: '',
216
+ tax: '',
217
+ paymentMethod: '',
218
+ };
219
+ }
220
+ async downloadInvoice(orderId, savePath) {
221
+ try {
222
+ const order = await this.getOrder(orderId);
223
+ if (!order.invoiceUrl) {
224
+ return false;
225
+ }
226
+ const page = await this.ensureBrowser();
227
+ // Construct full URL
228
+ const invoiceUrl = order.invoiceUrl.startsWith('http')
229
+ ? order.invoiceUrl
230
+ : `${this.config.baseUrl}${order.invoiceUrl}`;
231
+ // Set up download listener
232
+ const downloadPromise = page.waitForEvent('download', { timeout: 30000 });
233
+ // Navigate to invoice URL
234
+ await page.goto(invoiceUrl, { waitUntil: 'commit' }).catch(() => { });
235
+ // Wait for download
236
+ const download = await downloadPromise;
237
+ // Save file
238
+ await download.saveAs(savePath);
239
+ return true;
240
+ }
241
+ catch (error) {
242
+ console.error('Invoice download error:', error);
243
+ return false;
244
+ }
245
+ }
246
+ async searchProducts(query) {
247
+ const page = await this.ensureBrowser();
248
+ const searchUrl = `${this.config.baseUrl}/?s=${encodeURIComponent(query)}&post_type=product`;
249
+ await page.goto(searchUrl);
250
+ await page.waitForLoadState('networkidle');
251
+ // Check if we need to login
252
+ const needsLogin = await page.locator('form.woocommerce-form-login').count() > 0;
253
+ if (needsLogin) {
254
+ await this.login();
255
+ await page.goto(searchUrl);
256
+ await page.waitForLoadState('networkidle');
257
+ }
258
+ const products = [];
259
+ // Find all product items
260
+ const productItems = page.locator('li.product, .product-item');
261
+ const count = await productItems.count();
262
+ for (let i = 0; i < count; i++) {
263
+ const item = productItems.nth(i);
264
+ const link = item.locator('a').first();
265
+ const url = await link.getAttribute('href') || '';
266
+ const name = await item.locator('h2, h3, .woocommerce-loop-product__title').first().textContent() || '';
267
+ const price = await item.locator('.price').first().textContent().catch(() => undefined);
268
+ const img = await item.locator('img').first().getAttribute('src');
269
+ if (name && url) {
270
+ products.push({
271
+ id: '',
272
+ name: name.trim(),
273
+ url,
274
+ price: price?.trim(),
275
+ imageUrl: img || undefined,
276
+ });
277
+ }
278
+ }
279
+ return products;
280
+ }
281
+ async close() {
282
+ if (this.browser) {
283
+ await this.browser.close();
284
+ this.browser = undefined;
285
+ this.page = undefined;
286
+ }
287
+ }
288
+ }
289
+ export default EmovePlaywrightClient;