@simonfestl/husky-cli 1.26.0 → 1.29.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.
Files changed (38) hide show
  1. package/dist/commands/biz/invoices.d.ts +11 -0
  2. package/dist/commands/biz/invoices.js +661 -0
  3. package/dist/commands/biz/shopify.d.ts +3 -0
  4. package/dist/commands/biz/shopify.js +592 -0
  5. package/dist/commands/biz/supplier-feed.d.ts +3 -0
  6. package/dist/commands/biz/supplier-feed.js +168 -0
  7. package/dist/commands/biz.js +5 -1
  8. package/dist/commands/config.d.ts +3 -2
  9. package/dist/commands/config.js +4 -3
  10. package/dist/commands/e2e.js +1 -1
  11. package/dist/commands/plan.d.ts +14 -0
  12. package/dist/commands/plan.js +219 -0
  13. package/dist/commands/sop.d.ts +3 -0
  14. package/dist/commands/sop.js +458 -0
  15. package/dist/commands/task.js +7 -0
  16. package/dist/index.js +2 -0
  17. package/dist/lib/biz/gcs-upload.d.ts +86 -0
  18. package/dist/lib/biz/gcs-upload.js +189 -0
  19. package/dist/lib/biz/index.d.ts +5 -0
  20. package/dist/lib/biz/index.js +3 -0
  21. package/dist/lib/biz/invoice-extractor-registry.d.ts +22 -0
  22. package/dist/lib/biz/invoice-extractor-registry.js +416 -0
  23. package/dist/lib/biz/invoice-extractor-types.d.ts +127 -0
  24. package/dist/lib/biz/invoice-extractor-types.js +6 -0
  25. package/dist/lib/biz/pattern-detection.d.ts +48 -0
  26. package/dist/lib/biz/pattern-detection.js +205 -0
  27. package/dist/lib/biz/resolved-tickets.d.ts +86 -0
  28. package/dist/lib/biz/resolved-tickets.js +250 -0
  29. package/dist/lib/biz/shopify.d.ts +196 -0
  30. package/dist/lib/biz/shopify.js +429 -0
  31. package/dist/lib/biz/supplier-feed-types.d.ts +96 -0
  32. package/dist/lib/biz/supplier-feed-types.js +46 -0
  33. package/dist/lib/biz/supplier-feed.d.ts +32 -0
  34. package/dist/lib/biz/supplier-feed.js +244 -0
  35. package/dist/lib/permissions.d.ts +2 -1
  36. package/dist/types/roles.d.ts +3 -0
  37. package/dist/types/roles.js +14 -0
  38. package/package.json +1 -1
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Husky Biz Invoices Command
3
+ *
4
+ * Unified invoice extraction service
5
+ * - List and manage invoice sources
6
+ * - Extract invoices from all configured sources
7
+ * - Auto-upload to Gotess for transaction matching
8
+ */
9
+ import { Command } from "commander";
10
+ export declare const invoicesCommand: Command;
11
+ export default invoicesCommand;
@@ -0,0 +1,661 @@
1
+ /**
2
+ * Husky Biz Invoices Command
3
+ *
4
+ * Unified invoice extraction service
5
+ * - List and manage invoice sources
6
+ * - Extract invoices from all configured sources
7
+ * - Auto-upload to Gotess for transaction matching
8
+ */
9
+ import { Command } from "commander";
10
+ import { getConfig } from "../config.js";
11
+ import { getExtractor, getAvailableExtractorIds, hasCredentialsConfigured, getDefaultInvoiceDir, } from "../../lib/biz/invoice-extractor-registry.js";
12
+ import { GotessClient } from "../../lib/biz/gotess.js";
13
+ import { GCSUploadClient } from "../../lib/biz/gcs-upload.js";
14
+ export const invoicesCommand = new Command("invoices")
15
+ .description("Unified invoice extraction service");
16
+ // Helper: Get API URL and key
17
+ function getApiConfig() {
18
+ const config = getConfig();
19
+ if (!config.apiUrl) {
20
+ console.error("✗ API URL not configured. Run: husky config set api-url <url>");
21
+ process.exit(1);
22
+ }
23
+ return { apiUrl: config.apiUrl, apiKey: config.apiKey };
24
+ }
25
+ // Helper: Fetch from API
26
+ async function fetchApi(endpoint, options) {
27
+ const { apiUrl, apiKey } = getApiConfig();
28
+ const headers = {
29
+ "Content-Type": "application/json",
30
+ };
31
+ if (apiKey) {
32
+ headers["X-API-Key"] = apiKey;
33
+ }
34
+ const response = await fetch(`${apiUrl}${endpoint}`, {
35
+ ...options,
36
+ headers: { ...headers, ...options?.headers },
37
+ });
38
+ if (!response.ok) {
39
+ const error = await response.text();
40
+ throw new Error(`API error ${response.status}: ${error}`);
41
+ }
42
+ return response.json();
43
+ }
44
+ // ============================================================================
45
+ // husky biz invoices sources
46
+ // ============================================================================
47
+ invoicesCommand
48
+ .command("sources")
49
+ .description("List all invoice sources")
50
+ .option("--json", "Output as JSON")
51
+ .option("--seed", "Seed default sources if none exist")
52
+ .action(async (options) => {
53
+ try {
54
+ if (options.seed) {
55
+ console.log("Seeding default invoice sources...");
56
+ const result = await fetchApi("/api/invoice-sources", {
57
+ method: "POST",
58
+ body: JSON.stringify({ action: "seed" }),
59
+ });
60
+ console.log(`✓ Seeded ${result.seeded} invoice sources`);
61
+ if (result.seeded === 0) {
62
+ console.log(" (Sources already exist, no action taken)");
63
+ }
64
+ return;
65
+ }
66
+ const sources = await fetchApi("/api/invoice-sources");
67
+ if (options.json) {
68
+ console.log(JSON.stringify(sources, null, 2));
69
+ return;
70
+ }
71
+ if (sources.length === 0) {
72
+ console.log("\n No invoice sources configured.");
73
+ console.log(" Run: husky biz invoices sources --seed\n");
74
+ return;
75
+ }
76
+ console.log(`\n 📄 Invoice Sources (${sources.length})\n`);
77
+ // Header
78
+ console.log(` ${"Name".padEnd(20)} │ ` +
79
+ `${"Type".padEnd(12)} │ ` +
80
+ `${"Status".padEnd(18)} │ ` +
81
+ `${"Last Extracted".padEnd(20)} │ ` +
82
+ `${"Total"}`);
83
+ console.log(" " + "─".repeat(90));
84
+ // Sources
85
+ for (const source of sources) {
86
+ const statusIcon = {
87
+ active: "✓",
88
+ needs_credentials: "🔑",
89
+ disabled: "⏸",
90
+ error: "⚠",
91
+ }[source.status];
92
+ const lastExtracted = source.lastExtractedAt
93
+ ? new Date(source.lastExtractedAt).toLocaleDateString()
94
+ : "Never";
95
+ // Check local credentials
96
+ const hasLocalCreds = hasCredentialsConfigured(source.extractorId);
97
+ const credStatus = hasLocalCreds ? "✓ creds" : "✗ creds";
98
+ console.log(` ${source.name.padEnd(20)} │ ` +
99
+ `${source.type.padEnd(12)} │ ` +
100
+ `${statusIcon} ${source.status.padEnd(15)} │ ` +
101
+ `${lastExtracted.padEnd(20)} │ ` +
102
+ `${source.totalExtracted} (${credStatus})`);
103
+ }
104
+ console.log("");
105
+ }
106
+ catch (error) {
107
+ console.error("Error:", error.message);
108
+ process.exit(1);
109
+ }
110
+ });
111
+ // ============================================================================
112
+ // husky biz invoices status
113
+ // ============================================================================
114
+ invoicesCommand
115
+ .command("status")
116
+ .description("Show extraction status for all sources")
117
+ .action(async () => {
118
+ try {
119
+ const sources = await fetchApi("/api/invoice-sources");
120
+ const jobs = await fetchApi("/api/extraction-jobs?limit=10");
121
+ console.log("\n 📊 Invoice Extraction Status\n");
122
+ // Summary
123
+ const activeCount = sources.filter((s) => s.status === "active").length;
124
+ const needsCredsCount = sources.filter((s) => s.status === "needs_credentials").length;
125
+ console.log(` Active sources: ${activeCount}/${sources.length}`);
126
+ console.log(` Need credentials: ${needsCredsCount}`);
127
+ console.log("");
128
+ // Check local credentials status
129
+ console.log(" Local Credentials Status:");
130
+ for (const source of sources) {
131
+ const hasLocalCreds = hasCredentialsConfigured(source.extractorId);
132
+ const icon = hasLocalCreds ? "✓" : "✗";
133
+ console.log(` ${icon} ${source.name} (${source.extractorId})`);
134
+ }
135
+ console.log("");
136
+ // Recent jobs
137
+ if (jobs.length > 0) {
138
+ console.log(" Recent Extraction Jobs:");
139
+ for (const job of jobs.slice(0, 5)) {
140
+ const statusIcon = {
141
+ pending: "⏳",
142
+ running: "🔄",
143
+ completed: "✓",
144
+ failed: "✗",
145
+ }[job.status];
146
+ const date = new Date(job.startedAt).toLocaleDateString();
147
+ console.log(` ${statusIcon} ${job.sourceName} - ${job.status} (${date}) - ${job.invoicesExtracted} extracted`);
148
+ }
149
+ }
150
+ else {
151
+ console.log(" No extraction jobs yet.");
152
+ }
153
+ console.log("");
154
+ }
155
+ catch (error) {
156
+ console.error("Error:", error.message);
157
+ process.exit(1);
158
+ }
159
+ });
160
+ // ============================================================================
161
+ // husky biz invoices extract
162
+ // ============================================================================
163
+ invoicesCommand
164
+ .command("extract [source]")
165
+ .description("Extract invoices from source(s)")
166
+ .option("--all", "Extract from all active sources")
167
+ .option("--limit <n>", "Limit number of orders to check", parseInt)
168
+ .option("-o, --output <dir>", "Output directory for invoices")
169
+ .option("--gcs", "Upload extracted invoices to GCS bucket")
170
+ .option("--gcs-bucket <bucket>", "GCS bucket name (default: husky-invoices)")
171
+ .option("--upload", "Auto-upload to Gotess after extraction")
172
+ .option("--json", "Output as JSON")
173
+ .action(async (source, options) => {
174
+ try {
175
+ const outputDir = options.output || getDefaultInvoiceDir();
176
+ if (options.all) {
177
+ // Extract from all active sources
178
+ const sources = await fetchApi("/api/invoice-sources");
179
+ const activeSources = sources.filter((s) => s.status === "active" || hasCredentialsConfigured(s.extractorId));
180
+ if (activeSources.length === 0) {
181
+ console.log("\n No active sources to extract from.");
182
+ console.log(" Configure credentials first with:");
183
+ console.log(" husky config set <extractor>-username <user>");
184
+ console.log(" husky config set <extractor>-password <pass>\n");
185
+ return;
186
+ }
187
+ console.log(`\n Extracting invoices from ${activeSources.length} sources...\n`);
188
+ for (const sourceData of activeSources) {
189
+ console.log(` 📦 ${sourceData.name}`);
190
+ await extractFromSource(sourceData.extractorId, {
191
+ limit: options.limit,
192
+ outputDir,
193
+ upload: options.upload,
194
+ gcs: options.gcs,
195
+ gcsBucket: options.gcsBucket,
196
+ });
197
+ console.log("");
198
+ }
199
+ }
200
+ else if (source) {
201
+ // Extract from specific source
202
+ await extractFromSource(source, {
203
+ limit: options.limit,
204
+ outputDir,
205
+ upload: options.upload,
206
+ json: options.json,
207
+ gcs: options.gcs,
208
+ gcsBucket: options.gcsBucket,
209
+ });
210
+ }
211
+ else {
212
+ // Show available extractors
213
+ console.log("\n Usage: husky biz invoices extract <source>");
214
+ console.log(" husky biz invoices extract --all\n");
215
+ console.log(" Available extractors:");
216
+ for (const id of getAvailableExtractorIds()) {
217
+ const hasCreds = hasCredentialsConfigured(id);
218
+ const icon = hasCreds ? "✓" : "✗";
219
+ console.log(` ${icon} ${id}`);
220
+ }
221
+ console.log("");
222
+ }
223
+ }
224
+ catch (error) {
225
+ console.error("Error:", error.message);
226
+ process.exit(1);
227
+ }
228
+ });
229
+ async function extractFromSource(extractorId, options) {
230
+ const extractor = getExtractor(extractorId);
231
+ if (!extractor) {
232
+ console.error(` ✗ Unknown extractor: ${extractorId}`);
233
+ return;
234
+ }
235
+ if (!hasCredentialsConfigured(extractorId)) {
236
+ console.error(` ✗ Credentials not configured for ${extractorId}`);
237
+ console.error(` Run: husky config set ${extractorId}-username <user>`);
238
+ console.error(` husky config set ${extractorId}-password <pass>`);
239
+ return;
240
+ }
241
+ try {
242
+ await extractor.init({});
243
+ const result = await extractor.extractAll({
244
+ limit: options.limit,
245
+ outputDir: options.outputDir,
246
+ onProgress: (msg) => {
247
+ if (!options.json) {
248
+ console.log(` ${msg}`);
249
+ }
250
+ },
251
+ });
252
+ if (options.json) {
253
+ console.log(JSON.stringify(result, null, 2));
254
+ }
255
+ else {
256
+ console.log(` ✓ Extracted ${result.invoicesExtracted}/${result.ordersFound} invoices`);
257
+ if (result.invoicesFailed > 0) {
258
+ console.log(` ⚠ ${result.invoicesFailed} failed`);
259
+ }
260
+ if (result.invoices.length > 0) {
261
+ console.log(` 📁 Saved to: ${options.outputDir}`);
262
+ }
263
+ }
264
+ // Track GCS upload results for Gotess
265
+ let gcsResults = [];
266
+ // Upload to GCS if requested
267
+ if (options.gcs && result.invoices.length > 0) {
268
+ gcsResults = await uploadToGCS(result.invoices, extractorId, options.gcsBucket, options.json);
269
+ }
270
+ // Auto-upload to Gotess if requested
271
+ if (options.upload && result.invoices.length > 0) {
272
+ // Use GCS results if available, otherwise use local invoices
273
+ const invoicesWithGcs = gcsResults.length > 0
274
+ ? gcsResults
275
+ : result.invoices.map(inv => ({ invoice: inv }));
276
+ await uploadToGotess(invoicesWithGcs, extractorId, options.json);
277
+ }
278
+ }
279
+ catch (error) {
280
+ console.error(` ✗ Extraction failed: ${error.message}`);
281
+ }
282
+ finally {
283
+ await extractor.close();
284
+ }
285
+ }
286
+ async function uploadToGCS(invoices, source, bucketName, json) {
287
+ if (!json) {
288
+ console.log(" ☁️ Uploading to GCS...");
289
+ }
290
+ const results = [];
291
+ try {
292
+ const gcs = new GCSUploadClient(bucketName);
293
+ // Check bucket access
294
+ const accessCheck = await gcs.checkAccess();
295
+ if (!accessCheck.accessible) {
296
+ console.error(` ✗ GCS access error: ${accessCheck.error}`);
297
+ console.error(` Make sure bucket "${gcs.getBucketName()}" exists and you have access`);
298
+ return invoices.map(inv => ({ invoice: inv }));
299
+ }
300
+ let successCount = 0;
301
+ let failCount = 0;
302
+ for (const invoice of invoices) {
303
+ const result = await gcs.uploadInvoice(invoice.localPath, {
304
+ source,
305
+ orderNumber: invoice.orderNumber,
306
+ invoiceDate: invoice.invoiceDate,
307
+ });
308
+ if (result.success) {
309
+ successCount++;
310
+ results.push({ invoice, gcsUri: result.gcsUri });
311
+ if (!json) {
312
+ console.log(` ✓ ${invoice.filename} → ${result.gcsUri}`);
313
+ }
314
+ }
315
+ else {
316
+ failCount++;
317
+ results.push({ invoice });
318
+ if (!json) {
319
+ console.log(` ✗ ${invoice.filename}: ${result.error}`);
320
+ }
321
+ }
322
+ }
323
+ if (!json) {
324
+ console.log(` ☁️ Uploaded ${successCount}/${invoices.length} to GCS`);
325
+ if (failCount > 0) {
326
+ console.log(` ⚠ ${failCount} failed to upload`);
327
+ }
328
+ }
329
+ }
330
+ catch (error) {
331
+ console.error(` ✗ GCS upload failed: ${error.message}`);
332
+ return invoices.map(inv => ({ invoice: inv }));
333
+ }
334
+ return results;
335
+ }
336
+ async function uploadToGotess(invoicesWithGcs, source, json) {
337
+ if (!json) {
338
+ console.log(" 📤 Uploading to Gotess...");
339
+ }
340
+ try {
341
+ const config = getConfig();
342
+ if (!config.gotessToken || !config.gotessBookId) {
343
+ console.error(" ✗ Gotess not configured");
344
+ console.error(" Configure with: husky biz gotess login");
345
+ return;
346
+ }
347
+ const gotess = new GotessClient(config.gotessToken, config.gotessBookId);
348
+ let successCount = 0;
349
+ let failCount = 0;
350
+ for (const { invoice, gcsUri } of invoicesWithGcs) {
351
+ try {
352
+ const result = await gotess.createInvoice({
353
+ invoiceDate: invoice.invoiceDate,
354
+ amount: invoice.amount,
355
+ senderName: source,
356
+ filename: invoice.filename,
357
+ gcsUri: gcsUri,
358
+ });
359
+ successCount++;
360
+ if (!json) {
361
+ console.log(` ✓ ${invoice.filename} → Gotess ID: ${result.id}`);
362
+ }
363
+ }
364
+ catch (error) {
365
+ failCount++;
366
+ if (!json) {
367
+ console.log(` ✗ ${invoice.filename}: ${error.message}`);
368
+ }
369
+ }
370
+ }
371
+ if (!json) {
372
+ console.log(` 📤 Created ${successCount}/${invoicesWithGcs.length} Gotess records`);
373
+ if (failCount > 0) {
374
+ console.log(` ⚠ ${failCount} failed`);
375
+ }
376
+ }
377
+ }
378
+ catch (error) {
379
+ console.error(` ✗ Gotess upload failed: ${error.message}`);
380
+ }
381
+ }
382
+ // ============================================================================
383
+ // husky biz invoices pending
384
+ // ============================================================================
385
+ invoicesCommand
386
+ .command("pending")
387
+ .description("Show pending invoices to extract")
388
+ .option("--json", "Output as JSON")
389
+ .action(async (options) => {
390
+ try {
391
+ const sources = await fetchApi("/api/invoice-sources");
392
+ const pending = sources.filter((s) => s.pendingInvoices > 0);
393
+ if (options.json) {
394
+ console.log(JSON.stringify(pending, null, 2));
395
+ return;
396
+ }
397
+ if (pending.length === 0) {
398
+ console.log("\n No pending invoices to extract.\n");
399
+ return;
400
+ }
401
+ console.log(`\n 📋 Pending Invoices\n`);
402
+ let total = 0;
403
+ for (const source of pending) {
404
+ console.log(` ${source.name}: ${source.pendingInvoices} pending`);
405
+ total += source.pendingInvoices;
406
+ }
407
+ console.log(` ─────────────────────`);
408
+ console.log(` Total: ${total} pending`);
409
+ console.log("");
410
+ }
411
+ catch (error) {
412
+ console.error("Error:", error.message);
413
+ process.exit(1);
414
+ }
415
+ });
416
+ // ============================================================================
417
+ // husky biz invoices test
418
+ // ============================================================================
419
+ invoicesCommand
420
+ .command("test <source>")
421
+ .description("Test credentials for a source")
422
+ .action(async (source) => {
423
+ const extractor = getExtractor(source);
424
+ if (!extractor) {
425
+ console.error(`✗ Unknown extractor: ${source}`);
426
+ console.log("\nAvailable extractors:");
427
+ for (const id of getAvailableExtractorIds()) {
428
+ console.log(` - ${id}`);
429
+ }
430
+ process.exit(1);
431
+ }
432
+ if (!hasCredentialsConfigured(source)) {
433
+ console.error(`✗ Credentials not configured for ${source}`);
434
+ console.log(`\nConfigure with:`);
435
+ console.log(` husky config set ${source}-username <user>`);
436
+ console.log(` husky config set ${source}-password <pass>`);
437
+ process.exit(1);
438
+ }
439
+ try {
440
+ console.log(`Testing credentials for ${extractor.name}...`);
441
+ await extractor.init({});
442
+ const result = await extractor.testCredentials();
443
+ if (result.success) {
444
+ console.log(`✓ Successfully authenticated with ${extractor.name}`);
445
+ }
446
+ else {
447
+ console.error(`✗ Authentication failed: ${result.error}`);
448
+ process.exit(1);
449
+ }
450
+ }
451
+ catch (error) {
452
+ console.error(`✗ Test failed: ${error.message}`);
453
+ process.exit(1);
454
+ }
455
+ finally {
456
+ await extractor.close();
457
+ }
458
+ });
459
+ // ============================================================================
460
+ // husky biz invoices reconcile
461
+ // ============================================================================
462
+ // Vendor name patterns to extractor mapping
463
+ const VENDOR_SOURCE_MAPPING = {
464
+ wattiz: ["wattiz", "watti", "watt"],
465
+ skuterzone: ["skuterzone", "skuter", "skuterzon"],
466
+ emove: ["emove", "e-move", "emove distribution"],
467
+ };
468
+ function findSourceForVendor(vendorName) {
469
+ const normalized = vendorName.toLowerCase();
470
+ for (const [source, patterns] of Object.entries(VENDOR_SOURCE_MAPPING)) {
471
+ if (patterns.some((p) => normalized.includes(p))) {
472
+ return source;
473
+ }
474
+ }
475
+ return null;
476
+ }
477
+ invoicesCommand
478
+ .command("reconcile")
479
+ .description("Auto-reconcile missing invoices from Gotess")
480
+ .option("--dry-run", "Show what would be done without executing")
481
+ .option("--limit <n>", "Limit transactions to check", parseInt)
482
+ .option("--gcs", "Upload to GCS bucket")
483
+ .option("--gcs-bucket <bucket>", "GCS bucket name")
484
+ .option("--json", "Output as JSON")
485
+ .action(async (options) => {
486
+ try {
487
+ const config = getConfig();
488
+ if (!config.gotessToken || !config.gotessBookId) {
489
+ console.error("✗ Gotess not configured");
490
+ console.error(" Configure with: husky biz gotess login");
491
+ process.exit(1);
492
+ }
493
+ const gotess = new GotessClient(config.gotessToken, config.gotessBookId);
494
+ // Step 1: Get transactions missing invoices
495
+ console.log("\n 🔍 Checking Gotess for missing invoices...\n");
496
+ const missing = await gotess.getMissingInvoices();
497
+ if (missing.length === 0) {
498
+ console.log(" ✓ No missing invoices found!\n");
499
+ return;
500
+ }
501
+ console.log(` Found ${missing.length} transactions missing invoices\n`);
502
+ // Step 2: Group by potential source
503
+ const bySource = {};
504
+ const unmapped = [];
505
+ for (const tx of missing) {
506
+ const source = findSourceForVendor(tx.counterpart_name || "");
507
+ if (source) {
508
+ if (!bySource[source])
509
+ bySource[source] = [];
510
+ bySource[source].push(tx);
511
+ }
512
+ else {
513
+ unmapped.push(tx);
514
+ }
515
+ }
516
+ // Show breakdown
517
+ console.log(" 📊 Breakdown by source:\n");
518
+ for (const [source, txs] of Object.entries(bySource)) {
519
+ const hasCreds = hasCredentialsConfigured(source);
520
+ const credIcon = hasCreds ? "✓" : "✗";
521
+ console.log(` ${credIcon} ${source}: ${txs.length} transactions`);
522
+ for (const tx of txs.slice(0, 3)) {
523
+ console.log(` - ${tx.counterpart_name}: €${Math.abs(tx.amount).toFixed(2)} (${tx.value_date})`);
524
+ }
525
+ if (txs.length > 3) {
526
+ console.log(` ... and ${txs.length - 3} more`);
527
+ }
528
+ }
529
+ if (unmapped.length > 0) {
530
+ console.log(`\n ? Unknown sources: ${unmapped.length} transactions`);
531
+ for (const tx of unmapped.slice(0, 3)) {
532
+ console.log(` - ${tx.counterpart_name || "Unknown"}: €${Math.abs(tx.amount).toFixed(2)}`);
533
+ }
534
+ }
535
+ if (options.dryRun) {
536
+ console.log("\n [Dry run - no actions taken]\n");
537
+ return;
538
+ }
539
+ // Step 3: Extract from each mapped source
540
+ console.log("\n 🚀 Starting extraction...\n");
541
+ const outputDir = getDefaultInvoiceDir();
542
+ let totalExtracted = 0;
543
+ let totalMatched = 0;
544
+ for (const [source, txs] of Object.entries(bySource)) {
545
+ if (!hasCredentialsConfigured(source)) {
546
+ console.log(` ⏭ Skipping ${source} (no credentials configured)`);
547
+ continue;
548
+ }
549
+ console.log(`\n 📦 Extracting from ${source}...`);
550
+ const extractor = getExtractor(source);
551
+ if (!extractor)
552
+ continue;
553
+ try {
554
+ await extractor.init({});
555
+ const result = await extractor.extractAll({
556
+ limit: options.limit || 20,
557
+ outputDir,
558
+ onProgress: (msg) => console.log(` ${msg}`),
559
+ });
560
+ console.log(` ✓ Extracted ${result.invoicesExtracted} invoices`);
561
+ totalExtracted += result.invoicesExtracted;
562
+ // Upload to GCS if requested
563
+ let gcsResults = [];
564
+ if (options.gcs && result.invoices.length > 0) {
565
+ gcsResults = await uploadToGCS(result.invoices, source, options.gcsBucket, options.json);
566
+ }
567
+ // Upload to Gotess
568
+ if (result.invoices.length > 0) {
569
+ const invoicesWithGcs = gcsResults.length > 0
570
+ ? gcsResults
571
+ : result.invoices.map(inv => ({ invoice: inv }));
572
+ await uploadToGotess(invoicesWithGcs, source, options.json);
573
+ }
574
+ await extractor.close();
575
+ }
576
+ catch (error) {
577
+ console.error(` ✗ Error: ${error.message}`);
578
+ }
579
+ }
580
+ // Step 4: Run auto-matching
581
+ console.log("\n 🔗 Running auto-match...\n");
582
+ try {
583
+ const matchResult = await gotess.autoMatch();
584
+ totalMatched = matchResult.matched.length;
585
+ if (matchResult.matched.length > 0) {
586
+ console.log(` Found ${matchResult.matched.length} potential matches:`);
587
+ for (const { transaction, invoice } of matchResult.matched.slice(0, 5)) {
588
+ console.log(` - ${transaction.counterpart_name}: €${Math.abs(transaction.amount).toFixed(2)} → ${invoice.filename || invoice.sender_name}`);
589
+ }
590
+ // Actually link the matches
591
+ console.log("\n Linking invoices to transactions...");
592
+ let linked = 0;
593
+ for (const { transaction, invoice } of matchResult.matched) {
594
+ try {
595
+ await gotess.linkInvoice(transaction.id, invoice.id);
596
+ linked++;
597
+ }
598
+ catch (error) {
599
+ console.log(` ✗ Failed to link ${transaction.id}: ${error.message}`);
600
+ }
601
+ }
602
+ console.log(` ✓ Linked ${linked}/${matchResult.matched.length} invoices`);
603
+ }
604
+ if (matchResult.unmatched.length > 0) {
605
+ console.log(`\n Still missing invoices: ${matchResult.unmatched.length}`);
606
+ }
607
+ }
608
+ catch (error) {
609
+ console.error(` ✗ Auto-match failed: ${error.message}`);
610
+ }
611
+ // Summary
612
+ console.log("\n ═══════════════════════════════════════");
613
+ console.log(` 📊 Reconciliation Summary`);
614
+ console.log(` Transactions checked: ${missing.length}`);
615
+ console.log(` Invoices extracted: ${totalExtracted}`);
616
+ console.log(` Invoices matched: ${totalMatched}`);
617
+ console.log(` Still missing: ${missing.length - totalMatched}`);
618
+ console.log(" ═══════════════════════════════════════\n");
619
+ }
620
+ catch (error) {
621
+ console.error("Error:", error.message);
622
+ process.exit(1);
623
+ }
624
+ });
625
+ // ============================================================================
626
+ // husky biz invoices schedule
627
+ // ============================================================================
628
+ invoicesCommand
629
+ .command("schedule")
630
+ .description("Show/configure scheduled reconciliation")
631
+ .option("--enable", "Enable monthly reconciliation")
632
+ .option("--disable", "Disable scheduled reconciliation")
633
+ .option("--cron <expression>", "Custom cron expression")
634
+ .action(async (options) => {
635
+ console.log("\n 📅 Invoice Reconciliation Schedule\n");
636
+ if (options.enable || options.disable || options.cron) {
637
+ console.log(" Schedule configuration is managed via:");
638
+ console.log(" - Cloud Scheduler in GCP Console");
639
+ console.log(" - Or crontab on the accounting VM");
640
+ console.log("");
641
+ console.log(" Recommended cron: 0 6 1 * * (1st of month at 6 AM)");
642
+ console.log("");
643
+ console.log(" Command to run:");
644
+ console.log(" husky biz invoices reconcile --gcs");
645
+ console.log("");
646
+ }
647
+ else {
648
+ console.log(" To set up automated reconciliation:");
649
+ console.log("");
650
+ console.log(" 1. On a VM (crontab -e):");
651
+ console.log(" 0 6 1 * * /usr/local/bin/husky biz invoices reconcile --gcs >> /var/log/husky-reconcile.log 2>&1");
652
+ console.log("");
653
+ console.log(" 2. Via Cloud Scheduler:");
654
+ console.log(" gcloud scheduler jobs create http husky-invoice-reconcile \\");
655
+ console.log(" --schedule=\"0 6 1 * *\" \\");
656
+ console.log(" --uri=\"https://your-api/api/invoices/reconcile\" \\");
657
+ console.log(" --http-method=POST");
658
+ console.log("");
659
+ }
660
+ });
661
+ export default invoicesCommand;
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare const shopifyCommand: Command;
3
+ export default shopifyCommand;