diff --git a/src/commands/actors/info.ts b/src/commands/actors/info.ts index 8f4e36701..51c845436 100644 --- a/src/commands/actors/info.ts +++ b/src/commands/actors/info.ts @@ -1,4 +1,4 @@ -import type { Actor, ActorTaggedBuild, Build, User } from 'apify-client'; +import type { Actor, ActorChargeEvent, ActorTaggedBuild, Build, User } from 'apify-client'; import chalk from 'chalk'; import { ApifyCommand } from '../../lib/command-framework/apify-command.js'; @@ -14,19 +14,20 @@ interface HydratedActorInfo extends Omit { actorMaker?: User; } -interface PricingInfo { - pricingModel: 'PRICE_PER_DATASET_ITEM' | 'FLAT_PRICE_PER_MONTH' | 'PAY_PER_EVENT' | 'FREE'; - pricePerUnitUsd: number; - unitName: string; - startedAt: string; - createdAt: string; - apifyMarginPercentage: number; - notifiedAboutFutureChangeAt: string; - notifiedAboutChangeAt: string; - trialMinutes?: number; - pricingPerEvent?: { - actorChargeEvents: Record; - }; +// apify-client's ActorChargeEvent declares eventPriceUsd as required and is missing +// eventTieredPricingUsd. Tiered pay-per-event pricing shipped on the platform after the SDK type +// was last updated; remodel locally until upstream catches up: https://github.com/apify/apify-client-js +type ChargeEventShape = Omit & { + eventPriceUsd?: number; + eventTieredPricingUsd?: Record; +}; + +// Event prices are routinely sub-cent (e.g. $0.005, $0.00079) so plain toFixed(2) rounds them to +// "$0.00" and hides the real cost. Use 2 decimals at >= $0.01, otherwise 2 significant figures. +function formatEventPrice(price: number): string { + if (price === 0) return '$0.00'; + if (price >= 0.01) return `$${price.toFixed(2)}`; + return `$${Number(price.toPrecision(2))}`; } const eventTitleColumn = '\u200b'; @@ -210,7 +211,7 @@ export class ActorsInfoCommand extends ApifyCommand { } // Pricing info - const pricingInfo = Reflect.get(actorInfo, 'pricingInfos') as PricingInfo[] | undefined; + const pricingInfo = actorInfo.pricingInfos; if (pricingInfo?.length) { // We only print the latest pricing info @@ -245,10 +246,21 @@ export class ActorsInfoCommand extends ApifyCommand { const events = Object.values(latestPricingInfo.pricingPerEvent?.actorChargeEvents ?? {}); - for (const eventInfo of events) { + for (const eventInfo of events as ChargeEventShape[]) { + const flat = eventInfo.eventPriceUsd; + const tiered = eventInfo.eventTieredPricingUsd; + let priceLabel: string; + if (typeof flat === 'number') { + priceLabel = formatEventPrice(flat); + } else if (tiered && Object.keys(tiered).length > 0) { + const minPrice = Math.min(...Object.values(tiered).map((t) => t.tieredEventPriceUsd)); + priceLabel = `from ${formatEventPrice(minPrice)} (tiered)`; + } else { + priceLabel = 'N/A'; + } payPerEventTable.pushRow({ [eventTitleColumn]: eventInfo.eventTitle, - [eventPriceUsdColumn]: chalk.bold(`$${eventInfo.eventPriceUsd.toFixed(2)}`), + [eventPriceUsdColumn]: chalk.bold(priceLabel), }); } @@ -269,8 +281,10 @@ export class ActorsInfoCommand extends ApifyCommand { } default: { + // Runtime fallback for pricing models the SDK union doesn't know about yet. + const unknownModel = (latestPricingInfo as { pricingModel: string }).pricingModel; message.push( - `${chalk.yellow('Pricing information:')} ${chalk.bgGray(`Unknown pricing model (${chalk.yellow(latestPricingInfo.pricingModel)})`)}`, + `${chalk.yellow('Pricing information:')} ${chalk.bgGray(`Unknown pricing model (${chalk.yellow(unknownModel)})`)}`, ); } } diff --git a/test/e2e/commands/actors/info.test.ts b/test/e2e/commands/actors/info.test.ts index 5bc688ee5..30a42d5d5 100644 --- a/test/e2e/commands/actors/info.test.ts +++ b/test/e2e/commands/actors/info.test.ts @@ -45,4 +45,19 @@ describe('[e2e][api] actors info', () => { expect(result.stdout).toContain('was not found'); }); + + // Regression test for https://github.com/apify/apify-cli/issues/1171 + // The human-readable renderer crashed with "Cannot read properties of undefined (reading 'toFixed')" + // on PAY_PER_EVENT actors that use tiered pricing (eventTieredPricingUsd) instead of a flat + // eventPriceUsd. --json bypasses the renderer, so the bug only surfaces on the default output. + it('renders pricing for a tiered PAY_PER_EVENT actor without crashing', async () => { + const result = await runCli('apify', ['actors', 'info', 'lukaskrivka/google-maps-with-contact-details'], { + env: authEnv, + }); + + expect(result.exitCode, `stderr: ${result.stderr}\nstdout: ${result.stdout}`).toBe(0); + expect(result.stderr).not.toContain('toFixed'); + expect(result.stdout).toContain('Pricing information'); + expect(result.stdout).toContain('Pay per event'); + }); });