@a8techads/cli 0.4.0 → 0.4.1
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/dist/a8techads.js +1959 -149
- package/package.json +1 -1
package/dist/a8techads.js
CHANGED
|
@@ -2628,6 +2628,18 @@ async function login(opts = {}) {
|
|
|
2628
2628
|
const authUrl = opts.authUrl ?? DEFAULT_AUTH_URL;
|
|
2629
2629
|
const tokenEndpoint = `${authUrl}/.ory/hydra/oauth2/token`;
|
|
2630
2630
|
const authorizationEndpoint = `${authUrl}/.ory/hydra/oauth2/auth`;
|
|
2631
|
+
if (opts.forceLogin) {
|
|
2632
|
+
const existingCreds = loadCredentials();
|
|
2633
|
+
if (existingCreds.profiles[profileName]) {
|
|
2634
|
+
delete existingCreds.profiles[profileName];
|
|
2635
|
+
saveCredentials(existingCreds);
|
|
2636
|
+
}
|
|
2637
|
+
const existingCtx = loadContext();
|
|
2638
|
+
if (existingCtx.profiles[profileName]) {
|
|
2639
|
+
delete existingCtx.profiles[profileName];
|
|
2640
|
+
saveContext(existingCtx);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2631
2643
|
const codeVerifier = generateRandomCodeVerifier();
|
|
2632
2644
|
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
|
2633
2645
|
const state = generateRandomState();
|
|
@@ -3202,6 +3214,10 @@ function createProfileCommand() {
|
|
|
3202
3214
|
|
|
3203
3215
|
// src/utils/http.ts
|
|
3204
3216
|
async function apiRequest(opts) {
|
|
3217
|
+
const request = await buildAuthenticatedRequest(opts);
|
|
3218
|
+
return fetch(request.url, request.init);
|
|
3219
|
+
}
|
|
3220
|
+
async function buildAuthenticatedRequest(opts) {
|
|
3205
3221
|
await refreshTokenIfNeeded();
|
|
3206
3222
|
const creds = loadCredentials();
|
|
3207
3223
|
const profile = getCurrentProfile(creds);
|
|
@@ -3216,21 +3232,23 @@ async function apiRequest(opts) {
|
|
|
3216
3232
|
"Content-Type": "application/json",
|
|
3217
3233
|
...opts.headers
|
|
3218
3234
|
};
|
|
3219
|
-
if (context?.current_capability) {
|
|
3220
|
-
headers["X-Effective-Capability"] = context.current_capability;
|
|
3221
|
-
}
|
|
3222
3235
|
if (context?.impersonation) {
|
|
3223
3236
|
headers["X-Impersonate-Tenant"] = context.impersonation.target_tenant_id;
|
|
3224
3237
|
if (context.impersonation.effective_capability) {
|
|
3225
3238
|
headers["X-Impersonate-Capability"] = context.impersonation.effective_capability;
|
|
3226
3239
|
}
|
|
3240
|
+
} else if (context?.current_capability) {
|
|
3241
|
+
headers["X-Effective-Capability"] = context.current_capability;
|
|
3227
3242
|
}
|
|
3228
|
-
const url = `${profile.api_url}${opts.path}`;
|
|
3229
|
-
return
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3243
|
+
const url = `${opts.baseUrl ?? profile.api_url}${opts.path}`;
|
|
3244
|
+
return {
|
|
3245
|
+
url,
|
|
3246
|
+
init: {
|
|
3247
|
+
method: opts.method ?? "GET",
|
|
3248
|
+
headers,
|
|
3249
|
+
body: opts.body ? JSON.stringify(opts.body) : undefined
|
|
3250
|
+
}
|
|
3251
|
+
};
|
|
3234
3252
|
}
|
|
3235
3253
|
function validateTenantMatch(accessToken, contextTenantId) {
|
|
3236
3254
|
const claims = decodeJwt(accessToken);
|
|
@@ -3538,6 +3556,9 @@ function dspPrefix() {
|
|
|
3538
3556
|
function sspPrefix() {
|
|
3539
3557
|
return "/api/v1/ssp";
|
|
3540
3558
|
}
|
|
3559
|
+
function consolePrefix() {
|
|
3560
|
+
return "/api/v1/console";
|
|
3561
|
+
}
|
|
3541
3562
|
|
|
3542
3563
|
// src/utils/output.ts
|
|
3543
3564
|
function printData(data, columns, format = "table") {
|
|
@@ -3646,21 +3667,24 @@ Examples:
|
|
|
3646
3667
|
printData(rows, COLUMNS, opts.format);
|
|
3647
3668
|
});
|
|
3648
3669
|
addFormatOption(cmd.command("get").description("Get audience details by ID.").argument("<id>", "Audience ID")).action(async (id, opts) => {
|
|
3649
|
-
const
|
|
3650
|
-
|
|
3651
|
-
if (!resp.ok) {
|
|
3652
|
-
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
3653
|
-
process.exit(1);
|
|
3654
|
-
}
|
|
3655
|
-
printDetail(json.data ?? json, opts.format);
|
|
3670
|
+
const audience = await fetchAudience(id);
|
|
3671
|
+
printDetail(audience, opts.format);
|
|
3656
3672
|
});
|
|
3657
|
-
cmd.command("
|
|
3658
|
-
|
|
3659
|
-
|
|
3673
|
+
addFormatOption(cmd.command("size").description("Show the current estimated audience size.").argument("<id>", "Audience ID")).action(async (id, opts) => {
|
|
3674
|
+
const audience = await fetchAudience(id);
|
|
3675
|
+
const detail = {
|
|
3676
|
+
id: audience.id,
|
|
3677
|
+
name: audience.name,
|
|
3678
|
+
status: audience.status,
|
|
3679
|
+
estimatedSize: audience.estimatedSize ?? audience.estimated_size ?? null
|
|
3680
|
+
};
|
|
3681
|
+
printDetail(detail, opts.format);
|
|
3682
|
+
});
|
|
3683
|
+
cmd.command("create").description("Create a new audience.").option("--name <name>", "Audience name (required)").option("--type <type>", "Audience type: UPLOADED_LIST, RETARGETING, or LOOKALIKE", "UPLOADED_LIST").option("--description <desc>", "Description").option("--ttl <days>", "Membership TTL in days (default: 90)", "90").option("--goal-id <id>", "Conversion goal ID (required for RETARGETING type)").option("--seed <id>", "Seed audience ID (required for LOOKALIKE type)").option("--ratio <n>", "Expansion ratio for LOOKALIKE (default: 0.05)", "0.05").option("--from-json <file>", "Create from JSON file").addHelpText("after", `
|
|
3660
3684
|
Examples:
|
|
3661
3685
|
$ a8techads audiences create --name "High-Value Customers"
|
|
3662
|
-
$ a8techads audiences create --name "Retarget Pool" --
|
|
3663
|
-
$ a8techads audiences create --
|
|
3686
|
+
$ a8techads audiences create --name "Retarget Pool" --type RETARGETING --goal-id <id>
|
|
3687
|
+
$ a8techads audiences create --name "Similar Users" --type LOOKALIKE --seed <id> --ratio 0.05`).action(async (opts) => {
|
|
3664
3688
|
let body;
|
|
3665
3689
|
if (opts.fromJson) {
|
|
3666
3690
|
const { readFileSync: readFileSync3 } = await import("fs");
|
|
@@ -3674,6 +3698,10 @@ Examples:
|
|
|
3674
3698
|
console.error("Error: --goal-id is required for RETARGETING type.");
|
|
3675
3699
|
process.exit(1);
|
|
3676
3700
|
}
|
|
3701
|
+
if (opts.type === "LOOKALIKE" && !opts.seed) {
|
|
3702
|
+
console.error("Error: --seed is required for LOOKALIKE type.");
|
|
3703
|
+
process.exit(1);
|
|
3704
|
+
}
|
|
3677
3705
|
body = {
|
|
3678
3706
|
name: opts.name,
|
|
3679
3707
|
type: opts.type,
|
|
@@ -3684,6 +3712,9 @@ Examples:
|
|
|
3684
3712
|
if (opts.goalId) {
|
|
3685
3713
|
body.rules = { source: "conversion_goal", goal_id: opts.goalId, action: "positive", recency_days: 30 };
|
|
3686
3714
|
}
|
|
3715
|
+
if (opts.seed) {
|
|
3716
|
+
body.rules = { seed_audience_id: opts.seed, expansion_ratio: Number(opts.ratio) };
|
|
3717
|
+
}
|
|
3687
3718
|
}
|
|
3688
3719
|
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/audiences`, body });
|
|
3689
3720
|
const json = await resp.json();
|
|
@@ -3796,8 +3827,41 @@ File format (one per line):
|
|
|
3796
3827
|
}
|
|
3797
3828
|
console.log(`Upload complete. Status: ${json.data?.status ?? json.status}, Members: ${json.data?.uploadedCount ?? json.uploadedCount ?? "?"}`);
|
|
3798
3829
|
});
|
|
3830
|
+
cmd.command("wait-ready").description("Poll audience status until it becomes READY, ACTIVE, or ERROR.").argument("<id>", "Audience ID").option("--timeout <seconds>", "Timeout in seconds", "120").option("--interval <seconds>", "Polling interval in seconds", "3").action(async (id, opts) => {
|
|
3831
|
+
const timeoutMs = Number(opts.timeout) * 1000;
|
|
3832
|
+
const intervalMs = Number(opts.interval) * 1000;
|
|
3833
|
+
const started = Date.now();
|
|
3834
|
+
while (true) {
|
|
3835
|
+
const audience = await fetchAudience(id);
|
|
3836
|
+
const status = audience.status;
|
|
3837
|
+
const size = audience.estimatedSize ?? audience.estimated_size ?? null;
|
|
3838
|
+
console.log(`Status: ${status}${size != null ? ` | Size: ${Number(size).toLocaleString()}` : ""}`);
|
|
3839
|
+
if (status === "READY" || status === "ACTIVE") {
|
|
3840
|
+
console.log(`Audience ${id} is ready.`);
|
|
3841
|
+
return;
|
|
3842
|
+
}
|
|
3843
|
+
if (status === "ERROR" || status === "ARCHIVED") {
|
|
3844
|
+
console.error(`Audience ${id} reached terminal status: ${status}`);
|
|
3845
|
+
process.exit(1);
|
|
3846
|
+
}
|
|
3847
|
+
if (Date.now() - started >= timeoutMs) {
|
|
3848
|
+
console.error(`Timed out waiting for audience ${id}.`);
|
|
3849
|
+
process.exit(1);
|
|
3850
|
+
}
|
|
3851
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
3852
|
+
}
|
|
3853
|
+
});
|
|
3799
3854
|
return cmd;
|
|
3800
3855
|
}
|
|
3856
|
+
async function fetchAudience(id) {
|
|
3857
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/audiences/${id}` });
|
|
3858
|
+
const json = await resp.json();
|
|
3859
|
+
if (!resp.ok) {
|
|
3860
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
3861
|
+
process.exit(1);
|
|
3862
|
+
}
|
|
3863
|
+
return json.data ?? json;
|
|
3864
|
+
}
|
|
3801
3865
|
|
|
3802
3866
|
// src/commands/campaigns.ts
|
|
3803
3867
|
var COLUMNS2 = [
|
|
@@ -3942,8 +4006,178 @@ Examples:
|
|
|
3942
4006
|
}
|
|
3943
4007
|
console.log(`Campaign duplicated. New ID: ${json.data?.id ?? json.id}`);
|
|
3944
4008
|
});
|
|
4009
|
+
const targetingCmd = new Command("targeting").description("Show or update campaign targeting without editing full JSON.").addHelpText("after", `
|
|
4010
|
+
Examples:
|
|
4011
|
+
$ a8techads campaigns targeting show <campaign-id>
|
|
4012
|
+
$ a8techads campaigns targeting set-geo <campaign-id> --countries US,CA --mode target
|
|
4013
|
+
$ a8techads campaigns targeting add-audience <campaign-id> --audience <id> --mode include
|
|
4014
|
+
$ a8techads campaigns targeting set-day-parting <campaign-id> --timezone UTC --from-json schedule.json`);
|
|
4015
|
+
addFormatOption(targetingCmd.command("show").description("Show campaign targeting, audience include/exclude, and day parting.").argument("<id>", "Campaign ID")).action(async (id, opts) => {
|
|
4016
|
+
const campaign = await fetchCampaign(id);
|
|
4017
|
+
const detail = {
|
|
4018
|
+
campaignId: campaign.id,
|
|
4019
|
+
campaignName: campaign.name,
|
|
4020
|
+
targeting: campaign.targeting ?? {},
|
|
4021
|
+
timezone: campaign.timezone ?? "UTC",
|
|
4022
|
+
dayParting: campaign.dayParting ?? null
|
|
4023
|
+
};
|
|
4024
|
+
printDetail(detail, opts.format);
|
|
4025
|
+
});
|
|
4026
|
+
targetingCmd.command("set-geo").description("Replace campaign geo countries and mode.").argument("<id>", "Campaign ID").requiredOption("--countries <codes>", "Comma-separated country codes, e.g. US,CA").option("--mode <mode>", "target or block", "target").action(async (id, opts) => {
|
|
4027
|
+
const campaign = await fetchCampaign(id);
|
|
4028
|
+
const targeting = normalizeTargeting(campaign.targeting);
|
|
4029
|
+
const countries = parseCsvList(opts.countries);
|
|
4030
|
+
const mode = opts.mode === "block" ? "block" : "target";
|
|
4031
|
+
targeting.geo = {
|
|
4032
|
+
...targeting.geo ?? {},
|
|
4033
|
+
countries,
|
|
4034
|
+
mode,
|
|
4035
|
+
countriesMode: mode
|
|
4036
|
+
};
|
|
4037
|
+
await patchCampaign(id, { targeting });
|
|
4038
|
+
console.log(`Campaign ${id} geo targeting updated.`);
|
|
4039
|
+
});
|
|
4040
|
+
targetingCmd.command("add-audience").description("Add an audience to include or exclude targeting.").argument("<id>", "Campaign ID").requiredOption("--audience <audience-id>", "Audience ID").requiredOption("--mode <mode>", "include or exclude").action(async (id, opts) => {
|
|
4041
|
+
const mode = normalizeAudienceMode(opts.mode);
|
|
4042
|
+
const campaign = await fetchCampaign(id);
|
|
4043
|
+
const targeting = normalizeTargeting(campaign.targeting);
|
|
4044
|
+
const audiences = normalizeAudiences(targeting.audiences);
|
|
4045
|
+
const list = new Set(mode === "include" ? audiences.include : audiences.exclude);
|
|
4046
|
+
list.add(opts.audience);
|
|
4047
|
+
targeting.audiences = {
|
|
4048
|
+
...audiences,
|
|
4049
|
+
[mode]: Array.from(list)
|
|
4050
|
+
};
|
|
4051
|
+
await patchCampaign(id, { targeting });
|
|
4052
|
+
console.log(`Campaign ${id} audience ${opts.audience} added to ${mode}.`);
|
|
4053
|
+
});
|
|
4054
|
+
targetingCmd.command("remove-audience").description("Remove an audience from include or exclude targeting.").argument("<id>", "Campaign ID").requiredOption("--audience <audience-id>", "Audience ID").requiredOption("--mode <mode>", "include or exclude").action(async (id, opts) => {
|
|
4055
|
+
const mode = normalizeAudienceMode(opts.mode);
|
|
4056
|
+
const campaign = await fetchCampaign(id);
|
|
4057
|
+
const targeting = normalizeTargeting(campaign.targeting);
|
|
4058
|
+
const audiences = normalizeAudiences(targeting.audiences);
|
|
4059
|
+
targeting.audiences = {
|
|
4060
|
+
...audiences,
|
|
4061
|
+
[mode]: (mode === "include" ? audiences.include : audiences.exclude).filter((audId) => audId !== opts.audience)
|
|
4062
|
+
};
|
|
4063
|
+
await patchCampaign(id, { targeting });
|
|
4064
|
+
console.log(`Campaign ${id} audience ${opts.audience} removed from ${mode}.`);
|
|
4065
|
+
});
|
|
4066
|
+
targetingCmd.command("set-day-parting").description("Set or disable day parting schedule.").argument("<id>", "Campaign ID").option("--timezone <tz>", "Timezone (default: keep existing or UTC)").option("--from-json <file>", "Schedule JSON file").option("--disable", "Disable day parting").action(async (id, opts) => {
|
|
4067
|
+
if (!opts.disable && !opts.fromJson) {
|
|
4068
|
+
console.error("Error: provide --from-json <file> or --disable.");
|
|
4069
|
+
process.exit(1);
|
|
4070
|
+
}
|
|
4071
|
+
const campaign = await fetchCampaign(id);
|
|
4072
|
+
let dayParting;
|
|
4073
|
+
if (opts.disable) {
|
|
4074
|
+
dayParting = null;
|
|
4075
|
+
} else {
|
|
4076
|
+
const { readFileSync: readFileSync3 } = await import("fs");
|
|
4077
|
+
dayParting = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
|
|
4078
|
+
}
|
|
4079
|
+
await patchCampaign(id, {
|
|
4080
|
+
dayParting,
|
|
4081
|
+
timezone: opts.timezone ?? campaign.timezone ?? "UTC"
|
|
4082
|
+
});
|
|
4083
|
+
console.log(`Campaign ${id} day parting updated.`);
|
|
4084
|
+
});
|
|
4085
|
+
for (const [resourceName, field] of [
|
|
4086
|
+
["domain", "domains"],
|
|
4087
|
+
["keyword", "keywords"],
|
|
4088
|
+
["ip-range", "ipRanges"]
|
|
4089
|
+
]) {
|
|
4090
|
+
targetingCmd.command(`add-${resourceName}`).description(`Add a ${resourceName} to targeting list.`).argument("<id>", "Campaign ID").requiredOption(`--${resourceName} <value>`, `Value to add`).option("--mode <mode>", "target or block (default: keep current or target)", "target").action(async (id, opts) => {
|
|
4091
|
+
const campaign = await fetchCampaign(id);
|
|
4092
|
+
const targeting = normalizeTargeting(campaign.targeting);
|
|
4093
|
+
const existing = normalizeListTargeting(targeting[field]);
|
|
4094
|
+
const value = String(opts[camelizeOption(resourceName)]).trim();
|
|
4095
|
+
if (!value) {
|
|
4096
|
+
console.error(`Error: --${resourceName} is required.`);
|
|
4097
|
+
process.exit(1);
|
|
4098
|
+
}
|
|
4099
|
+
const list = new Set(existing.list);
|
|
4100
|
+
list.add(value);
|
|
4101
|
+
targeting[field] = {
|
|
4102
|
+
mode: opts.mode === "block" ? "block" : existing.mode,
|
|
4103
|
+
list: Array.from(list)
|
|
4104
|
+
};
|
|
4105
|
+
await patchCampaign(id, { targeting });
|
|
4106
|
+
console.log(`Campaign ${id} ${resourceName} added.`);
|
|
4107
|
+
});
|
|
4108
|
+
targetingCmd.command(`remove-${resourceName}`).description(`Remove a ${resourceName} from targeting list.`).argument("<id>", "Campaign ID").requiredOption(`--${resourceName} <value>`, `Value to remove`).action(async (id, opts) => {
|
|
4109
|
+
const campaign = await fetchCampaign(id);
|
|
4110
|
+
const targeting = normalizeTargeting(campaign.targeting);
|
|
4111
|
+
const existing = normalizeListTargeting(targeting[field]);
|
|
4112
|
+
const value = String(opts[camelizeOption(resourceName)]).trim();
|
|
4113
|
+
targeting[field] = {
|
|
4114
|
+
...existing,
|
|
4115
|
+
list: existing.list.filter((item) => item !== value)
|
|
4116
|
+
};
|
|
4117
|
+
await patchCampaign(id, { targeting });
|
|
4118
|
+
console.log(`Campaign ${id} ${resourceName} removed.`);
|
|
4119
|
+
});
|
|
4120
|
+
targetingCmd.command(`set-${resourceName}-mode`).description(`Set ${resourceName} targeting mode.`).argument("<id>", "Campaign ID").requiredOption("--mode <mode>", "target or block").action(async (id, opts) => {
|
|
4121
|
+
const campaign = await fetchCampaign(id);
|
|
4122
|
+
const targeting = normalizeTargeting(campaign.targeting);
|
|
4123
|
+
const existing = normalizeListTargeting(targeting[field]);
|
|
4124
|
+
targeting[field] = {
|
|
4125
|
+
...existing,
|
|
4126
|
+
mode: opts.mode === "block" ? "block" : "target"
|
|
4127
|
+
};
|
|
4128
|
+
await patchCampaign(id, { targeting });
|
|
4129
|
+
console.log(`Campaign ${id} ${resourceName} mode updated.`);
|
|
4130
|
+
});
|
|
4131
|
+
}
|
|
4132
|
+
cmd.addCommand(targetingCmd);
|
|
3945
4133
|
return cmd;
|
|
3946
4134
|
}
|
|
4135
|
+
async function fetchCampaign(id) {
|
|
4136
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/campaigns/${id}` });
|
|
4137
|
+
const json = await resp.json();
|
|
4138
|
+
if (!resp.ok) {
|
|
4139
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4140
|
+
process.exit(1);
|
|
4141
|
+
}
|
|
4142
|
+
return json.data ?? json;
|
|
4143
|
+
}
|
|
4144
|
+
async function patchCampaign(id, body) {
|
|
4145
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/campaigns/${id}`, body });
|
|
4146
|
+
const json = await resp.json().catch(() => ({}));
|
|
4147
|
+
if (!resp.ok) {
|
|
4148
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
4149
|
+
process.exit(1);
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
function parseCsvList(value) {
|
|
4153
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
4154
|
+
}
|
|
4155
|
+
function normalizeAudienceMode(value) {
|
|
4156
|
+
const normalized = String(value).toLowerCase();
|
|
4157
|
+
if (normalized !== "include" && normalized !== "exclude") {
|
|
4158
|
+
console.error("Error: --mode must be include or exclude.");
|
|
4159
|
+
process.exit(1);
|
|
4160
|
+
}
|
|
4161
|
+
return normalized;
|
|
4162
|
+
}
|
|
4163
|
+
function normalizeTargeting(targeting) {
|
|
4164
|
+
return targeting && typeof targeting === "object" ? { ...targeting } : {};
|
|
4165
|
+
}
|
|
4166
|
+
function normalizeAudiences(audiences) {
|
|
4167
|
+
return {
|
|
4168
|
+
include: Array.isArray(audiences?.include) ? audiences.include : [],
|
|
4169
|
+
exclude: Array.isArray(audiences?.exclude) ? audiences.exclude : []
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
4172
|
+
function normalizeListTargeting(value) {
|
|
4173
|
+
return {
|
|
4174
|
+
mode: value?.mode === "block" ? "block" : "target",
|
|
4175
|
+
list: Array.isArray(value?.list) ? value.list : []
|
|
4176
|
+
};
|
|
4177
|
+
}
|
|
4178
|
+
function camelizeOption(value) {
|
|
4179
|
+
return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
4180
|
+
}
|
|
3947
4181
|
|
|
3948
4182
|
// src/commands/variations.ts
|
|
3949
4183
|
var COLUMNS3 = [
|
|
@@ -4041,8 +4275,218 @@ Examples:
|
|
|
4041
4275
|
return cmd;
|
|
4042
4276
|
}
|
|
4043
4277
|
|
|
4044
|
-
// src/commands/
|
|
4278
|
+
// src/commands/media-assets.ts
|
|
4279
|
+
import { basename, extname } from "node:path";
|
|
4280
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
4045
4281
|
var COLUMNS4 = [
|
|
4282
|
+
{ key: "id", header: "ID", width: 36 },
|
|
4283
|
+
{ key: "name", header: "NAME", width: 24 },
|
|
4284
|
+
{ key: "type", header: "TYPE", width: 10 },
|
|
4285
|
+
{ key: "status", header: "STATUS", width: 10 },
|
|
4286
|
+
{ key: "adFormat", header: "FORMAT", width: 12 },
|
|
4287
|
+
{ key: "mimeType", header: "MIME", width: 20 }
|
|
4288
|
+
];
|
|
4289
|
+
function inferMimeType(filePath) {
|
|
4290
|
+
switch (extname(filePath).toLowerCase()) {
|
|
4291
|
+
case ".png":
|
|
4292
|
+
return "image/png";
|
|
4293
|
+
case ".jpg":
|
|
4294
|
+
case ".jpeg":
|
|
4295
|
+
return "image/jpeg";
|
|
4296
|
+
case ".gif":
|
|
4297
|
+
return "image/gif";
|
|
4298
|
+
case ".webp":
|
|
4299
|
+
return "image/webp";
|
|
4300
|
+
case ".svg":
|
|
4301
|
+
return "image/svg+xml";
|
|
4302
|
+
case ".mp4":
|
|
4303
|
+
return "video/mp4";
|
|
4304
|
+
case ".mov":
|
|
4305
|
+
return "video/quicktime";
|
|
4306
|
+
case ".html":
|
|
4307
|
+
return "text/html";
|
|
4308
|
+
case ".txt":
|
|
4309
|
+
return "text/plain";
|
|
4310
|
+
default:
|
|
4311
|
+
return "application/octet-stream";
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
function normalizeAsset(asset) {
|
|
4315
|
+
return {
|
|
4316
|
+
id: asset.id,
|
|
4317
|
+
name: asset.name,
|
|
4318
|
+
type: asset.type,
|
|
4319
|
+
status: asset.status,
|
|
4320
|
+
adFormat: asset.adFormat ?? asset.ad_format,
|
|
4321
|
+
mimeType: asset.mimeType ?? asset.mime_type
|
|
4322
|
+
};
|
|
4323
|
+
}
|
|
4324
|
+
function createMediaAssetsCommand() {
|
|
4325
|
+
const cmd = new Command("media-assets").description(`Media library management (DSP)
|
|
4326
|
+
|
|
4327
|
+
Requires: ADVERTISER capability.`).addHelpText("after", `
|
|
4328
|
+
Examples:
|
|
4329
|
+
$ a8techads media-assets list
|
|
4330
|
+
$ a8techads media-assets get <id>
|
|
4331
|
+
$ a8techads media-assets upload --file ./banner.png --ad-format BANNER
|
|
4332
|
+
$ a8techads media-assets pause <id>`);
|
|
4333
|
+
addFormatOption(cmd.command("list").description("List media assets.").option("--status <status>", "Filter by status").option("--type <type>", "Filter by type").option("--ad-format <format>", "Filter by ad format").option("--search <text>", "Search by name").option("--limit <n>", "Max results", "20").option("--offset <n>", "Offset", "0")).action(async (opts) => {
|
|
4334
|
+
const params = new URLSearchParams;
|
|
4335
|
+
params.set("limit", opts.limit);
|
|
4336
|
+
params.set("offset", opts.offset);
|
|
4337
|
+
if (opts.status)
|
|
4338
|
+
params.set("status", opts.status);
|
|
4339
|
+
if (opts.type)
|
|
4340
|
+
params.set("type", opts.type);
|
|
4341
|
+
if (opts.adFormat)
|
|
4342
|
+
params.set("adFormat", opts.adFormat);
|
|
4343
|
+
if (opts.search)
|
|
4344
|
+
params.set("search", opts.search);
|
|
4345
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/media-assets?${params}` });
|
|
4346
|
+
const json = await resp.json();
|
|
4347
|
+
if (!resp.ok) {
|
|
4348
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4349
|
+
process.exit(1);
|
|
4350
|
+
}
|
|
4351
|
+
const rows = (json.data ?? json).map(normalizeAsset);
|
|
4352
|
+
printData(rows, COLUMNS4, opts.format);
|
|
4353
|
+
});
|
|
4354
|
+
addFormatOption(cmd.command("active").description("List active media assets.").option("--ad-format <format>", "Filter by ad format").option("--width <n>", "Required width").option("--height <n>", "Required height").option("--limit <n>", "Max results", "20").option("--offset <n>", "Offset", "0")).action(async (opts) => {
|
|
4355
|
+
const params = new URLSearchParams;
|
|
4356
|
+
params.set("limit", opts.limit);
|
|
4357
|
+
params.set("offset", opts.offset);
|
|
4358
|
+
if (opts.adFormat)
|
|
4359
|
+
params.set("adFormat", opts.adFormat);
|
|
4360
|
+
if (opts.width)
|
|
4361
|
+
params.set("width", opts.width);
|
|
4362
|
+
if (opts.height)
|
|
4363
|
+
params.set("height", opts.height);
|
|
4364
|
+
const resp = await apiRequest({
|
|
4365
|
+
path: `${dspPrefix()}/media-assets/active?${params}`
|
|
4366
|
+
});
|
|
4367
|
+
const json = await resp.json();
|
|
4368
|
+
if (!resp.ok) {
|
|
4369
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4370
|
+
process.exit(1);
|
|
4371
|
+
}
|
|
4372
|
+
const rows = (json.data ?? json).map(normalizeAsset);
|
|
4373
|
+
printData(rows, COLUMNS4, opts.format);
|
|
4374
|
+
});
|
|
4375
|
+
addFormatOption(cmd.command("get").description("Get media asset details.").argument("<id>", "Media asset ID")).action(async (id, opts) => {
|
|
4376
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/media-assets/${id}` });
|
|
4377
|
+
const json = await resp.json();
|
|
4378
|
+
if (!resp.ok) {
|
|
4379
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4380
|
+
process.exit(1);
|
|
4381
|
+
}
|
|
4382
|
+
printDetail(json.data ?? json, opts.format);
|
|
4383
|
+
});
|
|
4384
|
+
addFormatOption(cmd.command("size-limits").description("Get media asset size limits.")).action(async (opts) => {
|
|
4385
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/media-assets/size-limits` });
|
|
4386
|
+
const json = await resp.json();
|
|
4387
|
+
if (!resp.ok) {
|
|
4388
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4389
|
+
process.exit(1);
|
|
4390
|
+
}
|
|
4391
|
+
printDetail(json.data ?? json, opts.format);
|
|
4392
|
+
});
|
|
4393
|
+
cmd.command("upload").description("Upload a media asset file.").requiredOption("--file <path>", "Path to local file").option("--name <name>", "Override asset name").option("--ad-format <format>", "Ad format, e.g. BANNER, NATIVE, VIDEO").action(async (opts) => {
|
|
4394
|
+
const filePath = opts.file;
|
|
4395
|
+
const fileName = basename(filePath);
|
|
4396
|
+
const request = await buildAuthenticatedRequest({
|
|
4397
|
+
method: "POST",
|
|
4398
|
+
path: `${dspPrefix()}/media-assets/upload`,
|
|
4399
|
+
headers: {}
|
|
4400
|
+
});
|
|
4401
|
+
const headers = new Headers(request.init.headers);
|
|
4402
|
+
headers.delete("Content-Type");
|
|
4403
|
+
const body = new FormData;
|
|
4404
|
+
const fileBytes = readFileSync3(filePath);
|
|
4405
|
+
body.append("file", new Blob([fileBytes], { type: inferMimeType(filePath) }), fileName);
|
|
4406
|
+
if (opts.name)
|
|
4407
|
+
body.append("name", opts.name);
|
|
4408
|
+
if (opts.adFormat)
|
|
4409
|
+
body.append("adFormat", opts.adFormat);
|
|
4410
|
+
const resp = await fetch(request.url, {
|
|
4411
|
+
...request.init,
|
|
4412
|
+
headers,
|
|
4413
|
+
body
|
|
4414
|
+
});
|
|
4415
|
+
const json = await resp.json();
|
|
4416
|
+
if (!resp.ok) {
|
|
4417
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
4418
|
+
process.exit(1);
|
|
4419
|
+
}
|
|
4420
|
+
const asset = json.data ?? json;
|
|
4421
|
+
console.log(`Media asset uploaded: ${asset.id}`);
|
|
4422
|
+
if (asset.name)
|
|
4423
|
+
console.log(` Name: ${asset.name}`);
|
|
4424
|
+
if (asset.cdnUrl ?? asset.cdn_url) {
|
|
4425
|
+
console.log(` CDN URL: ${asset.cdnUrl ?? asset.cdn_url}`);
|
|
4426
|
+
}
|
|
4427
|
+
});
|
|
4428
|
+
cmd.command("update").description("Update media asset metadata.").argument("<id>", "Media asset ID").option("--name <name>", "New name").option("--description <text>", "New description").option("--product-category <value>", "Product category").option("--target-audience <value>", "Target audience").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
4429
|
+
let body = {};
|
|
4430
|
+
if (opts.fromJson) {
|
|
4431
|
+
body = JSON.parse(readFileSync3(opts.fromJson, "utf-8"));
|
|
4432
|
+
} else {
|
|
4433
|
+
if (opts.name)
|
|
4434
|
+
body.name = opts.name;
|
|
4435
|
+
if (opts.description)
|
|
4436
|
+
body.description = opts.description;
|
|
4437
|
+
if (opts.productCategory)
|
|
4438
|
+
body.productCategory = opts.productCategory;
|
|
4439
|
+
if (opts.targetAudience)
|
|
4440
|
+
body.targetAudience = opts.targetAudience;
|
|
4441
|
+
}
|
|
4442
|
+
const resp = await apiRequest({
|
|
4443
|
+
method: "PATCH",
|
|
4444
|
+
path: `${dspPrefix()}/media-assets/${id}`,
|
|
4445
|
+
body
|
|
4446
|
+
});
|
|
4447
|
+
const json = await resp.json();
|
|
4448
|
+
if (!resp.ok) {
|
|
4449
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4450
|
+
process.exit(1);
|
|
4451
|
+
}
|
|
4452
|
+
console.log(`Media asset ${id} updated.`);
|
|
4453
|
+
});
|
|
4454
|
+
cmd.command("delete").description("Delete a media asset.").argument("<id>", "Media asset ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
4455
|
+
if (!opts.yes) {
|
|
4456
|
+
console.error("Add --yes to confirm deletion.");
|
|
4457
|
+
process.exit(1);
|
|
4458
|
+
}
|
|
4459
|
+
const resp = await apiRequest({
|
|
4460
|
+
method: "DELETE",
|
|
4461
|
+
path: `${dspPrefix()}/media-assets/${id}`
|
|
4462
|
+
});
|
|
4463
|
+
if (!resp.ok && resp.status !== 204) {
|
|
4464
|
+
const json = await resp.json();
|
|
4465
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4466
|
+
process.exit(1);
|
|
4467
|
+
}
|
|
4468
|
+
console.log(`Media asset ${id} deleted.`);
|
|
4469
|
+
});
|
|
4470
|
+
for (const action of ["pause", "resume", "archive", "unarchive"]) {
|
|
4471
|
+
cmd.command(action).description(`${action.charAt(0).toUpperCase() + action.slice(1)} a media asset.`).argument("<id>", "Media asset ID").action(async (id) => {
|
|
4472
|
+
const resp = await apiRequest({
|
|
4473
|
+
method: "PATCH",
|
|
4474
|
+
path: `${dspPrefix()}/media-assets/${id}/${action}`,
|
|
4475
|
+
body: {}
|
|
4476
|
+
});
|
|
4477
|
+
const json = await resp.json();
|
|
4478
|
+
if (!resp.ok) {
|
|
4479
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4480
|
+
process.exit(1);
|
|
4481
|
+
}
|
|
4482
|
+
console.log(`Media asset ${id} ${action}d.`);
|
|
4483
|
+
});
|
|
4484
|
+
}
|
|
4485
|
+
return cmd;
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
// src/commands/sites.ts
|
|
4489
|
+
var COLUMNS5 = [
|
|
4046
4490
|
{ key: "id", header: "ID", width: 36 },
|
|
4047
4491
|
{ key: "name", header: "NAME", width: 25 },
|
|
4048
4492
|
{ key: "domain", header: "DOMAIN", width: 25 },
|
|
@@ -4075,7 +4519,7 @@ Examples:
|
|
|
4075
4519
|
status: s.status,
|
|
4076
4520
|
zoneCount: s.zoneCount ?? s.zone_count ?? "-"
|
|
4077
4521
|
}));
|
|
4078
|
-
printData(rows,
|
|
4522
|
+
printData(rows, COLUMNS5, opts.format);
|
|
4079
4523
|
});
|
|
4080
4524
|
addFormatOption(cmd.command("get").description("Get site details by ID.").argument("<id>", "Site ID")).action(async (id, opts) => {
|
|
4081
4525
|
const resp = await apiRequest({ path: `${sspPrefix()}/sites/${id}` });
|
|
@@ -4089,8 +4533,8 @@ Examples:
|
|
|
4089
4533
|
cmd.command("create").description("Create a new site.").option("--name <name>", "Site name (required)").option("--domain <domain>", "Site domain (required)").option("--type <type>", "Site type: WEB, APP, CTV", "WEB").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
4090
4534
|
let body;
|
|
4091
4535
|
if (opts.fromJson) {
|
|
4092
|
-
const { readFileSync:
|
|
4093
|
-
body = JSON.parse(
|
|
4536
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4537
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4094
4538
|
} else {
|
|
4095
4539
|
if (!opts.name || !opts.domain) {
|
|
4096
4540
|
console.error("Error: --name and --domain are required.");
|
|
@@ -4109,8 +4553,8 @@ Examples:
|
|
|
4109
4553
|
cmd.command("update").description("Update a site.").argument("<id>", "Site ID").option("--name <name>", "New name").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
4110
4554
|
let body;
|
|
4111
4555
|
if (opts.fromJson) {
|
|
4112
|
-
const { readFileSync:
|
|
4113
|
-
body = JSON.parse(
|
|
4556
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4557
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4114
4558
|
} else {
|
|
4115
4559
|
body = {};
|
|
4116
4560
|
if (opts.name)
|
|
@@ -4161,7 +4605,7 @@ Examples:
|
|
|
4161
4605
|
}
|
|
4162
4606
|
|
|
4163
4607
|
// src/commands/zones.ts
|
|
4164
|
-
var
|
|
4608
|
+
var COLUMNS6 = [
|
|
4165
4609
|
{ key: "id", header: "ID", width: 36 },
|
|
4166
4610
|
{ key: "name", header: "NAME", width: 25 },
|
|
4167
4611
|
{ key: "format", header: "FORMAT", width: 18 },
|
|
@@ -4193,7 +4637,7 @@ Examples:
|
|
|
4193
4637
|
format: z.adFormat ?? z.ad_format ?? "-",
|
|
4194
4638
|
status: z.status
|
|
4195
4639
|
}));
|
|
4196
|
-
printData(rows,
|
|
4640
|
+
printData(rows, COLUMNS6, opts.format);
|
|
4197
4641
|
});
|
|
4198
4642
|
addFormatOption(cmd.command("get").description("Get zone details by ID.").argument("<id>", "Zone ID")).action(async (id, opts) => {
|
|
4199
4643
|
const resp = await apiRequest({ path: `${sspPrefix()}/zones/${id}` });
|
|
@@ -4207,8 +4651,8 @@ Examples:
|
|
|
4207
4651
|
cmd.command("create").description("Create a new ad zone.").option("--site <id>", "Site ID (required)").option("--name <name>", "Zone name (required)").option("--format <format>", "Ad format (e.g., banner_300x250)").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
4208
4652
|
let body;
|
|
4209
4653
|
if (opts.fromJson) {
|
|
4210
|
-
const { readFileSync:
|
|
4211
|
-
body = JSON.parse(
|
|
4654
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4655
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4212
4656
|
} else {
|
|
4213
4657
|
if (!opts.site || !opts.name) {
|
|
4214
4658
|
console.error("Error: --site and --name are required.");
|
|
@@ -4229,8 +4673,8 @@ Examples:
|
|
|
4229
4673
|
cmd.command("update").description("Update a zone.").argument("<id>", "Zone ID").option("--name <name>", "New name").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
4230
4674
|
let body;
|
|
4231
4675
|
if (opts.fromJson) {
|
|
4232
|
-
const { readFileSync:
|
|
4233
|
-
body = JSON.parse(
|
|
4676
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4677
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4234
4678
|
} else {
|
|
4235
4679
|
body = {};
|
|
4236
4680
|
if (opts.name)
|
|
@@ -4279,11 +4723,12 @@ Examples:
|
|
|
4279
4723
|
$ a8techads reports templates`);
|
|
4280
4724
|
addFormatOption(cmd.command("query").description(`Execute an ad-hoc analytics query.
|
|
4281
4725
|
|
|
4282
|
-
Requires: authenticated profile with DSP or SSP capability.`).option("--metrics <list>", "Comma-separated metrics (e.g., spend,impressions,clicks,ctr)", "impressions,clicks,spend").option("--dimensions <list>", "Comma-separated dimensions (e.g., date,campaign,geo)", "date").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("--limit <n>", "Max rows", "100").addHelpText("after", `
|
|
4726
|
+
Requires: authenticated profile with DSP or SSP capability.`).option("--metrics <list>", "Comma-separated metrics (e.g., spend,impressions,clicks,ctr)", "impressions,clicks,spend").option("--dimensions <list>", "Comma-separated dimensions (e.g., date,campaign,geo,goal,goal_id,goal_order)", "date").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("--limit <n>", "Max rows", "100").addHelpText("after", `
|
|
4283
4727
|
Examples:
|
|
4284
4728
|
$ a8techads reports query --metrics spend,impressions --dimensions date --from 2026-03-01 --to 2026-03-20
|
|
4285
4729
|
$ a8techads reports query --metrics revenue --dimensions publisher --format json
|
|
4286
|
-
$ a8techads reports query --dimensions geo --limit 10
|
|
4730
|
+
$ a8techads reports query --dimensions geo --limit 10
|
|
4731
|
+
$ a8techads reports query --dimensions goal,date --metrics conversions,spend,cpa --from 2026-03-01`)).action(async (opts) => {
|
|
4287
4732
|
const body = {
|
|
4288
4733
|
metrics: opts.metrics.split(","),
|
|
4289
4734
|
dimensions: opts.dimensions.split(","),
|
|
@@ -4299,19 +4744,20 @@ Examples:
|
|
|
4299
4744
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4300
4745
|
process.exit(1);
|
|
4301
4746
|
}
|
|
4302
|
-
const
|
|
4747
|
+
const data = json.data ?? json;
|
|
4748
|
+
const rows = data.rows ?? data.results ?? json.rows ?? json.results ?? [];
|
|
4303
4749
|
if (rows.length === 0) {
|
|
4304
4750
|
console.log("No results.");
|
|
4305
4751
|
return;
|
|
4306
4752
|
}
|
|
4307
4753
|
if (opts.format === "json") {
|
|
4308
|
-
console.log(JSON.stringify(
|
|
4754
|
+
console.log(JSON.stringify(data, null, 2));
|
|
4309
4755
|
return;
|
|
4310
4756
|
}
|
|
4311
4757
|
const keys = Object.keys(rows[0]);
|
|
4312
4758
|
const columns = keys.map((k) => ({ key: k, header: k.toUpperCase(), width: Math.max(k.length, 12) }));
|
|
4313
4759
|
printData(rows, columns, opts.format);
|
|
4314
|
-
const summary = json.
|
|
4760
|
+
const summary = data.summary ?? data.totals ?? json.summary ?? json.totals;
|
|
4315
4761
|
if (summary && opts.format === "table") {
|
|
4316
4762
|
console.log(`
|
|
4317
4763
|
Summary:`);
|
|
@@ -4327,7 +4773,9 @@ Summary:`);
|
|
|
4327
4773
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4328
4774
|
process.exit(1);
|
|
4329
4775
|
}
|
|
4330
|
-
const
|
|
4776
|
+
const data = json.data ?? json;
|
|
4777
|
+
const reports = data.reports ?? json.reports ?? data;
|
|
4778
|
+
const rows = (Array.isArray(reports) ? reports : []).map((r) => ({ id: r.id, name: r.name, createdAt: r.createdAt ?? r.created_at }));
|
|
4331
4779
|
printData(rows, [
|
|
4332
4780
|
{ key: "id", header: "ID", width: 36 },
|
|
4333
4781
|
{ key: "name", header: "NAME", width: 30 },
|
|
@@ -4350,7 +4798,9 @@ Summary:`);
|
|
|
4350
4798
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4351
4799
|
process.exit(1);
|
|
4352
4800
|
}
|
|
4353
|
-
const
|
|
4801
|
+
const data = json.data ?? json;
|
|
4802
|
+
const templates = data.templates ?? json.templates ?? data;
|
|
4803
|
+
const rows = (Array.isArray(templates) ? templates : []).map((t) => ({ id: t.id, name: t.name, description: t.description }));
|
|
4354
4804
|
printData(rows, [
|
|
4355
4805
|
{ key: "id", header: "ID", width: 20 },
|
|
4356
4806
|
{ key: "name", header: "NAME", width: 25 },
|
|
@@ -4360,8 +4810,8 @@ Summary:`);
|
|
|
4360
4810
|
cmd.command("create").description("Create a saved report.").option("--name <name>", "Report name (required)").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
4361
4811
|
let body;
|
|
4362
4812
|
if (opts.fromJson) {
|
|
4363
|
-
const { readFileSync:
|
|
4364
|
-
body = JSON.parse(
|
|
4813
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4814
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4365
4815
|
} else {
|
|
4366
4816
|
if (!opts.name) {
|
|
4367
4817
|
console.error("Error: --name is required.");
|
|
@@ -4463,8 +4913,8 @@ Examples:
|
|
|
4463
4913
|
cmd.command("settings-update").description("Update billing settings.").option("--auto-recharge <bool>", "Enable/disable auto-recharge (true/false)").option("--from-json <file>", "Update from JSON file").action(async (opts) => {
|
|
4464
4914
|
let body;
|
|
4465
4915
|
if (opts.fromJson) {
|
|
4466
|
-
const { readFileSync:
|
|
4467
|
-
body = JSON.parse(
|
|
4916
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4917
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
4468
4918
|
} else {
|
|
4469
4919
|
body = {};
|
|
4470
4920
|
if (opts.autoRecharge !== undefined)
|
|
@@ -4478,6 +4928,60 @@ Examples:
|
|
|
4478
4928
|
}
|
|
4479
4929
|
console.log("Billing settings updated.");
|
|
4480
4930
|
});
|
|
4931
|
+
addFormatOption(cmd.command("payment-methods").description("List saved payment methods.")).action(async (opts) => {
|
|
4932
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/payments/stripe/payment-methods` });
|
|
4933
|
+
const json = await resp.json();
|
|
4934
|
+
if (!resp.ok) {
|
|
4935
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4936
|
+
process.exit(1);
|
|
4937
|
+
}
|
|
4938
|
+
const columns = [
|
|
4939
|
+
{ key: "id", header: "ID", width: 30 },
|
|
4940
|
+
{ key: "type", header: "TYPE", width: 10 },
|
|
4941
|
+
{ key: "last4", header: "LAST4", width: 6 },
|
|
4942
|
+
{ key: "brand", header: "BRAND", width: 12 },
|
|
4943
|
+
{ key: "expires", header: "EXPIRES", width: 10 }
|
|
4944
|
+
];
|
|
4945
|
+
const rows = (json.data ?? json).map((pm) => ({
|
|
4946
|
+
id: pm.id,
|
|
4947
|
+
type: pm.type,
|
|
4948
|
+
last4: pm.last4 ?? pm.card?.last4,
|
|
4949
|
+
brand: pm.brand ?? pm.card?.brand,
|
|
4950
|
+
expires: pm.expires ?? (pm.card ? `${pm.card.exp_month}/${pm.card.exp_year}` : "-")
|
|
4951
|
+
}));
|
|
4952
|
+
printData(rows, columns, opts.format);
|
|
4953
|
+
});
|
|
4954
|
+
cmd.command("usdt-deposit").description("Initiate a USDT deposit.").option("--amount <dollars>", "Deposit amount in dollars (required)").option("--network <network>", "Blockchain network", "TRC20").option("--yes", "Skip confirmation prompt").action(async (opts) => {
|
|
4955
|
+
if (!opts.amount) {
|
|
4956
|
+
console.error("Error: --amount is required.");
|
|
4957
|
+
process.exit(1);
|
|
4958
|
+
}
|
|
4959
|
+
const amount = Number(opts.amount);
|
|
4960
|
+
if (isNaN(amount) || amount <= 0) {
|
|
4961
|
+
console.error("Error: Amount must be a positive number.");
|
|
4962
|
+
process.exit(1);
|
|
4963
|
+
}
|
|
4964
|
+
if (!opts.yes) {
|
|
4965
|
+
const rl = await import("readline");
|
|
4966
|
+
const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
|
|
4967
|
+
const answer = await new Promise((resolve) => iface.question(`Initiate USDT deposit of $${amount.toFixed(2)}? (y/N) `, resolve));
|
|
4968
|
+
iface.close();
|
|
4969
|
+
if (answer.toLowerCase() !== "y") {
|
|
4970
|
+
console.log("Cancelled.");
|
|
4971
|
+
process.exit(0);
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/payments/usdt/checkout`, body: { amount, network: opts.network } });
|
|
4975
|
+
const json = await resp.json();
|
|
4976
|
+
if (!resp.ok) {
|
|
4977
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4978
|
+
process.exit(1);
|
|
4979
|
+
}
|
|
4980
|
+
console.log(`USDT deposit of $${amount.toFixed(2)} initiated.`);
|
|
4981
|
+
if (json.data?.paymentUrl || json.paymentUrl) {
|
|
4982
|
+
console.log(`Payment URL: ${json.data?.paymentUrl ?? json.paymentUrl}`);
|
|
4983
|
+
}
|
|
4984
|
+
});
|
|
4481
4985
|
return cmd;
|
|
4482
4986
|
}
|
|
4483
4987
|
|
|
@@ -4494,7 +4998,7 @@ function usersPrefix() {
|
|
|
4494
4998
|
}
|
|
4495
4999
|
return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
|
|
4496
5000
|
}
|
|
4497
|
-
var
|
|
5001
|
+
var COLUMNS7 = [
|
|
4498
5002
|
{ key: "id", header: "ID", width: 36 },
|
|
4499
5003
|
{ key: "email", header: "EMAIL", width: 30 },
|
|
4500
5004
|
{ key: "name", header: "NAME", width: 20 },
|
|
@@ -4525,7 +5029,7 @@ Examples:
|
|
|
4525
5029
|
role: u.role,
|
|
4526
5030
|
status: u.status
|
|
4527
5031
|
}));
|
|
4528
|
-
printData(rows,
|
|
5032
|
+
printData(rows, COLUMNS7, opts.format);
|
|
4529
5033
|
});
|
|
4530
5034
|
addFormatOption(cmd.command("get").description("Get team member details.").argument("<id>", "User ID")).action(async (id, opts) => {
|
|
4531
5035
|
const resp = await apiRequest({ path: `${usersPrefix()}/users/${id}` });
|
|
@@ -4605,6 +5109,18 @@ Examples:
|
|
|
4605
5109
|
}
|
|
4606
5110
|
|
|
4607
5111
|
// src/commands/admin.ts
|
|
5112
|
+
async function confirmAction(message, yes) {
|
|
5113
|
+
if (yes)
|
|
5114
|
+
return;
|
|
5115
|
+
const rl = await import("readline");
|
|
5116
|
+
const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
|
|
5117
|
+
const answer = await new Promise((resolve) => iface.question(`${message} (y/N) `, resolve));
|
|
5118
|
+
iface.close();
|
|
5119
|
+
if (answer.toLowerCase() !== "y") {
|
|
5120
|
+
console.log("Cancelled.");
|
|
5121
|
+
process.exit(0);
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
4608
5124
|
function createAdminCommand() {
|
|
4609
5125
|
const cmd = new Command("admin").description(`Platform administration (Console)
|
|
4610
5126
|
|
|
@@ -4645,34 +5161,118 @@ Examples:
|
|
|
4645
5161
|
}
|
|
4646
5162
|
printDetail(json.data ?? json, opts.format);
|
|
4647
5163
|
});
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
5164
|
+
tenants.command("create").description("Create a new tenant with admin user.").option("--name <name>", "Company name (required)").option("--email <email>", "Admin email (required)").option("--admin-name <name>", "Admin user name").option("--capabilities <caps>", "Comma-separated capabilities: ADVERTISER,PUBLISHER", "ADVERTISER").addHelpText("after", `
|
|
5165
|
+
Examples:
|
|
5166
|
+
$ a8techads admin tenants create --name "Acme Corp" --email admin@acme.com
|
|
5167
|
+
$ a8techads admin tenants create --name "MediaCo" --email admin@media.co --capabilities ADVERTISER,PUBLISHER`).action(async (opts) => {
|
|
5168
|
+
if (!opts.name) {
|
|
5169
|
+
console.error("Error: --name is required.");
|
|
4652
5170
|
process.exit(1);
|
|
4653
5171
|
}
|
|
4654
|
-
|
|
5172
|
+
if (!opts.email) {
|
|
5173
|
+
console.error("Error: --email is required.");
|
|
5174
|
+
process.exit(1);
|
|
5175
|
+
}
|
|
5176
|
+
const capabilities = opts.capabilities.split(",").map((c) => c.trim().toUpperCase());
|
|
5177
|
+
const body = {
|
|
5178
|
+
tenant: {
|
|
5179
|
+
companyName: opts.name,
|
|
5180
|
+
capabilities
|
|
5181
|
+
},
|
|
5182
|
+
admin: {
|
|
5183
|
+
email: opts.email,
|
|
5184
|
+
name: opts.adminName ?? undefined
|
|
5185
|
+
}
|
|
5186
|
+
};
|
|
5187
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/tenants`, body });
|
|
4655
5188
|
const json = await resp.json();
|
|
4656
5189
|
if (!resp.ok) {
|
|
4657
|
-
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5190
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
4658
5191
|
process.exit(1);
|
|
4659
5192
|
}
|
|
4660
|
-
const
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
], opts.format);
|
|
5193
|
+
const data = json.data ?? json;
|
|
5194
|
+
const tenant = data.tenant ?? data;
|
|
5195
|
+
console.log(`Tenant created: ${tenant.id}`);
|
|
5196
|
+
if (tenant.companyName)
|
|
5197
|
+
console.log(` Name: ${tenant.companyName}`);
|
|
5198
|
+
if (tenant.status)
|
|
5199
|
+
console.log(` Status: ${tenant.status}`);
|
|
5200
|
+
if (data.admin) {
|
|
5201
|
+
console.log(` Admin: ${data.admin.email} (userId: ${data.admin.userId})`);
|
|
5202
|
+
}
|
|
4671
5203
|
});
|
|
4672
|
-
|
|
4673
|
-
const
|
|
4674
|
-
if (opts.
|
|
4675
|
-
|
|
5204
|
+
tenants.command("update").description("Update a tenant.").argument("<id>", "Tenant ID").option("--name <name>", "New company name").option("--status <status>", "New status: ACTIVE, SUSPENDED, TESTING").action(async (id, opts) => {
|
|
5205
|
+
const body = {};
|
|
5206
|
+
if (opts.name)
|
|
5207
|
+
body.companyName = opts.name;
|
|
5208
|
+
if (opts.status)
|
|
5209
|
+
body.status = opts.status.toUpperCase();
|
|
5210
|
+
if (Object.keys(body).length === 0) {
|
|
5211
|
+
console.error("Error: at least one of --name or --status is required.");
|
|
5212
|
+
process.exit(1);
|
|
5213
|
+
}
|
|
5214
|
+
const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/tenants/${id}`, body });
|
|
5215
|
+
if (!resp.ok) {
|
|
5216
|
+
const j = await resp.json();
|
|
5217
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5218
|
+
process.exit(1);
|
|
5219
|
+
}
|
|
5220
|
+
console.log(`Tenant ${id} updated.`);
|
|
5221
|
+
});
|
|
5222
|
+
tenants.command("suspend").description("Suspend a tenant (set status to SUSPENDED).").argument("<id>", "Tenant ID").action(async (id) => {
|
|
5223
|
+
const resp = await apiRequest({
|
|
5224
|
+
method: "PATCH",
|
|
5225
|
+
path: `${consolePrefix()}/tenants/${id}`,
|
|
5226
|
+
body: { status: "SUSPENDED" }
|
|
5227
|
+
});
|
|
5228
|
+
if (!resp.ok) {
|
|
5229
|
+
const j = await resp.json();
|
|
5230
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5231
|
+
process.exit(1);
|
|
5232
|
+
}
|
|
5233
|
+
console.log(`Tenant ${id} suspended.`);
|
|
5234
|
+
});
|
|
5235
|
+
tenants.command("activate").description("Activate a tenant (set status to ACTIVE).").argument("<id>", "Tenant ID").action(async (id) => {
|
|
5236
|
+
const resp = await apiRequest({
|
|
5237
|
+
method: "PATCH",
|
|
5238
|
+
path: `${consolePrefix()}/tenants/${id}`,
|
|
5239
|
+
body: { status: "ACTIVE" }
|
|
5240
|
+
});
|
|
5241
|
+
if (!resp.ok) {
|
|
5242
|
+
const j = await resp.json();
|
|
5243
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5244
|
+
process.exit(1);
|
|
5245
|
+
}
|
|
5246
|
+
console.log(`Tenant ${id} activated.`);
|
|
5247
|
+
});
|
|
5248
|
+
const members = tenants.command("members").description("Tenant member management");
|
|
5249
|
+
addFormatOption(members.command("list").description("List tenant members.").option("--tenant <id>", "Tenant ID (required)")).action(async (opts) => {
|
|
5250
|
+
if (!opts.tenant) {
|
|
5251
|
+
console.error("Error: --tenant is required.");
|
|
5252
|
+
process.exit(1);
|
|
5253
|
+
}
|
|
5254
|
+
const resp = await apiRequest({ path: `/api/v1/console/tenants/${opts.tenant}/members` });
|
|
5255
|
+
const json = await resp.json();
|
|
5256
|
+
if (!resp.ok) {
|
|
5257
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5258
|
+
process.exit(1);
|
|
5259
|
+
}
|
|
5260
|
+
const rows = (json.data ?? json).map((m) => ({
|
|
5261
|
+
id: m.userId ?? m.user_id ?? m.id,
|
|
5262
|
+
email: m.email,
|
|
5263
|
+
role: m.role,
|
|
5264
|
+
status: m.status
|
|
5265
|
+
}));
|
|
5266
|
+
printData(rows, [
|
|
5267
|
+
{ key: "id", header: "USER ID", width: 36 },
|
|
5268
|
+
{ key: "email", header: "EMAIL", width: 30 },
|
|
5269
|
+
{ key: "role", header: "ROLE", width: 20 }
|
|
5270
|
+
], opts.format);
|
|
5271
|
+
});
|
|
5272
|
+
addFormatOption(cmd.command("audit-logs").description("View audit logs.").option("--tenant <id>", "Filter by tenant").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
5273
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5274
|
+
if (opts.tenant)
|
|
5275
|
+
params.set("tenant_id", opts.tenant);
|
|
4676
5276
|
const resp = await apiRequest({ path: `/api/v1/console/audit-logs?${params}` });
|
|
4677
5277
|
const json = await resp.json();
|
|
4678
5278
|
if (!resp.ok) {
|
|
@@ -4712,42 +5312,28 @@ Examples:
|
|
|
4712
5312
|
}
|
|
4713
5313
|
console.log("Cache cleared.");
|
|
4714
5314
|
});
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
// src/commands/settings.ts
|
|
4719
|
-
function settingsPrefix() {
|
|
4720
|
-
const ctx = loadContext();
|
|
4721
|
-
const context = getCurrentContext(ctx);
|
|
4722
|
-
const app = context?.app ?? "dsp";
|
|
4723
|
-
if (app === "console") {
|
|
4724
|
-
console.error("Error: Settings commands are not available in Console mode.");
|
|
4725
|
-
console.error('Switch to DSP or SSP context: "a8techads context dsp" or "a8techads context ssp"');
|
|
4726
|
-
process.exit(1);
|
|
4727
|
-
}
|
|
4728
|
-
return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
|
|
4729
|
-
}
|
|
4730
|
-
function createSettingsCommand() {
|
|
4731
|
-
const cmd = new Command("settings").description(`Tenant settings (DSP / SSP only)
|
|
4732
|
-
|
|
4733
|
-
Requires: ADVERTISER or PUBLISHER capability.
|
|
4734
|
-
Not available in Console mode.`).addHelpText("after", `
|
|
4735
|
-
Examples:
|
|
4736
|
-
$ a8techads settings show
|
|
4737
|
-
$ a8techads settings update --from-json settings.json
|
|
4738
|
-
$ a8techads settings profile
|
|
4739
|
-
$ a8techads settings profile-update --company-name "New Name"`);
|
|
4740
|
-
addFormatOption(cmd.command("show").description("Show current tenant settings.")).action(async (opts) => {
|
|
4741
|
-
const resp = await apiRequest({ path: `${settingsPrefix()}/settings` });
|
|
5315
|
+
const tracking = cmd.command("tracking-identities").description("User identity graph stats");
|
|
5316
|
+
tracking.command("stats").description("Show tracking identity statistics.").action(async () => {
|
|
5317
|
+
const resp = await apiRequest({ path: "/api/v1/console/tracking-identities/stats" });
|
|
4742
5318
|
const json = await resp.json();
|
|
4743
5319
|
if (!resp.ok) {
|
|
4744
5320
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4745
5321
|
process.exit(1);
|
|
4746
5322
|
}
|
|
4747
|
-
|
|
5323
|
+
const data = json.data ?? json;
|
|
5324
|
+
console.log(`Tracking Identity Stats:`);
|
|
5325
|
+
console.log(` Total Identities: ${(data.total_identities ?? 0).toLocaleString()}`);
|
|
5326
|
+
console.log(` Active (30d): ${(data.active_30d ?? 0).toLocaleString()}`);
|
|
5327
|
+
console.log(` Active (7d): ${(data.active_7d ?? 0).toLocaleString()}`);
|
|
5328
|
+
console.log(` Active (1d): ${(data.active_1d ?? 0).toLocaleString()}`);
|
|
5329
|
+
if (data.earliest_seen)
|
|
5330
|
+
console.log(` Earliest Seen: ${data.earliest_seen}`);
|
|
5331
|
+
if (data.latest_seen)
|
|
5332
|
+
console.log(` Latest Seen: ${data.latest_seen}`);
|
|
4748
5333
|
});
|
|
4749
|
-
|
|
4750
|
-
|
|
5334
|
+
const finance = cmd.command("finance").description("Financial overview and audit");
|
|
5335
|
+
addFormatOption(finance.command("summary").description("Financial summary.")).action(async (opts) => {
|
|
5336
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/summary` });
|
|
4751
5337
|
const json = await resp.json();
|
|
4752
5338
|
if (!resp.ok) {
|
|
4753
5339
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
@@ -4755,85 +5341,232 @@ Examples:
|
|
|
4755
5341
|
}
|
|
4756
5342
|
printDetail(json.data ?? json, opts.format);
|
|
4757
5343
|
});
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
5344
|
+
finance.command("finalize-daily").description("Run daily billing finalization for a date.").option("--date <yyyy-mm-dd>", "Date to finalize (default: today UTC)").option("--yes", "Skip confirmation", false).action(async (opts) => {
|
|
5345
|
+
const targetDate = opts.date || new Date().toISOString().slice(0, 10);
|
|
5346
|
+
await confirmAction(`Run daily billing finalization for ${targetDate}?`, opts.yes);
|
|
5347
|
+
const body = {};
|
|
5348
|
+
if (opts.date)
|
|
5349
|
+
body.date = opts.date;
|
|
5350
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/finalize-daily`, body });
|
|
5351
|
+
const json = await resp.json();
|
|
5352
|
+
if (!resp.ok) {
|
|
5353
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4763
5354
|
process.exit(1);
|
|
4764
5355
|
}
|
|
4765
|
-
const
|
|
4766
|
-
|
|
4767
|
-
|
|
5356
|
+
const data = json.data ?? json;
|
|
5357
|
+
console.log(`Daily billing finalized for ${data.date ?? targetDate}.`);
|
|
5358
|
+
});
|
|
5359
|
+
addFormatOption(finance.command("events").description("List billing events.").option("--limit <n>", "Max results", "20").option("--aggregate-type <type>", "Filter by aggregate type").option("--event-type <type>", "Filter by event type")).action(async (opts) => {
|
|
5360
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5361
|
+
if (opts.aggregateType)
|
|
5362
|
+
params.set("aggregate_type", opts.aggregateType);
|
|
5363
|
+
if (opts.eventType)
|
|
5364
|
+
params.set("event_type", opts.eventType);
|
|
5365
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/events?${params}` });
|
|
5366
|
+
const json = await resp.json();
|
|
5367
|
+
if (!resp.ok) {
|
|
5368
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5369
|
+
process.exit(1);
|
|
5370
|
+
}
|
|
5371
|
+
const payload = json.data ?? json;
|
|
5372
|
+
const rows = (payload.events ?? payload).map((e) => ({
|
|
5373
|
+
id: e.id,
|
|
5374
|
+
eventType: e.eventType ?? e.event_type,
|
|
5375
|
+
aggregateType: e.aggregateType ?? e.aggregate_type,
|
|
5376
|
+
createdAt: e.createdAt ?? e.created_at
|
|
5377
|
+
}));
|
|
5378
|
+
printData(rows, [
|
|
5379
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5380
|
+
{ key: "eventType", header: "EVENT_TYPE", width: 25 },
|
|
5381
|
+
{ key: "aggregateType", header: "AGGREGATE_TYPE", width: 20 },
|
|
5382
|
+
{ key: "createdAt", header: "CREATED_AT", width: 22 }
|
|
5383
|
+
], opts.format);
|
|
5384
|
+
});
|
|
5385
|
+
addFormatOption(finance.command("operations").description("List billing operations.").option("--limit <n>", "Max results", "20").option("--type <type>", "Filter by operation type").option("--status <status>", "Filter by status")).action(async (opts) => {
|
|
5386
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5387
|
+
if (opts.type)
|
|
5388
|
+
params.set("type", opts.type);
|
|
5389
|
+
if (opts.status)
|
|
5390
|
+
params.set("status", opts.status);
|
|
5391
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/operations?${params}` });
|
|
5392
|
+
const json = await resp.json();
|
|
5393
|
+
if (!resp.ok) {
|
|
5394
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5395
|
+
process.exit(1);
|
|
5396
|
+
}
|
|
5397
|
+
const payload = json.data ?? json;
|
|
5398
|
+
const rows = (payload.operations ?? payload).map((o) => ({
|
|
5399
|
+
id: o.id,
|
|
5400
|
+
type: o.type,
|
|
5401
|
+
target: o.target ?? o.targetId ?? o.target_id,
|
|
5402
|
+
amount: o.amount,
|
|
5403
|
+
status: o.status,
|
|
5404
|
+
createdAt: o.createdAt ?? o.created_at
|
|
5405
|
+
}));
|
|
5406
|
+
printData(rows, [
|
|
5407
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5408
|
+
{ key: "type", header: "TYPE", width: 18 },
|
|
5409
|
+
{ key: "target", header: "TARGET", width: 20 },
|
|
5410
|
+
{ key: "amount", header: "AMOUNT", width: 12 },
|
|
5411
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5412
|
+
{ key: "createdAt", header: "CREATED_AT", width: 22 }
|
|
5413
|
+
], opts.format);
|
|
5414
|
+
});
|
|
5415
|
+
const invoices = cmd.command("invoices").description("Invoice management");
|
|
5416
|
+
addFormatOption(invoices.command("list").description("List invoices.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status").option("--tenant-type <type>", "Filter by tenant type")).action(async (opts) => {
|
|
5417
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5418
|
+
if (opts.status)
|
|
5419
|
+
params.set("status", opts.status);
|
|
5420
|
+
if (opts.tenantType)
|
|
5421
|
+
params.set("tenantType", opts.tenantType);
|
|
5422
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices?${params}` });
|
|
5423
|
+
const json = await resp.json();
|
|
5424
|
+
if (!resp.ok) {
|
|
5425
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5426
|
+
process.exit(1);
|
|
5427
|
+
}
|
|
5428
|
+
const rows = (json.data ?? json).map((i) => ({
|
|
5429
|
+
invoiceNumber: i.invoiceNumber ?? i.invoice_number ?? i.id,
|
|
5430
|
+
tenant: i.tenantName ?? i.tenant_name ?? i.tenantId ?? i.tenant_id,
|
|
5431
|
+
amount: i.amount,
|
|
5432
|
+
status: i.status,
|
|
5433
|
+
dueDate: i.dueDate ?? i.due_date
|
|
5434
|
+
}));
|
|
5435
|
+
printData(rows, [
|
|
5436
|
+
{ key: "invoiceNumber", header: "INVOICE#", width: 20 },
|
|
5437
|
+
{ key: "tenant", header: "TENANT", width: 25 },
|
|
5438
|
+
{ key: "amount", header: "AMOUNT", width: 12 },
|
|
5439
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5440
|
+
{ key: "dueDate", header: "DUE_DATE", width: 16 }
|
|
5441
|
+
], opts.format);
|
|
5442
|
+
});
|
|
5443
|
+
addFormatOption(invoices.command("preview").description("Preview unbilled invoices for current period.")).action(async (opts) => {
|
|
5444
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/preview?tenantType=advertiser` });
|
|
5445
|
+
const json = await resp.json();
|
|
5446
|
+
if (!resp.ok) {
|
|
5447
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5448
|
+
process.exit(1);
|
|
5449
|
+
}
|
|
5450
|
+
const data = json.data ?? json;
|
|
5451
|
+
if (data.summary) {
|
|
5452
|
+
console.log(`
|
|
5453
|
+
Period: ${data.summary.period} | Accounts: ${data.summary.accountCount} | Estimated Total Due: $${data.summary.totalAmount?.toFixed(2)} | As of: ${data.summary.asOfDate}
|
|
5454
|
+
`);
|
|
5455
|
+
}
|
|
5456
|
+
const rows = (data.previews ?? []).map((p) => ({
|
|
5457
|
+
tenant: p.tenantName,
|
|
5458
|
+
adSpend: p.adSpend,
|
|
5459
|
+
platformFee: p.platformFee,
|
|
5460
|
+
totalDue: p.totalDue,
|
|
5461
|
+
days: p.daysWithData,
|
|
5462
|
+
impressions: p.impressions
|
|
5463
|
+
}));
|
|
5464
|
+
printData(rows, [
|
|
5465
|
+
{ key: "tenant", header: "TENANT", width: 25 },
|
|
5466
|
+
{ key: "adSpend", header: "AD_SPEND", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
|
|
5467
|
+
{ key: "platformFee", header: "PLATFORM_FEE", width: 14, format: (v) => `$${Number(v).toFixed(2)}` },
|
|
5468
|
+
{ key: "totalDue", header: "TOTAL_DUE", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
|
|
5469
|
+
{ key: "days", header: "DAYS", width: 6 },
|
|
5470
|
+
{ key: "impressions", header: "IMPRESSIONS", width: 12 }
|
|
5471
|
+
], opts.format);
|
|
5472
|
+
});
|
|
5473
|
+
addFormatOption(invoices.command("get").description("Get invoice details.").argument("<id>", "Invoice ID")).action(async (id, opts) => {
|
|
5474
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/${id}` });
|
|
5475
|
+
const json = await resp.json();
|
|
5476
|
+
if (!resp.ok) {
|
|
5477
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5478
|
+
process.exit(1);
|
|
5479
|
+
}
|
|
5480
|
+
printDetail(json.data ?? json, opts.format);
|
|
5481
|
+
});
|
|
5482
|
+
invoices.command("issue").description("Issue an invoice.").argument("<id>", "Invoice ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5483
|
+
await confirmAction(`Issue invoice ${id}?`, opts.yes);
|
|
5484
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/invoices/${id}/issue` });
|
|
4768
5485
|
if (!resp.ok) {
|
|
4769
5486
|
const j = await resp.json();
|
|
4770
5487
|
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
4771
5488
|
process.exit(1);
|
|
4772
5489
|
}
|
|
4773
|
-
console.log(
|
|
5490
|
+
console.log(`Invoice ${id} issued.`);
|
|
4774
5491
|
});
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
if (opts.companyName)
|
|
4783
|
-
body.companyName = opts.companyName;
|
|
5492
|
+
invoices.command("void").description("Void an invoice.").argument("<id>", "Invoice ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5493
|
+
await confirmAction(`Void invoice ${id}?`, opts.yes);
|
|
5494
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/invoices/${id}/void` });
|
|
5495
|
+
if (!resp.ok) {
|
|
5496
|
+
const j = await resp.json();
|
|
5497
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5498
|
+
process.exit(1);
|
|
4784
5499
|
}
|
|
4785
|
-
|
|
5500
|
+
console.log(`Invoice ${id} voided.`);
|
|
5501
|
+
});
|
|
5502
|
+
invoices.command("mark-paid").description("Mark an invoice as paid.").argument("<id>", "Invoice ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5503
|
+
await confirmAction(`Mark invoice ${id} as paid?`, opts.yes);
|
|
5504
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/invoices/${id}/mark-paid` });
|
|
4786
5505
|
if (!resp.ok) {
|
|
4787
5506
|
const j = await resp.json();
|
|
4788
5507
|
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
4789
5508
|
process.exit(1);
|
|
4790
5509
|
}
|
|
4791
|
-
console.log(
|
|
5510
|
+
console.log(`Invoice ${id} marked as paid.`);
|
|
4792
5511
|
});
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
Requires: ADVERTISER capability.`).addHelpText("after", `
|
|
4801
|
-
Examples:
|
|
4802
|
-
$ a8techads invoices list
|
|
4803
|
-
$ a8techads invoices get <id>
|
|
4804
|
-
$ a8techads invoices spending`);
|
|
4805
|
-
addFormatOption(cmd.command("list").description("List invoices.")).action(async (opts) => {
|
|
4806
|
-
const resp = await apiRequest({ path: `${dspPrefix()}/invoices` });
|
|
5512
|
+
const statements = cmd.command("statements").description("Publisher statement management");
|
|
5513
|
+
addFormatOption(statements.command("list").description("List publisher statements.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
|
|
5514
|
+
const params = new URLSearchParams({ limit: opts.limit, tenantType: "publisher" });
|
|
5515
|
+
if (opts.status)
|
|
5516
|
+
params.set("status", opts.status);
|
|
5517
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices?${params}` });
|
|
4807
5518
|
const json = await resp.json();
|
|
4808
5519
|
if (!resp.ok) {
|
|
4809
5520
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4810
5521
|
process.exit(1);
|
|
4811
5522
|
}
|
|
4812
|
-
const columns = [
|
|
4813
|
-
{ key: "id", header: "ID", width: 36 },
|
|
4814
|
-
{ key: "period", header: "PERIOD", width: 20 },
|
|
4815
|
-
{ key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
|
|
4816
|
-
{ key: "status", header: "STATUS", width: 12 }
|
|
4817
|
-
];
|
|
4818
5523
|
const rows = (json.data ?? json).map((i) => ({
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
amount: i.amount
|
|
4822
|
-
status: i.status
|
|
5524
|
+
invoiceNumber: i.invoiceNumber ?? i.invoice_number ?? i.id,
|
|
5525
|
+
tenant: i.tenantName ?? i.tenant_name ?? i.tenantId ?? i.tenant_id,
|
|
5526
|
+
amount: i.amount,
|
|
5527
|
+
status: i.status,
|
|
5528
|
+
dueDate: i.dueDate ?? i.due_date
|
|
4823
5529
|
}));
|
|
4824
|
-
printData(rows,
|
|
5530
|
+
printData(rows, [
|
|
5531
|
+
{ key: "invoiceNumber", header: "INVOICE#", width: 20 },
|
|
5532
|
+
{ key: "tenant", header: "TENANT", width: 25 },
|
|
5533
|
+
{ key: "amount", header: "AMOUNT", width: 12 },
|
|
5534
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5535
|
+
{ key: "dueDate", header: "DUE_DATE", width: 16 }
|
|
5536
|
+
], opts.format);
|
|
4825
5537
|
});
|
|
4826
|
-
addFormatOption(
|
|
4827
|
-
const resp = await apiRequest({ path: `${
|
|
5538
|
+
addFormatOption(statements.command("preview").description("Preview unbilled statements for current period.")).action(async (opts) => {
|
|
5539
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/preview?tenantType=publisher` });
|
|
4828
5540
|
const json = await resp.json();
|
|
4829
5541
|
if (!resp.ok) {
|
|
4830
5542
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
4831
5543
|
process.exit(1);
|
|
4832
5544
|
}
|
|
4833
|
-
|
|
5545
|
+
const data = json.data ?? json;
|
|
5546
|
+
if (data.summary) {
|
|
5547
|
+
console.log(`
|
|
5548
|
+
Period: ${data.summary.period} | Accounts: ${data.summary.accountCount} | Estimated Net Payable: $${data.summary.totalAmount?.toFixed(2)} | As of: ${data.summary.asOfDate}
|
|
5549
|
+
`);
|
|
5550
|
+
}
|
|
5551
|
+
const rows = (data.previews ?? []).map((p) => ({
|
|
5552
|
+
tenant: p.tenantName,
|
|
5553
|
+
grossRevenue: p.grossRevenue,
|
|
5554
|
+
commission: p.platformCommission,
|
|
5555
|
+
netPayable: p.netPayable,
|
|
5556
|
+
days: p.daysWithData,
|
|
5557
|
+
impressions: p.impressions
|
|
5558
|
+
}));
|
|
5559
|
+
printData(rows, [
|
|
5560
|
+
{ key: "tenant", header: "TENANT", width: 25 },
|
|
5561
|
+
{ key: "grossRevenue", header: "GROSS_REV", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
|
|
5562
|
+
{ key: "commission", header: "COMMISSION", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
|
|
5563
|
+
{ key: "netPayable", header: "NET_PAY", width: 12, format: (v) => `$${Number(v).toFixed(2)}` },
|
|
5564
|
+
{ key: "days", header: "DAYS", width: 6 },
|
|
5565
|
+
{ key: "impressions", header: "IMPRESSIONS", width: 12 }
|
|
5566
|
+
], opts.format);
|
|
4834
5567
|
});
|
|
4835
|
-
addFormatOption(
|
|
4836
|
-
const resp = await apiRequest({ path: `${
|
|
5568
|
+
addFormatOption(statements.command("get").description("Get statement details.").argument("<id>", "Statement ID")).action(async (id, opts) => {
|
|
5569
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/invoices/${id}` });
|
|
4837
5570
|
const json = await resp.json();
|
|
4838
5571
|
if (!resp.ok) {
|
|
4839
5572
|
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
@@ -4841,12 +5574,1074 @@ Examples:
|
|
|
4841
5574
|
}
|
|
4842
5575
|
printDetail(json.data ?? json, opts.format);
|
|
4843
5576
|
});
|
|
4844
|
-
|
|
5577
|
+
const payoutsAdmin = cmd.command("payouts").description("Payout operations");
|
|
5578
|
+
addFormatOption(payoutsAdmin.command("list").description("List payouts.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
|
|
5579
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5580
|
+
if (opts.status)
|
|
5581
|
+
params.set("status", opts.status);
|
|
5582
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/payout-ops/payouts?${params}` });
|
|
5583
|
+
const json = await resp.json();
|
|
5584
|
+
if (!resp.ok) {
|
|
5585
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5586
|
+
process.exit(1);
|
|
5587
|
+
}
|
|
5588
|
+
const rows = (json.data ?? json).map((p) => ({
|
|
5589
|
+
id: p.id,
|
|
5590
|
+
publisher: p.publisherName ?? p.publisher_name ?? p.tenantId ?? p.tenant_id,
|
|
5591
|
+
amount: p.amount,
|
|
5592
|
+
status: p.status,
|
|
5593
|
+
requestedAt: p.requestedAt ?? p.requested_at ?? p.createdAt ?? p.created_at
|
|
5594
|
+
}));
|
|
5595
|
+
printData(rows, [
|
|
5596
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5597
|
+
{ key: "publisher", header: "PUBLISHER", width: 25 },
|
|
5598
|
+
{ key: "amount", header: "AMOUNT", width: 12 },
|
|
5599
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5600
|
+
{ key: "requestedAt", header: "REQUESTED_AT", width: 22 }
|
|
5601
|
+
], opts.format);
|
|
5602
|
+
});
|
|
5603
|
+
payoutsAdmin.command("approve").description("Approve a payout.").argument("<id>", "Payout ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5604
|
+
await confirmAction(`Approve payout ${id}?`, opts.yes);
|
|
5605
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payout-ops/payouts/${id}/approve` });
|
|
5606
|
+
if (!resp.ok) {
|
|
5607
|
+
const j = await resp.json();
|
|
5608
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5609
|
+
process.exit(1);
|
|
5610
|
+
}
|
|
5611
|
+
console.log(`Payout ${id} approved.`);
|
|
5612
|
+
});
|
|
5613
|
+
payoutsAdmin.command("reject").description("Reject a payout.").argument("<id>", "Payout ID").option("--yes", "Skip confirmation", false).option("--reason <reason>", "Rejection reason").action(async (id, opts) => {
|
|
5614
|
+
await confirmAction(`Reject payout ${id}?`, opts.yes);
|
|
5615
|
+
const body = {};
|
|
5616
|
+
if (opts.reason)
|
|
5617
|
+
body.reason = opts.reason;
|
|
5618
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payout-ops/payouts/${id}/reject`, body });
|
|
5619
|
+
if (!resp.ok) {
|
|
5620
|
+
const j = await resp.json();
|
|
5621
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5622
|
+
process.exit(1);
|
|
5623
|
+
}
|
|
5624
|
+
console.log(`Payout ${id} rejected.`);
|
|
5625
|
+
});
|
|
5626
|
+
payoutsAdmin.command("process").description("Process a payout.").argument("<id>", "Payout ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5627
|
+
await confirmAction(`Process payout ${id}?`, opts.yes);
|
|
5628
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payout-ops/payouts/${id}/process` });
|
|
5629
|
+
if (!resp.ok) {
|
|
5630
|
+
const j = await resp.json();
|
|
5631
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5632
|
+
process.exit(1);
|
|
5633
|
+
}
|
|
5634
|
+
console.log(`Payout ${id} processed.`);
|
|
5635
|
+
});
|
|
5636
|
+
payoutsAdmin.command("manual-create").description("Manually create a payout.").option("--tenant-id <id>", "Publisher tenant ID (required)").option("--amount <amount>", "Payout amount (required)").option("--reason <reason>", "Reason for manual payout").option("--wallet <wallet>", "Wallet address").option("--network <network>", "Payment network").option("--yes", "Skip confirmation", false).action(async (opts) => {
|
|
5637
|
+
if (!opts.tenantId) {
|
|
5638
|
+
console.error("Error: --tenant-id is required.");
|
|
5639
|
+
process.exit(1);
|
|
5640
|
+
}
|
|
5641
|
+
if (!opts.amount) {
|
|
5642
|
+
console.error("Error: --amount is required.");
|
|
5643
|
+
process.exit(1);
|
|
5644
|
+
}
|
|
5645
|
+
await confirmAction(`Create manual payout of ${opts.amount} for tenant ${opts.tenantId}?`, opts.yes);
|
|
5646
|
+
const body = { tenantId: opts.tenantId, amount: opts.amount };
|
|
5647
|
+
if (opts.reason)
|
|
5648
|
+
body.reason = opts.reason;
|
|
5649
|
+
if (opts.wallet)
|
|
5650
|
+
body.wallet = opts.wallet;
|
|
5651
|
+
if (opts.network)
|
|
5652
|
+
body.network = opts.network;
|
|
5653
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/manual-payout`, body });
|
|
5654
|
+
const json = await resp.json();
|
|
5655
|
+
if (!resp.ok) {
|
|
5656
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5657
|
+
process.exit(1);
|
|
5658
|
+
}
|
|
5659
|
+
console.log(`Manual payout created: ${(json.data ?? json).id ?? "OK"}`);
|
|
5660
|
+
});
|
|
5661
|
+
const deposits = cmd.command("deposits").description("Deposit operations");
|
|
5662
|
+
addFormatOption(deposits.command("list").description("List deposits.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
|
|
5663
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5664
|
+
if (opts.status)
|
|
5665
|
+
params.set("status", opts.status);
|
|
5666
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/payments-ops/deposits?${params}` });
|
|
5667
|
+
const json = await resp.json();
|
|
5668
|
+
if (!resp.ok) {
|
|
5669
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5670
|
+
process.exit(1);
|
|
5671
|
+
}
|
|
5672
|
+
const transactions = json.data?.transactions ?? json.transactions ?? json.data ?? json;
|
|
5673
|
+
const items = Array.isArray(transactions) ? transactions : [];
|
|
5674
|
+
const rows = items.map((d) => ({
|
|
5675
|
+
id: d.id,
|
|
5676
|
+
advertiser: d.advertiser?.name ?? d.advertiserName ?? d.advertiser_name ?? d.tenantId ?? d.tenant_id,
|
|
5677
|
+
amount: d.amount,
|
|
5678
|
+
status: d.status,
|
|
5679
|
+
date: d.createdAt ?? d.created_at ?? d.date
|
|
5680
|
+
}));
|
|
5681
|
+
printData(rows, [
|
|
5682
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5683
|
+
{ key: "advertiser", header: "ADVERTISER", width: 25 },
|
|
5684
|
+
{ key: "amount", header: "AMOUNT", width: 12 },
|
|
5685
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5686
|
+
{ key: "date", header: "DATE", width: 22 }
|
|
5687
|
+
], opts.format);
|
|
5688
|
+
});
|
|
5689
|
+
deposits.command("confirm").description("Confirm a deposit.").argument("<id>", "Deposit ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5690
|
+
await confirmAction(`Confirm deposit ${id}?`, opts.yes);
|
|
5691
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/deposits/${id}/confirm` });
|
|
5692
|
+
if (!resp.ok) {
|
|
5693
|
+
const j = await resp.json();
|
|
5694
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5695
|
+
process.exit(1);
|
|
5696
|
+
}
|
|
5697
|
+
console.log(`Deposit ${id} confirmed.`);
|
|
5698
|
+
});
|
|
5699
|
+
deposits.command("manual-create").description("Manually create a deposit.").option("--tenant-id <id>", "Advertiser tenant ID (required)").option("--amount <amount>", "Deposit amount (required)").option("--reason <reason>", "Reason for manual deposit").option("--reference <ref>", "External reference").option("--notes <notes>", "Additional notes").option("--yes", "Skip confirmation", false).action(async (opts) => {
|
|
5700
|
+
if (!opts.tenantId) {
|
|
5701
|
+
console.error("Error: --tenant-id is required.");
|
|
5702
|
+
process.exit(1);
|
|
5703
|
+
}
|
|
5704
|
+
if (!opts.amount) {
|
|
5705
|
+
console.error("Error: --amount is required.");
|
|
5706
|
+
process.exit(1);
|
|
5707
|
+
}
|
|
5708
|
+
await confirmAction(`Create manual deposit of ${opts.amount} for tenant ${opts.tenantId}?`, opts.yes);
|
|
5709
|
+
const body = { tenantId: opts.tenantId, amount: opts.amount };
|
|
5710
|
+
if (opts.reason)
|
|
5711
|
+
body.reason = opts.reason;
|
|
5712
|
+
if (opts.reference)
|
|
5713
|
+
body.reference = opts.reference;
|
|
5714
|
+
if (opts.notes)
|
|
5715
|
+
body.notes = opts.notes;
|
|
5716
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/manual-deposit`, body });
|
|
5717
|
+
const json = await resp.json();
|
|
5718
|
+
if (!resp.ok) {
|
|
5719
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5720
|
+
process.exit(1);
|
|
5721
|
+
}
|
|
5722
|
+
console.log(`Manual deposit created: ${(json.data ?? json).id ?? "OK"}`);
|
|
5723
|
+
});
|
|
5724
|
+
const refunds = cmd.command("refunds").description("Refund operations");
|
|
5725
|
+
addFormatOption(refunds.command("list").description("List refunds.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
|
|
5726
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
5727
|
+
if (opts.status)
|
|
5728
|
+
params.set("status", opts.status);
|
|
5729
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/payments-ops/refunds?${params}` });
|
|
5730
|
+
const json = await resp.json();
|
|
5731
|
+
if (!resp.ok) {
|
|
5732
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5733
|
+
process.exit(1);
|
|
5734
|
+
}
|
|
5735
|
+
const rows = (json.data ?? json).map((r) => ({
|
|
5736
|
+
id: r.id,
|
|
5737
|
+
advertiser: r.advertiserName ?? r.advertiser_name ?? r.accountId ?? r.account_id,
|
|
5738
|
+
amount: r.amount,
|
|
5739
|
+
reason: r.reason,
|
|
5740
|
+
status: r.status
|
|
5741
|
+
}));
|
|
5742
|
+
printData(rows, [
|
|
5743
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5744
|
+
{ key: "advertiser", header: "ADVERTISER", width: 25 },
|
|
5745
|
+
{ key: "amount", header: "AMOUNT", width: 12 },
|
|
5746
|
+
{ key: "reason", header: "REASON", width: 20 },
|
|
5747
|
+
{ key: "status", header: "STATUS", width: 12 }
|
|
5748
|
+
], opts.format);
|
|
5749
|
+
});
|
|
5750
|
+
addFormatOption(refunds.command("get").description("Get refund details.").argument("<id>", "Refund ID")).action(async (id, opts) => {
|
|
5751
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/payments-ops/refunds/${id}` });
|
|
5752
|
+
const json = await resp.json();
|
|
5753
|
+
if (!resp.ok) {
|
|
5754
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5755
|
+
process.exit(1);
|
|
5756
|
+
}
|
|
5757
|
+
printDetail(json.data ?? json, opts.format);
|
|
5758
|
+
});
|
|
5759
|
+
refunds.command("create").description("Create a refund.").option("--account-id <id>", "Account ID (required)").option("--amount <amount>", "Refund amount (required)").option("--reason <reason>", "Refund reason").option("--yes", "Skip confirmation", false).action(async (opts) => {
|
|
5760
|
+
if (!opts.accountId) {
|
|
5761
|
+
console.error("Error: --account-id is required.");
|
|
5762
|
+
process.exit(1);
|
|
5763
|
+
}
|
|
5764
|
+
if (!opts.amount) {
|
|
5765
|
+
console.error("Error: --amount is required.");
|
|
5766
|
+
process.exit(1);
|
|
5767
|
+
}
|
|
5768
|
+
await confirmAction(`Create refund of ${opts.amount} for account ${opts.accountId}?`, opts.yes);
|
|
5769
|
+
const body = { accountId: opts.accountId, amount: opts.amount };
|
|
5770
|
+
if (opts.reason)
|
|
5771
|
+
body.reason = opts.reason;
|
|
5772
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds`, body });
|
|
5773
|
+
const json = await resp.json();
|
|
5774
|
+
if (!resp.ok) {
|
|
5775
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5776
|
+
process.exit(1);
|
|
5777
|
+
}
|
|
5778
|
+
console.log(`Refund created: ${(json.data ?? json).id ?? "OK"}`);
|
|
5779
|
+
});
|
|
5780
|
+
refunds.command("approve").description("Approve a refund.").argument("<id>", "Refund ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5781
|
+
await confirmAction(`Approve refund ${id}?`, opts.yes);
|
|
5782
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds/${id}/approve` });
|
|
5783
|
+
if (!resp.ok) {
|
|
5784
|
+
const j = await resp.json();
|
|
5785
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5786
|
+
process.exit(1);
|
|
5787
|
+
}
|
|
5788
|
+
console.log(`Refund ${id} approved.`);
|
|
5789
|
+
});
|
|
5790
|
+
refunds.command("reject").description("Reject a refund.").argument("<id>", "Refund ID").option("--yes", "Skip confirmation", false).option("--reason <reason>", "Rejection reason").action(async (id, opts) => {
|
|
5791
|
+
await confirmAction(`Reject refund ${id}?`, opts.yes);
|
|
5792
|
+
const body = {};
|
|
5793
|
+
if (opts.reason)
|
|
5794
|
+
body.reason = opts.reason;
|
|
5795
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds/${id}/reject`, body });
|
|
5796
|
+
if (!resp.ok) {
|
|
5797
|
+
const j = await resp.json();
|
|
5798
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5799
|
+
process.exit(1);
|
|
5800
|
+
}
|
|
5801
|
+
console.log(`Refund ${id} rejected.`);
|
|
5802
|
+
});
|
|
5803
|
+
refunds.command("process").description("Process an approved refund.").argument("<id>", "Refund ID").option("--yes", "Skip confirmation", false).action(async (id, opts) => {
|
|
5804
|
+
await confirmAction(`Process refund ${id}?`, opts.yes);
|
|
5805
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/payments-ops/refunds/${id}/process` });
|
|
5806
|
+
if (!resp.ok) {
|
|
5807
|
+
const j = await resp.json();
|
|
5808
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5809
|
+
process.exit(1);
|
|
5810
|
+
}
|
|
5811
|
+
console.log(`Refund ${id} processed.`);
|
|
5812
|
+
});
|
|
5813
|
+
const balances = cmd.command("balances").description("Account balance management");
|
|
5814
|
+
addFormatOption(balances.command("list").description("List account balances.")).action(async (opts) => {
|
|
5815
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/billing-ops/balances` });
|
|
5816
|
+
const json = await resp.json();
|
|
5817
|
+
if (!resp.ok) {
|
|
5818
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5819
|
+
process.exit(1);
|
|
5820
|
+
}
|
|
5821
|
+
printDetail(json.data ?? json, opts.format);
|
|
5822
|
+
});
|
|
5823
|
+
balances.command("adjust").description("Adjust an account balance.").option("--tenant-id <id>", "Tenant ID (required)").option("--amount <amount>", "Adjustment amount (required)").option("--reason <reason>", "Reason for adjustment").option("--account-type <type>", "Account type", "advertiser").option("--yes", "Skip confirmation", false).action(async (opts) => {
|
|
5824
|
+
if (!opts.tenantId) {
|
|
5825
|
+
console.error("Error: --tenant-id is required.");
|
|
5826
|
+
process.exit(1);
|
|
5827
|
+
}
|
|
5828
|
+
if (!opts.amount) {
|
|
5829
|
+
console.error("Error: --amount is required.");
|
|
5830
|
+
process.exit(1);
|
|
5831
|
+
}
|
|
5832
|
+
await confirmAction(`Adjust balance by ${opts.amount} for tenant ${opts.tenantId} (${opts.accountType})?`, opts.yes);
|
|
5833
|
+
const body = { tenantId: opts.tenantId, amount: opts.amount, accountType: opts.accountType };
|
|
5834
|
+
if (opts.reason)
|
|
5835
|
+
body.reason = opts.reason;
|
|
5836
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/billing-ops/adjust`, body });
|
|
5837
|
+
const json = await resp.json();
|
|
5838
|
+
if (!resp.ok) {
|
|
5839
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5840
|
+
process.exit(1);
|
|
5841
|
+
}
|
|
5842
|
+
console.log(`Balance adjusted for tenant ${opts.tenantId}.`);
|
|
5843
|
+
});
|
|
5844
|
+
return cmd;
|
|
5845
|
+
}
|
|
5846
|
+
|
|
5847
|
+
// src/commands/external-ssp.ts
|
|
5848
|
+
var COLUMNS8 = [
|
|
5849
|
+
{ key: "id", header: "ID", width: 36 },
|
|
5850
|
+
{ key: "name", header: "NAME", width: 25 },
|
|
5851
|
+
{ key: "code", header: "CODE", width: 15 },
|
|
5852
|
+
{ key: "status", header: "STATUS", width: 12 },
|
|
5853
|
+
{ key: "formats", header: "FORMATS", width: 25, format: (v) => Array.isArray(v) ? v.join(", ") : "-" }
|
|
5854
|
+
];
|
|
5855
|
+
function createExternalSspCommand() {
|
|
5856
|
+
const cmd = new Command("external-ssp").description(`External SSP Partner management (Console)
|
|
5857
|
+
|
|
5858
|
+
Requires: platform_owner or platform_admin role.`).addHelpText("after", `
|
|
5859
|
+
Examples:
|
|
5860
|
+
$ a8techads external-ssp list
|
|
5861
|
+
$ a8techads external-ssp get <id>
|
|
5862
|
+
$ a8techads external-ssp create --name "OpenX" --code OPENX --formats BANNER,VIDEO
|
|
5863
|
+
$ a8techads external-ssp activate <id>
|
|
5864
|
+
$ a8techads external-ssp pause <id>`);
|
|
5865
|
+
addFormatOption(cmd.command("list").description("List all external SSP partners.").option("--status <status>", "Filter by status (TESTING, ACTIVE, SUSPENDED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
5866
|
+
const params = new URLSearchParams;
|
|
5867
|
+
if (opts.status)
|
|
5868
|
+
params.set("status", opts.status);
|
|
5869
|
+
params.set("limit", opts.limit);
|
|
5870
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/ssp-partners?${params}` });
|
|
5871
|
+
const json = await resp.json();
|
|
5872
|
+
if (!resp.ok) {
|
|
5873
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5874
|
+
process.exit(1);
|
|
5875
|
+
}
|
|
5876
|
+
const rows = (json.data ?? json).map((p) => ({
|
|
5877
|
+
id: p.id,
|
|
5878
|
+
name: p.name,
|
|
5879
|
+
code: p.code,
|
|
5880
|
+
status: p.status,
|
|
5881
|
+
formats: p.supportedFormats ?? p.supported_formats ?? []
|
|
5882
|
+
}));
|
|
5883
|
+
printData(rows, COLUMNS8, opts.format);
|
|
5884
|
+
});
|
|
5885
|
+
addFormatOption(cmd.command("get").description("Get external SSP partner details (includes API key).").argument("<id>", "Partner ID")).action(async (id, opts) => {
|
|
5886
|
+
const resp = await apiRequest({ path: `${consolePrefix()}/ssp-partners/${id}` });
|
|
5887
|
+
const json = await resp.json();
|
|
5888
|
+
if (!resp.ok) {
|
|
5889
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
5890
|
+
process.exit(1);
|
|
5891
|
+
}
|
|
5892
|
+
printDetail(json.data ?? json, opts.format);
|
|
5893
|
+
});
|
|
5894
|
+
cmd.command("create").description("Create a new external SSP partner.").option("--name <name>", "Partner name (required)").option("--code <code>", "Partner code (auto-generated from name if omitted)").option("--formats <formats>", "Supported formats, comma-separated (default: BANNER,VIDEO,NATIVE)").addHelpText("after", `
|
|
5895
|
+
Examples:
|
|
5896
|
+
$ a8techads external-ssp create --name "OpenX"
|
|
5897
|
+
$ a8techads external-ssp create --name "AppLovin" --code APPLOVIN --formats BANNER,VIDEO`).action(async (opts) => {
|
|
5898
|
+
if (!opts.name) {
|
|
5899
|
+
console.error('Error: --name is required. Run "a8techads external-ssp create --help".');
|
|
5900
|
+
process.exit(1);
|
|
5901
|
+
}
|
|
5902
|
+
const body = { name: opts.name };
|
|
5903
|
+
if (opts.code)
|
|
5904
|
+
body.code = opts.code;
|
|
5905
|
+
if (opts.formats)
|
|
5906
|
+
body.supportedFormats = opts.formats.split(",").map((f) => f.trim());
|
|
5907
|
+
const resp = await apiRequest({ method: "POST", path: `${consolePrefix()}/ssp-partners`, body });
|
|
5908
|
+
const json = await resp.json();
|
|
5909
|
+
if (!resp.ok) {
|
|
5910
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
5911
|
+
process.exit(1);
|
|
5912
|
+
}
|
|
5913
|
+
const data = json.data ?? json;
|
|
5914
|
+
console.log(`Partner created: ${data.id}`);
|
|
5915
|
+
if (data.code)
|
|
5916
|
+
console.log(` Code: ${data.code}`);
|
|
5917
|
+
if (data.apiKey)
|
|
5918
|
+
console.log(` API Key: ${data.apiKey}`);
|
|
5919
|
+
});
|
|
5920
|
+
cmd.command("update").description("Update an external SSP partner.").argument("<id>", "Partner ID").option("--name <name>", "New partner name").option("--formats <formats>", "Supported formats, comma-separated").action(async (id, opts) => {
|
|
5921
|
+
const body = {};
|
|
5922
|
+
if (opts.name)
|
|
5923
|
+
body.name = opts.name;
|
|
5924
|
+
if (opts.formats)
|
|
5925
|
+
body.supportedFormats = opts.formats.split(",").map((f) => f.trim());
|
|
5926
|
+
const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/ssp-partners/${id}`, body });
|
|
5927
|
+
if (!resp.ok) {
|
|
5928
|
+
const j = await resp.json();
|
|
5929
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5930
|
+
process.exit(1);
|
|
5931
|
+
}
|
|
5932
|
+
console.log(`Partner ${id} updated.`);
|
|
5933
|
+
});
|
|
5934
|
+
cmd.command("delete").description("Delete an external SSP partner.").argument("<id>", "Partner ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
5935
|
+
if (!opts.yes) {
|
|
5936
|
+
console.error("Add --yes to confirm deletion.");
|
|
5937
|
+
process.exit(1);
|
|
5938
|
+
}
|
|
5939
|
+
const resp = await apiRequest({ method: "DELETE", path: `${consolePrefix()}/ssp-partners/${id}` });
|
|
5940
|
+
if (!resp.ok && resp.status !== 204) {
|
|
5941
|
+
console.error(`Error: ${resp.statusText}`);
|
|
5942
|
+
process.exit(1);
|
|
5943
|
+
}
|
|
5944
|
+
console.log(`Partner ${id} deleted.`);
|
|
5945
|
+
});
|
|
5946
|
+
cmd.command("activate").description("Activate an external SSP partner (TESTING/SUSPENDED → ACTIVE).").argument("<id>", "Partner ID").action(async (id) => {
|
|
5947
|
+
const resp = await apiRequest({
|
|
5948
|
+
method: "PATCH",
|
|
5949
|
+
path: `${consolePrefix()}/ssp-partners/${id}/status`,
|
|
5950
|
+
body: { status: "ACTIVE" }
|
|
5951
|
+
});
|
|
5952
|
+
if (!resp.ok) {
|
|
5953
|
+
const j = await resp.json();
|
|
5954
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5955
|
+
process.exit(1);
|
|
5956
|
+
}
|
|
5957
|
+
console.log(`Partner ${id} activated.`);
|
|
5958
|
+
});
|
|
5959
|
+
cmd.command("pause").description("Pause an external SSP partner (ACTIVE → SUSPENDED).").argument("<id>", "Partner ID").action(async (id) => {
|
|
5960
|
+
const resp = await apiRequest({
|
|
5961
|
+
method: "PATCH",
|
|
5962
|
+
path: `${consolePrefix()}/ssp-partners/${id}/status`,
|
|
5963
|
+
body: { status: "SUSPENDED" }
|
|
5964
|
+
});
|
|
5965
|
+
if (!resp.ok) {
|
|
5966
|
+
const j = await resp.json();
|
|
5967
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
5968
|
+
process.exit(1);
|
|
5969
|
+
}
|
|
5970
|
+
console.log(`Partner ${id} paused.`);
|
|
5971
|
+
});
|
|
5972
|
+
return cmd;
|
|
5973
|
+
}
|
|
5974
|
+
|
|
5975
|
+
// src/commands/settings.ts
|
|
5976
|
+
function settingsPrefix() {
|
|
5977
|
+
const ctx = loadContext();
|
|
5978
|
+
const context = getCurrentContext(ctx);
|
|
5979
|
+
const app = context?.app ?? "dsp";
|
|
5980
|
+
if (app === "console") {
|
|
5981
|
+
console.error("Error: Settings commands are not available in Console mode.");
|
|
5982
|
+
console.error('Switch to DSP or SSP context: "a8techads context dsp" or "a8techads context ssp"');
|
|
5983
|
+
process.exit(1);
|
|
5984
|
+
}
|
|
5985
|
+
return app === "ssp" ? "/api/v1/ssp" : "/api/v1/dsp";
|
|
5986
|
+
}
|
|
5987
|
+
function createSettingsCommand() {
|
|
5988
|
+
const cmd = new Command("settings").description(`Tenant settings (DSP / SSP only)
|
|
5989
|
+
|
|
5990
|
+
Requires: ADVERTISER or PUBLISHER capability.
|
|
5991
|
+
Not available in Console mode.`).addHelpText("after", `
|
|
5992
|
+
Examples:
|
|
5993
|
+
$ a8techads settings show
|
|
5994
|
+
$ a8techads settings update --from-json settings.json
|
|
5995
|
+
$ a8techads settings profile
|
|
5996
|
+
$ a8techads settings profile-update --company-name "New Name"`);
|
|
5997
|
+
addFormatOption(cmd.command("show").description("Show current tenant settings.")).action(async (opts) => {
|
|
5998
|
+
const resp = await apiRequest({ path: `${settingsPrefix()}/settings` });
|
|
5999
|
+
const json = await resp.json();
|
|
6000
|
+
if (!resp.ok) {
|
|
6001
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6002
|
+
process.exit(1);
|
|
6003
|
+
}
|
|
6004
|
+
printDetail(json.data ?? json, opts.format);
|
|
6005
|
+
});
|
|
6006
|
+
addFormatOption(cmd.command("profile").description("Show tenant profile (contact/business info).")).action(async (opts) => {
|
|
6007
|
+
const resp = await apiRequest({ path: `${settingsPrefix()}/settings/profile` });
|
|
6008
|
+
const json = await resp.json();
|
|
6009
|
+
if (!resp.ok) {
|
|
6010
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6011
|
+
process.exit(1);
|
|
6012
|
+
}
|
|
6013
|
+
printDetail(json.data ?? json, opts.format);
|
|
6014
|
+
});
|
|
6015
|
+
cmd.command("update").description(`Update tenant settings.
|
|
6016
|
+
|
|
6017
|
+
Requires: admin role.`).option("--from-json <file>", "Update from JSON file").action(async (opts) => {
|
|
6018
|
+
if (!opts.fromJson) {
|
|
6019
|
+
console.error("Error: --from-json is required for settings update.");
|
|
6020
|
+
process.exit(1);
|
|
6021
|
+
}
|
|
6022
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
6023
|
+
const body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6024
|
+
const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings`, body });
|
|
6025
|
+
if (!resp.ok) {
|
|
6026
|
+
const j = await resp.json();
|
|
6027
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
6028
|
+
process.exit(1);
|
|
6029
|
+
}
|
|
6030
|
+
console.log("Settings updated.");
|
|
6031
|
+
});
|
|
6032
|
+
cmd.command("profile-update").description("Update tenant profile.").option("--company-name <name>", "Company name").option("--from-json <file>", "Update from JSON file").action(async (opts) => {
|
|
6033
|
+
let body;
|
|
6034
|
+
if (opts.fromJson) {
|
|
6035
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
6036
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6037
|
+
} else {
|
|
6038
|
+
body = {};
|
|
6039
|
+
if (opts.companyName)
|
|
6040
|
+
body.companyName = opts.companyName;
|
|
6041
|
+
}
|
|
6042
|
+
const resp = await apiRequest({ method: "PATCH", path: `${settingsPrefix()}/settings/profile`, body });
|
|
6043
|
+
if (!resp.ok) {
|
|
6044
|
+
const j = await resp.json();
|
|
6045
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
6046
|
+
process.exit(1);
|
|
6047
|
+
}
|
|
6048
|
+
console.log("Profile updated.");
|
|
6049
|
+
});
|
|
6050
|
+
return cmd;
|
|
6051
|
+
}
|
|
6052
|
+
|
|
6053
|
+
// src/commands/invoices.ts
|
|
6054
|
+
function createInvoicesCommand() {
|
|
6055
|
+
const cmd = new Command("invoices").description(`Invoice management (DSP)
|
|
6056
|
+
|
|
6057
|
+
Requires: ADVERTISER capability.`).addHelpText("after", `
|
|
6058
|
+
Examples:
|
|
6059
|
+
$ a8techads invoices list
|
|
6060
|
+
$ a8techads invoices get <id>
|
|
6061
|
+
$ a8techads invoices spending`);
|
|
6062
|
+
addFormatOption(cmd.command("list").description("List invoices.")).action(async (opts) => {
|
|
6063
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/invoices` });
|
|
6064
|
+
const json = await resp.json();
|
|
6065
|
+
if (!resp.ok) {
|
|
6066
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6067
|
+
process.exit(1);
|
|
6068
|
+
}
|
|
6069
|
+
const columns = [
|
|
6070
|
+
{ key: "id", header: "ID", width: 36 },
|
|
6071
|
+
{ key: "period", header: "PERIOD", width: 20 },
|
|
6072
|
+
{ key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
|
|
6073
|
+
{ key: "status", header: "STATUS", width: 12 }
|
|
6074
|
+
];
|
|
6075
|
+
const rows = (json.data ?? json).map((i) => ({
|
|
6076
|
+
id: i.id,
|
|
6077
|
+
period: i.period ?? i.billingPeriod,
|
|
6078
|
+
amount: i.amount ?? i.totalAmount,
|
|
6079
|
+
status: i.status
|
|
6080
|
+
}));
|
|
6081
|
+
printData(rows, columns, opts.format);
|
|
6082
|
+
});
|
|
6083
|
+
addFormatOption(cmd.command("get").description("Get invoice details.").argument("<id>", "Invoice ID")).action(async (id, opts) => {
|
|
6084
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/invoices/${id}` });
|
|
6085
|
+
const json = await resp.json();
|
|
6086
|
+
if (!resp.ok) {
|
|
6087
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6088
|
+
process.exit(1);
|
|
6089
|
+
}
|
|
6090
|
+
printDetail(json.data ?? json, opts.format);
|
|
6091
|
+
});
|
|
6092
|
+
addFormatOption(cmd.command("spending").description("Show current period spending summary.")).action(async (opts) => {
|
|
6093
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/invoices/spending` });
|
|
6094
|
+
const json = await resp.json();
|
|
6095
|
+
if (!resp.ok) {
|
|
6096
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6097
|
+
process.exit(1);
|
|
6098
|
+
}
|
|
6099
|
+
printDetail(json.data ?? json, opts.format);
|
|
6100
|
+
});
|
|
6101
|
+
return cmd;
|
|
6102
|
+
}
|
|
6103
|
+
|
|
6104
|
+
// src/commands/conversion-goals.ts
|
|
6105
|
+
var COLUMNS9 = [
|
|
6106
|
+
{ key: "id", header: "ID", width: 36 },
|
|
6107
|
+
{ key: "name", header: "NAME", width: 24 },
|
|
6108
|
+
{ key: "goalOrder", header: "G#", width: 4 },
|
|
6109
|
+
{ key: "conversionType", header: "TYPE", width: 20 },
|
|
6110
|
+
{ key: "valueType", header: "VALUE", width: 10 },
|
|
6111
|
+
{ key: "status", header: "STATUS", width: 10 }
|
|
6112
|
+
];
|
|
6113
|
+
function createConversionGoalsCommand() {
|
|
6114
|
+
const cmd = new Command("conversion-goals").description(`Conversion goal management (DSP)
|
|
6115
|
+
|
|
6116
|
+
Requires: ADVERTISER capability.`).addHelpText("after", `
|
|
6117
|
+
Examples:
|
|
6118
|
+
$ a8techads conversion-goals list
|
|
6119
|
+
$ a8techads conversion-goals get <id>
|
|
6120
|
+
$ a8techads conversion-goals create --name "Purchase" --conversion-type PURCHASE_CC --goal-order 1
|
|
6121
|
+
$ a8techads conversion-goals pause <id>`);
|
|
6122
|
+
addFormatOption(cmd.command("list").description("List conversion goals.").option("--status <status>", "Filter by status (ACTIVE, PAUSED, ARCHIVED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
6123
|
+
const params = new URLSearchParams;
|
|
6124
|
+
if (opts.status)
|
|
6125
|
+
params.set("status", opts.status);
|
|
6126
|
+
params.set("limit", opts.limit);
|
|
6127
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/conversion-goals?${params}` });
|
|
6128
|
+
const json = await resp.json();
|
|
6129
|
+
if (!resp.ok) {
|
|
6130
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6131
|
+
process.exit(1);
|
|
6132
|
+
}
|
|
6133
|
+
const rows = (json.data ?? json).map((g) => ({
|
|
6134
|
+
id: g.id,
|
|
6135
|
+
name: g.name,
|
|
6136
|
+
goalOrder: `G${g.goalOrder ?? g.goal_order}`,
|
|
6137
|
+
conversionType: g.conversionType ?? g.conversion_type,
|
|
6138
|
+
valueType: g.valueType ?? g.value_type,
|
|
6139
|
+
status: g.status
|
|
6140
|
+
}));
|
|
6141
|
+
printData(rows, COLUMNS9, opts.format);
|
|
6142
|
+
});
|
|
6143
|
+
addFormatOption(cmd.command("get").description("Get conversion goal details.").argument("<id>", "Goal ID")).action(async (id, opts) => {
|
|
6144
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/conversion-goals/${id}` });
|
|
6145
|
+
const json = await resp.json();
|
|
6146
|
+
if (!resp.ok) {
|
|
6147
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6148
|
+
process.exit(1);
|
|
6149
|
+
}
|
|
6150
|
+
printDetail(json.data ?? json, opts.format);
|
|
6151
|
+
});
|
|
6152
|
+
cmd.command("create").description(`Create a conversion goal.
|
|
6153
|
+
|
|
6154
|
+
Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
|
|
6155
|
+
PURCHASE_CARRIER, SUBSCRIPTION_CC, SUBSCRIPTION_CARRIER, WEBSITE_INTERACTION, MULTIPLE, OTHER`).option("--name <name>", "Goal name (required)").option("--conversion-type <type>", "Conversion type (required)").option("--goal-order <n>", "Priority 1-10, maps to G1-G10 (required)").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC", "NO_VALUE").option("--fixed-value <amount>", "Fixed value per conversion (when value-type is FIXED)").option("--count-type <type>", "ONE per user or EVERY conversion", "EVERY").option("--window <hours>", "Attribution window in hours", "720").option("--description <text>", "Goal description").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
6156
|
+
let body;
|
|
6157
|
+
if (opts.fromJson) {
|
|
6158
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
6159
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6160
|
+
} else {
|
|
6161
|
+
if (!opts.name) {
|
|
6162
|
+
console.error("Error: --name is required");
|
|
6163
|
+
process.exit(1);
|
|
6164
|
+
}
|
|
6165
|
+
if (!opts.conversionType) {
|
|
6166
|
+
console.error("Error: --conversion-type is required");
|
|
6167
|
+
process.exit(1);
|
|
6168
|
+
}
|
|
6169
|
+
if (!opts.goalOrder) {
|
|
6170
|
+
console.error("Error: --goal-order is required (1-10)");
|
|
6171
|
+
process.exit(1);
|
|
6172
|
+
}
|
|
6173
|
+
body = {
|
|
6174
|
+
name: opts.name,
|
|
6175
|
+
conversionType: opts.conversionType,
|
|
6176
|
+
goalOrder: Number(opts.goalOrder),
|
|
6177
|
+
valueType: opts.valueType,
|
|
6178
|
+
countType: opts.countType,
|
|
6179
|
+
conversionWindowHours: Number(opts.window)
|
|
6180
|
+
};
|
|
6181
|
+
if (opts.fixedValue)
|
|
6182
|
+
body.fixedValue = Number(opts.fixedValue);
|
|
6183
|
+
if (opts.description)
|
|
6184
|
+
body.description = opts.description;
|
|
6185
|
+
}
|
|
6186
|
+
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/conversion-goals`, body });
|
|
6187
|
+
const json = await resp.json();
|
|
6188
|
+
if (!resp.ok) {
|
|
6189
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
6190
|
+
process.exit(1);
|
|
6191
|
+
}
|
|
6192
|
+
const goal = json.data ?? json;
|
|
6193
|
+
console.log(`Conversion goal created: ${goal.id}`);
|
|
6194
|
+
console.log(` Goal ID: ${goal.goalId ?? goal.goal_id}`);
|
|
6195
|
+
console.log(` Postback URL: ${goal.postbackUrl ?? goal.postback_url ?? "(see get)"}`);
|
|
6196
|
+
});
|
|
6197
|
+
cmd.command("update").description("Update a conversion goal.").argument("<id>", "Goal ID").option("--name <name>", "New name").option("--description <text>", "New description").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC").option("--fixed-value <amount>", "Fixed value").option("--count-type <type>", "ONE or EVERY").option("--window <hours>", "Attribution window").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
6198
|
+
let body;
|
|
6199
|
+
if (opts.fromJson) {
|
|
6200
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
6201
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6202
|
+
} else {
|
|
6203
|
+
body = {};
|
|
6204
|
+
if (opts.name)
|
|
6205
|
+
body.name = opts.name;
|
|
6206
|
+
if (opts.description)
|
|
6207
|
+
body.description = opts.description;
|
|
6208
|
+
if (opts.valueType)
|
|
6209
|
+
body.valueType = opts.valueType;
|
|
6210
|
+
if (opts.fixedValue)
|
|
6211
|
+
body.fixedValue = Number(opts.fixedValue);
|
|
6212
|
+
if (opts.countType)
|
|
6213
|
+
body.countType = opts.countType;
|
|
6214
|
+
if (opts.window)
|
|
6215
|
+
body.conversionWindowHours = Number(opts.window);
|
|
6216
|
+
}
|
|
6217
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/conversion-goals/${id}`, body });
|
|
6218
|
+
if (!resp.ok) {
|
|
6219
|
+
const j = await resp.json();
|
|
6220
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
6221
|
+
process.exit(1);
|
|
6222
|
+
}
|
|
6223
|
+
console.log(`Conversion goal ${id} updated.`);
|
|
6224
|
+
});
|
|
6225
|
+
cmd.command("delete").description("Delete a conversion goal.").argument("<id>", "Goal ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
6226
|
+
if (!opts.yes) {
|
|
6227
|
+
console.error("Add --yes to confirm deletion.");
|
|
6228
|
+
process.exit(1);
|
|
6229
|
+
}
|
|
6230
|
+
const resp = await apiRequest({ method: "DELETE", path: `${dspPrefix()}/conversion-goals/${id}` });
|
|
6231
|
+
if (!resp.ok && resp.status !== 204) {
|
|
6232
|
+
console.error(`Error: ${resp.statusText}`);
|
|
6233
|
+
process.exit(1);
|
|
6234
|
+
}
|
|
6235
|
+
console.log(`Conversion goal ${id} deleted.`);
|
|
6236
|
+
});
|
|
6237
|
+
for (const action of ["pause", "activate", "archive"]) {
|
|
6238
|
+
cmd.command(action).description(`${action.charAt(0).toUpperCase() + action.slice(1)} a conversion goal.`).argument("<id>", "Goal ID").action(async (id) => {
|
|
6239
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/conversion-goals/${id}/${action}` });
|
|
6240
|
+
if (!resp.ok) {
|
|
6241
|
+
const j = await resp.json();
|
|
6242
|
+
console.error(`Error: ${j.error ?? resp.statusText}`);
|
|
6243
|
+
process.exit(1);
|
|
6244
|
+
}
|
|
6245
|
+
console.log(`Conversion goal ${id} ${action}d.`);
|
|
6246
|
+
});
|
|
6247
|
+
}
|
|
6248
|
+
cmd.command("regenerate-secret").description("Regenerate secret key and postback URL.").argument("<id>", "Goal ID").action(async (id) => {
|
|
6249
|
+
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/conversion-goals/${id}/regenerate-secret` });
|
|
6250
|
+
const json = await resp.json();
|
|
6251
|
+
if (!resp.ok) {
|
|
6252
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6253
|
+
process.exit(1);
|
|
6254
|
+
}
|
|
6255
|
+
const goal = json.data ?? json;
|
|
6256
|
+
console.log(`Secret regenerated for goal ${id}`);
|
|
6257
|
+
console.log(` New postback URL: ${goal.postbackUrl ?? goal.postback_url}`);
|
|
6258
|
+
});
|
|
6259
|
+
return cmd;
|
|
6260
|
+
}
|
|
6261
|
+
|
|
6262
|
+
// src/commands/algorithms.ts
|
|
6263
|
+
var COLUMNS10 = [
|
|
6264
|
+
{ key: "id", header: "ID", width: 36 },
|
|
6265
|
+
{ key: "name", header: "NAME", width: 28 },
|
|
6266
|
+
{ key: "goal", header: "GOAL", width: 8 },
|
|
6267
|
+
{ key: "conversionGoalId", header: "CONV_GOAL", width: 36 },
|
|
6268
|
+
{ key: "status", header: "STATUS", width: 10 },
|
|
6269
|
+
{ key: "campaignCount", header: "CAMPAIGNS", width: 10 }
|
|
6270
|
+
];
|
|
6271
|
+
function createAlgorithmsCommand() {
|
|
6272
|
+
const cmd = new Command("algorithms").description(`Bidder algorithm management (DSP)
|
|
6273
|
+
|
|
6274
|
+
Requires: ADVERTISER capability.`).addHelpText("after", `
|
|
6275
|
+
Examples:
|
|
6276
|
+
$ a8techads algorithms list
|
|
6277
|
+
$ a8techads algorithms get <id>
|
|
6278
|
+
$ a8techads algorithms create --name "CPA Base" --optimization-goal CPA --target-value 5
|
|
6279
|
+
$ a8techads algorithms update <id> --conversion-goal-id <goal-id>
|
|
6280
|
+
$ a8techads algorithms pause <id>`);
|
|
6281
|
+
addFormatOption(cmd.command("list").description("List bidder algorithms.").option("--status <status>", "Filter by status (ACTIVE, PAUSED, ARCHIVED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
6282
|
+
const params = new URLSearchParams;
|
|
6283
|
+
if (opts.status)
|
|
6284
|
+
params.set("status", opts.status);
|
|
6285
|
+
params.set("limit", opts.limit);
|
|
6286
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms?${params}` });
|
|
6287
|
+
const json = await resp.json();
|
|
6288
|
+
if (!resp.ok) {
|
|
6289
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6290
|
+
process.exit(1);
|
|
6291
|
+
}
|
|
6292
|
+
const rows = (json.data ?? json).map((algo) => ({
|
|
6293
|
+
id: algo.id,
|
|
6294
|
+
name: algo.name,
|
|
6295
|
+
goal: algo.optimizationGoal,
|
|
6296
|
+
conversionGoalId: algo.conversionGoalId,
|
|
6297
|
+
status: algo.status,
|
|
6298
|
+
campaignCount: algo.campaignCount ?? 0
|
|
6299
|
+
}));
|
|
6300
|
+
printData(rows, COLUMNS10, opts.format);
|
|
6301
|
+
});
|
|
6302
|
+
addFormatOption(cmd.command("get").description("Get bidder algorithm details.").argument("<id>", "Algorithm ID")).action(async (id, opts) => {
|
|
6303
|
+
const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms/${id}` });
|
|
6304
|
+
const json = await resp.json();
|
|
6305
|
+
if (!resp.ok) {
|
|
6306
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6307
|
+
process.exit(1);
|
|
6308
|
+
}
|
|
6309
|
+
printDetail(json.data ?? json, opts.format);
|
|
6310
|
+
});
|
|
6311
|
+
cmd.command("create").description("Create a bidder algorithm.").option("--name <name>", "Algorithm name (required)").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
|
|
6312
|
+
const body = await buildAlgorithmBody(opts, true);
|
|
6313
|
+
const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/bidder-algorithms`, body });
|
|
6314
|
+
const json = await resp.json();
|
|
6315
|
+
if (!resp.ok) {
|
|
6316
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
6317
|
+
process.exit(1);
|
|
6318
|
+
}
|
|
6319
|
+
console.log(`Algorithm created: ${json.data?.id ?? json.id}`);
|
|
6320
|
+
});
|
|
6321
|
+
cmd.command("update").description("Update a bidder algorithm.").argument("<id>", "Algorithm ID").option("--name <name>", "Algorithm name").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
|
|
6322
|
+
const body = await buildAlgorithmBody(opts, false);
|
|
6323
|
+
const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/bidder-algorithms/${id}`, body });
|
|
6324
|
+
const json = await resp.json().catch(() => ({}));
|
|
6325
|
+
if (!resp.ok) {
|
|
6326
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
6327
|
+
process.exit(1);
|
|
6328
|
+
}
|
|
6329
|
+
console.log(`Algorithm ${id} updated.`);
|
|
6330
|
+
});
|
|
6331
|
+
cmd.command("archive").description("Archive a bidder algorithm by setting status to ARCHIVED.").argument("<id>", "Algorithm ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
|
|
6332
|
+
if (!opts.yes) {
|
|
6333
|
+
console.error("Add --yes to confirm archiving.");
|
|
6334
|
+
process.exit(1);
|
|
6335
|
+
}
|
|
6336
|
+
const resp = await apiRequest({
|
|
6337
|
+
method: "PATCH",
|
|
6338
|
+
path: `${dspPrefix()}/bidder-algorithms/${id}`,
|
|
6339
|
+
body: { status: "ARCHIVED" }
|
|
6340
|
+
});
|
|
6341
|
+
const json = await resp.json().catch(() => ({}));
|
|
6342
|
+
if (!resp.ok) {
|
|
6343
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
6344
|
+
process.exit(1);
|
|
6345
|
+
}
|
|
6346
|
+
console.log(`Algorithm ${id} archived.`);
|
|
6347
|
+
});
|
|
6348
|
+
for (const action of ["pause", "activate"]) {
|
|
6349
|
+
cmd.command(action).description(`${action.charAt(0).toUpperCase() + action.slice(1)} a bidder algorithm.`).argument("<id>", "Algorithm ID").action(async (id) => {
|
|
6350
|
+
const resp = await apiRequest({
|
|
6351
|
+
method: "PATCH",
|
|
6352
|
+
path: `${dspPrefix()}/bidder-algorithms/${id}/${action}`
|
|
6353
|
+
});
|
|
6354
|
+
const json = await resp.json().catch(() => ({}));
|
|
6355
|
+
if (!resp.ok) {
|
|
6356
|
+
console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
|
|
6357
|
+
process.exit(1);
|
|
6358
|
+
}
|
|
6359
|
+
console.log(`Algorithm ${id} ${action}d.`);
|
|
6360
|
+
});
|
|
6361
|
+
}
|
|
6362
|
+
return cmd;
|
|
6363
|
+
}
|
|
6364
|
+
async function buildAlgorithmBody(opts, creating) {
|
|
6365
|
+
if (opts.fromJson) {
|
|
6366
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
6367
|
+
return JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6368
|
+
}
|
|
6369
|
+
const body = {};
|
|
6370
|
+
if (opts.name)
|
|
6371
|
+
body.name = opts.name;
|
|
6372
|
+
if (opts.description)
|
|
6373
|
+
body.description = opts.description;
|
|
6374
|
+
if (opts.optimizationGoal)
|
|
6375
|
+
body.optimizationGoal = String(opts.optimizationGoal).toUpperCase();
|
|
6376
|
+
if (opts.targetValue !== undefined)
|
|
6377
|
+
body.targetValue = Number(opts.targetValue);
|
|
6378
|
+
if (opts.conversionGoalId)
|
|
6379
|
+
body.conversionGoalId = opts.conversionGoalId;
|
|
6380
|
+
if (creating) {
|
|
6381
|
+
if (!body.name) {
|
|
6382
|
+
console.error("Error: --name is required.");
|
|
6383
|
+
process.exit(1);
|
|
6384
|
+
}
|
|
6385
|
+
if (!body.optimizationGoal) {
|
|
6386
|
+
console.error("Error: --optimization-goal is required.");
|
|
6387
|
+
process.exit(1);
|
|
6388
|
+
}
|
|
6389
|
+
if (body.targetValue === undefined) {
|
|
6390
|
+
console.error("Error: --target-value is required.");
|
|
6391
|
+
process.exit(1);
|
|
6392
|
+
}
|
|
6393
|
+
}
|
|
6394
|
+
return body;
|
|
6395
|
+
}
|
|
6396
|
+
|
|
6397
|
+
// src/commands/payouts.ts
|
|
6398
|
+
function createPayoutsCommand() {
|
|
6399
|
+
const cmd = new Command("payouts").description(`Payout management (SSP)
|
|
6400
|
+
|
|
6401
|
+
Requires: PUBLISHER capability.`).addHelpText("after", `
|
|
6402
|
+
Examples:
|
|
6403
|
+
$ a8techads payouts balance
|
|
6404
|
+
$ a8techads payouts account
|
|
6405
|
+
$ a8techads payouts list --limit 10
|
|
6406
|
+
$ a8techads payouts request --amount 50 --method usdt --yes`);
|
|
6407
|
+
addFormatOption(cmd.command("balance").description("Show current payout balance.")).action(async (opts) => {
|
|
6408
|
+
const resp = await apiRequest({ path: `${sspPrefix()}/payouts/balance` });
|
|
6409
|
+
const json = await resp.json();
|
|
6410
|
+
if (!resp.ok) {
|
|
6411
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6412
|
+
process.exit(1);
|
|
6413
|
+
}
|
|
6414
|
+
printDetail(json.data ?? json, opts.format);
|
|
6415
|
+
});
|
|
6416
|
+
addFormatOption(cmd.command("account").description("Show payout account details.")).action(async (opts) => {
|
|
6417
|
+
const resp = await apiRequest({ path: `${sspPrefix()}/payouts/account` });
|
|
6418
|
+
const json = await resp.json();
|
|
6419
|
+
if (!resp.ok) {
|
|
6420
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6421
|
+
process.exit(1);
|
|
6422
|
+
}
|
|
6423
|
+
printDetail(json.data ?? json, opts.format);
|
|
6424
|
+
});
|
|
6425
|
+
addFormatOption(cmd.command("list").description("List payout history.").option("--limit <n>", "Max results", "20").option("--status <status>", "Filter by status")).action(async (opts) => {
|
|
6426
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
6427
|
+
if (opts.status)
|
|
6428
|
+
params.set("status", opts.status);
|
|
6429
|
+
const resp = await apiRequest({ path: `${sspPrefix()}/payouts/list?${params}` });
|
|
6430
|
+
const json = await resp.json();
|
|
6431
|
+
if (!resp.ok) {
|
|
6432
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6433
|
+
process.exit(1);
|
|
6434
|
+
}
|
|
6435
|
+
const columns = [
|
|
6436
|
+
{ key: "id", header: "ID", width: 36 },
|
|
6437
|
+
{ key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
|
|
6438
|
+
{ key: "status", header: "STATUS", width: 14 },
|
|
6439
|
+
{ key: "method", header: "METHOD", width: 10 },
|
|
6440
|
+
{ key: "requestedAt", header: "REQUESTED_AT", width: 20 }
|
|
6441
|
+
];
|
|
6442
|
+
const rows = (json.data ?? json).map((t) => ({
|
|
6443
|
+
id: t.id,
|
|
6444
|
+
amount: t.amount,
|
|
6445
|
+
status: t.status,
|
|
6446
|
+
method: t.method,
|
|
6447
|
+
requestedAt: t.requestedAt ?? t.requested_at
|
|
6448
|
+
}));
|
|
6449
|
+
printData(rows, columns, opts.format);
|
|
6450
|
+
});
|
|
6451
|
+
cmd.command("request").description("Request a payout.").option("--amount <dollars>", "Payout amount in dollars (required)").option("--method <method>", "Payout method", "usdt").option("--wallet <address>", "Wallet address").option("--network <network>", "Network for crypto payouts", "TRC20").option("--yes", "Skip confirmation prompt").action(async (opts) => {
|
|
6452
|
+
if (!opts.amount) {
|
|
6453
|
+
console.error("Error: --amount is required.");
|
|
6454
|
+
process.exit(1);
|
|
6455
|
+
}
|
|
6456
|
+
const amount = Number(opts.amount);
|
|
6457
|
+
if (isNaN(amount) || amount <= 0) {
|
|
6458
|
+
console.error("Error: Amount must be a positive number.");
|
|
6459
|
+
process.exit(1);
|
|
6460
|
+
}
|
|
6461
|
+
if (!opts.yes) {
|
|
6462
|
+
const rl = await import("readline");
|
|
6463
|
+
const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
|
|
6464
|
+
const answer = await new Promise((resolve) => iface.question(`Request payout of $${amount.toFixed(2)}? (y/N) `, resolve));
|
|
6465
|
+
iface.close();
|
|
6466
|
+
if (answer.toLowerCase() !== "y") {
|
|
6467
|
+
console.log("Cancelled.");
|
|
6468
|
+
process.exit(0);
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
6471
|
+
let resp;
|
|
6472
|
+
if (opts.method === "usdt") {
|
|
6473
|
+
const body = { amount, walletAddress: opts.wallet, network: opts.network };
|
|
6474
|
+
resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/payments/usdt/payout`, body });
|
|
6475
|
+
} else {
|
|
6476
|
+
resp = await apiRequest({ method: "POST", path: `${sspPrefix()}/payouts/request`, body: { amount } });
|
|
6477
|
+
}
|
|
6478
|
+
const json = await resp.json();
|
|
6479
|
+
if (!resp.ok) {
|
|
6480
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6481
|
+
process.exit(1);
|
|
6482
|
+
}
|
|
6483
|
+
console.log(`Payout of $${amount.toFixed(2)} requested successfully.`);
|
|
6484
|
+
});
|
|
6485
|
+
return cmd;
|
|
6486
|
+
}
|
|
6487
|
+
|
|
6488
|
+
// src/commands/statements.ts
|
|
6489
|
+
function createStatementsCommand() {
|
|
6490
|
+
const cmd = new Command("statements").description(`Statement management (SSP)
|
|
6491
|
+
|
|
6492
|
+
Requires: PUBLISHER capability.`).addHelpText("after", `
|
|
6493
|
+
Examples:
|
|
6494
|
+
$ a8techads statements list
|
|
6495
|
+
$ a8techads statements get <id>`);
|
|
6496
|
+
addFormatOption(cmd.command("list").description("List statements.").option("--limit <n>", "Max results", "20")).action(async (opts) => {
|
|
6497
|
+
const params = new URLSearchParams({ limit: opts.limit });
|
|
6498
|
+
const resp = await apiRequest({ path: `${sspPrefix()}/statements?${params}` });
|
|
6499
|
+
const json = await resp.json();
|
|
6500
|
+
if (!resp.ok) {
|
|
6501
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6502
|
+
process.exit(1);
|
|
6503
|
+
}
|
|
6504
|
+
const columns = [
|
|
6505
|
+
{ key: "id", header: "ID", width: 36 },
|
|
6506
|
+
{ key: "number", header: "NUMBER", width: 14 },
|
|
6507
|
+
{ key: "period", header: "PERIOD", width: 16 },
|
|
6508
|
+
{ key: "amount", header: "AMOUNT", width: 12, format: (v) => v != null ? `$${Number(v).toFixed(2)}` : "-" },
|
|
6509
|
+
{ key: "status", header: "STATUS", width: 12 }
|
|
6510
|
+
];
|
|
6511
|
+
const rows = (json.data ?? json).map((t) => ({
|
|
6512
|
+
id: t.id,
|
|
6513
|
+
number: t.number,
|
|
6514
|
+
period: t.period,
|
|
6515
|
+
amount: t.amount,
|
|
6516
|
+
status: t.status
|
|
6517
|
+
}));
|
|
6518
|
+
printData(rows, columns, opts.format);
|
|
6519
|
+
});
|
|
6520
|
+
addFormatOption(cmd.command("get <id>").description("Show statement details.")).action(async (id, opts) => {
|
|
6521
|
+
const resp = await apiRequest({ path: `${sspPrefix()}/statements/${id}` });
|
|
6522
|
+
const json = await resp.json();
|
|
6523
|
+
if (!resp.ok) {
|
|
6524
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6525
|
+
process.exit(1);
|
|
6526
|
+
}
|
|
6527
|
+
printDetail(json.data ?? json, opts.format);
|
|
6528
|
+
});
|
|
6529
|
+
return cmd;
|
|
6530
|
+
}
|
|
6531
|
+
|
|
6532
|
+
// src/commands/simulator.ts
|
|
6533
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
6534
|
+
async function confirmAction2(message, yes) {
|
|
6535
|
+
if (yes)
|
|
6536
|
+
return;
|
|
6537
|
+
const rl = await import("readline");
|
|
6538
|
+
const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
|
|
6539
|
+
const answer = await new Promise((resolve) => iface.question(`${message} (y/N) `, resolve));
|
|
6540
|
+
iface.close();
|
|
6541
|
+
if (answer.toLowerCase() !== "y") {
|
|
6542
|
+
console.log("Cancelled.");
|
|
6543
|
+
process.exit(0);
|
|
6544
|
+
}
|
|
6545
|
+
}
|
|
6546
|
+
function createSimulatorCommand() {
|
|
6547
|
+
const cmd = new Command("simulator").description(`Simulator control and inspection
|
|
6548
|
+
|
|
6549
|
+
Used for replay and traffic verification workflows.`).addHelpText("after", `
|
|
6550
|
+
Examples:
|
|
6551
|
+
$ a8techads simulator status
|
|
6552
|
+
$ a8techads simulator start
|
|
6553
|
+
$ a8techads simulator stop --yes
|
|
6554
|
+
$ a8techads simulator reset --yes
|
|
6555
|
+
$ a8techads simulator config show
|
|
6556
|
+
$ a8techads simulator config update --from-json ./simulator-config.json`);
|
|
6557
|
+
addFormatOption(cmd.command("status").description("Show simulator runtime status and counters.")).action(async (opts) => {
|
|
6558
|
+
const resp = await simulatorRequest("GET", "/api/v1/simulator/status");
|
|
6559
|
+
const json = await resp.json();
|
|
6560
|
+
if (!resp.ok) {
|
|
6561
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6562
|
+
process.exit(1);
|
|
6563
|
+
}
|
|
6564
|
+
printDetail(json, opts.format);
|
|
6565
|
+
});
|
|
6566
|
+
cmd.command("start").description("Start the simulator.").option("--from-json <file>", "Optional JSON file used as start config override").action(async (opts) => {
|
|
6567
|
+
let body = undefined;
|
|
6568
|
+
if (opts.fromJson) {
|
|
6569
|
+
body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6570
|
+
}
|
|
6571
|
+
const resp = await simulatorRequest("POST", "/api/v1/simulator/start", body);
|
|
6572
|
+
const json = await resp.json();
|
|
6573
|
+
if (!resp.ok) {
|
|
6574
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6575
|
+
process.exit(1);
|
|
6576
|
+
}
|
|
6577
|
+
console.log(json.message ?? "Simulator started.");
|
|
6578
|
+
});
|
|
6579
|
+
cmd.command("stop").description("Stop the simulator.").option("--yes", "Skip confirmation prompt").action(async (opts) => {
|
|
6580
|
+
await confirmAction2("Stop simulator?", opts.yes);
|
|
6581
|
+
const resp = await simulatorRequest("POST", "/api/v1/simulator/stop");
|
|
6582
|
+
const json = await resp.json();
|
|
6583
|
+
if (!resp.ok) {
|
|
6584
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6585
|
+
process.exit(1);
|
|
6586
|
+
}
|
|
6587
|
+
console.log(json.message ?? "Simulator stopped.");
|
|
6588
|
+
});
|
|
6589
|
+
cmd.command("reset").description("Reset simulator counters and stats.").option("--yes", "Skip confirmation prompt").action(async (opts) => {
|
|
6590
|
+
await confirmAction2("Reset simulator stats?", opts.yes);
|
|
6591
|
+
const resp = await simulatorRequest("POST", "/api/v1/simulator/reset");
|
|
6592
|
+
const json = await resp.json();
|
|
6593
|
+
if (!resp.ok) {
|
|
6594
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6595
|
+
process.exit(1);
|
|
6596
|
+
}
|
|
6597
|
+
console.log(json.message ?? "Simulator stats reset.");
|
|
6598
|
+
});
|
|
6599
|
+
const config = cmd.command("config").description("Simulator config management");
|
|
6600
|
+
addFormatOption(config.command("show").description("Show current simulator config.")).action(async (opts) => {
|
|
6601
|
+
const resp = await simulatorRequest("GET", "/api/v1/simulator/config");
|
|
6602
|
+
const json = await resp.json();
|
|
6603
|
+
if (!resp.ok) {
|
|
6604
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6605
|
+
process.exit(1);
|
|
6606
|
+
}
|
|
6607
|
+
printDetail(json, opts.format);
|
|
6608
|
+
});
|
|
6609
|
+
config.command("update").description("Update simulator config from a JSON file.").requiredOption("--from-json <file>", "JSON file path").action(async (opts) => {
|
|
6610
|
+
const body = JSON.parse(readFileSync4(opts.fromJson, "utf-8"));
|
|
6611
|
+
const resp = await simulatorRequest("POST", "/api/v1/simulator/config", body);
|
|
6612
|
+
const json = await resp.json();
|
|
6613
|
+
if (!resp.ok) {
|
|
6614
|
+
console.error(`Error: ${json.error ?? resp.statusText}`);
|
|
6615
|
+
process.exit(1);
|
|
6616
|
+
}
|
|
6617
|
+
console.log("Simulator config updated.");
|
|
6618
|
+
if (json.config) {
|
|
6619
|
+
printDetail(json.config, "table");
|
|
6620
|
+
}
|
|
6621
|
+
});
|
|
6622
|
+
return cmd;
|
|
6623
|
+
}
|
|
6624
|
+
async function simulatorRequest(method, path, body) {
|
|
6625
|
+
const creds = loadCredentials();
|
|
6626
|
+
const profile = getCurrentProfile(creds);
|
|
6627
|
+
if (!profile) {
|
|
6628
|
+
throw new Error('No active profile. Run "a8techads auth login" to authenticate.');
|
|
6629
|
+
}
|
|
6630
|
+
const baseUrl = toConsoleApiBase(profile.api_url);
|
|
6631
|
+
const request = await buildAuthenticatedRequest({ method, path, body, baseUrl });
|
|
6632
|
+
return fetch(request.url, request.init);
|
|
6633
|
+
}
|
|
6634
|
+
function toConsoleApiBase(apiUrl) {
|
|
6635
|
+
const url = new URL(apiUrl);
|
|
6636
|
+
if (url.hostname.startsWith("api.")) {
|
|
6637
|
+
url.hostname = url.hostname.replace(/^api\./, "console.");
|
|
6638
|
+
}
|
|
6639
|
+
return url.origin;
|
|
4845
6640
|
}
|
|
4846
6641
|
|
|
4847
6642
|
// src/index.ts
|
|
4848
6643
|
function createProgram() {
|
|
4849
|
-
const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.
|
|
6644
|
+
const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.1").addHelpText("after", `
|
|
4850
6645
|
Command Groups:
|
|
4851
6646
|
auth Authentication (login, logout, token, status)
|
|
4852
6647
|
profile Multi-profile management
|
|
@@ -4854,13 +6649,21 @@ Command Groups:
|
|
|
4854
6649
|
audiences Audience management (DSP)
|
|
4855
6650
|
campaigns Campaign management (DSP)
|
|
4856
6651
|
variations Ad variation management (DSP)
|
|
6652
|
+
conversion-goals Conversion goal management (DSP)
|
|
6653
|
+
media-assets Media library management (DSP)
|
|
6654
|
+
algorithms Bidder algorithm management (DSP)
|
|
4857
6655
|
sites Site management (SSP)
|
|
4858
6656
|
zones Ad zone management (SSP)
|
|
4859
6657
|
reports Analytics and reporting
|
|
4860
6658
|
billing Billing and payments (DSP)
|
|
4861
6659
|
invoices Invoice management (DSP)
|
|
6660
|
+
payouts Payout management (SSP)
|
|
6661
|
+
statements Statement management (SSP)
|
|
6662
|
+
simulator Simulator control and replay support
|
|
4862
6663
|
users Team member management
|
|
4863
6664
|
settings Tenant settings
|
|
6665
|
+
admin Platform administration (Console)
|
|
6666
|
+
external-ssp External SSP partner management (Console)
|
|
4864
6667
|
|
|
4865
6668
|
Getting Started:
|
|
4866
6669
|
$ a8techads auth login # Authenticate
|
|
@@ -4874,14 +6677,21 @@ Getting Started:
|
|
|
4874
6677
|
program2.addCommand(createAudiencesCommand());
|
|
4875
6678
|
program2.addCommand(createCampaignsCommand());
|
|
4876
6679
|
program2.addCommand(createVariationsCommand());
|
|
6680
|
+
program2.addCommand(createMediaAssetsCommand());
|
|
6681
|
+
program2.addCommand(createConversionGoalsCommand());
|
|
6682
|
+
program2.addCommand(createAlgorithmsCommand());
|
|
4877
6683
|
program2.addCommand(createSitesCommand());
|
|
4878
6684
|
program2.addCommand(createZonesCommand());
|
|
4879
6685
|
program2.addCommand(createReportsCommand());
|
|
4880
6686
|
program2.addCommand(createBillingCommand());
|
|
6687
|
+
program2.addCommand(createPayoutsCommand());
|
|
6688
|
+
program2.addCommand(createStatementsCommand());
|
|
6689
|
+
program2.addCommand(createSimulatorCommand());
|
|
4881
6690
|
program2.addCommand(createUsersCommand());
|
|
4882
6691
|
program2.addCommand(createSettingsCommand());
|
|
4883
6692
|
program2.addCommand(createInvoicesCommand());
|
|
4884
6693
|
program2.addCommand(createAdminCommand());
|
|
6694
|
+
program2.addCommand(createExternalSspCommand());
|
|
4885
6695
|
return program2;
|
|
4886
6696
|
}
|
|
4887
6697
|
|