diff --git a/plugins/metrics/README.md b/plugins/metrics/README.md index d1dccc2c2..cb56157eb 100644 --- a/plugins/metrics/README.md +++ b/plugins/metrics/README.md @@ -126,11 +126,11 @@ Perform a Frequently Bought Together query on the Metrics API analysis endpoint. ```sh-session USAGE - $ commercelayer metrics:fbt [-F ] [-i ...] + $ commercelayer metrics:fbt -i ... FLAGS - -F, --filter= the filter to apply to the query in JSON format (enclosed in single quotes) - -i, --in=... a list of SKU or bundle IDs associated as line items to one or more orders + -i, --in=... (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 diff --git a/plugins/metrics/src/commands/metrics/fbt.ts b/plugins/metrics/src/commands/metrics/fbt.ts index d1b1429a7..959dca141 100644 --- a/plugins/metrics/src/commands/metrics/fbt.ts +++ b/plugins/metrics/src/commands/metrics/fbt.ts @@ -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' @@ -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 }) } @@ -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() + } + } diff --git a/plugins/metrics/src/common.ts b/plugins/metrics/src/common.ts index 73c5a0f08..9d9977c65 100644 --- a/plugins/metrics/src/common.ts +++ b/plugins/metrics/src/common.ts @@ -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 } diff --git a/plugins/metrics/test/commands/metrics/fbt.test.ts b/plugins/metrics/test/commands/metrics/fbt.test.ts index ba5c01c30..2b741bac6 100644 --- a/plugins/metrics/test/commands/metrics/fbt.test.ts +++ b/plugins/metrics/test/commands/metrics/fbt.test.ts @@ -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') + }) })