@hasna/microservices 0.0.4 → 0.0.6

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 (57) hide show
  1. package/bin/index.js +9 -1
  2. package/bin/mcp.js +9 -1
  3. package/dist/index.js +9 -1
  4. package/microservices/microservice-ads/src/cli/index.ts +198 -0
  5. package/microservices/microservice-ads/src/db/campaigns.ts +304 -0
  6. package/microservices/microservice-ads/src/mcp/index.ts +160 -0
  7. package/microservices/microservice-company/package.json +27 -0
  8. package/microservices/microservice-company/src/cli/index.ts +1126 -0
  9. package/microservices/microservice-company/src/db/company.ts +854 -0
  10. package/microservices/microservice-company/src/db/database.ts +93 -0
  11. package/microservices/microservice-company/src/db/migrations.ts +214 -0
  12. package/microservices/microservice-company/src/db/workflow-migrations.ts +44 -0
  13. package/microservices/microservice-company/src/index.ts +60 -0
  14. package/microservices/microservice-company/src/lib/audit.ts +168 -0
  15. package/microservices/microservice-company/src/lib/finance.ts +299 -0
  16. package/microservices/microservice-company/src/lib/settings.ts +85 -0
  17. package/microservices/microservice-company/src/lib/workflows.ts +698 -0
  18. package/microservices/microservice-company/src/mcp/index.ts +991 -0
  19. package/microservices/microservice-contracts/src/cli/index.ts +410 -23
  20. package/microservices/microservice-contracts/src/db/contracts.ts +430 -1
  21. package/microservices/microservice-contracts/src/db/migrations.ts +83 -0
  22. package/microservices/microservice-contracts/src/mcp/index.ts +312 -3
  23. package/microservices/microservice-domains/src/cli/index.ts +673 -0
  24. package/microservices/microservice-domains/src/db/domains.ts +613 -0
  25. package/microservices/microservice-domains/src/index.ts +21 -0
  26. package/microservices/microservice-domains/src/lib/brandsight.ts +285 -0
  27. package/microservices/microservice-domains/src/lib/godaddy.ts +328 -0
  28. package/microservices/microservice-domains/src/lib/namecheap.ts +474 -0
  29. package/microservices/microservice-domains/src/lib/registrar.ts +355 -0
  30. package/microservices/microservice-domains/src/mcp/index.ts +413 -0
  31. package/microservices/microservice-hiring/src/cli/index.ts +318 -8
  32. package/microservices/microservice-hiring/src/db/hiring.ts +503 -0
  33. package/microservices/microservice-hiring/src/db/migrations.ts +21 -0
  34. package/microservices/microservice-hiring/src/index.ts +29 -0
  35. package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
  36. package/microservices/microservice-hiring/src/mcp/index.ts +245 -0
  37. package/microservices/microservice-payments/src/cli/index.ts +255 -3
  38. package/microservices/microservice-payments/src/db/migrations.ts +18 -0
  39. package/microservices/microservice-payments/src/db/payments.ts +552 -0
  40. package/microservices/microservice-payments/src/mcp/index.ts +223 -0
  41. package/microservices/microservice-payroll/src/cli/index.ts +269 -0
  42. package/microservices/microservice-payroll/src/db/migrations.ts +26 -0
  43. package/microservices/microservice-payroll/src/db/payroll.ts +636 -0
  44. package/microservices/microservice-payroll/src/mcp/index.ts +246 -0
  45. package/microservices/microservice-shipping/src/cli/index.ts +211 -3
  46. package/microservices/microservice-shipping/src/db/migrations.ts +8 -0
  47. package/microservices/microservice-shipping/src/db/shipping.ts +453 -3
  48. package/microservices/microservice-shipping/src/mcp/index.ts +149 -1
  49. package/microservices/microservice-social/src/cli/index.ts +244 -2
  50. package/microservices/microservice-social/src/db/migrations.ts +33 -0
  51. package/microservices/microservice-social/src/db/social.ts +378 -4
  52. package/microservices/microservice-social/src/mcp/index.ts +221 -1
  53. package/microservices/microservice-subscriptions/src/cli/index.ts +315 -0
  54. package/microservices/microservice-subscriptions/src/db/migrations.ts +68 -0
  55. package/microservices/microservice-subscriptions/src/db/subscriptions.ts +567 -3
  56. package/microservices/microservice-subscriptions/src/mcp/index.ts +267 -1
  57. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { Command } from "commander";
4
+ import { readFileSync, writeFileSync } from "node:fs";
4
5
  import {
5
6
  createPlan,
6
7
  getPlan,
@@ -19,6 +20,20 @@ import {
19
20
  getChurnRate,
20
21
  listExpiring,
21
22
  getSubscriberStats,
23
+ pauseSubscriber,
24
+ resumeSubscriber,
25
+ extendTrial,
26
+ createDunning,
27
+ listDunning,
28
+ updateDunning,
29
+ bulkImportSubscribers,
30
+ exportSubscribers,
31
+ parseImportCsv,
32
+ getLtv,
33
+ getNrr,
34
+ getCohortReport,
35
+ comparePlans,
36
+ getExpiringRenewals,
22
37
  } from "../db/subscriptions.js";
23
38
 
24
39
  const program = new Command();
@@ -281,8 +296,308 @@ subCmd
281
296
  }
282
297
  });
283
298
 
299
+ // --- Pause / Resume ---
300
+
301
+ subCmd
302
+ .command("pause")
303
+ .description("Pause a subscription")
304
+ .argument("<id>", "Subscriber ID")
305
+ .option("--resume-date <date>", "Scheduled resume date (YYYY-MM-DD)")
306
+ .option("--json", "Output as JSON", false)
307
+ .action((id, opts) => {
308
+ const subscriber = pauseSubscriber(id, opts.resumeDate);
309
+ if (!subscriber) {
310
+ console.error(`Subscriber '${id}' not found or cannot be paused.`);
311
+ process.exit(1);
312
+ }
313
+
314
+ if (opts.json) {
315
+ console.log(JSON.stringify(subscriber, null, 2));
316
+ } else {
317
+ console.log(`Paused subscription for ${subscriber.customer_name}`);
318
+ if (subscriber.resume_at) console.log(` Scheduled resume: ${subscriber.resume_at}`);
319
+ }
320
+ });
321
+
322
+ subCmd
323
+ .command("resume")
324
+ .description("Resume a paused subscription")
325
+ .argument("<id>", "Subscriber ID")
326
+ .option("--json", "Output as JSON", false)
327
+ .action((id, opts) => {
328
+ const subscriber = resumeSubscriber(id);
329
+ if (!subscriber) {
330
+ console.error(`Subscriber '${id}' not found or not paused.`);
331
+ process.exit(1);
332
+ }
333
+
334
+ if (opts.json) {
335
+ console.log(JSON.stringify(subscriber, null, 2));
336
+ } else {
337
+ console.log(`Resumed subscription for ${subscriber.customer_name}`);
338
+ }
339
+ });
340
+
341
+ // --- Trial Extension ---
342
+
343
+ subCmd
344
+ .command("extend-trial")
345
+ .description("Extend a subscriber's trial period")
346
+ .argument("<id>", "Subscriber ID")
347
+ .requiredOption("--days <days>", "Number of days to extend")
348
+ .option("--json", "Output as JSON", false)
349
+ .action((id, opts) => {
350
+ const subscriber = extendTrial(id, parseInt(opts.days));
351
+ if (!subscriber) {
352
+ console.error(`Subscriber '${id}' not found.`);
353
+ process.exit(1);
354
+ }
355
+
356
+ if (opts.json) {
357
+ console.log(JSON.stringify(subscriber, null, 2));
358
+ } else {
359
+ console.log(`Extended trial for ${subscriber.customer_name} by ${opts.days} days`);
360
+ console.log(` New trial end: ${subscriber.trial_ends_at}`);
361
+ }
362
+ });
363
+
364
+ // --- Bulk Import/Export ---
365
+
366
+ subCmd
367
+ .command("import")
368
+ .description("Bulk import subscribers from a CSV file")
369
+ .requiredOption("--file <path>", "Path to CSV file")
370
+ .option("--json", "Output as JSON", false)
371
+ .action((opts) => {
372
+ const csvContent = readFileSync(opts.file, "utf-8");
373
+ const data = parseImportCsv(csvContent);
374
+ const imported = bulkImportSubscribers(data);
375
+
376
+ if (opts.json) {
377
+ console.log(JSON.stringify({ imported: imported.length, subscribers: imported }, null, 2));
378
+ } else {
379
+ console.log(`Imported ${imported.length} subscriber(s) from ${opts.file}`);
380
+ for (const s of imported) {
381
+ console.log(` ${s.customer_name} <${s.customer_email}> (${s.id})`);
382
+ }
383
+ }
384
+ });
385
+
386
+ subCmd
387
+ .command("export")
388
+ .description("Export subscribers")
389
+ .option("--format <format>", "Output format (csv/json)", "csv")
390
+ .option("--file <path>", "Output file path (prints to stdout if omitted)")
391
+ .action((opts) => {
392
+ const output = exportSubscribers(opts.format as "csv" | "json");
393
+ if (opts.file) {
394
+ writeFileSync(opts.file, output, "utf-8");
395
+ console.log(`Exported to ${opts.file}`);
396
+ } else {
397
+ console.log(output);
398
+ }
399
+ });
400
+
401
+ // --- Dunning ---
402
+
403
+ const dunningCmd = program
404
+ .command("dunning")
405
+ .description("Dunning attempt management");
406
+
407
+ dunningCmd
408
+ .command("list")
409
+ .description("List dunning attempts")
410
+ .option("--subscriber <id>", "Filter by subscriber ID")
411
+ .option("--status <status>", "Filter by status")
412
+ .option("--limit <n>", "Limit results", "20")
413
+ .option("--json", "Output as JSON", false)
414
+ .action((opts) => {
415
+ const attempts = listDunning({
416
+ subscriber_id: opts.subscriber,
417
+ status: opts.status,
418
+ limit: parseInt(opts.limit),
419
+ });
420
+
421
+ if (opts.json) {
422
+ console.log(JSON.stringify(attempts, null, 2));
423
+ } else {
424
+ if (attempts.length === 0) {
425
+ console.log("No dunning attempts found.");
426
+ return;
427
+ }
428
+ for (const a of attempts) {
429
+ console.log(` [${a.created_at}] #${a.attempt_number} ${a.status} — subscriber: ${a.subscriber_id}`);
430
+ if (a.next_retry_at) console.log(` Next retry: ${a.next_retry_at}`);
431
+ }
432
+ console.log(`\n${attempts.length} attempt(s)`);
433
+ }
434
+ });
435
+
436
+ dunningCmd
437
+ .command("create")
438
+ .description("Create a dunning attempt")
439
+ .requiredOption("--subscriber <id>", "Subscriber ID")
440
+ .option("--attempt <n>", "Attempt number", "1")
441
+ .option("--status <status>", "Status", "pending")
442
+ .option("--next-retry <date>", "Next retry date")
443
+ .option("--json", "Output as JSON", false)
444
+ .action((opts) => {
445
+ const attempt = createDunning({
446
+ subscriber_id: opts.subscriber,
447
+ attempt_number: parseInt(opts.attempt),
448
+ status: opts.status,
449
+ next_retry_at: opts.nextRetry,
450
+ });
451
+
452
+ if (opts.json) {
453
+ console.log(JSON.stringify(attempt, null, 2));
454
+ } else {
455
+ console.log(`Created dunning attempt #${attempt.attempt_number} for subscriber ${attempt.subscriber_id}`);
456
+ }
457
+ });
458
+
459
+ dunningCmd
460
+ .command("update")
461
+ .description("Update a dunning attempt")
462
+ .argument("<id>", "Dunning attempt ID")
463
+ .option("--status <status>", "New status")
464
+ .option("--next-retry <date>", "Next retry date")
465
+ .option("--json", "Output as JSON", false)
466
+ .action((id, opts) => {
467
+ const input: Record<string, unknown> = {};
468
+ if (opts.status) input.status = opts.status;
469
+ if (opts.nextRetry !== undefined) input.next_retry_at = opts.nextRetry;
470
+
471
+ const attempt = updateDunning(id, input);
472
+ if (!attempt) {
473
+ console.error(`Dunning attempt '${id}' not found.`);
474
+ process.exit(1);
475
+ }
476
+
477
+ if (opts.json) {
478
+ console.log(JSON.stringify(attempt, null, 2));
479
+ } else {
480
+ console.log(`Updated dunning attempt ${attempt.id} — status: ${attempt.status}`);
481
+ }
482
+ });
483
+
284
484
  // --- Analytics ---
285
485
 
486
+ program
487
+ .command("ltv")
488
+ .description("Show lifetime value per subscriber and average")
489
+ .option("--json", "Output as JSON", false)
490
+ .action((opts) => {
491
+ const result = getLtv();
492
+
493
+ if (opts.json) {
494
+ console.log(JSON.stringify(result, null, 2));
495
+ } else {
496
+ if (result.subscribers.length === 0) {
497
+ console.log("No subscribers found.");
498
+ return;
499
+ }
500
+ console.log("Lifetime Value Report:");
501
+ for (const s of result.subscribers) {
502
+ console.log(` ${s.customer_name} <${s.customer_email}> — $${s.ltv.toFixed(2)} (${s.months_active}mo on ${s.plan_name})`);
503
+ }
504
+ console.log(`\nAverage LTV: $${result.average_ltv.toFixed(2)}`);
505
+ }
506
+ });
507
+
508
+ program
509
+ .command("nrr")
510
+ .description("Calculate net revenue retention for a month")
511
+ .requiredOption("--month <month>", "Month in YYYY-MM format")
512
+ .option("--json", "Output as JSON", false)
513
+ .action((opts) => {
514
+ const result = getNrr(opts.month);
515
+
516
+ if (opts.json) {
517
+ console.log(JSON.stringify(result, null, 2));
518
+ } else {
519
+ console.log(`NRR for ${result.month}:`);
520
+ console.log(` Start MRR: $${result.start_mrr.toFixed(2)}`);
521
+ console.log(` Expansion: +$${result.expansion.toFixed(2)}`);
522
+ console.log(` Contraction: -$${result.contraction.toFixed(2)}`);
523
+ console.log(` Churn: -$${result.churn.toFixed(2)}`);
524
+ console.log(` NRR: ${result.nrr.toFixed(2)}%`);
525
+ }
526
+ });
527
+
528
+ program
529
+ .command("cohort-report")
530
+ .description("Show cohort retention analysis")
531
+ .option("--months <n>", "Number of months to analyze", "6")
532
+ .option("--json", "Output as JSON", false)
533
+ .action((opts) => {
534
+ const report = getCohortReport(parseInt(opts.months));
535
+
536
+ if (opts.json) {
537
+ console.log(JSON.stringify(report, null, 2));
538
+ } else {
539
+ if (report.length === 0) {
540
+ console.log("No cohort data available.");
541
+ return;
542
+ }
543
+ console.log("Cohort Retention Report:");
544
+ console.log(" Cohort | Total | Retained | Retention");
545
+ console.log(" -----------|-------|----------|----------");
546
+ for (const c of report) {
547
+ console.log(` ${c.cohort} | ${String(c.total).padStart(5)} | ${String(c.retained).padStart(8)} | ${c.retention_rate.toFixed(1)}%`);
548
+ }
549
+ }
550
+ });
551
+
552
+ planCmd
553
+ .command("compare")
554
+ .description("Compare two plans side by side")
555
+ .argument("<id1>", "First plan ID")
556
+ .argument("<id2>", "Second plan ID")
557
+ .option("--json", "Output as JSON", false)
558
+ .action((id1, id2, opts) => {
559
+ const result = comparePlans(id1, id2);
560
+ if (!result) {
561
+ console.error("One or both plans not found.");
562
+ process.exit(1);
563
+ }
564
+
565
+ if (opts.json) {
566
+ console.log(JSON.stringify(result, null, 2));
567
+ } else {
568
+ console.log(`Plan Comparison:`);
569
+ console.log(` ${result.plan1.name} vs ${result.plan2.name}`);
570
+ console.log(` Price: $${result.plan1.price}/${result.plan1.interval} vs $${result.plan2.price}/${result.plan2.interval}`);
571
+ console.log(` Price diff: $${result.price_diff} (${result.price_diff_pct > 0 ? "+" : ""}${result.price_diff_pct}%)`);
572
+ console.log(` Interval match: ${result.interval_match ? "Yes" : "No"}`);
573
+ if (result.common_features.length) console.log(` Common features: ${result.common_features.join(", ")}`);
574
+ if (result.features_only_in_plan1.length) console.log(` Only in ${result.plan1.name}: ${result.features_only_in_plan1.join(", ")}`);
575
+ if (result.features_only_in_plan2.length) console.log(` Only in ${result.plan2.name}: ${result.features_only_in_plan2.join(", ")}`);
576
+ }
577
+ });
578
+
579
+ program
580
+ .command("expiring-renewals")
581
+ .description("List subscribers with renewals expiring soon")
582
+ .option("--days <days>", "Days ahead to check", "7")
583
+ .option("--json", "Output as JSON", false)
584
+ .action((opts) => {
585
+ const expiring = getExpiringRenewals(parseInt(opts.days));
586
+
587
+ if (opts.json) {
588
+ console.log(JSON.stringify(expiring, null, 2));
589
+ } else {
590
+ if (expiring.length === 0) {
591
+ console.log(`No renewals expiring in the next ${opts.days} days.`);
592
+ return;
593
+ }
594
+ for (const s of expiring) {
595
+ console.log(` ${s.customer_name} <${s.customer_email}> — renews ${s.current_period_end}`);
596
+ }
597
+ console.log(`\n${expiring.length} upcoming renewal(s)`);
598
+ }
599
+ });
600
+
286
601
  program
287
602
  .command("mrr")
288
603
  .description("Get monthly recurring revenue")
@@ -54,4 +54,72 @@ export const MIGRATIONS: MigrationEntry[] = [
54
54
  CREATE INDEX IF NOT EXISTS idx_events_occurred ON events(occurred_at);
55
55
  `,
56
56
  },
57
+ {
58
+ id: 2,
59
+ name: "add_paused_status_and_dunning",
60
+ sql: `
61
+ -- Recreate subscribers table with 'paused' in CHECK constraint
62
+ CREATE TABLE subscribers_new (
63
+ id TEXT PRIMARY KEY,
64
+ plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE RESTRICT,
65
+ customer_name TEXT NOT NULL,
66
+ customer_email TEXT NOT NULL,
67
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('trialing', 'active', 'past_due', 'canceled', 'expired', 'paused')),
68
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
69
+ trial_ends_at TEXT,
70
+ current_period_start TEXT NOT NULL DEFAULT (datetime('now')),
71
+ current_period_end TEXT,
72
+ canceled_at TEXT,
73
+ resume_at TEXT,
74
+ metadata TEXT NOT NULL DEFAULT '{}',
75
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
76
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
77
+ );
78
+
79
+ INSERT INTO subscribers_new (id, plan_id, customer_name, customer_email, status, started_at, trial_ends_at, current_period_start, current_period_end, canceled_at, metadata, created_at, updated_at)
80
+ SELECT id, plan_id, customer_name, customer_email, status, started_at, trial_ends_at, current_period_start, current_period_end, canceled_at, metadata, created_at, updated_at
81
+ FROM subscribers;
82
+
83
+ DROP TABLE subscribers;
84
+ ALTER TABLE subscribers_new RENAME TO subscribers;
85
+
86
+ CREATE INDEX IF NOT EXISTS idx_subscribers_plan ON subscribers(plan_id);
87
+ CREATE INDEX IF NOT EXISTS idx_subscribers_email ON subscribers(customer_email);
88
+ CREATE INDEX IF NOT EXISTS idx_subscribers_status ON subscribers(status);
89
+ CREATE INDEX IF NOT EXISTS idx_subscribers_period_end ON subscribers(current_period_end);
90
+
91
+ -- Add 'paused' and 'resumed' to events type CHECK
92
+ CREATE TABLE events_new (
93
+ id TEXT PRIMARY KEY,
94
+ subscriber_id TEXT NOT NULL REFERENCES subscribers(id) ON DELETE CASCADE,
95
+ type TEXT NOT NULL CHECK (type IN ('created', 'upgraded', 'downgraded', 'canceled', 'renewed', 'payment_failed', 'paused', 'resumed', 'trial_extended')),
96
+ occurred_at TEXT NOT NULL DEFAULT (datetime('now')),
97
+ details TEXT NOT NULL DEFAULT '{}'
98
+ );
99
+
100
+ INSERT INTO events_new (id, subscriber_id, type, occurred_at, details)
101
+ SELECT id, subscriber_id, type, occurred_at, details
102
+ FROM events;
103
+
104
+ DROP TABLE events;
105
+ ALTER TABLE events_new RENAME TO events;
106
+
107
+ CREATE INDEX IF NOT EXISTS idx_events_subscriber ON events(subscriber_id);
108
+ CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
109
+ CREATE INDEX IF NOT EXISTS idx_events_occurred ON events(occurred_at);
110
+
111
+ -- Dunning attempts table
112
+ CREATE TABLE IF NOT EXISTS dunning_attempts (
113
+ id TEXT PRIMARY KEY,
114
+ subscriber_id TEXT NOT NULL REFERENCES subscribers(id) ON DELETE CASCADE,
115
+ attempt_number INTEGER NOT NULL DEFAULT 1,
116
+ status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'retrying', 'failed', 'recovered')),
117
+ next_retry_at TEXT,
118
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
119
+ );
120
+
121
+ CREATE INDEX IF NOT EXISTS idx_dunning_subscriber ON dunning_attempts(subscriber_id);
122
+ CREATE INDEX IF NOT EXISTS idx_dunning_status ON dunning_attempts(status);
123
+ `,
124
+ },
57
125
  ];