@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.
- package/bin/index.js +9 -1
- package/bin/mcp.js +9 -1
- package/dist/index.js +9 -1
- package/microservices/microservice-ads/src/cli/index.ts +198 -0
- package/microservices/microservice-ads/src/db/campaigns.ts +304 -0
- package/microservices/microservice-ads/src/mcp/index.ts +160 -0
- package/microservices/microservice-company/package.json +27 -0
- package/microservices/microservice-company/src/cli/index.ts +1126 -0
- package/microservices/microservice-company/src/db/company.ts +854 -0
- package/microservices/microservice-company/src/db/database.ts +93 -0
- package/microservices/microservice-company/src/db/migrations.ts +214 -0
- package/microservices/microservice-company/src/db/workflow-migrations.ts +44 -0
- package/microservices/microservice-company/src/index.ts +60 -0
- package/microservices/microservice-company/src/lib/audit.ts +168 -0
- package/microservices/microservice-company/src/lib/finance.ts +299 -0
- package/microservices/microservice-company/src/lib/settings.ts +85 -0
- package/microservices/microservice-company/src/lib/workflows.ts +698 -0
- package/microservices/microservice-company/src/mcp/index.ts +991 -0
- package/microservices/microservice-contracts/src/cli/index.ts +410 -23
- package/microservices/microservice-contracts/src/db/contracts.ts +430 -1
- package/microservices/microservice-contracts/src/db/migrations.ts +83 -0
- package/microservices/microservice-contracts/src/mcp/index.ts +312 -3
- package/microservices/microservice-domains/src/cli/index.ts +673 -0
- package/microservices/microservice-domains/src/db/domains.ts +613 -0
- package/microservices/microservice-domains/src/index.ts +21 -0
- package/microservices/microservice-domains/src/lib/brandsight.ts +285 -0
- package/microservices/microservice-domains/src/lib/godaddy.ts +328 -0
- package/microservices/microservice-domains/src/lib/namecheap.ts +474 -0
- package/microservices/microservice-domains/src/lib/registrar.ts +355 -0
- package/microservices/microservice-domains/src/mcp/index.ts +413 -0
- package/microservices/microservice-hiring/src/cli/index.ts +318 -8
- package/microservices/microservice-hiring/src/db/hiring.ts +503 -0
- package/microservices/microservice-hiring/src/db/migrations.ts +21 -0
- package/microservices/microservice-hiring/src/index.ts +29 -0
- package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
- package/microservices/microservice-hiring/src/mcp/index.ts +245 -0
- package/microservices/microservice-payments/src/cli/index.ts +255 -3
- package/microservices/microservice-payments/src/db/migrations.ts +18 -0
- package/microservices/microservice-payments/src/db/payments.ts +552 -0
- package/microservices/microservice-payments/src/mcp/index.ts +223 -0
- package/microservices/microservice-payroll/src/cli/index.ts +269 -0
- package/microservices/microservice-payroll/src/db/migrations.ts +26 -0
- package/microservices/microservice-payroll/src/db/payroll.ts +636 -0
- package/microservices/microservice-payroll/src/mcp/index.ts +246 -0
- package/microservices/microservice-shipping/src/cli/index.ts +211 -3
- package/microservices/microservice-shipping/src/db/migrations.ts +8 -0
- package/microservices/microservice-shipping/src/db/shipping.ts +453 -3
- package/microservices/microservice-shipping/src/mcp/index.ts +149 -1
- package/microservices/microservice-social/src/cli/index.ts +244 -2
- package/microservices/microservice-social/src/db/migrations.ts +33 -0
- package/microservices/microservice-social/src/db/social.ts +378 -4
- package/microservices/microservice-social/src/mcp/index.ts +221 -1
- package/microservices/microservice-subscriptions/src/cli/index.ts +315 -0
- package/microservices/microservice-subscriptions/src/db/migrations.ts +68 -0
- package/microservices/microservice-subscriptions/src/db/subscriptions.ts +567 -3
- package/microservices/microservice-subscriptions/src/mcp/index.ts +267 -1
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -3961,7 +3961,8 @@ var CATEGORIES = [
|
|
|
3961
3961
|
"Operations",
|
|
3962
3962
|
"Productivity",
|
|
3963
3963
|
"HR",
|
|
3964
|
-
"Analytics"
|
|
3964
|
+
"Analytics",
|
|
3965
|
+
"Management"
|
|
3965
3966
|
];
|
|
3966
3967
|
var MICROSERVICES = [
|
|
3967
3968
|
{
|
|
@@ -4097,6 +4098,13 @@ var MICROSERVICES = [
|
|
|
4097
4098
|
category: "HR",
|
|
4098
4099
|
tags: ["payroll", "salary", "wages", "deductions", "tax", "employees", "pay-stubs"]
|
|
4099
4100
|
},
|
|
4101
|
+
{
|
|
4102
|
+
name: "company",
|
|
4103
|
+
displayName: "Company",
|
|
4104
|
+
description: "AI agent control plane for autonomous company operations \u2014 organizations, teams, members, customers, and vendors",
|
|
4105
|
+
category: "Management",
|
|
4106
|
+
tags: ["company", "organization", "teams", "members", "customers", "vendors", "management"]
|
|
4107
|
+
},
|
|
4100
4108
|
{
|
|
4101
4109
|
name: "transcriber",
|
|
4102
4110
|
displayName: "Transcriber",
|
package/bin/mcp.js
CHANGED
|
@@ -19462,7 +19462,8 @@ var CATEGORIES = [
|
|
|
19462
19462
|
"Operations",
|
|
19463
19463
|
"Productivity",
|
|
19464
19464
|
"HR",
|
|
19465
|
-
"Analytics"
|
|
19465
|
+
"Analytics",
|
|
19466
|
+
"Management"
|
|
19466
19467
|
];
|
|
19467
19468
|
var MICROSERVICES = [
|
|
19468
19469
|
{
|
|
@@ -19598,6 +19599,13 @@ var MICROSERVICES = [
|
|
|
19598
19599
|
category: "HR",
|
|
19599
19600
|
tags: ["payroll", "salary", "wages", "deductions", "tax", "employees", "pay-stubs"]
|
|
19600
19601
|
},
|
|
19602
|
+
{
|
|
19603
|
+
name: "company",
|
|
19604
|
+
displayName: "Company",
|
|
19605
|
+
description: "AI agent control plane for autonomous company operations \u2014 organizations, teams, members, customers, and vendors",
|
|
19606
|
+
category: "Management",
|
|
19607
|
+
tags: ["company", "organization", "teams", "members", "customers", "vendors", "management"]
|
|
19608
|
+
},
|
|
19601
19609
|
{
|
|
19602
19610
|
name: "transcriber",
|
|
19603
19611
|
displayName: "Transcriber",
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,8 @@ var CATEGORIES = [
|
|
|
6
6
|
"Operations",
|
|
7
7
|
"Productivity",
|
|
8
8
|
"HR",
|
|
9
|
-
"Analytics"
|
|
9
|
+
"Analytics",
|
|
10
|
+
"Management"
|
|
10
11
|
];
|
|
11
12
|
var MICROSERVICES = [
|
|
12
13
|
{
|
|
@@ -142,6 +143,13 @@ var MICROSERVICES = [
|
|
|
142
143
|
category: "HR",
|
|
143
144
|
tags: ["payroll", "salary", "wages", "deductions", "tax", "employees", "pay-stubs"]
|
|
144
145
|
},
|
|
146
|
+
{
|
|
147
|
+
name: "company",
|
|
148
|
+
displayName: "Company",
|
|
149
|
+
description: "AI agent control plane for autonomous company operations \u2014 organizations, teams, members, customers, and vendors",
|
|
150
|
+
category: "Management",
|
|
151
|
+
tags: ["company", "organization", "teams", "members", "customers", "vendors", "management"]
|
|
152
|
+
},
|
|
145
153
|
{
|
|
146
154
|
name: "transcriber",
|
|
147
155
|
displayName: "Transcriber",
|
|
@@ -12,6 +12,15 @@ import {
|
|
|
12
12
|
getCampaignStats,
|
|
13
13
|
getSpendByPlatform,
|
|
14
14
|
getPlatforms,
|
|
15
|
+
bulkPause,
|
|
16
|
+
bulkResume,
|
|
17
|
+
getRankedCampaigns,
|
|
18
|
+
checkBudgetStatus,
|
|
19
|
+
comparePlatforms,
|
|
20
|
+
exportCampaigns,
|
|
21
|
+
cloneCampaign,
|
|
22
|
+
getBudgetRemaining,
|
|
23
|
+
getAdGroupStats,
|
|
15
24
|
} from "../db/campaigns.js";
|
|
16
25
|
import {
|
|
17
26
|
createAdGroup,
|
|
@@ -218,6 +227,118 @@ campaignCmd
|
|
|
218
227
|
}
|
|
219
228
|
});
|
|
220
229
|
|
|
230
|
+
// 1. Bulk pause/resume
|
|
231
|
+
campaignCmd
|
|
232
|
+
.command("bulk-pause")
|
|
233
|
+
.description("Pause all active campaigns on a platform")
|
|
234
|
+
.requiredOption("--platform <platform>", "Platform (google/meta/linkedin/tiktok)")
|
|
235
|
+
.option("--json", "Output as JSON", false)
|
|
236
|
+
.action((opts) => {
|
|
237
|
+
const result = bulkPause(opts.platform);
|
|
238
|
+
if (opts.json) {
|
|
239
|
+
console.log(JSON.stringify(result, null, 2));
|
|
240
|
+
} else {
|
|
241
|
+
console.log(`Paused ${result.updated_count} campaign(s) on ${result.platform}`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
campaignCmd
|
|
246
|
+
.command("bulk-resume")
|
|
247
|
+
.description("Resume all paused campaigns on a platform")
|
|
248
|
+
.requiredOption("--platform <platform>", "Platform (google/meta/linkedin/tiktok)")
|
|
249
|
+
.option("--json", "Output as JSON", false)
|
|
250
|
+
.action((opts) => {
|
|
251
|
+
const result = bulkResume(opts.platform);
|
|
252
|
+
if (opts.json) {
|
|
253
|
+
console.log(JSON.stringify(result, null, 2));
|
|
254
|
+
} else {
|
|
255
|
+
console.log(`Resumed ${result.updated_count} campaign(s) on ${result.platform}`);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// 3. Budget alerts
|
|
260
|
+
campaignCmd
|
|
261
|
+
.command("check-budget")
|
|
262
|
+
.description("Check budget status for a campaign")
|
|
263
|
+
.argument("<id>", "Campaign ID")
|
|
264
|
+
.option("--json", "Output as JSON", false)
|
|
265
|
+
.action((id, opts) => {
|
|
266
|
+
const status = checkBudgetStatus(id);
|
|
267
|
+
if (!status) {
|
|
268
|
+
console.error(`Campaign '${id}' not found.`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (opts.json) {
|
|
273
|
+
console.log(JSON.stringify(status, null, 2));
|
|
274
|
+
} else {
|
|
275
|
+
console.log(`Budget Status: ${status.campaign_name}`);
|
|
276
|
+
console.log(` Over budget: ${status.over_budget ? "YES" : "No"}`);
|
|
277
|
+
console.log(` Daily remaining: $${status.daily_remaining}`);
|
|
278
|
+
console.log(` Total remaining: $${status.total_remaining}`);
|
|
279
|
+
console.log(` % used: ${status.pct_used}%`);
|
|
280
|
+
console.log(` Days active: ${status.days_active}`);
|
|
281
|
+
console.log(` Expected spend: $${status.expected_spend}`);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// 5. CSV export
|
|
286
|
+
campaignCmd
|
|
287
|
+
.command("export")
|
|
288
|
+
.description("Export campaigns to CSV or JSON")
|
|
289
|
+
.option("--format <format>", "Format (csv/json)", "csv")
|
|
290
|
+
.action((opts) => {
|
|
291
|
+
const output = exportCampaigns(opts.format);
|
|
292
|
+
console.log(output);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// 6. Campaign cloning
|
|
296
|
+
campaignCmd
|
|
297
|
+
.command("clone")
|
|
298
|
+
.description("Clone a campaign with all ad groups and ads")
|
|
299
|
+
.argument("<id>", "Campaign ID to clone")
|
|
300
|
+
.requiredOption("--name <name>", "New campaign name")
|
|
301
|
+
.option("--json", "Output as JSON", false)
|
|
302
|
+
.action((id, opts) => {
|
|
303
|
+
const cloned = cloneCampaign(id, opts.name);
|
|
304
|
+
if (!cloned) {
|
|
305
|
+
console.error(`Campaign '${id}' not found.`);
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (opts.json) {
|
|
310
|
+
console.log(JSON.stringify(cloned, null, 2));
|
|
311
|
+
} else {
|
|
312
|
+
console.log(`Cloned campaign: ${cloned.name} (${cloned.id})`);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// 7. Budget remaining
|
|
317
|
+
campaignCmd
|
|
318
|
+
.command("budget-remaining")
|
|
319
|
+
.description("Show budget remaining for a campaign")
|
|
320
|
+
.argument("<id>", "Campaign ID")
|
|
321
|
+
.option("--json", "Output as JSON", false)
|
|
322
|
+
.action((id, opts) => {
|
|
323
|
+
const remaining = getBudgetRemaining(id);
|
|
324
|
+
if (!remaining) {
|
|
325
|
+
console.error(`Campaign '${id}' not found.`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (opts.json) {
|
|
330
|
+
console.log(JSON.stringify(remaining, null, 2));
|
|
331
|
+
} else {
|
|
332
|
+
console.log(`Budget Remaining: ${remaining.campaign_name}`);
|
|
333
|
+
console.log(` Daily budget: $${remaining.budget_daily}`);
|
|
334
|
+
console.log(` Total budget: $${remaining.budget_total}`);
|
|
335
|
+
console.log(` Spend: $${remaining.spend}`);
|
|
336
|
+
console.log(` Daily remaining: $${remaining.daily_remaining}`);
|
|
337
|
+
console.log(` Total remaining: $${remaining.total_remaining}`);
|
|
338
|
+
console.log(` Days remaining at current rate: ${remaining.days_remaining_at_daily_rate}`);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
221
342
|
// --- Ad Groups ---
|
|
222
343
|
|
|
223
344
|
const adGroupCmd = program
|
|
@@ -269,6 +390,35 @@ adGroupCmd
|
|
|
269
390
|
}
|
|
270
391
|
});
|
|
271
392
|
|
|
393
|
+
// 8. Ad group stats
|
|
394
|
+
adGroupCmd
|
|
395
|
+
.command("stats")
|
|
396
|
+
.description("Show aggregated stats for an ad group")
|
|
397
|
+
.argument("<id>", "Ad group ID")
|
|
398
|
+
.option("--json", "Output as JSON", false)
|
|
399
|
+
.action((id, opts) => {
|
|
400
|
+
const stats = getAdGroupStats(id);
|
|
401
|
+
if (!stats) {
|
|
402
|
+
console.error(`Ad group '${id}' not found.`);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (opts.json) {
|
|
407
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
408
|
+
} else {
|
|
409
|
+
console.log(`Ad Group Stats: ${stats.ad_group_name}`);
|
|
410
|
+
console.log(` Campaign: ${stats.campaign_id}`);
|
|
411
|
+
console.log(` Total ads: ${stats.total_ads}`);
|
|
412
|
+
console.log(` Active ads: ${stats.active_ads}`);
|
|
413
|
+
if (Object.keys(stats.metrics).length > 0) {
|
|
414
|
+
console.log(" Aggregated metrics:");
|
|
415
|
+
for (const [key, value] of Object.entries(stats.metrics)) {
|
|
416
|
+
console.log(` ${key}: ${value}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
272
422
|
// --- Ads ---
|
|
273
423
|
|
|
274
424
|
const adCmd = program
|
|
@@ -327,8 +477,26 @@ adCmd
|
|
|
327
477
|
program
|
|
328
478
|
.command("stats")
|
|
329
479
|
.description("Show campaign statistics")
|
|
480
|
+
.option("--sort-by <metric>", "Sort campaigns by metric (roas/ctr/spend)")
|
|
481
|
+
.option("--limit <n>", "Limit ranked results", "10")
|
|
330
482
|
.option("--json", "Output as JSON", false)
|
|
331
483
|
.action((opts) => {
|
|
484
|
+
// 2. Performance ranking via --sort-by
|
|
485
|
+
if (opts.sortBy) {
|
|
486
|
+
const ranked = getRankedCampaigns(opts.sortBy, parseInt(opts.limit));
|
|
487
|
+
if (opts.json) {
|
|
488
|
+
console.log(JSON.stringify(ranked, null, 2));
|
|
489
|
+
} else {
|
|
490
|
+
console.log(`Campaigns ranked by ${opts.sortBy}:`);
|
|
491
|
+
for (let i = 0; i < ranked.length; i++) {
|
|
492
|
+
const c = ranked[i];
|
|
493
|
+
const ctr = c.impressions > 0 ? ((c.clicks / c.impressions) * 100).toFixed(2) : "0.00";
|
|
494
|
+
console.log(` ${i + 1}. ${c.name} [${c.platform}] — ROAS: ${c.roas}, CTR: ${ctr}%, Spend: $${c.spend}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
332
500
|
const stats = getCampaignStats();
|
|
333
501
|
|
|
334
502
|
if (opts.json) {
|
|
@@ -345,6 +513,36 @@ program
|
|
|
345
513
|
}
|
|
346
514
|
});
|
|
347
515
|
|
|
516
|
+
// 4. Cross-platform comparison
|
|
517
|
+
program
|
|
518
|
+
.command("compare-platforms")
|
|
519
|
+
.description("Compare ROAS/CPA/spend across platforms side-by-side")
|
|
520
|
+
.option("--json", "Output as JSON", false)
|
|
521
|
+
.action((opts) => {
|
|
522
|
+
const comparison = comparePlatforms();
|
|
523
|
+
|
|
524
|
+
if (opts.json) {
|
|
525
|
+
console.log(JSON.stringify(comparison, null, 2));
|
|
526
|
+
} else {
|
|
527
|
+
if (comparison.length === 0) {
|
|
528
|
+
console.log("No platform data.");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
console.log("Platform Comparison:");
|
|
532
|
+
console.log(" Platform | Campaigns | Spend | ROAS | CTR | CPA");
|
|
533
|
+
console.log(" -------------|-----------|--------------|-------|--------|-------");
|
|
534
|
+
for (const p of comparison) {
|
|
535
|
+
const platform = p.platform.padEnd(12);
|
|
536
|
+
const campaigns = String(p.campaign_count).padEnd(9);
|
|
537
|
+
const spend = `$${p.total_spend.toFixed(2)}`.padEnd(12);
|
|
538
|
+
const roas = p.avg_roas.toFixed(2).padEnd(5);
|
|
539
|
+
const ctr = `${p.avg_ctr.toFixed(2)}%`.padEnd(6);
|
|
540
|
+
const cpa = p.avg_cpa > 0 ? `$${p.avg_cpa.toFixed(2)}` : "N/A";
|
|
541
|
+
console.log(` ${platform} | ${campaigns} | ${spend} | ${roas} | ${ctr} | ${cpa}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
348
546
|
program
|
|
349
547
|
.command("spend")
|
|
350
548
|
.description("Show spend by platform")
|
|
@@ -491,3 +491,307 @@ export function getPlatforms(): string[] {
|
|
|
491
491
|
).all() as { platform: string }[];
|
|
492
492
|
return rows.map((r) => r.platform);
|
|
493
493
|
}
|
|
494
|
+
|
|
495
|
+
// --- QoL Features ---
|
|
496
|
+
|
|
497
|
+
// 1. Bulk pause/resume by platform
|
|
498
|
+
export interface BulkUpdateResult {
|
|
499
|
+
updated_count: number;
|
|
500
|
+
platform: Platform;
|
|
501
|
+
new_status: CampaignStatus;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function bulkPause(platform: Platform): BulkUpdateResult {
|
|
505
|
+
const db = getDatabase();
|
|
506
|
+
const result = db.prepare(
|
|
507
|
+
"UPDATE campaigns SET status = 'paused', updated_at = datetime('now') WHERE platform = ? AND status = 'active'"
|
|
508
|
+
).run(platform);
|
|
509
|
+
return { updated_count: result.changes, platform, new_status: "paused" };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function bulkResume(platform: Platform): BulkUpdateResult {
|
|
513
|
+
const db = getDatabase();
|
|
514
|
+
const result = db.prepare(
|
|
515
|
+
"UPDATE campaigns SET status = 'active', updated_at = datetime('now') WHERE platform = ? AND status = 'paused'"
|
|
516
|
+
).run(platform);
|
|
517
|
+
return { updated_count: result.changes, platform, new_status: "active" };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 2. Performance ranking
|
|
521
|
+
export type RankMetric = "roas" | "ctr" | "spend";
|
|
522
|
+
|
|
523
|
+
export function getRankedCampaigns(sortBy: RankMetric = "roas", limit: number = 10): Campaign[] {
|
|
524
|
+
const db = getDatabase();
|
|
525
|
+
let orderExpr: string;
|
|
526
|
+
|
|
527
|
+
switch (sortBy) {
|
|
528
|
+
case "roas":
|
|
529
|
+
orderExpr = "roas DESC";
|
|
530
|
+
break;
|
|
531
|
+
case "ctr":
|
|
532
|
+
// CTR = clicks / impressions (handle division by zero)
|
|
533
|
+
orderExpr = "CASE WHEN impressions > 0 THEN CAST(clicks AS REAL) / impressions ELSE 0 END DESC";
|
|
534
|
+
break;
|
|
535
|
+
case "spend":
|
|
536
|
+
orderExpr = "spend DESC";
|
|
537
|
+
break;
|
|
538
|
+
default:
|
|
539
|
+
orderExpr = "roas DESC";
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const rows = db.prepare(
|
|
543
|
+
`SELECT * FROM campaigns ORDER BY ${orderExpr} LIMIT ?`
|
|
544
|
+
).all(limit) as CampaignRow[];
|
|
545
|
+
return rows.map(rowToCampaign);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// 3. Budget alerts
|
|
549
|
+
export interface BudgetStatus {
|
|
550
|
+
campaign_id: string;
|
|
551
|
+
campaign_name: string;
|
|
552
|
+
over_budget: boolean;
|
|
553
|
+
daily_remaining: number;
|
|
554
|
+
total_remaining: number;
|
|
555
|
+
pct_used: number;
|
|
556
|
+
days_active: number;
|
|
557
|
+
expected_spend: number;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function checkBudgetStatus(id: string): BudgetStatus | null {
|
|
561
|
+
const campaign = getCampaign(id);
|
|
562
|
+
if (!campaign) return null;
|
|
563
|
+
|
|
564
|
+
const now = new Date();
|
|
565
|
+
let daysActive = 1;
|
|
566
|
+
|
|
567
|
+
if (campaign.start_date) {
|
|
568
|
+
const start = new Date(campaign.start_date);
|
|
569
|
+
const diffMs = now.getTime() - start.getTime();
|
|
570
|
+
daysActive = Math.max(1, Math.ceil(diffMs / (1000 * 60 * 60 * 24)));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const expectedSpend = campaign.budget_daily * daysActive;
|
|
574
|
+
const overBudget = campaign.budget_total > 0
|
|
575
|
+
? campaign.spend > campaign.budget_total
|
|
576
|
+
: campaign.spend > expectedSpend;
|
|
577
|
+
|
|
578
|
+
const dailySpentToday = daysActive > 0 ? campaign.spend / daysActive : campaign.spend;
|
|
579
|
+
const dailyRemaining = Math.max(0, campaign.budget_daily - dailySpentToday);
|
|
580
|
+
|
|
581
|
+
const totalRemaining = campaign.budget_total > 0
|
|
582
|
+
? Math.max(0, campaign.budget_total - campaign.spend)
|
|
583
|
+
: Math.max(0, expectedSpend - campaign.spend);
|
|
584
|
+
|
|
585
|
+
const pctUsed = campaign.budget_total > 0
|
|
586
|
+
? (campaign.spend / campaign.budget_total) * 100
|
|
587
|
+
: expectedSpend > 0 ? (campaign.spend / expectedSpend) * 100 : 0;
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
campaign_id: id,
|
|
591
|
+
campaign_name: campaign.name,
|
|
592
|
+
over_budget: overBudget,
|
|
593
|
+
daily_remaining: Math.round(dailyRemaining * 100) / 100,
|
|
594
|
+
total_remaining: Math.round(totalRemaining * 100) / 100,
|
|
595
|
+
pct_used: Math.round(pctUsed * 100) / 100,
|
|
596
|
+
days_active: daysActive,
|
|
597
|
+
expected_spend: Math.round(expectedSpend * 100) / 100,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 4. Cross-platform comparison
|
|
602
|
+
export interface PlatformComparison {
|
|
603
|
+
platform: Platform;
|
|
604
|
+
campaign_count: number;
|
|
605
|
+
total_spend: number;
|
|
606
|
+
total_impressions: number;
|
|
607
|
+
total_clicks: number;
|
|
608
|
+
total_conversions: number;
|
|
609
|
+
avg_roas: number;
|
|
610
|
+
avg_ctr: number;
|
|
611
|
+
avg_cpa: number;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function comparePlatforms(): PlatformComparison[] {
|
|
615
|
+
const db = getDatabase();
|
|
616
|
+
const rows = db.prepare(`
|
|
617
|
+
SELECT
|
|
618
|
+
platform,
|
|
619
|
+
COUNT(*) as campaign_count,
|
|
620
|
+
COALESCE(SUM(spend), 0) as total_spend,
|
|
621
|
+
COALESCE(SUM(impressions), 0) as total_impressions,
|
|
622
|
+
COALESCE(SUM(clicks), 0) as total_clicks,
|
|
623
|
+
COALESCE(SUM(conversions), 0) as total_conversions,
|
|
624
|
+
COALESCE(AVG(CASE WHEN roas > 0 THEN roas END), 0) as avg_roas,
|
|
625
|
+
CASE WHEN SUM(impressions) > 0
|
|
626
|
+
THEN CAST(SUM(clicks) AS REAL) / SUM(impressions) * 100
|
|
627
|
+
ELSE 0
|
|
628
|
+
END as avg_ctr,
|
|
629
|
+
CASE WHEN SUM(conversions) > 0
|
|
630
|
+
THEN SUM(spend) / SUM(conversions)
|
|
631
|
+
ELSE 0
|
|
632
|
+
END as avg_cpa
|
|
633
|
+
FROM campaigns
|
|
634
|
+
GROUP BY platform
|
|
635
|
+
ORDER BY total_spend DESC
|
|
636
|
+
`).all() as PlatformComparison[];
|
|
637
|
+
return rows;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 5. CSV export
|
|
641
|
+
export function exportCampaigns(format: "csv" | "json" = "csv"): string {
|
|
642
|
+
const campaigns = listCampaigns();
|
|
643
|
+
|
|
644
|
+
if (format === "json") {
|
|
645
|
+
return JSON.stringify(campaigns, null, 2);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// CSV format
|
|
649
|
+
const headers = [
|
|
650
|
+
"id", "platform", "name", "status", "budget_daily", "budget_total",
|
|
651
|
+
"spend", "impressions", "clicks", "conversions", "roas",
|
|
652
|
+
"start_date", "end_date", "created_at", "updated_at",
|
|
653
|
+
];
|
|
654
|
+
|
|
655
|
+
const rows = campaigns.map((c) =>
|
|
656
|
+
headers.map((h) => {
|
|
657
|
+
const val = c[h as keyof Campaign];
|
|
658
|
+
if (val === null || val === undefined) return "";
|
|
659
|
+
if (typeof val === "object") return JSON.stringify(val);
|
|
660
|
+
const str = String(val);
|
|
661
|
+
// Escape CSV fields that contain commas or quotes
|
|
662
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
|
663
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
664
|
+
}
|
|
665
|
+
return str;
|
|
666
|
+
}).join(",")
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
return [headers.join(","), ...rows].join("\n");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// 6. Campaign cloning
|
|
673
|
+
export function cloneCampaign(id: string, newName: string): Campaign | null {
|
|
674
|
+
const original = getCampaign(id);
|
|
675
|
+
if (!original) return null;
|
|
676
|
+
|
|
677
|
+
const db = getDatabase();
|
|
678
|
+
|
|
679
|
+
// Clone the campaign itself (reset metrics)
|
|
680
|
+
const cloned = createCampaign({
|
|
681
|
+
platform: original.platform,
|
|
682
|
+
name: newName,
|
|
683
|
+
status: "draft",
|
|
684
|
+
budget_daily: original.budget_daily,
|
|
685
|
+
budget_total: original.budget_total,
|
|
686
|
+
start_date: original.start_date || undefined,
|
|
687
|
+
end_date: original.end_date || undefined,
|
|
688
|
+
metadata: original.metadata,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Clone ad groups and their ads
|
|
692
|
+
const adGroups = listAdGroups(id);
|
|
693
|
+
for (const ag of adGroups) {
|
|
694
|
+
const clonedGroup = createAdGroup({
|
|
695
|
+
campaign_id: cloned.id,
|
|
696
|
+
name: ag.name,
|
|
697
|
+
targeting: ag.targeting,
|
|
698
|
+
status: "draft",
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
const ads = listAds(ag.id);
|
|
702
|
+
for (const ad of ads) {
|
|
703
|
+
createAd({
|
|
704
|
+
ad_group_id: clonedGroup.id,
|
|
705
|
+
headline: ad.headline,
|
|
706
|
+
description: ad.description || undefined,
|
|
707
|
+
creative_url: ad.creative_url || undefined,
|
|
708
|
+
status: "draft",
|
|
709
|
+
metrics: {},
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return getCampaign(cloned.id)!;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// 7. Budget remaining (reuses checkBudgetStatus but provides a focused view)
|
|
718
|
+
export interface BudgetRemaining {
|
|
719
|
+
campaign_id: string;
|
|
720
|
+
campaign_name: string;
|
|
721
|
+
budget_daily: number;
|
|
722
|
+
budget_total: number;
|
|
723
|
+
spend: number;
|
|
724
|
+
daily_remaining: number;
|
|
725
|
+
total_remaining: number;
|
|
726
|
+
days_remaining_at_daily_rate: number;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function getBudgetRemaining(id: string): BudgetRemaining | null {
|
|
730
|
+
const campaign = getCampaign(id);
|
|
731
|
+
if (!campaign) return null;
|
|
732
|
+
|
|
733
|
+
const now = new Date();
|
|
734
|
+
let daysActive = 1;
|
|
735
|
+
if (campaign.start_date) {
|
|
736
|
+
const start = new Date(campaign.start_date);
|
|
737
|
+
const diffMs = now.getTime() - start.getTime();
|
|
738
|
+
daysActive = Math.max(1, Math.ceil(diffMs / (1000 * 60 * 60 * 24)));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const avgDailySpend = campaign.spend / daysActive;
|
|
742
|
+
const dailyRemaining = Math.max(0, campaign.budget_daily - avgDailySpend);
|
|
743
|
+
const totalRemaining = campaign.budget_total > 0
|
|
744
|
+
? Math.max(0, campaign.budget_total - campaign.spend)
|
|
745
|
+
: 0;
|
|
746
|
+
const daysRemainingAtRate = avgDailySpend > 0 && campaign.budget_total > 0
|
|
747
|
+
? Math.floor(totalRemaining / avgDailySpend)
|
|
748
|
+
: 0;
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
campaign_id: id,
|
|
752
|
+
campaign_name: campaign.name,
|
|
753
|
+
budget_daily: campaign.budget_daily,
|
|
754
|
+
budget_total: campaign.budget_total,
|
|
755
|
+
spend: campaign.spend,
|
|
756
|
+
daily_remaining: Math.round(dailyRemaining * 100) / 100,
|
|
757
|
+
total_remaining: Math.round(totalRemaining * 100) / 100,
|
|
758
|
+
days_remaining_at_daily_rate: daysRemainingAtRate,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// 8. Ad group stats — aggregate metrics for ads in a group
|
|
763
|
+
export interface AdGroupStats {
|
|
764
|
+
ad_group_id: string;
|
|
765
|
+
ad_group_name: string;
|
|
766
|
+
campaign_id: string;
|
|
767
|
+
total_ads: number;
|
|
768
|
+
active_ads: number;
|
|
769
|
+
metrics: Record<string, number>;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export function getAdGroupStats(adGroupId: string): AdGroupStats | null {
|
|
773
|
+
const adGroup = getAdGroup(adGroupId);
|
|
774
|
+
if (!adGroup) return null;
|
|
775
|
+
|
|
776
|
+
const ads = listAds(adGroupId);
|
|
777
|
+
const activeAds = ads.filter((a) => a.status === "active").length;
|
|
778
|
+
|
|
779
|
+
// Aggregate numeric metrics from all ads
|
|
780
|
+
const aggregated: Record<string, number> = {};
|
|
781
|
+
for (const ad of ads) {
|
|
782
|
+
for (const [key, value] of Object.entries(ad.metrics)) {
|
|
783
|
+
if (typeof value === "number") {
|
|
784
|
+
aggregated[key] = (aggregated[key] || 0) + value;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
ad_group_id: adGroupId,
|
|
791
|
+
ad_group_name: adGroup.name,
|
|
792
|
+
campaign_id: adGroup.campaign_id,
|
|
793
|
+
total_ads: ads.length,
|
|
794
|
+
active_ads: activeAds,
|
|
795
|
+
metrics: aggregated,
|
|
796
|
+
};
|
|
797
|
+
}
|