Skip to content

Commit 8a2a064

Browse files
authored
fix(actors info): handle tiered pay-per-event pricing (#1173)
1 parent 4f54803 commit 8a2a064

2 files changed

Lines changed: 47 additions & 18 deletions

File tree

src/commands/actors/info.ts

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Actor, ActorTaggedBuild, Build, User } from 'apify-client';
1+
import type { Actor, ActorChargeEvent, ActorTaggedBuild, Build, User } from 'apify-client';
22
import chalk from 'chalk';
33

44
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
@@ -14,19 +14,20 @@ interface HydratedActorInfo extends Omit<Actor, 'taggedBuilds'> {
1414
actorMaker?: User;
1515
}
1616

17-
interface PricingInfo {
18-
pricingModel: 'PRICE_PER_DATASET_ITEM' | 'FLAT_PRICE_PER_MONTH' | 'PAY_PER_EVENT' | 'FREE';
19-
pricePerUnitUsd: number;
20-
unitName: string;
21-
startedAt: string;
22-
createdAt: string;
23-
apifyMarginPercentage: number;
24-
notifiedAboutFutureChangeAt: string;
25-
notifiedAboutChangeAt: string;
26-
trialMinutes?: number;
27-
pricingPerEvent?: {
28-
actorChargeEvents: Record<string, { eventTitle: string; eventDescription: string; eventPriceUsd: number }>;
29-
};
17+
// apify-client's ActorChargeEvent declares eventPriceUsd as required and is missing
18+
// eventTieredPricingUsd. Tiered pay-per-event pricing shipped on the platform after the SDK type
19+
// was last updated; remodel locally until upstream catches up: https://github.com/apify/apify-client-js
20+
type ChargeEventShape = Omit<ActorChargeEvent, 'eventPriceUsd'> & {
21+
eventPriceUsd?: number;
22+
eventTieredPricingUsd?: Record<string, { tieredEventPriceUsd: number }>;
23+
};
24+
25+
// Event prices are routinely sub-cent (e.g. $0.005, $0.00079) so plain toFixed(2) rounds them to
26+
// "$0.00" and hides the real cost. Use 2 decimals at >= $0.01, otherwise 2 significant figures.
27+
function formatEventPrice(price: number): string {
28+
if (price === 0) return '$0.00';
29+
if (price >= 0.01) return `$${price.toFixed(2)}`;
30+
return `$${Number(price.toPrecision(2))}`;
3031
}
3132

3233
const eventTitleColumn = '\u200b';
@@ -210,7 +211,7 @@ export class ActorsInfoCommand extends ApifyCommand<typeof ActorsInfoCommand> {
210211
}
211212

212213
// Pricing info
213-
const pricingInfo = Reflect.get(actorInfo, 'pricingInfos') as PricingInfo[] | undefined;
214+
const pricingInfo = actorInfo.pricingInfos;
214215

215216
if (pricingInfo?.length) {
216217
// We only print the latest pricing info
@@ -245,10 +246,21 @@ export class ActorsInfoCommand extends ApifyCommand<typeof ActorsInfoCommand> {
245246

246247
const events = Object.values(latestPricingInfo.pricingPerEvent?.actorChargeEvents ?? {});
247248

248-
for (const eventInfo of events) {
249+
for (const eventInfo of events as ChargeEventShape[]) {
250+
const flat = eventInfo.eventPriceUsd;
251+
const tiered = eventInfo.eventTieredPricingUsd;
252+
let priceLabel: string;
253+
if (typeof flat === 'number') {
254+
priceLabel = formatEventPrice(flat);
255+
} else if (tiered && Object.keys(tiered).length > 0) {
256+
const minPrice = Math.min(...Object.values(tiered).map((t) => t.tieredEventPriceUsd));
257+
priceLabel = `from ${formatEventPrice(minPrice)} (tiered)`;
258+
} else {
259+
priceLabel = 'N/A';
260+
}
249261
payPerEventTable.pushRow({
250262
[eventTitleColumn]: eventInfo.eventTitle,
251-
[eventPriceUsdColumn]: chalk.bold(`$${eventInfo.eventPriceUsd.toFixed(2)}`),
263+
[eventPriceUsdColumn]: chalk.bold(priceLabel),
252264
});
253265
}
254266

@@ -269,8 +281,10 @@ export class ActorsInfoCommand extends ApifyCommand<typeof ActorsInfoCommand> {
269281
}
270282

271283
default: {
284+
// Runtime fallback for pricing models the SDK union doesn't know about yet.
285+
const unknownModel = (latestPricingInfo as { pricingModel: string }).pricingModel;
272286
message.push(
273-
`${chalk.yellow('Pricing information:')} ${chalk.bgGray(`Unknown pricing model (${chalk.yellow(latestPricingInfo.pricingModel)})`)}`,
287+
`${chalk.yellow('Pricing information:')} ${chalk.bgGray(`Unknown pricing model (${chalk.yellow(unknownModel)})`)}`,
274288
);
275289
}
276290
}

test/e2e/commands/actors/info.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,19 @@ describe('[e2e][api] actors info', () => {
4545

4646
expect(result.stdout).toContain('was not found');
4747
});
48+
49+
// Regression test for https://github.com/apify/apify-cli/issues/1171
50+
// The human-readable renderer crashed with "Cannot read properties of undefined (reading 'toFixed')"
51+
// on PAY_PER_EVENT actors that use tiered pricing (eventTieredPricingUsd) instead of a flat
52+
// eventPriceUsd. --json bypasses the renderer, so the bug only surfaces on the default output.
53+
it('renders pricing for a tiered PAY_PER_EVENT actor without crashing', async () => {
54+
const result = await runCli('apify', ['actors', 'info', 'lukaskrivka/google-maps-with-contact-details'], {
55+
env: authEnv,
56+
});
57+
58+
expect(result.exitCode, `stderr: ${result.stderr}\nstdout: ${result.stdout}`).toBe(0);
59+
expect(result.stderr).not.toContain('toFixed');
60+
expect(result.stdout).toContain('Pricing information');
61+
expect(result.stdout).toContain('Pay per event');
62+
});
4863
});

0 commit comments

Comments
 (0)