Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions plugins/metrics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,11 @@ Perform a Frequently Bought Together query on the Metrics API analysis endpoint.

```sh-session
USAGE
$ commercelayer metrics:fbt [-F <value>] [-i <value>...]
$ commercelayer metrics:fbt -i <value>...

FLAGS
-F, --filter=<value> the filter to apply to the query in JSON format (enclosed in single quotes)
-i, --in=<value>... a list of SKU or bundle IDs associated as line items to one or more orders
-i, --in=<value>... (required) a list of SKU or bundle IDs associated as line items to one or more orders (the orders
must contain all of them)

DESCRIPTION
perform a Frequently Bought Together query on the Metrics API analysis endpoint
Expand Down
39 changes: 23 additions & 16 deletions plugins/metrics/src/commands/metrics/fbt.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { clColor } from '@commercelayer/cli-core'
import * as cliux from '@commercelayer/cli-ux'
import { Flags } from '@oclif/core'
import { BaseFilterCommand } from '../../base'
import type { MetricsQueryFbt } from '../../common'
import { BaseCommand } from '../../base'
import type { MetricsFbtItem, MetricsQueryFbt } from '../../common'
import { metricsRequest } from '../../request'


export default class MetricsFbt extends BaseFilterCommand {
export default class MetricsFbt extends BaseCommand {

static operation = 'fbt'

Expand All @@ -18,15 +18,14 @@ export default class MetricsFbt extends BaseFilterCommand {
'commercelayer metrics:fbt --in xYZkjABcde,yzXKjYzaCx'
]

// The FBT query only takes the item IDs: the Metrics API refuses a query
// without them (400) and documents no other filter
static override flags = {
in: Flags.string({
char: 'i',
description: 'a list of SKU or bundle IDs associated as line items to one or more orders',
required: false,
multiple: true,
relationships: [
{ type: 'some', flags: ['in', 'filter'] }
],
description: 'a list of SKU or bundle IDs associated as line items to one or more orders (the orders must contain all of them)',
required: true,
multiple: true
})
}

Expand All @@ -37,31 +36,39 @@ export default class MetricsFbt extends BaseFilterCommand {

this.checkAcessTokenData(flags.accessToken, flags)

const ids = this.multivalFlag(flags.in)
const filterObject = this.filterFlag(flags.filter)
const ids = this.multivalFlag(flags.in).filter(Boolean)
if (ids.length === 0) this.error(`Specify at least one SKU or bundle ID with the ${clColor.cli.flag('--in')} flag`)

const query: MetricsQueryFbt = (ids.length > 0) ? {
const query: MetricsQueryFbt = {
filter: {
...filterObject,
line_items: {
item_ids: {
in: ids
}
}
}
} : {}
}

const response = await metricsRequest(MetricsFbt.operation, query, undefined, flags)

if (response.ok) {
cliux.action.stop(clColor.msg.success('Done'))
const jsonRes = await response.json()
const data = jsonRes.data
if (data?.length > 0) this.log(String(data))
const data: MetricsFbtItem[] = jsonRes.data ?? []
if (data.length > 0) this.printItems(data)
else this.log(clColor.dim(String('\nNo data found for the given SKU or bundle IDs\n')))
}
else await this.printResponse(response)

}


/** One line per item, from the most frequently bought together: name (type ID): number of orders */
private printItems(items: MetricsFbtItem[]): void {
this.log()
for (const item of items)
this.log(`${clColor.cyanBright(item.name || item.item_id)} ${clColor.dim(`(${item.type} ${item.item_id})`)}: ${clColor.yellow(item.value)}`)
this.log()
}

}
10 changes: 9 additions & 1 deletion plugins/metrics/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,15 @@ export type MetricsFilterFbt = {
}
}
export type MetricsQueryFbt = {
filter?: MetricsFilterFbt
filter: MetricsFilterFbt
}

/** An item of an FBT query result */
export type MetricsFbtItem = {
item_id: string
value: number
type: string
name?: string
}


Expand Down
36 changes: 29 additions & 7 deletions plugins/metrics/test/commands/metrics/fbt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,41 @@ import { AUTH, mockQuery, useMockedApi } from '../../helpers'
describe('metrics:fbt', () => {
useMockedApi()

it('asks for the items bought together with the given ones', async () => {
const q = mockQuery('analysis/fbt', 200, { data: [{ id: 'sku2', label: 'Socks', value: 5 }] })
it('asks for the items bought together with the given ones and prints one line per item', async () => {
const q = mockQuery('analysis/fbt', 200, {
data: [
{ item_id: 'sku2', value: 5, type: 'skus', name: 'Socks' },
{ item_id: 'bnd1', value: 2, type: 'bundles' },
],
})
const { stdout, error } = await runCommand(['metrics:fbt', ...AUTH, '-i', 'sku1,sku3'])
expect(error).to.equal(undefined)
expect(q.body).to.deep.equal({ filter: { line_items: { item_ids: { in: ['sku1', 'sku3'] } } } })
expect(stdout).to.not.contain('No data found')
expect(stdout).to.match(/Socks.*\(skus sku2\).*5/)
expect(stdout).to.match(/bnd1.*\(bundles bnd1\).*2/)
expect(stdout).to.not.contain('[object Object]')
})

it('sends an empty query without item IDs', async () => {
it('collects the IDs of repeated --in flags', async () => {
const q = mockQuery('analysis/fbt', 200, { data: [] })
const { stdout } = await runCommand(['metrics:fbt', ...AUTH, '-F', '{"order":{"market_code_eq":"EU"}}'])
// MetricsQueryFbt only allows a filter on line_items.item_ids
expect(q.body).to.deep.equal({})
const { stdout } = await runCommand(['metrics:fbt', ...AUTH, '-i', 'sku1', '-i', 'sku2, sku3'])
expect(q.body.filter.line_items.item_ids.in).to.deep.equal(['sku1', 'sku2', 'sku3'])
expect(stdout).to.contain('No data found')
})

it('requires the item IDs', async () => {
const { error } = await runCommand(['metrics:fbt', ...AUTH])
expect(error?.message).to.match(/Missing required flag in/)
})

it('refuses a filter: the FBT query only takes item IDs', async () => {
const { error } = await runCommand(['metrics:fbt', ...AUTH, '-i', 'sku1', '-F', '{"order":{"market_code_eq":"EU"}}'])
expect(error?.message).to.match(/Nonexistent flag|Unexpected argument/)
})

it('prints the API errors', async () => {
mockQuery('analysis/fbt', 400, { errors: [{ title: 'Bad request', detail: 'invalid filter' }] })
const { stdout } = await runCommand(['metrics:fbt', ...AUTH, '-i', 'sku1'])
expect(stdout).to.contain('invalid filter')
})
})
Loading