@wavyx/pdcli 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/README.md +9 -0
- package/oclif.manifest.json +1960 -706
- package/package.json +4 -1
- package/src/base-command.js +22 -3
- package/src/commands/activity/list.js +48 -6
- package/src/commands/deal/history.js +73 -0
- package/src/commands/deal/list.js +41 -3
- package/src/commands/deal/product/add.js +69 -0
- package/src/commands/deal/product/list.js +56 -0
- package/src/commands/deal/product/remove.js +52 -0
- package/src/commands/deal/product/update.js +78 -0
- package/src/commands/funnel.js +7 -49
- package/src/commands/org/list.js +41 -3
- package/src/commands/person/list.js +41 -3
- package/src/commands/product/list.js +36 -3
- package/src/commands/user/find.js +50 -0
- package/src/commands/user/list.js +36 -0
- package/src/commands/webhook/create.js +8 -2
- package/src/lib/aliases.js +85 -5
- package/src/lib/changelog.js +85 -0
- package/src/lib/client.js +135 -117
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wavyx/pdcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -82,6 +82,9 @@
|
|
|
82
82
|
},
|
|
83
83
|
"alias": {
|
|
84
84
|
"description": "Command shortcuts (alias set/list/unset)"
|
|
85
|
+
},
|
|
86
|
+
"deal:product": {
|
|
87
|
+
"description": "Manage products (line items) on a deal"
|
|
85
88
|
}
|
|
86
89
|
},
|
|
87
90
|
"hooks": {
|
package/src/base-command.js
CHANGED
|
@@ -24,7 +24,7 @@ export default class BaseCommand extends Command {
|
|
|
24
24
|
}),
|
|
25
25
|
'resolve-fields': Flags.boolean({
|
|
26
26
|
description:
|
|
27
|
-
'Resolve custom-field hash keys to names (and option ids to labels) in json/yaml/csv output of
|
|
27
|
+
'Resolve custom-field hash keys to names (and option ids to labels) in json/yaml/csv output of get and core list commands',
|
|
28
28
|
helpGroup: 'GLOBAL',
|
|
29
29
|
default: false,
|
|
30
30
|
}),
|
|
@@ -151,12 +151,31 @@ export default class BaseCommand extends Command {
|
|
|
151
151
|
/**
|
|
152
152
|
* @param {object | object[]} data
|
|
153
153
|
* @param {Record<string, import('./lib/output/table.js').Column>} columns
|
|
154
|
+
* @param {{ entity?: string }} [options] entity context enables
|
|
155
|
+
* --resolve-fields custom-field resolution on machine-format lists
|
|
154
156
|
*/
|
|
155
|
-
async outputResults(data, columns) {
|
|
157
|
+
async outputResults(data, columns, { entity } = {}) {
|
|
158
|
+
if (
|
|
159
|
+
entity &&
|
|
160
|
+
this.flags['resolve-fields'] &&
|
|
161
|
+
this.resolveFormat() !== 'table' &&
|
|
162
|
+
Array.isArray(data) &&
|
|
163
|
+
data.some((row) => row?.custom_fields)
|
|
164
|
+
) {
|
|
165
|
+
const { getFields, makeResolver } = await import('./lib/fields.js')
|
|
166
|
+
// getFields is memoized per run — one defs fetch covers the whole list.
|
|
167
|
+
const resolver = makeResolver(await getFields(this.apiClient, entity))
|
|
168
|
+
data = data.map((row) =>
|
|
169
|
+
row?.custom_fields ? resolver.resolveCustomFields(row) : row,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
156
173
|
if (this.flags.jq) {
|
|
157
174
|
// node-jq ships a native binary — load it only when actually used.
|
|
175
|
+
// Single records pass UNWRAPPED: `--jq .id` works on a get without
|
|
176
|
+
// the historical `.[0]` indirection (changed in 0.9.0).
|
|
158
177
|
const { run } = await import('node-jq')
|
|
159
|
-
const input = JSON.stringify(
|
|
178
|
+
const input = JSON.stringify(data)
|
|
160
179
|
const result = await run(this.flags.jq, input, {
|
|
161
180
|
input: 'string',
|
|
162
181
|
output: 'pretty',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core'
|
|
2
2
|
import BaseCommand from '../../base-command.js'
|
|
3
3
|
import { collectPages } from '../../lib/pagination.js'
|
|
4
|
+
import { CliError } from '../../lib/errors.js'
|
|
4
5
|
|
|
5
6
|
const columns = {
|
|
6
7
|
id: { header: 'ID' },
|
|
@@ -27,7 +28,9 @@ export default class ActivityListCommand extends BaseCommand {
|
|
|
27
28
|
deal: Flags.integer({ description: 'Filter by deal ID' }),
|
|
28
29
|
person: Flags.integer({ description: 'Filter by person ID' }),
|
|
29
30
|
org: Flags.integer({ description: 'Filter by organization ID' }),
|
|
30
|
-
type: Flags.string({
|
|
31
|
+
type: Flags.string({
|
|
32
|
+
description: 'Filter by activity type key (applied client-side)',
|
|
33
|
+
}),
|
|
31
34
|
done: Flags.boolean({
|
|
32
35
|
description: 'Only completed activities',
|
|
33
36
|
exclusive: ['todo'],
|
|
@@ -36,26 +39,65 @@ export default class ActivityListCommand extends BaseCommand {
|
|
|
36
39
|
description: 'Only open (not done) activities',
|
|
37
40
|
exclusive: ['done'],
|
|
38
41
|
}),
|
|
42
|
+
filter: Flags.integer({ description: 'Filter by saved filter ID' }),
|
|
43
|
+
ids: Flags.string({
|
|
44
|
+
description: 'Comma-separated IDs to fetch (max 100)',
|
|
45
|
+
// The API silently drops `ids` when filter_id is present — refuse
|
|
46
|
+
// the combination instead (matches deal bulk-update).
|
|
47
|
+
exclusive: ['filter'],
|
|
48
|
+
}),
|
|
49
|
+
'sort-by': Flags.string({
|
|
50
|
+
description: 'Sort field',
|
|
51
|
+
options: ['id', 'update_time', 'add_time', 'due_date'],
|
|
52
|
+
}),
|
|
53
|
+
'sort-direction': Flags.string({
|
|
54
|
+
description: 'Sort direction',
|
|
55
|
+
options: ['asc', 'desc'],
|
|
56
|
+
}),
|
|
57
|
+
'updated-since': Flags.string({
|
|
58
|
+
description:
|
|
59
|
+
'Only items updated at/after this RFC3339 time (no fractional seconds)',
|
|
60
|
+
}),
|
|
61
|
+
'updated-until': Flags.string({
|
|
62
|
+
description:
|
|
63
|
+
'Only items updated before this RFC3339 time (no fractional seconds)',
|
|
64
|
+
}),
|
|
39
65
|
}
|
|
40
66
|
|
|
41
67
|
async run() {
|
|
42
68
|
const { flags } = await this.parse(ActivityListCommand)
|
|
43
|
-
const limit = flags.limit ??
|
|
69
|
+
const limit = flags.limit ?? 500
|
|
70
|
+
|
|
71
|
+
const idList = flags.ids
|
|
72
|
+
?.split(',')
|
|
73
|
+
.map((v) => v.trim())
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
if (idList && idList.length > 100) {
|
|
76
|
+
throw new CliError('--ids accepts at most 100 IDs', { exitCode: 64 })
|
|
77
|
+
}
|
|
44
78
|
|
|
45
79
|
const query = {
|
|
46
80
|
owner_id: flags.owner,
|
|
47
81
|
deal_id: flags.deal,
|
|
48
82
|
person_id: flags.person,
|
|
49
83
|
org_id: flags.org,
|
|
50
|
-
type: flags.type,
|
|
51
84
|
done: flags.done ? true : flags.todo ? false : undefined,
|
|
52
|
-
|
|
85
|
+
filter_id: flags.filter,
|
|
86
|
+
ids: idList?.join(','),
|
|
87
|
+
sort_by: flags['sort-by'],
|
|
88
|
+
sort_direction: flags['sort-direction'],
|
|
89
|
+
updated_since: flags['updated-since'],
|
|
90
|
+
updated_until: flags['updated-until'],
|
|
91
|
+
limit: Math.min(limit, 500),
|
|
53
92
|
}
|
|
54
93
|
|
|
55
|
-
|
|
94
|
+
let items = await collectPages(
|
|
56
95
|
this.apiClient.pageV2('/api/v2/activities', query),
|
|
57
96
|
limit,
|
|
58
97
|
)
|
|
59
|
-
|
|
98
|
+
// The v2 activities endpoint has no `type` query param (it rejects
|
|
99
|
+
// unknown params with a 400) — filter client-side instead.
|
|
100
|
+
if (flags.type) items = items.filter((a) => a.type === flags.type)
|
|
101
|
+
await this.outputResults(items, columns, { entity: 'activity' })
|
|
60
102
|
}
|
|
61
103
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core'
|
|
2
|
+
import BaseCommand from '../../base-command.js'
|
|
3
|
+
import { fetchChangelog } from '../../lib/changelog.js'
|
|
4
|
+
import { getFields, makeResolver } from '../../lib/fields.js'
|
|
5
|
+
|
|
6
|
+
const columns = {
|
|
7
|
+
time: { header: 'Time' },
|
|
8
|
+
field_key: { header: 'Field' },
|
|
9
|
+
old_value: { header: 'Old' },
|
|
10
|
+
new_value: { header: 'New' },
|
|
11
|
+
actor_user_id: { header: 'Actor' },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default class DealHistoryCommand extends BaseCommand {
|
|
15
|
+
static description =
|
|
16
|
+
'Field-change history for a deal, newest-first (the API’s native order)'
|
|
17
|
+
|
|
18
|
+
static examples = [
|
|
19
|
+
'<%= config.bin %> deal history 42',
|
|
20
|
+
'<%= config.bin %> deal history 42 --field stage_id',
|
|
21
|
+
'<%= config.bin %> deal history 42 --limit 20 --resolve-fields',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
static args = {
|
|
25
|
+
id: Args.integer({ required: true, description: 'Deal ID' }),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static flags = {
|
|
29
|
+
...BaseCommand.baseFlags,
|
|
30
|
+
field: Flags.string({
|
|
31
|
+
description: 'Show only changes to this field key (e.g. stage_id)',
|
|
32
|
+
}),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async run() {
|
|
36
|
+
const { args, flags } = await this.parse(DealHistoryCommand)
|
|
37
|
+
|
|
38
|
+
// A field filter drops rows client-side, so the fetch can't be bounded
|
|
39
|
+
// by --limit in that case; otherwise cap the API page directly.
|
|
40
|
+
const fetchLimit = flags.field ? undefined : flags.limit
|
|
41
|
+
let rows = await fetchChangelog(this.apiClient, args.id, {
|
|
42
|
+
limit: fetchLimit,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if (flags.field) {
|
|
46
|
+
rows = rows.filter((r) => r.field_key === flags.field)
|
|
47
|
+
}
|
|
48
|
+
if (flags.limit != null) {
|
|
49
|
+
rows = rows.slice(0, flags.limit)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The Field column is the one place hash keys appear as DATA — resolve
|
|
53
|
+
// them (and enum/set option ids) to names under --resolve-fields.
|
|
54
|
+
if (flags['resolve-fields'] && rows.length > 0) {
|
|
55
|
+
const resolver = makeResolver(await getFields(this.apiClient, 'deal'))
|
|
56
|
+
// changelog values are stringified — option ids resolve numerically
|
|
57
|
+
const label = (key, value) =>
|
|
58
|
+
resolver.optionIdToLabel(key, Number(value)) ?? value
|
|
59
|
+
rows = rows.map((r) => {
|
|
60
|
+
const name = resolver.keyToName(r.field_key)
|
|
61
|
+
if (!name) return r
|
|
62
|
+
return {
|
|
63
|
+
...r,
|
|
64
|
+
field_key: name,
|
|
65
|
+
old_value: label(r.field_key, r.old_value),
|
|
66
|
+
new_value: label(r.field_key, r.new_value),
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await this.outputResults(rows, columns)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core'
|
|
2
2
|
import BaseCommand from '../../base-command.js'
|
|
3
3
|
import { collectPages } from '../../lib/pagination.js'
|
|
4
|
+
import { CliError } from '../../lib/errors.js'
|
|
4
5
|
|
|
5
6
|
const columns = {
|
|
6
7
|
id: { header: 'ID' },
|
|
@@ -37,11 +38,42 @@ export default class DealListCommand extends BaseCommand {
|
|
|
37
38
|
owner: Flags.integer({ description: 'Filter by owner (user) ID' }),
|
|
38
39
|
person: Flags.integer({ description: 'Filter by person ID' }),
|
|
39
40
|
org: Flags.integer({ description: 'Filter by organization ID' }),
|
|
41
|
+
filter: Flags.integer({ description: 'Filter by saved filter ID' }),
|
|
42
|
+
ids: Flags.string({
|
|
43
|
+
description: 'Comma-separated IDs to fetch (max 100)',
|
|
44
|
+
// The API silently drops `ids` when filter_id is present — refuse
|
|
45
|
+
// the combination instead (matches deal bulk-update).
|
|
46
|
+
exclusive: ['filter'],
|
|
47
|
+
}),
|
|
48
|
+
'sort-by': Flags.string({
|
|
49
|
+
description: 'Sort field',
|
|
50
|
+
options: ['id', 'update_time', 'add_time'],
|
|
51
|
+
}),
|
|
52
|
+
'sort-direction': Flags.string({
|
|
53
|
+
description: 'Sort direction',
|
|
54
|
+
options: ['asc', 'desc'],
|
|
55
|
+
}),
|
|
56
|
+
'updated-since': Flags.string({
|
|
57
|
+
description:
|
|
58
|
+
'Only items updated at/after this RFC3339 time (no fractional seconds)',
|
|
59
|
+
}),
|
|
60
|
+
'updated-until': Flags.string({
|
|
61
|
+
description:
|
|
62
|
+
'Only items updated before this RFC3339 time (no fractional seconds)',
|
|
63
|
+
}),
|
|
40
64
|
}
|
|
41
65
|
|
|
42
66
|
async run() {
|
|
43
67
|
const { flags } = await this.parse(DealListCommand)
|
|
44
|
-
const limit = flags.limit ??
|
|
68
|
+
const limit = flags.limit ?? 500
|
|
69
|
+
|
|
70
|
+
const idList = flags.ids
|
|
71
|
+
?.split(',')
|
|
72
|
+
.map((v) => v.trim())
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
if (idList && idList.length > 100) {
|
|
75
|
+
throw new CliError('--ids accepts at most 100 IDs', { exitCode: 64 })
|
|
76
|
+
}
|
|
45
77
|
|
|
46
78
|
const query = {
|
|
47
79
|
status: flags.status,
|
|
@@ -50,13 +82,19 @@ export default class DealListCommand extends BaseCommand {
|
|
|
50
82
|
owner_id: flags.owner,
|
|
51
83
|
person_id: flags.person,
|
|
52
84
|
org_id: flags.org,
|
|
53
|
-
|
|
85
|
+
filter_id: flags.filter,
|
|
86
|
+
ids: idList?.join(','),
|
|
87
|
+
sort_by: flags['sort-by'],
|
|
88
|
+
sort_direction: flags['sort-direction'],
|
|
89
|
+
updated_since: flags['updated-since'],
|
|
90
|
+
updated_until: flags['updated-until'],
|
|
91
|
+
limit: Math.min(limit, 500),
|
|
54
92
|
}
|
|
55
93
|
|
|
56
94
|
const items = await collectPages(
|
|
57
95
|
this.apiClient.pageV2('/api/v2/deals', query),
|
|
58
96
|
limit,
|
|
59
97
|
)
|
|
60
|
-
await this.outputResults(items, columns)
|
|
98
|
+
await this.outputResults(items, columns, { entity: 'deal' })
|
|
61
99
|
}
|
|
62
100
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core'
|
|
2
|
+
import BaseCommand from '../../../base-command.js'
|
|
3
|
+
import { CliError } from '../../../lib/errors.js'
|
|
4
|
+
import { buildWriteBody } from '../../../lib/input.js'
|
|
5
|
+
import { outputRecord } from '../../../lib/entity-view.js'
|
|
6
|
+
|
|
7
|
+
/** Coerce a numeric flag, failing with a clean input error on garbage. */
|
|
8
|
+
function num(name, value) {
|
|
9
|
+
if (value === undefined) return undefined
|
|
10
|
+
const n = Number(value)
|
|
11
|
+
if (!Number.isFinite(n)) {
|
|
12
|
+
throw new CliError(`Invalid number for --${name}: "${value}"`, {
|
|
13
|
+
exitCode: 64,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
return n
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default class DealProductAddCommand extends BaseCommand {
|
|
20
|
+
static description = 'Attach a product to a deal'
|
|
21
|
+
|
|
22
|
+
static examples = [
|
|
23
|
+
'<%= config.bin %> deal product add 42 --product 10 --price 90',
|
|
24
|
+
'<%= config.bin %> deal product add 42 --product 10 --price 90 --quantity 3',
|
|
25
|
+
'<%= config.bin %> deal product add 42 --product 10 --price 90 --discount 10 --discount-type percentage',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
static args = {
|
|
29
|
+
id: Args.integer({ required: true, description: 'Deal ID' }),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static flags = {
|
|
33
|
+
...BaseCommand.baseFlags,
|
|
34
|
+
product: Flags.integer({ required: true, description: 'Product ID' }),
|
|
35
|
+
price: Flags.string({
|
|
36
|
+
required: true,
|
|
37
|
+
description: 'Item price (per unit)',
|
|
38
|
+
}),
|
|
39
|
+
quantity: Flags.string({ description: 'Quantity', default: '1' }),
|
|
40
|
+
discount: Flags.string({ description: 'Discount value' }),
|
|
41
|
+
'discount-type': Flags.string({
|
|
42
|
+
description: 'Discount type',
|
|
43
|
+
options: ['percentage', 'amount'],
|
|
44
|
+
}),
|
|
45
|
+
tax: Flags.string({ description: 'Product tax percentage' }),
|
|
46
|
+
comments: Flags.string({ description: 'Comments' }),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async run() {
|
|
50
|
+
const { args, flags } = await this.parse(DealProductAddCommand)
|
|
51
|
+
|
|
52
|
+
const body = buildWriteBody({
|
|
53
|
+
typed: {
|
|
54
|
+
product_id: flags.product,
|
|
55
|
+
item_price: num('price', flags.price),
|
|
56
|
+
quantity: num('quantity', flags.quantity),
|
|
57
|
+
discount: num('discount', flags.discount),
|
|
58
|
+
discount_type: flags['discount-type'],
|
|
59
|
+
tax: num('tax', flags.tax),
|
|
60
|
+
comments: flags.comments,
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const res = await this.apiClient.post(`/api/v2/deals/${args.id}/products`, {
|
|
65
|
+
body,
|
|
66
|
+
})
|
|
67
|
+
await outputRecord(this, res.data)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core'
|
|
2
|
+
import BaseCommand from '../../../base-command.js'
|
|
3
|
+
import { collectPages } from '../../../lib/pagination.js'
|
|
4
|
+
|
|
5
|
+
const columns = {
|
|
6
|
+
id: { header: 'ID' },
|
|
7
|
+
product_id: { header: 'Product' },
|
|
8
|
+
name: { header: 'Name' },
|
|
9
|
+
item_price: { header: 'Item price' },
|
|
10
|
+
quantity: { header: 'Qty' },
|
|
11
|
+
discount: { header: 'Discount' },
|
|
12
|
+
sum: { header: 'Sum' },
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default class DealProductListCommand extends BaseCommand {
|
|
16
|
+
static description = 'List products attached to a deal'
|
|
17
|
+
|
|
18
|
+
static examples = [
|
|
19
|
+
'<%= config.bin %> deal product list 42',
|
|
20
|
+
'<%= config.bin %> deal product list 42 --sort-by add_time --sort-direction desc',
|
|
21
|
+
'<%= config.bin %> deal product list 42 --output json',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
static args = {
|
|
25
|
+
id: Args.integer({ required: true, description: 'Deal ID' }),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static flags = {
|
|
29
|
+
...BaseCommand.baseFlags,
|
|
30
|
+
'sort-by': Flags.string({
|
|
31
|
+
description: 'Field to sort by',
|
|
32
|
+
options: ['id', 'add_time', 'update_time', 'order_nr'],
|
|
33
|
+
}),
|
|
34
|
+
'sort-direction': Flags.string({
|
|
35
|
+
description: 'Sort direction',
|
|
36
|
+
options: ['asc', 'desc'],
|
|
37
|
+
}),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async run() {
|
|
41
|
+
const { args, flags } = await this.parse(DealProductListCommand)
|
|
42
|
+
const limit = flags.limit ?? 500
|
|
43
|
+
|
|
44
|
+
const query = {
|
|
45
|
+
sort_by: flags['sort-by'],
|
|
46
|
+
sort_direction: flags['sort-direction'],
|
|
47
|
+
limit: Math.min(limit, 500),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const items = await collectPages(
|
|
51
|
+
this.apiClient.pageV2(`/api/v2/deals/${args.id}/products`, query),
|
|
52
|
+
limit,
|
|
53
|
+
)
|
|
54
|
+
await this.outputResults(items, columns)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import BaseCommand from '../../../base-command.js'
|
|
4
|
+
import { confirmAction } from '../../../lib/confirm.js'
|
|
5
|
+
import { CliError } from '../../../lib/errors.js'
|
|
6
|
+
|
|
7
|
+
export default class DealProductRemoveCommand extends BaseCommand {
|
|
8
|
+
static description = 'Remove a product attached to a deal'
|
|
9
|
+
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> deal product remove 42 --attachment 3',
|
|
12
|
+
'<%= config.bin %> deal product remove 42 --attachment 3 --yes',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
static args = {
|
|
16
|
+
id: Args.integer({ required: true, description: 'Deal ID' }),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static flags = {
|
|
20
|
+
...BaseCommand.baseFlags,
|
|
21
|
+
attachment: Flags.integer({
|
|
22
|
+
required: true,
|
|
23
|
+
description: 'Deal-product (attachment) ID',
|
|
24
|
+
}),
|
|
25
|
+
yes: Flags.boolean({
|
|
26
|
+
char: 'y',
|
|
27
|
+
description: 'Skip the confirmation prompt',
|
|
28
|
+
default: false,
|
|
29
|
+
}),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async run() {
|
|
33
|
+
const { args, flags } = await this.parse(DealProductRemoveCommand)
|
|
34
|
+
|
|
35
|
+
const ok = await confirmAction(
|
|
36
|
+
`Remove product attachment ${flags.attachment} from deal ${args.id}?`,
|
|
37
|
+
flags.yes,
|
|
38
|
+
)
|
|
39
|
+
if (!ok) {
|
|
40
|
+
throw new CliError('Aborted', { exitCode: 1 })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await this.apiClient.del(
|
|
44
|
+
`/api/v2/deals/${args.id}/products/${flags.attachment}`,
|
|
45
|
+
)
|
|
46
|
+
this.log(
|
|
47
|
+
chalk.green(
|
|
48
|
+
`Removed product attachment ${flags.attachment} from deal ${args.id}`,
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core'
|
|
2
|
+
import BaseCommand from '../../../base-command.js'
|
|
3
|
+
import { buildWriteBody } from '../../../lib/input.js'
|
|
4
|
+
import { outputRecord } from '../../../lib/entity-view.js'
|
|
5
|
+
import { CliError } from '../../../lib/errors.js'
|
|
6
|
+
|
|
7
|
+
/** Coerce a numeric flag, failing with a clean input error on garbage. */
|
|
8
|
+
function num(name, value) {
|
|
9
|
+
if (value === undefined) return undefined
|
|
10
|
+
const n = Number(value)
|
|
11
|
+
if (!Number.isFinite(n)) {
|
|
12
|
+
throw new CliError(`Invalid number for --${name}: "${value}"`, {
|
|
13
|
+
exitCode: 64,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
return n
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default class DealProductUpdateCommand extends BaseCommand {
|
|
20
|
+
static description =
|
|
21
|
+
'Update a product attached to a deal (v2 PATCH — only provided fields change)'
|
|
22
|
+
|
|
23
|
+
static examples = [
|
|
24
|
+
'<%= config.bin %> deal product update 42 --attachment 3 --quantity 5',
|
|
25
|
+
'<%= config.bin %> deal product update 42 --attachment 3 --price 120',
|
|
26
|
+
'<%= config.bin %> deal product update 42 --attachment 3 --discount 15 --discount-type amount',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
static args = {
|
|
30
|
+
id: Args.integer({ required: true, description: 'Deal ID' }),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static flags = {
|
|
34
|
+
...BaseCommand.baseFlags,
|
|
35
|
+
attachment: Flags.integer({
|
|
36
|
+
required: true,
|
|
37
|
+
description: 'Deal-product (attachment) ID',
|
|
38
|
+
}),
|
|
39
|
+
product: Flags.integer({ description: 'Product ID' }),
|
|
40
|
+
price: Flags.string({ description: 'Item price (per unit)' }),
|
|
41
|
+
quantity: Flags.string({ description: 'Quantity' }),
|
|
42
|
+
discount: Flags.string({ description: 'Discount value' }),
|
|
43
|
+
'discount-type': Flags.string({
|
|
44
|
+
description: 'Discount type',
|
|
45
|
+
options: ['percentage', 'amount'],
|
|
46
|
+
}),
|
|
47
|
+
tax: Flags.string({ description: 'Product tax percentage' }),
|
|
48
|
+
comments: Flags.string({ description: 'Comments' }),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async run() {
|
|
52
|
+
const { args, flags } = await this.parse(DealProductUpdateCommand)
|
|
53
|
+
|
|
54
|
+
const body = buildWriteBody({
|
|
55
|
+
typed: {
|
|
56
|
+
product_id: flags.product,
|
|
57
|
+
item_price: num('price', flags.price),
|
|
58
|
+
quantity: num('quantity', flags.quantity),
|
|
59
|
+
discount: num('discount', flags.discount),
|
|
60
|
+
discount_type: flags['discount-type'],
|
|
61
|
+
tax: num('tax', flags.tax),
|
|
62
|
+
comments: flags.comments,
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (Object.keys(body).length === 0) {
|
|
67
|
+
throw new CliError('Nothing to update — pass at least one field flag', {
|
|
68
|
+
exitCode: 64,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const res = await this.apiClient.patch(
|
|
73
|
+
`/api/v2/deals/${args.id}/products/${flags.attachment}`,
|
|
74
|
+
{ body },
|
|
75
|
+
)
|
|
76
|
+
await outputRecord(this, res.data)
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/commands/funnel.js
CHANGED
|
@@ -3,12 +3,8 @@ import BaseCommand from '../base-command.js'
|
|
|
3
3
|
import { collectPages } from '../lib/pagination.js'
|
|
4
4
|
import { parsePeriod, formatApiDatetime } from '../lib/period.js'
|
|
5
5
|
import { computeFunnel, computeExactFunnel } from '../lib/analytics.js'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
/** Token cost of one GET /deals/{id}/changelog request (rate-limit budget). */
|
|
9
|
-
const CHANGELOG_COST = 20
|
|
10
|
-
/** Above this deal count, mining gets expensive — warn before proceeding. */
|
|
11
|
-
const MINE_WARN_THRESHOLD = 100
|
|
6
|
+
import { mineMany } from '../lib/changelog.js'
|
|
7
|
+
import { CliError } from '../lib/errors.js'
|
|
12
8
|
|
|
13
9
|
export default class FunnelCommand extends BaseCommand {
|
|
14
10
|
static description =
|
|
@@ -134,54 +130,16 @@ export default class FunnelCommand extends BaseCommand {
|
|
|
134
130
|
}
|
|
135
131
|
|
|
136
132
|
/**
|
|
137
|
-
* Mine real stage transitions from each deal's v1 changelog
|
|
138
|
-
*
|
|
139
|
-
* the
|
|
140
|
-
*
|
|
133
|
+
* Mine real stage transitions from each deal's v1 changelog (one request per
|
|
134
|
+
* deal, paced by the rate limiter), then compute the exact funnel. Mining,
|
|
135
|
+
* the large-set warning, and the skip-on-ApiError behavior live in the
|
|
136
|
+
* shared changelog lib so this command and `deal history` stay in step.
|
|
141
137
|
* @param {object[]} deals deals to mine (current stage_id needed per deal)
|
|
142
138
|
* @param {object[]} stages
|
|
143
139
|
* @param {number} pipelineId
|
|
144
140
|
*/
|
|
145
141
|
async mineExactFunnel(deals, stages, pipelineId) {
|
|
146
|
-
|
|
147
|
-
process.stderr.write(
|
|
148
|
-
`Mining stage history for ${deals.length} deals ` +
|
|
149
|
-
`(~${deals.length} requests, ${CHANGELOG_COST} tokens each); ` +
|
|
150
|
-
`rate limiting may slow this down.\n`,
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const transitionsByDeal = []
|
|
155
|
-
let skipped = 0
|
|
156
|
-
for (const deal of deals) {
|
|
157
|
-
try {
|
|
158
|
-
const rows = await collectPages(
|
|
159
|
-
this.apiClient.pageV2(`/api/v1/deals/${deal.id}/changelog`, {
|
|
160
|
-
limit: 500,
|
|
161
|
-
}),
|
|
162
|
-
)
|
|
163
|
-
transitionsByDeal.push({
|
|
164
|
-
dealId: deal.id,
|
|
165
|
-
stageId: deal.stage_id,
|
|
166
|
-
rows,
|
|
167
|
-
})
|
|
168
|
-
} catch (err) {
|
|
169
|
-
// One bad changelog request must not abort the whole mine: skip the
|
|
170
|
-
// deal, count it, and warn once after mining completes.
|
|
171
|
-
if (err instanceof ApiError) {
|
|
172
|
-
skipped++
|
|
173
|
-
continue
|
|
174
|
-
}
|
|
175
|
-
throw err
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (skipped > 0) {
|
|
180
|
-
process.stderr.write(
|
|
181
|
-
`skipped ${skipped} deal(s) whose changelog could not be fetched\n`,
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
|
|
142
|
+
const transitionsByDeal = await mineMany(this.apiClient, deals)
|
|
185
143
|
return computeExactFunnel(transitionsByDeal, stages, { pipelineId })
|
|
186
144
|
}
|
|
187
145
|
}
|