diff --git a/.changeset/pr-170-review-feedback.md b/.changeset/pr-170-review-feedback.md new file mode 100644 index 0000000..d3e8d38 --- /dev/null +++ b/.changeset/pr-170-review-feedback.md @@ -0,0 +1,5 @@ +--- +'scope3': patch +--- + +Remove placeholder test stubs for deleted resources, reject storefront persona at runtime, remove unsafe index signatures from campaign input types, add response validation to campaign create/update, and type campaign sub-endpoint responses. diff --git a/src/__tests__/cli/commands/storefront.test.ts b/src/__tests__/cli/commands/storefront.test.ts deleted file mode 100644 index 0106a37..0000000 --- a/src/__tests__/cli/commands/storefront.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('removed', () => { - it.todo('resource removed in v3'); -}); diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index a790509..8a4496d 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -53,6 +53,12 @@ describe('Scope3Client', () => { expect(() => new Scope3Client({ apiKey: 'test-key' } as any)).toThrow('persona is required'); }); + it('should reject storefront persona', () => { + expect(() => new Scope3Client({ apiKey: 'test-key', persona: 'storefront' })).toThrow( + 'Scope3Client only supports the buyer persona' + ); + }); + it('should default to v2 version', () => { const client = new Scope3Client({ apiKey: 'test-key', persona: 'buyer' }); expect(client.version).toBe('v2'); diff --git a/src/__tests__/resources/agents.test.ts b/src/__tests__/resources/agents.test.ts deleted file mode 100644 index 0106a37..0000000 --- a/src/__tests__/resources/agents.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('removed', () => { - it.todo('resource removed in v3'); -}); diff --git a/src/__tests__/resources/bundles.test.ts b/src/__tests__/resources/bundles.test.ts deleted file mode 100644 index 0106a37..0000000 --- a/src/__tests__/resources/bundles.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('removed', () => { - it.todo('resource removed in v3'); -}); diff --git a/src/__tests__/resources/conversion-events.test.ts b/src/__tests__/resources/conversion-events.test.ts deleted file mode 100644 index 0106a37..0000000 --- a/src/__tests__/resources/conversion-events.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('removed', () => { - it.todo('resource removed in v3'); -}); diff --git a/src/__tests__/resources/creative-sets.test.ts b/src/__tests__/resources/creative-sets.test.ts deleted file mode 100644 index 0106a37..0000000 --- a/src/__tests__/resources/creative-sets.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('removed', () => { - it.todo('resource removed in v3'); -}); diff --git a/src/__tests__/resources/inventory-sources.test.ts b/src/__tests__/resources/inventory-sources.test.ts deleted file mode 100644 index 0106a37..0000000 --- a/src/__tests__/resources/inventory-sources.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('removed', () => { - it.todo('resource removed in v3'); -}); diff --git a/src/__tests__/resources/notifications.test.ts b/src/__tests__/resources/notifications.test.ts deleted file mode 100644 index 0106a37..0000000 --- a/src/__tests__/resources/notifications.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('removed', () => { - it.todo('resource removed in v3'); -}); diff --git a/src/__tests__/resources/products.test.ts b/src/__tests__/resources/products.test.ts deleted file mode 100644 index 0106a37..0000000 --- a/src/__tests__/resources/products.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('removed', () => { - it.todo('resource removed in v3'); -}); diff --git a/src/__tests__/resources/readiness.test.ts b/src/__tests__/resources/readiness.test.ts deleted file mode 100644 index 0106a37..0000000 --- a/src/__tests__/resources/readiness.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('removed', () => { - it.todo('resource removed in v3'); -}); diff --git a/src/__tests__/resources/sales-agents.test.ts b/src/__tests__/resources/sales-agents.test.ts deleted file mode 100644 index 0106a37..0000000 --- a/src/__tests__/resources/sales-agents.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('removed', () => { - it.todo('resource removed in v3'); -}); diff --git a/src/__tests__/resources/signals.test.ts b/src/__tests__/resources/signals.test.ts deleted file mode 100644 index 0106a37..0000000 --- a/src/__tests__/resources/signals.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('removed', () => { - it.todo('resource removed in v3'); -}); diff --git a/src/__tests__/resources/storefront.test.ts b/src/__tests__/resources/storefront.test.ts deleted file mode 100644 index 0106a37..0000000 --- a/src/__tests__/resources/storefront.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe('removed', () => { - it.todo('resource removed in v3'); -}); diff --git a/src/cli/commands/config.ts b/src/cli/commands/config.ts index ae914c2..c5a29e5 100644 --- a/src/cli/commands/config.ts +++ b/src/cli/commands/config.ts @@ -40,9 +40,9 @@ configCommand } // Validate persona - if (key === 'persona' && !['buyer', 'storefront'].includes(value)) { + if (key === 'persona' && value !== 'buyer') { console.error(chalk.red(`Invalid persona: ${value}`)); - console.error('Valid personas: buyer, storefront'); + console.error('Valid personas: buyer'); process.exit(1); } diff --git a/src/cli/index.ts b/src/cli/index.ts index ff7a738..2997d79 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -43,7 +43,7 @@ program .option('--base-url ', 'Custom API base URL') .option('--format ', 'Output format: json, table, or yaml (default: table)') .option('--debug', 'Enable debug mode') - .option('--persona ', 'API persona: buyer or storefront (default: buyer)'); + .option('--persona ', 'API persona (default: buyer)'); program.hook('preAction', (_thisCommand, actionCommand) => { const skipCommands = ['login', 'logout', 'config', 'commands']; diff --git a/src/client.ts b/src/client.ts index 646f87a..edcbb3a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -63,7 +63,10 @@ export class Scope3Client { throw new Error('apiKey is required'); } if (!config.persona) { - throw new Error('persona is required (buyer or storefront)'); + throw new Error('persona is required'); + } + if (config.persona !== 'buyer') { + throw new Error('Scope3Client only supports the buyer persona'); } this.version = config.version ?? 'v2'; diff --git a/src/index.ts b/src/index.ts index 1e9eb0d..8ef5679 100644 --- a/src/index.ts +++ b/src/index.ts @@ -131,6 +131,12 @@ export type { CreateCampaignInput, UpdateCampaignInput, ListCampaignsParams, + RefinementItem, + AutoSelectProductsResult, + CampaignProductEntry, + CampaignProductsResult, + CampaignMediaBuyStatus, + MediaBuyStatusValue, // Test Cohorts TestCohort, CreateTestCohortInput, diff --git a/src/resources/campaigns.ts b/src/resources/campaigns.ts index 1d44020..ae56498 100644 --- a/src/resources/campaigns.ts +++ b/src/resources/campaigns.ts @@ -10,6 +10,10 @@ import type { ListCampaignsParams, PaginatedApiResponse, ApiResponse, + AutoSelectProductsResult, + CampaignProductsResult, + CampaignMediaBuyStatus, + RefinementItem, } from '../types'; import { campaignSchemas } from '../schemas/registry'; import { shouldValidateResponse, validateResponse } from '../validation'; @@ -60,7 +64,11 @@ export class CampaignsResource { * @returns Created campaign */ async create(data: CreateCampaignInput): Promise> { - return this.adapter.request>('POST', '/campaigns', data); + const result = await this.adapter.request>('POST', '/campaigns', data); + if (shouldValidateResponse(this.adapter.validate)) { + result.data = validateResponse(campaignSchemas.response, result.data) as unknown as Campaign; + } + return result; } /** @@ -70,11 +78,15 @@ export class CampaignsResource { * @returns Updated campaign */ async update(id: string, data: UpdateCampaignInput): Promise> { - return this.adapter.request>( + const result = await this.adapter.request>( 'PUT', `/campaigns/${validateResourceId(id)}`, data ); + if (shouldValidateResponse(this.adapter.validate)) { + result.data = validateResponse(campaignSchemas.response, result.data) as unknown as Campaign; + } + return result; } /** @@ -88,14 +100,18 @@ export class CampaignsResource { /** * Auto-select products for a campaign * @param id Campaign ID - * @param data Optional configuration for product selection - * @returns Auto-selection result + * @param data Optional refinement and configuration for product selection + * @returns Auto-selection result with selected products */ async autoSelectProducts( id: string, - data?: Record - ): Promise>> { - return this.adapter.request>>( + data?: { + refine?: RefinementItem[]; + maxProducts?: number; + minBudgetPerProduct?: number; + } + ): Promise> { + return this.adapter.request>( 'POST', `/campaigns/${validateResourceId(id)}/auto-select-products`, data @@ -105,10 +121,10 @@ export class CampaignsResource { /** * Get media buy status for a campaign * @param id Campaign ID - * @returns Media buy status + * @returns Media buy statuses */ - async getMediaBuyStatus(id: string): Promise>> { - return this.adapter.request>>( + async getMediaBuyStatus(id: string): Promise> { + return this.adapter.request>( 'GET', `/campaigns/${validateResourceId(id)}/media-buy-status` ); @@ -117,10 +133,10 @@ export class CampaignsResource { /** * Get products associated with a campaign * @param id Campaign ID - * @returns Campaign products + * @returns Campaign products with summary */ - async getProducts(id: string): Promise>> { - return this.adapter.request>>( + async getProducts(id: string): Promise> { + return this.adapter.request>( 'GET', `/campaigns/${validateResourceId(id)}/products` ); diff --git a/src/types/index.ts b/src/types/index.ts index b8024f1..c89068d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -247,7 +247,6 @@ export interface CreateCampaignInput { brief?: string; constraints?: CampaignConstraints; performanceConfig?: PerformanceConfig; - [key: string]: unknown; } export interface UpdateCampaignInput { @@ -258,7 +257,6 @@ export interface UpdateCampaignInput { brief?: string; constraints?: CampaignConstraints; performanceConfig?: PerformanceConfig; - [key: string]: unknown; } export interface ListCampaignsParams extends PaginationParams { @@ -267,6 +265,95 @@ export interface ListCampaignsParams extends PaginationParams { status?: CampaignStatus; } +export type RefinementItem = + | { scope: 'request'; ask: string } + | { scope: 'product'; id: string; action: 'include' | 'omit' | 'moreLikeThis'; ask?: string }; + +export interface AutoSelectProductsResult { + campaignId: string; + discoveryId: string; + selectedProducts: Array<{ + productId: string; + name: string; + salesAgentId: string; + groupId: string; + groupName: string; + cpm?: number; + budget: number; + pricingOptionId?: string; + }>; + budgetContext: { + campaignBudget: number; + totalAllocated: number; + remainingBudget: number; + currency: string; + }; + productCount: number; + previouslySelectedCount?: number; +} + +export interface CampaignProductEntry { + productId: string; + productName?: string; + salesAgentId: string; + salesAgentName?: string; + publisherDomain?: string; + publisherName?: string; + bidPrice?: number; + budget?: number; + pricingOptionId?: string; + pricingModel?: string; + selectedAt: string; + searchContext?: { id: string; brief: string }; + mediaBuys: Array<{ MediaBuyId: string; Status: string; name: string }>; +} + +export interface CampaignProductsResult { + campaignId: string; + discoveryId: string | null; + products: CampaignProductEntry[]; + searchContexts: Array<{ + id: string; + brief: string; + channels: string[]; + countries: string[]; + createdAt: string; + productCount: number; + }>; + summary: { + totalProducts: number; + productsOnMediaBuys: number; + productsPending: number; + }; +} + +export type MediaBuyStatusValue = + | 'DRAFT' + | 'PENDING_APPROVAL' + | 'INPUT_REQUIRED' + | 'ACTIVE' + | 'PAUSED' + | 'COMPLETED' + | 'CANCELED' + | 'FAILED' + | 'REJECTED' + | 'ARCHIVED'; + +export interface CampaignMediaBuyStatus { + campaign_id: string; + media_buys: Array<{ + media_buy_id: string; + adcp_media_buy_id: string; + internal_status: string; + adcp_status: string | null; + previous_internal_status: string; + previous_adcp_status: string | null; + updated: boolean; + }>; + agents_queried: number; + errors: Array<{ media_buy_id: string; error: string }>; +} + // ============================================================================ // Test Cohort Types (Buyer Persona) // ============================================================================