From 0aac9a54b70612c5b9193bbf2c7535062cf40cd9 Mon Sep 17 00:00:00 2001 From: Ben Miner Date: Wed, 27 May 2026 12:50:31 -0500 Subject: [PATCH 1/3] fix: address review feedback from PR #170 - Delete placeholder test files for removed resources instead of stubs - Reject storefront persona in Scope3Client constructor at runtime - Remove index signatures from CreateCampaignInput/UpdateCampaignInput - Add response validation to campaigns create/update methods - Type autoSelectProducts, getMediaBuyStatus, getProducts responses using shapes from OpenAPI spec --- src/__tests__/cli/commands/storefront.test.ts | 3 - src/__tests__/client.test.ts | 6 ++ src/__tests__/resources/agents.test.ts | 3 - src/__tests__/resources/bundles.test.ts | 3 - .../resources/conversion-events.test.ts | 3 - src/__tests__/resources/creative-sets.test.ts | 3 - .../resources/inventory-sources.test.ts | 3 - src/__tests__/resources/notifications.test.ts | 3 - src/__tests__/resources/products.test.ts | 3 - src/__tests__/resources/readiness.test.ts | 3 - src/__tests__/resources/sales-agents.test.ts | 3 - src/__tests__/resources/signals.test.ts | 3 - src/__tests__/resources/storefront.test.ts | 3 - src/cli/commands/config.ts | 4 +- src/cli/index.ts | 2 +- src/client.ts | 5 +- src/index.ts | 5 ++ src/resources/campaigns.ts | 41 ++++++++---- src/types/index.ts | 66 ++++++++++++++++++- 19 files changed, 110 insertions(+), 55 deletions(-) delete mode 100644 src/__tests__/cli/commands/storefront.test.ts delete mode 100644 src/__tests__/resources/agents.test.ts delete mode 100644 src/__tests__/resources/bundles.test.ts delete mode 100644 src/__tests__/resources/conversion-events.test.ts delete mode 100644 src/__tests__/resources/creative-sets.test.ts delete mode 100644 src/__tests__/resources/inventory-sources.test.ts delete mode 100644 src/__tests__/resources/notifications.test.ts delete mode 100644 src/__tests__/resources/products.test.ts delete mode 100644 src/__tests__/resources/readiness.test.ts delete mode 100644 src/__tests__/resources/sales-agents.test.ts delete mode 100644 src/__tests__/resources/signals.test.ts delete mode 100644 src/__tests__/resources/storefront.test.ts 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..a2bcbc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -131,6 +131,11 @@ export type { CreateCampaignInput, UpdateCampaignInput, ListCampaignsParams, + AutoSelectProductsResult, + CampaignProductEntry, + CampaignProductsResult, + CampaignMediaBuyStatus, + MediaBuyStatusValue, // Test Cohorts TestCohort, CreateTestCohortInput, diff --git a/src/resources/campaigns.ts b/src/resources/campaigns.ts index 1d44020..57261ad 100644 --- a/src/resources/campaigns.ts +++ b/src/resources/campaigns.ts @@ -10,6 +10,9 @@ import type { ListCampaignsParams, PaginatedApiResponse, ApiResponse, + AutoSelectProductsResult, + CampaignProductsResult, + CampaignMediaBuyStatus, } from '../types'; import { campaignSchemas } from '../schemas/registry'; import { shouldValidateResponse, validateResponse } from '../validation'; @@ -60,7 +63,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 +77,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 +99,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?: Array>; + maxProducts?: number; + minBudgetPerProduct?: number; + } + ): Promise> { + return this.adapter.request>( 'POST', `/campaigns/${validateResourceId(id)}/auto-select-products`, data @@ -105,10 +120,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 +132,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..713507b 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,70 @@ export interface ListCampaignsParams extends PaginationParams { status?: CampaignStatus; } +export interface AutoSelectProductsResult { + campaignId: string; + discoveryId: string; + selectedProducts: Array<{ + productId: string; + name: string; + salesAgentId: string; + groupId: string; + groupName: string; + cpm?: number; + }>; +} + +export interface CampaignProductEntry { + productId: string; + productName?: string; + salesAgentId: string; + salesAgentName?: string; + publisherDomain?: string; + publisherName?: string; + bidPrice?: number; + budget?: number; + pricingOptionId?: string; + pricingModel?: 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'; + +export interface CampaignMediaBuyStatus { + mediaBuys: Array<{ + MediaBuyId: string; + name: string; + Status: string; + startTime?: string; + endTime?: string; + }>; +} + // ============================================================================ // Test Cohort Types (Buyer Persona) // ============================================================================ From d32f66f382c0b7131762b89327d673dda870301a Mon Sep 17 00:00:00 2001 From: Ben Miner Date: Wed, 27 May 2026 13:26:20 -0500 Subject: [PATCH 2/3] chore: add changeset for review feedback fixes --- .changeset/pr-170-review-feedback.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pr-170-review-feedback.md 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. From 5f5a86d57781f75504253bfbccf94f9878869d66 Mon Sep 17 00:00:00 2001 From: Ben Miner Date: Wed, 27 May 2026 15:36:58 -0500 Subject: [PATCH 3/3] fix: align campaign types with generated OpenAPI schemas - AutoSelectProductsResult: add budget (required), pricingOptionId, budgetContext, productCount, previouslySelectedCount - CampaignProductEntry: add selectedAt (required), searchContext, mediaBuys (required) - MediaBuyStatusValue: add FAILED, REJECTED, ARCHIVED - CampaignMediaBuyStatus: match GetAdcpStatusOutput schema shape (campaign_id, media_buys with full status fields, agents_queried, errors) - Add RefinementItem discriminated union type, use in autoSelectProducts --- src/index.ts | 1 + src/resources/campaigns.ts | 3 ++- src/types/index.ts | 39 +++++++++++++++++++++++++++++++------- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index a2bcbc1..8ef5679 100644 --- a/src/index.ts +++ b/src/index.ts @@ -131,6 +131,7 @@ export type { CreateCampaignInput, UpdateCampaignInput, ListCampaignsParams, + RefinementItem, AutoSelectProductsResult, CampaignProductEntry, CampaignProductsResult, diff --git a/src/resources/campaigns.ts b/src/resources/campaigns.ts index 57261ad..ae56498 100644 --- a/src/resources/campaigns.ts +++ b/src/resources/campaigns.ts @@ -13,6 +13,7 @@ import type { AutoSelectProductsResult, CampaignProductsResult, CampaignMediaBuyStatus, + RefinementItem, } from '../types'; import { campaignSchemas } from '../schemas/registry'; import { shouldValidateResponse, validateResponse } from '../validation'; @@ -105,7 +106,7 @@ export class CampaignsResource { async autoSelectProducts( id: string, data?: { - refine?: Array>; + refine?: RefinementItem[]; maxProducts?: number; minBudgetPerProduct?: number; } diff --git a/src/types/index.ts b/src/types/index.ts index 713507b..c89068d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -265,6 +265,10 @@ 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; @@ -275,7 +279,17 @@ export interface AutoSelectProductsResult { 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 { @@ -289,6 +303,9 @@ export interface CampaignProductEntry { budget?: number; pricingOptionId?: string; pricingModel?: string; + selectedAt: string; + searchContext?: { id: string; brief: string }; + mediaBuys: Array<{ MediaBuyId: string; Status: string; name: string }>; } export interface CampaignProductsResult { @@ -317,16 +334,24 @@ export type MediaBuyStatusValue = | 'ACTIVE' | 'PAUSED' | 'COMPLETED' - | 'CANCELED'; + | 'CANCELED' + | 'FAILED' + | 'REJECTED' + | 'ARCHIVED'; export interface CampaignMediaBuyStatus { - mediaBuys: Array<{ - MediaBuyId: string; - name: string; - Status: string; - startTime?: string; - endTime?: string; + 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 }>; } // ============================================================================