diff --git a/docs/openapi/api.yaml b/docs/openapi/api.yaml index 8c431717f..d53ecb7e1 100644 --- a/docs/openapi/api.yaml +++ b/docs/openapi/api.yaml @@ -119,14 +119,6 @@ paths: $ref: './organization-api.yaml#/organization' /organizations/{organizationId}/brands: $ref: './brands-api.yaml#/brands-for-organization' - /v2/orgs/{spaceCatId}/llmo-customer-config: - $ref: './customer-config-api.yaml#/llmo-customer-config-by-space-cat-id-v2' - /v2/orgs/{spaceCatId}/llmo-customer-config-lean: - $ref: './customer-config-api.yaml#/llmo-customer-config-lean-by-space-cat-id-v2' - /v2/orgs/{spaceCatId}/llmo-topics: - $ref: './customer-config-api.yaml#/llmo-topics-by-space-cat-id-v2' - /v2/orgs/{spaceCatId}/llmo-prompts: - $ref: './customer-config-api.yaml#/llmo-prompts-by-space-cat-id-v2' /v2/orgs/{spaceCatId}/brands: $ref: './brands-v2-api.yaml#/v2-brands-for-org' /v2/orgs/{spaceCatId}/brands/{brandId}: @@ -136,11 +128,11 @@ paths: /v2/orgs/{spaceCatId}/topics/{topicId}: $ref: './topics-v2-api.yaml#/v2-topic-for-org' /v2/orgs/{spaceCatId}/brands/{brandId}/prompts: - $ref: './customer-config-api.yaml#/v2-prompts-by-brand' + $ref: './prompts-v2-api.yaml#/v2-prompts-by-brand' /v2/orgs/{spaceCatId}/brands/{brandId}/prompts/{promptId}: - $ref: './customer-config-api.yaml#/v2-prompts-by-brand-and-id' + $ref: './prompts-v2-api.yaml#/v2-prompts-by-brand-and-id' /v2/orgs/{spaceCatId}/brands/{brandId}/prompts/delete: - $ref: './customer-config-api.yaml#/v2-prompts-by-brand-bulk-delete' + $ref: './prompts-v2-api.yaml#/v2-prompts-by-brand-bulk-delete' /organizations/{organizationId}/sites: $ref: './sites-api.yaml#/sites-for-organization' /organizations/{organizationId}/entitlements: diff --git a/docs/openapi/customer-config-api.yaml b/docs/openapi/customer-config-api.yaml deleted file mode 100644 index 8d119387e..000000000 --- a/docs/openapi/customer-config-api.yaml +++ /dev/null @@ -1,784 +0,0 @@ -llmo-customer-config-by-space-cat-id-v2: - parameters: - - name: spaceCatId - in: path - required: true - description: SpaceCat Organization ID (UUID) - schema: - type: string - format: uuid - example: 'a1b2c3d4-5678-90ab-cdef-1234567890ab' - - name: status - in: query - required: false - description: Filter by status (active, pending, deleted). Default excludes deleted items. - schema: - type: string - enum: [active, pending, deleted] - example: 'active' - get: - tags: - - customer-config - summary: Retrieves customer configuration for an organization (v2) - description: | - This endpoint provides the complete customer configuration including brands, categories, topics, and prompts - for the given SpaceCat Organization. Returns 404 if no customer configuration exists. - - By default, deleted items (categories, topics, prompts) are excluded. - Use the `status` query parameter to filter by specific status or include deleted items. - - The configuration includes: - - Brand information and metadata - - URLs, social accounts, and brand aliases - - Competitors and related brands - - Earned media content sources - - Top-level categories and topics collections - - Prompts with category/topic references - - Available industry verticals - operationId: getCustomerConfigBySpaceCatIdV2 - security: - - ims_key: [ ] - - api_key: [ ] - responses: - '200': - description: Customer configuration retrieved successfully - content: - application/json: - schema: - $ref: './schemas.yaml#/CustomerConfig' - examples: - adobe: - $ref: './examples.yaml#/CustomerConfigAdobe' - '400': - $ref: './responses.yaml#/400' - '404': - description: Customer configuration not found - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: "Customer configuration not found" - '500': - $ref: './responses.yaml#/500' - post: - tags: - - customer-config - summary: Saves customer configuration for an organization (v2) - description: | - Saves the complete customer configuration for the given SpaceCat Organization to S3. - - The configuration must include: - - customer.customerName - - customer.imsOrgID (must match the IMS Org ID in the URL) - - customer.brands (array of brands) - - Note: This does NOT automatically update V1 site-level LLMO configs. - operationId: saveCustomerConfigBySpaceCatIdV2 - security: - - ims_key: [ ] - - api_key: [ ] - requestBody: - required: true - content: - application/json: - schema: - $ref: './schemas.yaml#/CustomerConfig' - examples: - adobe: - $ref: './examples.yaml#/CustomerConfigAdobe' - responses: - '200': - description: Customer configuration saved successfully - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: "Customer configuration saved successfully" - '400': - description: Bad request - invalid data, IMS Org ID mismatch, or storage not configured - content: - application/json: - schema: - type: object - properties: - message: - type: string - '500': - $ref: './responses.yaml#/500' - patch: - tags: - - customer-config - summary: Patches (merges) customer configuration for an organization (v2) - description: | - Merges the provided partial configuration with the existing configuration in S3. - This allows incremental updates without sending the entire config. - - - Only the fields provided in the request will be updated - - Arrays (brands, categories, topics) are merged by ID - - Unchanged items preserve their metadata (updatedBy, updatedAt) - - Modified or new items get updated metadata with current user and timestamp - - Use this endpoint to: - - Add new brands without affecting existing ones - - Update specific categories or topics - - Add prompts to existing brands - - Update brand metadata - - The endpoint tracks and returns statistics about what was modified. - operationId: patchCustomerConfigBySpaceCatIdV2 - security: - - ims_key: [ ] - - api_key: [ ] - requestBody: - required: true - content: - application/json: - schema: - $ref: './schemas.yaml#/CustomerConfig' - examples: - addBrand: - summary: Add a new brand - value: - customer: - brands: - - id: "new-brand" - name: "New Brand" - status: "active" - origin: "manual" - region: ["US"] - description: "A new brand" - vertical: "Software & Technology" - urls: - - value: "https://example.com" - regions: ["US"] - prompts: [] - updateCategory: - summary: Update specific categories - value: - customer: - categories: - - id: "category-1" - name: "Updated Category Name" - status: "active" - origin: "human" - responses: - '200': - description: Customer configuration updated successfully - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: "Customer configuration updated successfully" - stats: - type: object - properties: - categories: - type: object - properties: - total: - type: integer - modified: - type: integer - topics: - type: object - properties: - total: - type: integer - modified: - type: integer - brands: - type: object - properties: - total: - type: integer - modified: - type: integer - prompts: - type: object - properties: - total: - type: integer - modified: - type: integer - '400': - description: Bad request - invalid data or storage not configured - content: - application/json: - schema: - type: object - properties: - message: - type: string - '403': - $ref: './responses.yaml#/403' - '404': - description: Organization not found - content: - application/json: - schema: - type: object - properties: - message: - type: string - '500': - $ref: './responses.yaml#/500' - -llmo-customer-config-lean-by-space-cat-id-v2: - parameters: - - name: spaceCatId - in: path - required: true - description: SpaceCat Organization ID (UUID) - schema: - type: string - format: uuid - example: 'a1b2c3d4-5678-90ab-cdef-1234567890ab' - - name: status - in: query - required: false - description: Filter by status (active, pending, deleted). Default excludes deleted items. Affects brand totals counts. - schema: - type: string - enum: [active, pending, deleted] - example: 'active' - get: - tags: - - customer-config - summary: Retrieves lean customer configuration (without prompts) for an IMS Org (v2) - description: | - This endpoint provides the customer configuration without the prompts array in each brand. - Useful for getting brand metadata without the potentially large prompts payload. - - By default, deleted items are excluded from totals counts. - Use the `status` query parameter to filter which items are counted. - - The configuration includes everything except brand.prompts[]. - Each brand includes totalCategories, totalTopics, and totalPrompts counts. - operationId: getCustomerConfigLeanBySpaceCatIdV2 - security: - - ims_key: [ ] - - api_key: [ ] - responses: - '200': - description: Lean customer configuration retrieved successfully - content: - application/json: - schema: - $ref: './schemas.yaml#/CustomerConfig' - '400': - $ref: './responses.yaml#/400' - '404': - description: Customer configuration not found - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: "Customer configuration not found" - '500': - $ref: './responses.yaml#/500' - -llmo-topics-by-space-cat-id-v2: - parameters: - - name: spaceCatId - in: path - required: true - description: SpaceCat Organization ID (UUID) - schema: - type: string - format: uuid - example: 'a1b2c3d4-5678-90ab-cdef-1234567890ab' - - name: brandId - in: query - required: false - description: Filter topics by brand ID (returns only topics used by that brand's prompts) - schema: - type: string - example: 'chevrolet' - - name: status - in: query - required: false - description: Filter by status (active, pending, deleted). Default excludes deleted topics. - schema: - type: string - enum: [active, pending, deleted] - example: 'active' - get: - tags: - - customer-config - summary: Retrieves topics for an IMS Org (v2) - description: | - Returns the top-level topics collection from the customer configuration. - Topics are shared across all brands. - - By default, deleted topics are excluded. - Use the `status` query parameter to filter by specific status or include deleted topics. - - Optionally filter by `brandId` to return only topics used by that brand's prompts. - operationId: getTopicsBySpaceCatIdV2 - security: - - ims_key: [ ] - - api_key: [ ] - responses: - '200': - description: Topics retrieved successfully - content: - application/json: - schema: - type: object - properties: - topics: - type: array - items: - $ref: './schemas.yaml#/CustomerConfigTopic' - '400': - $ref: './responses.yaml#/400' - '404': - description: Customer configuration not found - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: "Customer configuration not found" - '500': - $ref: './responses.yaml#/500' - -llmo-prompts-by-space-cat-id-v2: - parameters: - - name: spaceCatId - in: path - required: true - description: SpaceCat Organization ID (UUID) - schema: - type: string - format: uuid - example: 'a1b2c3d4-5678-90ab-cdef-1234567890ab' - - name: brandId - in: query - required: false - description: Filter prompts by brand ID - schema: - type: string - example: 'chevrolet' - - name: categoryId - in: query - required: false - description: Filter prompts by category ID - schema: - type: string - example: 'product-features' - - name: topicId - in: query - required: false - description: Filter prompts by topic ID - schema: - type: string - example: 'performance' - - name: status - in: query - required: false - description: Filter by status (active, pending, deleted). Default excludes deleted prompts. - schema: - type: string - enum: [active, pending, deleted] - example: 'active' - get: - tags: - - customer-config - summary: Retrieves prompts for an IMS Org (v2) - description: | - Returns all prompts from all brands, enriched with brand, category, and topic information. - - By default, deleted prompts are excluded. - Use the `status` query parameter to filter by specific status or include deleted prompts. - - Supports optional filtering by brandId, categoryId, and/or topicId query parameters. - - Each prompt is enriched with: - - brandId and brandName - - Full category object (id, name, origin) - - Full topic object (id, name, categoryId) - operationId: getPromptsBySpaceCatIdV2 - security: - - ims_key: [ ] - - api_key: [ ] - responses: - '200': - description: Prompts retrieved successfully - content: - application/json: - schema: - type: object - properties: - prompts: - type: array - items: - allOf: - - $ref: './schemas.yaml#/CustomerConfigPrompt' - - type: object - properties: - brandId: - type: string - description: ID of the brand this prompt belongs to - brandName: - type: string - description: Name of the brand this prompt belongs to - category: - $ref: './schemas.yaml#/CustomerConfigCategory' - description: Full category object - topic: - $ref: './schemas.yaml#/CustomerConfigTopic' - description: Full topic object - '400': - $ref: './responses.yaml#/400' - '404': - description: Customer configuration not found - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: "Customer configuration not found" - '500': - $ref: './responses.yaml#/500' - -# ── Brand-scoped prompts CRUD (Aurora) ── - -v2-prompts-by-brand: - parameters: - - name: spaceCatId - in: path - required: true - description: SpaceCat Organization ID (UUID) - schema: - type: string - format: uuid - - name: brandId - in: path - required: true - description: Brand ID (UUID, config id, or brand name) - schema: - type: string - - name: limit - in: query - required: false - schema: - type: integer - minimum: 1 - maximum: 5000 - default: 100 - description: Page size - - name: page - in: query - required: false - schema: - type: integer - minimum: 1 - default: 1 - description: Page number (1-based) - - name: categoryId - in: query - required: false - schema: - type: string - description: Filter by category business key - - name: topicId - in: query - required: false - schema: - type: string - description: Filter by topic business key - - name: status - in: query - required: false - schema: - type: string - enum: [active, pending, deleted] - description: Filter by status; default excludes deleted - - name: search - in: query - required: false - schema: - type: string - description: Free-text search across prompt text and name (ILIKE match) - - name: region - in: query - required: false - schema: - type: string - description: Filter by region (matches prompts whose regions array contains this value) - - name: origin - in: query - required: false - schema: - type: string - enum: [ai, human] - description: Filter by origin (ai or human) - - name: sort - in: query - required: false - schema: - type: string - enum: [topic, prompt, category, origin, status, updatedAt] - description: Sort column. Default is updatedAt descending - - name: order - in: query - required: false - schema: - type: string - enum: [asc, desc] - default: desc - description: Sort direction. Default is desc - get: - tags: - - customer-config - summary: List prompts for a brand - description: | - Returns a paginated list of prompts for the given brand. - Upsert uniqueness: match by id or by (text, regions). - operationId: listPromptsByBrand - security: - - ims_key: [ ] - - api_key: [ ] - responses: - '200': - description: Prompts retrieved successfully - content: - application/json: - schema: - $ref: './schemas.yaml#/V2PromptListResponse' - '400': - $ref: './responses.yaml#/400' - '404': - description: Organization or brand not found - '503': - description: PostgREST unavailable (DATA_SERVICE_PROVIDER=postgres required) - '500': - $ref: './responses.yaml#/500' - post: - tags: - - customer-config - summary: Create or upsert prompts (bulk) - description: | - Upserts prompts. Match by id or by (text, regions). If match found, update; else insert. - operationId: createPromptsByBrand - security: - - ims_key: [ ] - - api_key: [ ] - requestBody: - required: true - content: - application/json: - schema: - type: array - minItems: 1 - maxItems: 3000 - items: - $ref: './schemas.yaml#/V2PromptInput' - responses: - '201': - description: Prompts created/updated - content: - application/json: - schema: - type: object - properties: - created: - type: integer - updated: - type: integer - prompts: - type: array - description: Simplified prompt objects (no uuid, category, or topic — use GET to fetch full details) - items: - type: object - properties: - id: - type: string - description: Business key (prompt_id) - prompt: - type: string - regions: - type: array - items: - type: string - categoryId: - type: - - string - - 'null' - topicId: - type: - - string - - 'null' - status: - type: string - origin: - type: string - source: - type: string - updatedBy: - type: - - string - - 'null' - updatedAt: - type: - - string - - 'null' - '400': - $ref: './responses.yaml#/400' - '404': - description: Organization or brand not found - '503': - description: PostgREST unavailable - '500': - $ref: './responses.yaml#/500' - -v2-prompts-by-brand-and-id: - parameters: - - name: spaceCatId - in: path - required: true - schema: - type: string - format: uuid - - name: brandId - in: path - required: true - schema: - type: string - - name: promptId - in: path - required: true - schema: - type: string - description: prompt_id (business key) - get: - tags: - - customer-config - summary: Get a single prompt - operationId: getPromptByBrandAndId - security: - - ims_key: [ ] - - api_key: [ ] - responses: - '200': - description: Prompt retrieved - content: - application/json: - schema: - $ref: './schemas.yaml#/V2Prompt' - '404': - description: Organization, brand, or prompt not found - '503': - description: PostgREST unavailable - '500': - $ref: './responses.yaml#/500' - patch: - tags: - - customer-config - summary: Update a single prompt - operationId: updatePromptByBrandAndId - security: - - ims_key: [ ] - - api_key: [ ] - requestBody: - content: - application/json: - schema: - $ref: './schemas.yaml#/V2PromptInput' - responses: - '200': - description: Prompt updated - content: - application/json: - schema: - $ref: './schemas.yaml#/V2Prompt' - '404': - description: Organization, brand, or prompt not found - '503': - description: PostgREST unavailable - '500': - $ref: './responses.yaml#/500' - delete: - tags: - - customer-config - summary: Soft-delete a prompt - description: Sets status to 'deleted'. - operationId: deletePromptByBrandAndId - security: - - ims_key: [ ] - - api_key: [ ] - responses: - '204': - description: Prompt deleted - '404': - description: Organization, brand, or prompt not found - '503': - description: PostgREST unavailable - '500': - $ref: './responses.yaml#/500' - -v2-prompts-by-brand-bulk-delete: - parameters: - - name: spaceCatId - in: path - required: true - description: SpaceCat Organization ID (UUID) - schema: - type: string - format: uuid - - name: brandId - in: path - required: true - description: Brand ID (UUID, config id, or brand name) - schema: - type: string - post: - tags: - - customer-config - summary: Bulk soft-delete prompts - description: | - Soft-deletes multiple prompts by setting their status to 'deleted'. - Uses POST instead of DELETE because the body-data middleware does not parse DELETE request bodies. - See ADR-001 for details. - operationId: bulkDeletePromptsByBrand - security: - - ims_key: [ ] - - api_key: [ ] - requestBody: - required: true - content: - application/json: - schema: - $ref: './schemas.yaml#/V2PromptBulkDeleteRequest' - responses: - '200': - description: Bulk delete completed - content: - application/json: - schema: - $ref: './schemas.yaml#/V2PromptBulkDeleteResponse' - '400': - $ref: './responses.yaml#/400' - '404': - description: Organization or brand not found - '503': - description: PostgREST unavailable - '500': - $ref: './responses.yaml#/500' \ No newline at end of file diff --git a/docs/openapi/prompts-v2-api.yaml b/docs/openapi/prompts-v2-api.yaml new file mode 100644 index 000000000..fa5b7d0e5 --- /dev/null +++ b/docs/openapi/prompts-v2-api.yaml @@ -0,0 +1,324 @@ +# ── Brand-scoped prompts CRUD (Aurora) ── + +v2-prompts-by-brand: + parameters: + - name: spaceCatId + in: path + required: true + description: SpaceCat Organization ID (UUID) + schema: + type: string + format: uuid + - name: brandId + in: path + required: true + description: Brand ID (UUID, config id, or brand name) + schema: + type: string + - name: limit + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 5000 + default: 100 + description: Page size + - name: page + in: query + required: false + schema: + type: integer + minimum: 1 + default: 1 + description: Page number (1-based) + - name: categoryId + in: query + required: false + schema: + type: string + description: Filter by category business key + - name: topicId + in: query + required: false + schema: + type: string + description: Filter by topic business key + - name: status + in: query + required: false + schema: + type: string + enum: [active, pending, deleted] + description: Filter by status; default excludes deleted + - name: search + in: query + required: false + schema: + type: string + description: Free-text search across prompt text and name (ILIKE match) + - name: region + in: query + required: false + schema: + type: string + description: Filter by region (matches prompts whose regions array contains this value) + - name: origin + in: query + required: false + schema: + type: string + enum: [ai, human] + description: Filter by origin (ai or human) + - name: sort + in: query + required: false + schema: + type: string + enum: [topic, prompt, category, origin, status, updatedAt] + description: Sort column. Default is updatedAt descending + - name: order + in: query + required: false + schema: + type: string + enum: [asc, desc] + default: desc + description: Sort direction. Default is desc + get: + tags: + - customer-config + summary: List prompts for a brand + description: | + Returns a paginated list of prompts for the given brand. + Upsert uniqueness: match by id or by (text, regions). + operationId: listPromptsByBrand + security: + - ims_key: [ ] + - api_key: [ ] + responses: + '200': + description: Prompts retrieved successfully + content: + application/json: + schema: + $ref: './schemas.yaml#/V2PromptListResponse' + '400': + $ref: './responses.yaml#/400' + '404': + description: Organization or brand not found + '503': + description: PostgREST unavailable (DATA_SERVICE_PROVIDER=postgres required) + '500': + $ref: './responses.yaml#/500' + post: + tags: + - customer-config + summary: Create or upsert prompts (bulk) + description: | + Upserts prompts. Match by id or by (text, regions). If match found, update; else insert. + operationId: createPromptsByBrand + security: + - ims_key: [ ] + - api_key: [ ] + requestBody: + required: true + content: + application/json: + schema: + type: array + minItems: 1 + maxItems: 3000 + items: + $ref: './schemas.yaml#/V2PromptInput' + responses: + '201': + description: Prompts created/updated + content: + application/json: + schema: + type: object + properties: + created: + type: integer + updated: + type: integer + prompts: + type: array + description: Simplified prompt objects (no uuid, category, or topic — use GET to fetch full details) + items: + type: object + properties: + id: + type: string + description: Business key (prompt_id) + prompt: + type: string + regions: + type: array + items: + type: string + categoryId: + type: + - string + - 'null' + topicId: + type: + - string + - 'null' + status: + type: string + origin: + type: string + source: + type: string + updatedBy: + type: + - string + - 'null' + updatedAt: + type: + - string + - 'null' + '400': + $ref: './responses.yaml#/400' + '404': + description: Organization or brand not found + '503': + description: PostgREST unavailable + '500': + $ref: './responses.yaml#/500' + +v2-prompts-by-brand-and-id: + parameters: + - name: spaceCatId + in: path + required: true + schema: + type: string + format: uuid + - name: brandId + in: path + required: true + schema: + type: string + - name: promptId + in: path + required: true + schema: + type: string + description: prompt_id (business key) + get: + tags: + - customer-config + summary: Get a single prompt + operationId: getPromptByBrandAndId + security: + - ims_key: [ ] + - api_key: [ ] + responses: + '200': + description: Prompt retrieved + content: + application/json: + schema: + $ref: './schemas.yaml#/V2Prompt' + '404': + description: Organization, brand, or prompt not found + '503': + description: PostgREST unavailable + '500': + $ref: './responses.yaml#/500' + patch: + tags: + - customer-config + summary: Update a single prompt + operationId: updatePromptByBrandAndId + security: + - ims_key: [ ] + - api_key: [ ] + requestBody: + content: + application/json: + schema: + $ref: './schemas.yaml#/V2PromptInput' + responses: + '200': + description: Prompt updated + content: + application/json: + schema: + $ref: './schemas.yaml#/V2Prompt' + '404': + description: Organization, brand, or prompt not found + '503': + description: PostgREST unavailable + '500': + $ref: './responses.yaml#/500' + delete: + tags: + - customer-config + summary: Soft-delete a prompt + description: Sets status to 'deleted'. + operationId: deletePromptByBrandAndId + security: + - ims_key: [ ] + - api_key: [ ] + responses: + '204': + description: Prompt deleted + '404': + description: Organization, brand, or prompt not found + '503': + description: PostgREST unavailable + '500': + $ref: './responses.yaml#/500' + +v2-prompts-by-brand-bulk-delete: + parameters: + - name: spaceCatId + in: path + required: true + description: SpaceCat Organization ID (UUID) + schema: + type: string + format: uuid + - name: brandId + in: path + required: true + description: Brand ID (UUID, config id, or brand name) + schema: + type: string + post: + tags: + - customer-config + summary: Bulk soft-delete prompts + description: | + Soft-deletes multiple prompts by setting their status to 'deleted'. + Uses POST instead of DELETE because the body-data middleware does not parse DELETE request bodies. + See ADR-001 for details. + operationId: bulkDeletePromptsByBrand + security: + - ims_key: [ ] + - api_key: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: './schemas.yaml#/V2PromptBulkDeleteRequest' + responses: + '200': + description: Bulk delete completed + content: + application/json: + schema: + $ref: './schemas.yaml#/V2PromptBulkDeleteResponse' + '400': + $ref: './responses.yaml#/400' + '404': + description: Organization or brand not found + '503': + description: PostgREST unavailable + '500': + $ref: './responses.yaml#/500' diff --git a/src/routes/required-capabilities.js b/src/routes/required-capabilities.js index c8df73093..13e9ae997 100644 --- a/src/routes/required-capabilities.js +++ b/src/routes/required-capabilities.js @@ -201,12 +201,6 @@ const routeRequiredCapabilities = { 'GET /org/:spaceCatId/brands/:brandId/brand-presence/sentiment-movers': 'brand:read', 'GET /org/:spaceCatId/brands/all/brand-presence/share-of-voice': 'brand:read', 'GET /org/:spaceCatId/brands/:brandId/brand-presence/share-of-voice': 'brand:read', - 'GET /v2/orgs/:spaceCatId/llmo-customer-config': 'organization:read', - 'GET /v2/orgs/:spaceCatId/llmo-customer-config-lean': 'organization:read', - 'GET /v2/orgs/:spaceCatId/llmo-topics': 'organization:read', - 'GET /v2/orgs/:spaceCatId/llmo-prompts': 'organization:read', - 'POST /v2/orgs/:spaceCatId/llmo-customer-config': 'organization:write', - 'PATCH /v2/orgs/:spaceCatId/llmo-customer-config': 'organization:write', 'GET /organizations/:organizationId/projects': 'project:read', 'GET /organizations/:organizationId/projects/:projectId/sites': 'site:read', 'GET /organizations/:organizationId/by-project-name/:projectName/sites': 'site:read', diff --git a/src/support/customer-config-mapper.js b/src/support/customer-config-mapper.js index 2263db1ab..1ad3b6df8 100644 --- a/src/support/customer-config-mapper.js +++ b/src/support/customer-config-mapper.js @@ -332,191 +332,8 @@ export function convertV1ToV2(llmoConfig, brandName, imsOrgId) { }; } -/** - * Converts Customer Config (V2) to LLMO config (V1) - * @param {object} customerConfig - V2 Customer configuration - * @returns {object} V1 LLMO configuration - */ -export function convertV2ToV1(customerConfig) { - if (!isNonEmptyObject(customerConfig?.customer)) { - throw new Error('Customer config is required'); - } - - const { customer } = customerConfig; - const brands = customer.brands || []; - - if (brands.length === 0) { - throw new Error('At least one brand is required'); - } - - const brand = brands[0]; - - // Build lookup maps from top-level collections OR from inline prompt data - const categoriesMap = new Map(); - (customer.categories || []).forEach((cat) => { - categoriesMap.set(cat.id, cat); - }); - - const topicsMap = new Map(); - (customer.topics || []).forEach((topic) => { - topicsMap.set(topic.id, topic); - }); - - // If categories/topics not in collections, extract from prompts - if (brands.length > 0) { - brands[0].prompts?.forEach((prompt) => { - if (prompt.category && !categoriesMap.has(prompt.category.id)) { - categoriesMap.set(prompt.category.id, prompt.category); - } - if (prompt.topic && !topicsMap.has(prompt.topic.id)) { - topicsMap.set(prompt.topic.id, prompt.topic); - } - }); - } - - const llmoConfig = { - entities: {}, - categories: {}, - topics: {}, - aiTopics: {}, - deleted: { - prompts: {}, - }, - brands: { - aliases: brand.brandAliases.map((alias) => { - // Find first active category from prompts - const firstActivePrompt = brand.prompts.find((p) => p.status !== 'deleted'); - const firstCategoryId = firstActivePrompt?.categoryId || null; - return { - aliases: [alias.name], - category: firstCategoryId, - region: alias.regions, - aliasMode: 'extend', - updatedBy: brand.updatedBy || 'system', - updatedAt: brand.updatedAt || new Date().toISOString(), - }; - }), - }, - competitors: { - competitors: brand.competitors.map((comp) => ({ - name: comp.name, - url: comp.url, - regions: comp.regions, - })), - }, - }; - - // Add cdnBucketConfig from customer level if this brand's baseUrl matches - if (customer.cdnBucketConfigs - && customer.cdnBucketConfigs.length > 0 - && brand.baseUrl) { - const matchingCdnConfig = customer.cdnBucketConfigs.find( - (config) => config.urls && config.urls.includes(brand.baseUrl), - ); - - if (matchingCdnConfig) { - // eslint-disable-next-line no-unused-vars - const { urls: _urls, ...cdnConfig } = matchingCdnConfig; - llmoConfig.cdnBucketConfig = cdnConfig; - } - } - - // Rebuild V1 structure by grouping prompts - const promptsByTopic = new Map(); - - brand.prompts.forEach((prompt) => { - // Handle both inline objects and ID references - const categoryId = prompt.categoryId || prompt.category?.id; - const topicId = prompt.topicId || prompt.topic?.id; - - if (!categoryId || !topicId) return; - - const key = `${categoryId}::${topicId}`; - - if (!promptsByTopic.has(key)) { - promptsByTopic.set(key, []); - } - promptsByTopic.get(key).push(prompt); - }); - - // Process grouped prompts - promptsByTopic.forEach((prompts, key) => { - const [categoryId, topicId] = key.split('::'); - const category = categoriesMap.get(categoryId); - const topic = topicsMap.get(topicId); - - if (!category || !topic) return; - - const isAITopic = prompts.length > 0 && prompts.every((p) => p.origin === 'ai'); - const allDeleted = prompts.every((p) => p.status === 'deleted'); - - const activePrompts = prompts.filter((p) => p.status !== 'deleted'); - const deletedPrompts = prompts.filter((p) => p.status === 'deleted'); - - // Add category if not already added (only for active topics) - if (!allDeleted && !llmoConfig.categories[categoryId]) { - llmoConfig.categories[categoryId] = { - name: category.name, - region: prompts[0]?.regions || ['gl'], - urls: [], // Category URLs are lost in V2 - origin: category.origin || 'human', - updatedBy: category.updatedBy || 'system', - updatedAt: category.updatedAt || new Date().toISOString(), - }; - } - - // Add to appropriate section - if (isAITopic && !allDeleted) { - llmoConfig.aiTopics[topicId] = { - name: topic.name, - category: categoryId, - prompts: activePrompts.map((p) => ({ - prompt: p.prompt, - regions: p.regions, - origin: p.origin, - source: p.source, - updatedBy: p.updatedBy || 'system', - updatedAt: p.updatedAt || new Date().toISOString(), - })), - }; - } else if (!allDeleted) { - llmoConfig.topics[topicId] = { - name: topic.name, - category: categoryId, - prompts: activePrompts.map((p) => ({ - id: p.id, - prompt: p.prompt, - regions: p.regions, - origin: p.origin, - source: p.source, - status: p.status || 'active', - updatedBy: p.updatedBy || 'system', - updatedAt: p.updatedAt || new Date().toISOString(), - })), - }; - } - - // Add deleted prompts - deletedPrompts.forEach((p) => { - llmoConfig.deleted.prompts[p.id] = { - prompt: p.prompt, - regions: p.regions, - origin: p.origin, - source: p.source, - updatedBy: p.updatedBy || 'system', - updatedAt: p.updatedAt || new Date().toISOString(), - topic: topic.name, - category: category.name, - }; - }); - }); - - return llmoConfig; -} - export { generateBrandId }; export default { convertV1ToV2, - convertV2ToV1, }; diff --git a/src/support/customer-config-v2-metadata.js b/src/support/customer-config-v2-metadata.js deleted file mode 100644 index 1d87a7a7f..000000000 --- a/src/support/customer-config-v2-metadata.js +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright 2026 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -/* eslint-disable no-param-reassign */ - -import { deepEqual } from '@adobe/spacecat-shared-utils'; - -/** - * Removes metadata fields (updatedBy, updatedAt, status) from an object. - * @param {object} obj - The object to clean. - * @returns {object} A new object without metadata fields. - */ -const stripMetadata = (obj) => { - if (!obj || typeof obj !== 'object') return obj; - const { - // eslint-disable-next-line no-unused-vars - updatedAt, updatedBy, status, ...rest - } = obj; - return rest; -}; - -/** - * Merges arrays of objects by ID, preserving metadata for unchanged items. - * @param {Array} newItems - New array of items. - * @param {Array} oldItems - Old array of items. - * @param {string} userId - User ID performing the update. - * @param {string} timestamp - ISO timestamp. - * @returns {object} Object with merged items array and stats. - */ -const mergeArrayById = (newItems, oldItems, userId, timestamp) => { - const oldItemsMap = new Map(); - oldItems.forEach((item) => { - if (item.id) { - oldItemsMap.set(item.id, item); - } - }); - - let modified = 0; - const merged = newItems.map((newItem) => { - const oldItem = oldItemsMap.get(newItem.id); - - if (oldItem) { - const cleanNew = stripMetadata(newItem); - const cleanOld = stripMetadata(oldItem); - - if (deepEqual(cleanNew, cleanOld)) { - // Content unchanged, preserve metadata - return { - ...newItem, - updatedBy: oldItem.updatedBy, - updatedAt: oldItem.updatedAt, - }; - } - } - - // New or modified item - modified += 1; - return { - ...newItem, - updatedBy: userId, - updatedAt: timestamp, - }; - }); - - return { items: merged, modified, total: merged.length }; -}; - -/** - * Merges brand prompts, handling nested prompt arrays. - * @param {Array} newPrompts - New prompts array. - * @param {Array} oldPrompts - Old prompts array. - * @param {string} userId - User ID. - * @param {string} timestamp - ISO timestamp. - * @returns {object} Merged prompts with stats. - */ -const mergePrompts = (newPrompts, oldPrompts, userId, timestamp) => { - const oldPromptsMap = new Map(); - oldPrompts.forEach((prompt) => { - if (prompt.id) { - oldPromptsMap.set(prompt.id, prompt); - } - }); - - let modified = 0; - const merged = newPrompts.map((newPrompt) => { - const oldPrompt = oldPromptsMap.get(newPrompt.id); - - if (oldPrompt) { - const cleanNew = stripMetadata(newPrompt); - const cleanOld = stripMetadata(oldPrompt); - - if (deepEqual(cleanNew, cleanOld)) { - return { - ...newPrompt, - updatedBy: oldPrompt.updatedBy, - updatedAt: oldPrompt.updatedAt, - }; - } - } - - modified += 1; - return { - ...newPrompt, - updatedBy: userId, - updatedAt: timestamp, - }; - }); - - return { prompts: merged, modified, total: merged.length }; -}; - -/** - * Merges brands array, including nested prompts. - * @param {Array} newBrands - New brands array. - * @param {Array} oldBrands - Old brands array. - * @param {string} userId - User ID. - * @param {string} timestamp - ISO timestamp. - * @returns {object} Merged brands with stats. - */ -const mergeBrands = (newBrands, oldBrands, userId, timestamp) => { - const oldBrandsMap = new Map(); - oldBrands.forEach((brand) => { - if (brand.id) { - oldBrandsMap.set(brand.id, brand); - } - }); - - let brandsModified = 0; - let promptsModified = 0; - let promptsTotal = 0; - - const merged = newBrands.map((newBrand) => { - const oldBrand = oldBrandsMap.get(newBrand.id); - - // Merge prompts if they exist - let mergedPrompts = newBrand.prompts; - if (newBrand.prompts) { - const promptsResult = mergePrompts( - newBrand.prompts, - oldBrand?.prompts || [], - userId, - timestamp, - ); - mergedPrompts = promptsResult.prompts; - promptsModified += promptsResult.modified; - promptsTotal += promptsResult.total; - } - - const brandWithoutPrompts = { ...newBrand }; - delete brandWithoutPrompts.prompts; - - if (oldBrand) { - const oldBrandWithoutPrompts = { ...oldBrand }; - delete oldBrandWithoutPrompts.prompts; - - const cleanNew = stripMetadata(brandWithoutPrompts); - const cleanOld = stripMetadata(oldBrandWithoutPrompts); - - if (deepEqual(cleanNew, cleanOld)) { - // Brand metadata unchanged - return { - ...newBrand, - updatedBy: oldBrand.updatedBy, - updatedAt: oldBrand.updatedAt, - prompts: mergedPrompts, - }; - } - } - - // Brand is new or modified - brandsModified += 1; - return { - ...newBrand, - updatedBy: userId, - updatedAt: timestamp, - prompts: mergedPrompts, - }; - }); - - return { - brands: merged, - brandsModified, - brandsTotal: merged.length, - promptsModified, - promptsTotal, - }; -}; - -/** - * Merges V2 customer config updates with existing config. - * @param {object} updates - Partial config updates. - * @param {object} oldConfig - Existing config from S3. - * @param {string} userId - User ID performing the update. - * @returns {object} Merged config and stats. - */ -export function mergeCustomerConfigV2(updates, oldConfig, userId) { - const timestamp = new Date().toISOString(); - - // Start with existing config, then apply updates - const mergedCustomer = { - ...(oldConfig?.customer || {}), - ...(updates.customer || {}), - }; - - const stats = { - categories: { total: 0, modified: 0 }, - topics: { total: 0, modified: 0 }, - brands: { total: 0, modified: 0 }, - prompts: { total: 0, modified: 0 }, - }; - - // Merge categories - if (updates.customer?.categories) { - const result = mergeArrayById( - updates.customer.categories, - oldConfig?.customer?.categories || [], - userId, - timestamp, - ); - mergedCustomer.categories = result.items; - stats.categories = { total: result.total, modified: result.modified }; - } else if (oldConfig?.customer?.categories) { - mergedCustomer.categories = oldConfig.customer.categories; - stats.categories.total = oldConfig.customer.categories.length; - } - - // Merge topics - if (updates.customer?.topics) { - const result = mergeArrayById( - updates.customer.topics, - oldConfig?.customer?.topics || [], - userId, - timestamp, - ); - mergedCustomer.topics = result.items; - stats.topics = { total: result.total, modified: result.modified }; - } else if (oldConfig?.customer?.topics) { - mergedCustomer.topics = oldConfig.customer.topics; - stats.topics.total = oldConfig.customer.topics.length; - } - - // Merge brands (including nested prompts) - if (updates.customer?.brands) { - const result = mergeBrands( - updates.customer.brands, - oldConfig?.customer?.brands || [], - userId, - timestamp, - ); - mergedCustomer.brands = result.brands; - stats.brands = { - total: result.brandsTotal, - modified: result.brandsModified, - }; - stats.prompts = { - total: result.promptsTotal, - modified: result.promptsModified, - }; - } else if (oldConfig?.customer?.brands) { - mergedCustomer.brands = oldConfig.customer.brands; - stats.brands.total = oldConfig.customer.brands.length; - // Count existing prompts - stats.prompts.total = oldConfig.customer.brands.reduce( - (sum, brand) => sum + (brand.prompts?.length || 0), - 0, - ); - } - - // Preserve availableVerticals if not in updates - if (!updates.customer?.availableVerticals && oldConfig?.customer?.availableVerticals) { - mergedCustomer.availableVerticals = oldConfig.customer.availableVerticals; - } - - return { - mergedConfig: { customer: mergedCustomer }, - stats, - }; -} - -// Export stripMetadata for testing -export { stripMetadata }; diff --git a/test/routes/index.test.js b/test/routes/index.test.js index 68c45112c..5c87fbaf9 100755 --- a/test/routes/index.test.js +++ b/test/routes/index.test.js @@ -153,8 +153,6 @@ describe('getRouteHandlers', () => { const mockBrandsController = { getBrandsForOrganization: sinon.stub(), getBrandGuidelinesForSite: sinon.stub(), - getCustomerConfig: sinon.stub(), - saveCustomerConfig: sinon.stub(), listBrandsForOrg: sinon.stub(), listCategoriesForOrg: sinon.stub(), createCategoryForOrg: sinon.stub(), diff --git a/test/support/customer-config-mapper.test.js b/test/support/customer-config-mapper.test.js index a74aa48e3..b7495f474 100644 --- a/test/support/customer-config-mapper.test.js +++ b/test/support/customer-config-mapper.test.js @@ -13,7 +13,7 @@ /* eslint-env mocha */ import { expect } from 'chai'; -import { convertV1ToV2, convertV2ToV1 } from '../../src/support/customer-config-mapper.js'; +import { convertV1ToV2 } from '../../src/support/customer-config-mapper.js'; describe('Customer Config Mapper', () => { describe('convertV1ToV2', () => { @@ -712,1374 +712,4 @@ describe('Customer Config Mapper', () => { expect(result.customer.brands[0].brandAliases[1].name).to.equal('Fallback'); }); }); - - describe('convertV2ToV1', () => { - it('converts customer config to LLMO config', () => { - const customerConfig = { - customer: { - customerName: 'Adobe', - imsOrgID: '1234@AdobeOrg', - brands: [ - { - id: 'adobe-photoshop', - name: 'Adobe Photoshop', - brandAliases: [ - { name: 'Photoshop', regions: ['GL'] }, - ], - competitors: [ - { name: 'GIMP', url: 'https://gimp.org', regions: ['GL'] }, - ], - prompts: [ - { - id: 'prompt-1', - prompt: 'What is the best photo editing software?', - regions: ['us', 'gb'], - origin: 'human', - source: 'config', - status: 'active', - category: { - id: 'photoshop-photo-editing', - name: 'Photo Editing', - }, - topic: { - id: 'photoshop-topic-1', - name: 'Photo Retouching', - }, - }, - ], - }, - ], - }, - }; - - const result = convertV2ToV1(customerConfig); - - expect(result).to.have.property('brands'); - expect(result.brands.aliases).to.have.lengthOf(1); - expect(result.brands.aliases[0].aliases[0]).to.equal('Photoshop'); - - expect(result.competitors.competitors).to.have.lengthOf(1); - expect(result.competitors.competitors[0].name).to.equal('GIMP'); - - expect(Object.keys(result.categories)).to.have.lengthOf(1); - expect(Object.keys(result.topics)).to.have.lengthOf(1); - expect(result.categories['photoshop-photo-editing'].name).to.equal('Photo Editing'); - expect(result.topics['photoshop-topic-1'].name).to.equal('Photo Retouching'); - expect(result.topics['photoshop-topic-1'].prompts).to.have.lengthOf(1); - }); - - it('throws error if customer config is missing', () => { - expect(() => convertV2ToV1(null)).to.throw('Customer config is required'); - }); - - it('throws error if customer config is empty object', () => { - expect(() => convertV2ToV1({})).to.throw('Customer config is required'); - }); - - it('throws error if no brands exist', () => { - const customerConfig = { - customer: { - customerName: 'Adobe', - imsOrgID: '1234@AdobeOrg', - brands: [], - }, - }; - - expect(() => convertV2ToV1(customerConfig)).to.throw('At least one brand is required'); - }); - - it('handles customer with undefined brands array', () => { - const customerConfig = { - customer: { - customerName: 'Adobe', - imsOrgID: '1234@AdobeOrg', - // brands is undefined - }, - }; - - expect(() => convertV2ToV1(customerConfig)).to.throw('At least one brand is required'); - }); - - it('uses categories and topics from top-level collections', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Test prompt', - status: 'active', - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['us'], - origin: 'human', - source: 'config', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1', origin: 'human' }], - topics: [{ id: 'topic-1', name: 'Topic 1', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.categories['cat-1'].name).to.equal('Category 1'); - expect(result.topics['topic-1'].name).to.equal('Topic 1'); - }); - - it('extracts categories from inline prompt data when not in collections', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Test prompt', - status: 'active', - category: { id: 'inline-cat', name: 'Inline Category' }, - topic: { id: 'inline-topic', name: 'Inline Topic' }, - regions: ['us'], - origin: 'human', - source: 'config', - }, - ], - }, - ], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.categories['inline-cat'].name).to.equal('Inline Category'); - expect(result.topics['inline-topic'].name).to.equal('Inline Topic'); - }); - - it('handles prompts without categoryId or topicId', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Orphaned prompt', - status: 'active', - regions: ['us'], - }, - ], - }, - ], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(Object.keys(result.topics)).to.have.lengthOf(0); - }); - - it('finds first active category for brand aliases', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Active prompt', - status: 'active', - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['us'], - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1' }], - topics: [{ id: 'topic-1', name: 'Topic 1', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.brands.aliases[0].category).to.equal('cat-1'); - }); - - it('handles brand with no active prompts for alias category', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [], - }, - ], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.brands.aliases[0].category).to.be.null; - }); - - it('includes cdnBucketConfig when baseUrl matches', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - baseUrl: 'https://example.com', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [], - }, - ], - cdnBucketConfigs: [ - { - urls: ['https://example.com'], - bucket: 'test-bucket', - region: 'us-east-1', - }, - ], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.cdnBucketConfig).to.exist; - expect(result.cdnBucketConfig.bucket).to.equal('test-bucket'); - expect(result.cdnBucketConfig.urls).to.be.undefined; - }); - - it('excludes cdnBucketConfig when baseUrl does not match', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - baseUrl: 'https://different.com', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [], - }, - ], - cdnBucketConfigs: [ - { - urls: ['https://example.com'], - bucket: 'test-bucket', - }, - ], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.cdnBucketConfig).to.be.undefined; - }); - - it('handles missing cdnBucketConfigs', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - baseUrl: 'https://example.com', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [], - }, - ], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.cdnBucketConfig).to.be.undefined; - }); - - it('handles empty cdnBucketConfigs array', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - baseUrl: 'https://example.com', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [], - }, - ], - cdnBucketConfigs: [], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.cdnBucketConfig).to.be.undefined; - }); - - it('handles brand without baseUrl', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [], - }, - ], - cdnBucketConfigs: [ - { - urls: ['https://example.com'], - bucket: 'test-bucket', - }, - ], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.cdnBucketConfig).to.be.undefined; - }); - - it('groups prompts by topic and identifies AI topics', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'AI prompt 1', - status: 'active', - categoryId: 'cat-1', - topicId: 'topic-ai', - regions: ['us'], - origin: 'ai', - source: 'flow', - }, - { - id: 'p2', - prompt: 'AI prompt 2', - status: 'active', - categoryId: 'cat-1', - topicId: 'topic-ai', - regions: ['us'], - origin: 'ai', - source: 'flow', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1', origin: 'human' }], - topics: [{ id: 'topic-ai', name: 'AI Topic', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.aiTopics['topic-ai']).to.exist; - expect(result.aiTopics['topic-ai'].prompts).to.have.lengthOf(2); - expect(result.topics['topic-ai']).to.not.exist; - }); - - it('groups prompts by topic and identifies human topics', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Human prompt', - status: 'active', - categoryId: 'cat-1', - topicId: 'topic-human', - regions: ['us'], - origin: 'human', - source: 'config', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1', origin: 'human' }], - topics: [{ id: 'topic-human', name: 'Human Topic', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.topics['topic-human']).to.exist; - expect(result.topics['topic-human'].prompts).to.have.lengthOf(1); - expect(result.aiTopics['topic-human']).to.not.exist; - }); - - it('separates active and deleted prompts', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Active prompt', - status: 'active', - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['us'], - origin: 'human', - source: 'config', - }, - { - id: 'p2', - prompt: 'Deleted prompt', - status: 'deleted', - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['us'], - origin: 'human', - source: 'config', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1', origin: 'human' }], - topics: [{ id: 'topic-1', name: 'Topic 1', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.topics['topic-1'].prompts).to.have.lengthOf(1); - expect(result.topics['topic-1'].prompts[0].id).to.equal('p1'); - expect(result.deleted.prompts.p2).to.exist; - }); - - it('adds category only for active topics', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Deleted prompt', - status: 'deleted', - categoryId: 'cat-deleted', - topicId: 'topic-deleted', - regions: ['us'], - origin: 'human', - source: 'config', - }, - ], - }, - ], - categories: [{ id: 'cat-deleted', name: 'Deleted Category', status: 'deleted' }], - topics: [{ - id: 'topic-deleted', name: 'Deleted Topic', categoryId: 'cat-deleted', status: 'deleted', - }], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.categories['cat-deleted']).to.not.exist; - }); - - it('uses category regions from prompts', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Test prompt', - status: 'active', - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['jp', 'kr'], - origin: 'human', - source: 'config', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1', origin: 'human' }], - topics: [{ id: 'topic-1', name: 'Topic 1', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.categories['cat-1'].region).to.deep.equal(['jp', 'kr']); - }); - - it('uses default regions when prompt has no regions', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Test prompt', - status: 'active', - categoryId: 'cat-1', - topicId: 'topic-1', - origin: 'human', - source: 'config', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1', origin: 'human' }], - topics: [{ id: 'topic-1', name: 'Topic 1', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.categories['cat-1'].region).to.deep.equal(['gl']); - }); - - it('preserves category metadata', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Test prompt', - status: 'active', - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['us'], - origin: 'human', - source: 'config', - }, - ], - }, - ], - categories: [{ - id: 'cat-1', - name: 'Category 1', - origin: 'ai', - updatedBy: 'admin', - updatedAt: '2024-01-01T00:00:00.000Z', - }], - topics: [{ id: 'topic-1', name: 'Topic 1', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - const category = result.categories['cat-1']; - expect(category.origin).to.equal('ai'); - expect(category.updatedBy).to.equal('admin'); - expect(category.updatedAt).to.equal('2024-01-01T00:00:00.000Z'); - }); - - it('uses default category metadata when not provided', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Test prompt', - status: 'active', - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['us'], - origin: 'human', - source: 'config', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1' }], - topics: [{ id: 'topic-1', name: 'Topic 1', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - const category = result.categories['cat-1']; - expect(category.origin).to.equal('human'); - expect(category.updatedBy).to.equal('system'); - }); - - it('preserves brand metadata in aliases', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - updatedBy: 'user1', - updatedAt: '2024-01-01T00:00:00.000Z', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [], - }, - ], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.brands.aliases[0].updatedBy).to.equal('user1'); - expect(result.brands.aliases[0].updatedAt).to.equal('2024-01-01T00:00:00.000Z'); - }); - - it('uses default brand metadata when not provided', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [], - }, - ], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.brands.aliases[0].updatedBy).to.equal('system'); - expect(result.brands.aliases[0].updatedAt).to.be.a('string'); - }); - - it('preserves AI topic prompt metadata', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'AI prompt', - status: 'active', - categoryId: 'cat-1', - topicId: 'topic-ai', - regions: ['us'], - origin: 'ai', - source: 'flow', - updatedBy: 'agent', - updatedAt: '2024-01-01T00:00:00.000Z', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1' }], - topics: [{ id: 'topic-ai', name: 'AI Topic', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - const prompt = result.aiTopics['topic-ai'].prompts[0]; - expect(prompt.updatedBy).to.equal('agent'); - expect(prompt.updatedAt).to.equal('2024-01-01T00:00:00.000Z'); - }); - - it('omits prompt ID from AI topic prompts', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'AI prompt', - status: 'active', - categoryId: 'cat-1', - topicId: 'topic-ai', - regions: ['us'], - origin: 'ai', - source: 'flow', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1' }], - topics: [{ id: 'topic-ai', name: 'AI Topic', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.aiTopics['topic-ai'].prompts[0].id).to.be.undefined; - }); - - it('includes prompt ID in human topic prompts', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Human prompt', - status: 'active', - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['us'], - origin: 'human', - source: 'config', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1' }], - topics: [{ id: 'topic-1', name: 'Topic 1', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.topics['topic-1'].prompts[0].id).to.equal('p1'); - }); - - it('uses default prompt status when not provided', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Test prompt', - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['us'], - origin: 'human', - source: 'config', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1' }], - topics: [{ id: 'topic-1', name: 'Topic 1', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(result.topics['topic-1'].prompts[0].status).to.equal('active'); - }); - - it('uses default prompt metadata when not provided', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Test prompt', - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['us'], - origin: 'human', - source: 'config', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1' }], - topics: [{ id: 'topic-1', name: 'Topic 1', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - const prompt = result.topics['topic-1'].prompts[0]; - expect(prompt.updatedBy).to.equal('system'); - expect(prompt.updatedAt).to.be.a('string'); - }); - - it('skips category and topic when not found in maps', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Orphan prompt', - categoryId: 'missing-cat', - topicId: 'missing-topic', - regions: ['us'], - }, - ], - }, - ], - categories: [], - topics: [], - }, - }; - - const result = convertV2ToV1(customerConfig); - expect(Object.keys(result.categories)).to.have.lengthOf(0); - expect(Object.keys(result.topics)).to.have.lengthOf(0); - }); - - it('moves all prompts to deleted section when all have deleted status', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Active but deleted topic 1', - status: 'deleted', - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['us'], - origin: 'human', - source: 'config', - updatedBy: 'admin', - updatedAt: '2024-01-01T00:00:00.000Z', - }, - { - id: 'p2', - prompt: 'Active but deleted topic 2', - status: 'deleted', - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['gb'], - origin: 'ai', - source: 'flow', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1', status: 'deleted' }], - topics: [{ - id: 'topic-1', name: 'Topic 1', categoryId: 'cat-1', status: 'deleted', - }], - }, - }; - - const result = convertV2ToV1(customerConfig); - - // Should not create topics or aiTopics - expect(result.topics['topic-1']).to.not.exist; - expect(result.aiTopics['topic-1']).to.not.exist; - - // Should move all prompts to deleted section - expect(result.deleted.prompts.p1).to.exist; - expect(result.deleted.prompts.p1.prompt).to.equal('Active but deleted topic 1'); - expect(result.deleted.prompts.p1.topic).to.equal('Topic 1'); - expect(result.deleted.prompts.p1.category).to.equal('Category 1'); - expect(result.deleted.prompts.p1.updatedBy).to.equal('admin'); - expect(result.deleted.prompts.p1.updatedAt).to.equal('2024-01-01T00:00:00.000Z'); - - expect(result.deleted.prompts.p2).to.exist; - expect(result.deleted.prompts.p2.prompt).to.equal('Active but deleted topic 2'); - expect(result.deleted.prompts.p2.topic).to.equal('Topic 1'); - expect(result.deleted.prompts.p2.category).to.equal('Category 1'); - }); - - it('uses default metadata when missing in allDeleted prompts', () => { - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Prompt without metadata', - status: 'deleted', - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['us'], - origin: 'human', - source: 'config', - }, - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1' }], - topics: [{ id: 'topic-1', name: 'Topic 1', categoryId: 'cat-1' }], - }, - }; - - const result = convertV2ToV1(customerConfig); - - expect(result.deleted.prompts.p1.updatedBy).to.equal('system'); - expect(result.deleted.prompts.p1.updatedAt).to.be.a('string'); - }); - - it('handles mixed active and inactive prompts all marked for deletion', () => { - // This is an edge case where prompts have mixed statuses but all are considered deleted - // Testing lines 516-525: the allDeleted block that processes activePrompts - const customerConfig = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'test-brand', - name: 'Test Brand', - brandAliases: [{ name: 'Test', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Prompt 1', - status: 'active', // Active status - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['us'], - origin: 'human', - source: 'config', - }, - { - id: 'p2', - prompt: 'Prompt 2', - status: 'active', // Active status - categoryId: 'cat-1', - topicId: 'topic-1', - regions: ['gb'], - origin: 'human', - source: 'config', - }, - // All prompts will be treated as deleted via the allDeleted logic - ], - }, - ], - categories: [{ id: 'cat-1', name: 'Category 1', status: 'deleted' }], - topics: [{ - id: 'topic-1', name: 'Topic 1', categoryId: 'cat-1', status: 'deleted', - }], - }, - }; - - const result = convertV2ToV1(customerConfig); - - // Since category and topic are deleted but prompts are active, - // and allDeleted logic depends on prompt status, not category/topic status, - // this should create a normal topic (not all deleted) - expect(result.topics['topic-1']).to.exist; - expect(result.topics['topic-1'].prompts).to.have.lengthOf(2); - }); - }); - - describe('roundtrip conversion', () => { - it('maintains data integrity through V1→V2→V1', () => { - const originalLlmo = { - brands: { - aliases: [ - { name: 'Test Brand', regions: ['US'] }, - ], - }, - competitors: { - competitors: [ - { name: 'Competitor', url: 'https://example.com', regions: ['GL'] }, - ], - }, - categories: { - 'cat-1': { - name: 'Category 1', - region: 'us', - urls: [], - }, - }, - topics: { - 'topic-1': { - name: 'Topic 1', - category: 'cat-1', - prompts: [ - { - id: 'prompt-1', - prompt: 'Test prompt', - regions: ['us'], - origin: 'human', - source: 'config', - status: 'active', - }, - ], - }, - }, - }; - - const v2 = convertV1ToV2(originalLlmo, 'Test Company', 'test@org'); - const backToV1 = convertV2ToV1(v2); - - expect(backToV1.brands.aliases[0].aliases[0]).to.equal('Test Brand'); - expect(backToV1.competitors.competitors[0].name).to.equal('Competitor'); - expect(Object.values(backToV1.categories)[0].name).to.equal('Category 1'); - expect(Object.values(backToV1.topics)[0].name).to.equal('Topic 1'); - expect(Object.values(backToV1.topics)[0].prompts[0].prompt).to.equal('Test prompt'); - expect(Object.values(backToV1.topics)[0].prompts[0].id).to.equal('prompt-1'); - }); - - it('handles deleted prompts correctly', () => { - const v2Config = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'brand-1', - name: 'Test Brand', - brandAliases: [{ name: 'Test Brand', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Active', - status: 'active', - categoryId: 'c1', - topicId: 't1', - regions: ['us'], - }, - { - id: 'p2', - prompt: 'Deleted', - status: 'deleted', - categoryId: 'c1', - topicId: 't1', - regions: ['us'], - }, - ], - }, - ], - categories: [{ id: 'c1', name: 'Category 1' }], - topics: [{ id: 't1', name: 'Topic 1', categoryId: 'c1' }], - }, - }; - - const v1Config = convertV2ToV1(v2Config); - - expect(v1Config.topics.t1.prompts).to.have.lengthOf(1); - expect(v1Config.deleted.prompts.p2).to.exist; - expect(v1Config.deleted.prompts.p2.prompt).to.equal('Deleted'); - }); - - it('handles all prompts deleted in a topic', () => { - const v2Config = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'brand-1', - name: 'Test Brand', - brandAliases: [ - { name: 'Test Brand', regions: ['us'] }, - ], - competitors: [], - prompts: [ - { - id: 'p1', - prompt: 'Deleted 1', - status: 'deleted', - categoryId: 'c1', - topicId: 't1', - regions: ['us'], - }, - { - id: 'p2', - prompt: 'Deleted 2', - status: 'deleted', - categoryId: 'c1', - topicId: 't1', - regions: ['us'], - }, - ], - }, - ], - categories: [ - { id: 'c1', name: 'Category 1', status: 'deleted' }, - ], - topics: [ - { - id: 't1', name: 'Topic 1', categoryId: 'c1', status: 'deleted', - }, - ], - }, - }; - - const v1Config = convertV2ToV1(v2Config); - - expect(v1Config.topics.t1).to.not.exist; - expect(v1Config.deleted.prompts.p1).to.exist; - expect(v1Config.deleted.prompts.p2).to.exist; - }); - }); - - describe('Deterministic ID generation', () => { - it('generates consistent prompt IDs from brand name and prompt text', () => { - const llmoConfig = { - brands: { - aliases: [{ name: 'Adobe Photoshop', regions: ['US'] }], - }, - categories: { - 'cat-1': { name: 'Photo Editing', region: 'us', urls: [] }, - }, - topics: { - 'topic-1': { - name: 'Retouching', - category: 'cat-1', - prompts: [ - { prompt: 'What is the best photo editor?', regions: ['us'] }, - { prompt: 'How do I retouch photos?', regions: ['us'] }, - ], - }, - }, - }; - - const result1 = convertV1ToV2(llmoConfig, 'Adobe', '1234@AdobeOrg'); - const result2 = convertV1ToV2(llmoConfig, 'Adobe', '1234@AdobeOrg'); - - // Same prompt text should generate same ID - const brand1 = result1.customer.brands[0]; - const brand2 = result2.customer.brands[0]; - expect(brand1.prompts[0].id).to.equal(brand2.prompts[0].id); - expect(brand1.prompts[1].id).to.equal(brand2.prompts[1].id); - - // Different prompt text should generate different IDs - expect(brand1.prompts[0].id).to.not.equal(brand1.prompts[1].id); - - // IDs should be in format: brandslug-hash - expect(result1.customer.brands[0].prompts[0].id).to.match(/^adobe-[a-f0-9]{8}$/); - }); - - it('uses existing prompt IDs when present', () => { - const llmoConfig = { - brands: { - aliases: [{ name: 'Test Brand', regions: ['US'] }], - }, - categories: { - 'cat-1': { name: 'Category', region: 'us', urls: [] }, - }, - topics: { - 'topic-1': { - name: 'Topic', - category: 'cat-1', - prompts: [ - { id: 'custom-id-123', prompt: 'Test prompt', regions: ['us'] }, - ], - }, - }, - }; - - const result = convertV1ToV2(llmoConfig, 'TestCo', 'test@org'); - - // Should preserve existing ID - expect(result.customer.brands[0].prompts[0].id).to.equal('custom-id-123'); - }); - - it('generates deterministic IDs for AI topics as well', () => { - const llmoConfig = { - brands: { - aliases: [{ name: 'Test Brand', regions: ['US'] }], - }, - categories: { - 'cat-1': { name: 'Category', region: 'us', urls: [] }, - }, - topics: {}, - aiTopics: { - 'ai-topic-1': { - name: 'AI Topic', - category: 'cat-1', - prompts: [ - { prompt: 'AI generated question?', regions: ['us'] }, - ], - }, - }, - }; - - const result1 = convertV1ToV2(llmoConfig, 'TestCo', 'test@org'); - const result2 = convertV1ToV2(llmoConfig, 'TestCo', 'test@org'); - - // AI topic prompts should also get deterministic IDs - const brand1 = result1.customer.brands[0]; - const brand2 = result2.customer.brands[0]; - expect(brand1.prompts[0].id).to.equal(brand2.prompts[0].id); - expect(brand1.prompts[0].origin).to.equal('ai'); - }); - }); - - describe('V2 to V1 conversion - AI topics', () => { - it('converts AI-generated prompts back to aiTopics section', () => { - const v2Config = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'brand-1', - name: 'Test Brand', - brandAliases: [{ name: 'Test Brand', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'ai-prompt-1', - prompt: 'AI question?', - status: 'active', - categoryId: 'c1', - topicId: 't1', - regions: ['us'], - origin: 'ai', - source: 'flow', - }, - ], - }, - ], - categories: [{ id: 'c1', name: 'Category 1' }], - topics: [{ id: 't1', name: 'AI Topic', categoryId: 'c1' }], - }, - }; - - const v1Config = convertV2ToV1(v2Config); - - // Should be in aiTopics, not regular topics - expect(v1Config.aiTopics).to.exist; - expect(v1Config.aiTopics.t1).to.exist; - expect(v1Config.aiTopics.t1.prompts).to.have.lengthOf(1); - expect(v1Config.aiTopics.t1.prompts[0].prompt).to.equal('AI question?'); - }); - - it('handles all AI topic prompts being deleted', () => { - const v2Config = { - customer: { - customerName: 'Test', - brands: [ - { - id: 'brand-1', - name: 'Test Brand', - brandAliases: [{ name: 'Test Brand', regions: ['us'] }], - competitors: [], - prompts: [ - { - id: 'ai-prompt-1', - prompt: 'Deleted AI question?', - status: 'deleted', - categoryId: 'c1', - topicId: 't1', - regions: ['us'], - origin: 'ai', - source: 'flow', - }, - ], - }, - ], - categories: [{ id: 'c1', name: 'Category 1' }], - topics: [{ - id: 't1', name: 'AI Topic', categoryId: 'c1', status: 'deleted', - }], - }, - }; - - const v1Config = convertV2ToV1(v2Config); - - // Should NOT be in aiTopics since all prompts deleted - expect(v1Config.aiTopics.t1).to.not.exist; - // Should be in deleted section - expect(v1Config.deleted.prompts['ai-prompt-1']).to.exist; - expect(v1Config.deleted.prompts['ai-prompt-1'].prompt).to.equal('Deleted AI question?'); - }); - }); }); diff --git a/test/support/customer-config-v2-metadata.test.js b/test/support/customer-config-v2-metadata.test.js deleted file mode 100644 index 2cf2515b7..000000000 --- a/test/support/customer-config-v2-metadata.test.js +++ /dev/null @@ -1,1760 +0,0 @@ -/* - * Copyright 2026 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import { expect } from 'chai'; -import { mergeCustomerConfigV2 } from '../../src/support/customer-config-v2-metadata.js'; - -describe('Customer Config V2 Metadata', () => { - describe('mergeCustomerConfigV2', () => { - const userId = 'test-user@example.com'; - - it('should merge new brands with existing config', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - updatedBy: 'old-user@example.com', - updatedAt: '2026-01-01T00:00:00.000Z', - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-2', - name: 'Brand Two', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.brands).to.have.lengthOf(1); - expect(mergedConfig.customer.brands[0].id).to.equal('brand-2'); - expect(mergedConfig.customer.brands[0].updatedBy).to.equal(userId); - expect(stats.brands.total).to.equal(1); - expect(stats.brands.modified).to.equal(1); - }); - - it('should preserve metadata for unchanged brands', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - updatedBy: 'old-user@example.com', - updatedAt: '2026-01-01T00:00:00.000Z', - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.brands[0].updatedBy).to.equal('old-user@example.com'); - expect(mergedConfig.customer.brands[0].updatedAt).to.equal('2026-01-01T00:00:00.000Z'); - expect(stats.brands.modified).to.equal(0); - }); - - it('should update metadata for modified brands', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - updatedBy: 'old-user@example.com', - updatedAt: '2026-01-01T00:00:00.000Z', - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One Updated', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.brands[0].name).to.equal('Brand One Updated'); - expect(mergedConfig.customer.brands[0].updatedBy).to.equal(userId); - expect(mergedConfig.customer.brands[0].updatedAt).to.not.equal('2026-01-01T00:00:00.000Z'); - expect(stats.brands.modified).to.equal(1); - }); - - it('should merge categories', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - categories: [ - { - id: 'cat-1', - name: 'Category One', - status: 'active', - }, - ], - }, - }; - - const updates = { - customer: { - categories: [ - { - id: 'cat-2', - name: 'Category Two', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.categories).to.have.lengthOf(1); - expect(mergedConfig.customer.categories[0].id).to.equal('cat-2'); - expect(stats.categories.total).to.equal(1); - expect(stats.categories.modified).to.equal(1); - }); - - it('should merge brand prompts', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { - id: 'prompt-1', - prompt: 'What is this?', - status: 'active', - updatedBy: 'old-user@example.com', - updatedAt: '2026-01-01T00:00:00.000Z', - }, - ], - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { - id: 'prompt-1', - prompt: 'What is this?', - status: 'active', - }, - { - id: 'prompt-2', - prompt: 'New prompt', - status: 'active', - }, - ], - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.brands[0].prompts).to.have.lengthOf(2); - expect(mergedConfig.customer.brands[0].prompts[0].updatedBy).to.equal('old-user@example.com'); - expect(mergedConfig.customer.brands[0].prompts[1].updatedBy).to.equal(userId); - expect(stats.prompts.total).to.equal(2); - expect(stats.prompts.modified).to.equal(1); - }); - - it('should preserve existing customer fields when not in updates', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - availableVerticals: ['Software & Technology'], - brands: [], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.customerName).to.equal('Test Customer'); - expect(mergedConfig.customer.imsOrgID).to.equal('TEST123@AdobeOrg'); - expect(mergedConfig.customer.availableVerticals).to.deep.equal(['Software & Technology']); - }); - - it('should handle null existing config', () => { - const updates = { - customer: { - customerName: 'New Customer', - imsOrgID: 'NEW123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, null, userId); - - expect(mergedConfig.customer.customerName).to.equal('New Customer'); - expect(mergedConfig.customer.brands).to.have.lengthOf(1); - expect(stats.brands.modified).to.equal(1); - }); - - it('should handle empty arrays in updates', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - }, - ], - }, - }; - - const updates = { - customer: { - brands: [], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.brands).to.have.lengthOf(0); - expect(stats.brands.total).to.equal(0); - }); - - it('should preserve categories when not in updates', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - categories: [ - { - id: 'cat-1', - name: 'Category One', - status: 'active', - }, - ], - brands: [], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.categories).to.have.lengthOf(1); - expect(mergedConfig.customer.categories[0].name).to.equal('Category One'); - }); - - it('should count existing prompts when brands not in updates', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { id: 'p1', prompt: 'Q1', status: 'active' }, - { id: 'p2', prompt: 'Q2', status: 'active' }, - ], - }, - ], - }, - }; - - const updates = { - customer: { - categories: [ - { - id: 'cat-1', - name: 'Category One', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.brands).to.have.lengthOf(1); - expect(stats.prompts.total).to.equal(2); - expect(stats.prompts.modified).to.equal(0); - }); - - it('should handle brands without prompts', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - // When prompts is not in the update, it should be undefined/not set - expect(mergedConfig.customer.brands[0].prompts).to.be.undefined; - expect(stats.prompts.total).to.equal(0); - }); - - it('should handle updates without customer object', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [], - }, - }; - - const updates = {}; - - const { mergedConfig } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.customerName).to.equal('Test Customer'); - expect(mergedConfig.customer.imsOrgID).to.equal('TEST123@AdobeOrg'); - }); - - it('should preserve topics when not in updates', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - topics: [ - { - id: 'topic-1', - name: 'Topic One', - categoryId: 'cat-1', - status: 'active', - }, - ], - brands: [], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.topics).to.have.lengthOf(1); - expect(mergedConfig.customer.topics[0].name).to.equal('Topic One'); - expect(stats.topics.total).to.equal(1); - }); - - it('should merge topics', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - topics: [ - { - id: 'topic-1', - name: 'Topic One', - categoryId: 'cat-1', - status: 'active', - updatedBy: 'old-user@example.com', - updatedAt: '2026-01-01T00:00:00.000Z', - }, - ], - }, - }; - - const updates = { - customer: { - topics: [ - { - id: 'topic-1', - name: 'Topic One Updated', - categoryId: 'cat-1', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.topics[0].name).to.equal('Topic One Updated'); - expect(mergedConfig.customer.topics[0].updatedBy).to.equal(userId); - expect(stats.topics.modified).to.equal(1); - }); - - it('should preserve metadata for unchanged array items', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - categories: [ - { - id: 'cat-1', - name: 'Category One', - status: 'active', - updatedBy: 'old-user@example.com', - updatedAt: '2026-01-01T00:00:00.000Z', - }, - ], - }, - }; - - // Same content, should preserve metadata - const updates = { - customer: { - categories: [ - { - id: 'cat-1', - name: 'Category One', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.categories[0].updatedBy).to.equal('old-user@example.com'); - expect(mergedConfig.customer.categories[0].updatedAt).to.equal('2026-01-01T00:00:00.000Z'); - expect(stats.categories.modified).to.equal(0); - }); - - it('should handle array comparisons with different lengths', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - region: ['US', 'GB'], - updatedBy: 'old-user@example.com', - updatedAt: '2026-01-01T00:00:00.000Z', - }, - ], - }, - }; - - // Same content but different region array should update - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - region: ['US'], // Changed array length - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.brands[0].region).to.deep.equal(['US']); - expect(mergedConfig.customer.brands[0].updatedBy).to.equal(userId); - expect(stats.brands.modified).to.equal(1); - }); - - it('should handle nested objects in arrays', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - urls: [ - { value: 'https://example.com', regions: ['US'] }, - ], - updatedBy: 'old-user@example.com', - updatedAt: '2026-01-01T00:00:00.000Z', - }, - ], - }, - }; - - // Same nested objects should preserve metadata - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - urls: [ - { value: 'https://example.com', regions: ['US'] }, - ], - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.brands[0].updatedBy).to.equal('old-user@example.com'); - expect(stats.brands.modified).to.equal(0); - }); - - it('should update prompts and preserve brand metadata when only prompts change', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - updatedBy: 'old-user@example.com', - updatedAt: '2026-01-01T00:00:00.000Z', - prompts: [ - { - id: 'p1', - prompt: 'Question 1', - status: 'active', - updatedBy: 'old-user@example.com', - updatedAt: '2026-01-01T00:00:00.000Z', - }, - ], - }, - ], - }, - }; - - // Update prompt but not brand - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { - id: 'p1', - prompt: 'Question 1 Updated', - status: 'active', - }, - ], - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - // Brand metadata should be preserved - expect(mergedConfig.customer.brands[0].updatedBy).to.equal('old-user@example.com'); - expect(mergedConfig.customer.brands[0].updatedAt).to.equal('2026-01-01T00:00:00.000Z'); - - // But prompt should be updated - expect(mergedConfig.customer.brands[0].prompts[0].prompt).to.equal('Question 1 Updated'); - expect(mergedConfig.customer.brands[0].prompts[0].updatedBy).to.equal(userId); - expect(stats.prompts.modified).to.equal(1); - }); - - it('should preserve prompt metadata when prompt is unchanged', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { - id: 'p1', - prompt: 'Question 1', - status: 'active', - regions: ['US', 'GB'], - updatedBy: 'old-user@example.com', - updatedAt: '2026-01-01T00:00:00.000Z', - }, - ], - }, - ], - }, - }; - - // Same prompt content - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { - id: 'p1', - prompt: 'Question 1', - status: 'active', - regions: ['US', 'GB'], - }, - ], - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.brands[0].prompts[0].updatedBy).to.equal('old-user@example.com'); - expect(mergedConfig.customer.brands[0].prompts[0].updatedAt).to.equal('2026-01-01T00:00:00.000Z'); - expect(stats.prompts.modified).to.equal(0); - }); - - it('should handle adding new prompts to existing brand', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { - id: 'p1', - prompt: 'Question 1', - status: 'active', - }, - ], - }, - ], - }, - }; - - // Add a new prompt - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { - id: 'p1', - prompt: 'Question 1', - status: 'active', - }, - { - id: 'p2', - prompt: 'Question 2', - status: 'active', - }, - ], - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.brands[0].prompts).to.have.lengthOf(2); - expect(stats.prompts.total).to.equal(2); - expect(stats.prompts.modified).to.equal(1); // Only the new prompt - }); - - it('should handle brands with prompts when old brand has no prompts', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - // No prompts field - }, - ], - }, - }; - - // Add prompts to existing brand - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { - id: 'p1', - prompt: 'Question 1', - status: 'active', - }, - ], - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.brands[0].prompts).to.have.lengthOf(1); - expect(mergedConfig.customer.brands[0].prompts[0].id).to.equal('p1'); - expect(stats.prompts.total).to.equal(1); - expect(stats.prompts.modified).to.equal(1); - }); - - it('should handle null/undefined arrays gracefully', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - }, - }; - - // Updates with null/undefined should be handled gracefully - const updates = { - customer: { - categories: null, // This should be treated as no update - brands: undefined, // This should be treated as no update - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.customerName).to.equal('Test Customer'); - expect(stats.categories.total).to.equal(0); - expect(stats.brands.total).to.equal(0); - }); - - it('should handle reduce operation when counting prompts from preserved brands', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { id: 'p1', prompt: 'Q1', status: 'active' }, - { id: 'p2', prompt: 'Q2', status: 'active' }, - ], - }, - { - id: 'brand-2', - name: 'Brand Two', - status: 'active', - prompts: [ - { id: 'p3', prompt: 'Q3', status: 'active' }, - ], - }, - { - id: 'brand-3', - name: 'Brand Three', - status: 'active', - // No prompts - }, - ], - }, - }; - - // Update something else, brands should be preserved - const updates = { - customer: { - categories: [ - { id: 'cat-1', name: 'Category One', status: 'active' }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - // All brands should be preserved - expect(mergedConfig.customer.brands).to.have.lengthOf(3); - // Should correctly count prompts across all brands - expect(stats.prompts.total).to.equal(3); // 2 + 1 + 0 - }); - - it('should handle complex nested updates with mixed changes', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - categories: [ - { id: 'cat-1', name: 'Category One', status: 'active' }, - ], - topics: [ - { - id: 'topic-1', name: 'Topic One', categoryId: 'cat-1', status: 'active', - }, - ], - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { - id: 'p1', prompt: 'Q1', status: 'active', regions: ['US'], - }, - ], - }, - ], - }, - }; - - // Complex update: add category, update topic, add brand, update prompt - const updates = { - customer: { - categories: [ - { id: 'cat-1', name: 'Category One', status: 'active' }, // unchanged - { id: 'cat-2', name: 'Category Two', status: 'active' }, // new - ], - topics: [ - { - id: 'topic-1', name: 'Topic One Modified', categoryId: 'cat-1', status: 'active', - }, // changed - ], - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { - id: 'p1', prompt: 'Q1 Updated', status: 'active', regions: ['US', 'GB'], - }, // changed - ], - }, - { - id: 'brand-2', - name: 'Brand Two', - status: 'active', // new brand - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - // Verify categories - expect(stats.categories.total).to.equal(2); - expect(stats.categories.modified).to.equal(1); // Only cat-2 is new - - // Verify topics - expect(stats.topics.total).to.equal(1); - expect(stats.topics.modified).to.equal(1); // topic-1 was modified - - // Verify brands - expect(stats.brands.total).to.equal(2); - expect(stats.brands.modified).to.equal(1); // Only brand-2 is new - - // Verify prompts - expect(stats.prompts.total).to.equal(1); - expect(stats.prompts.modified).to.equal(1); // p1 was modified - - // Check content - expect(mergedConfig.customer.categories[1].name).to.equal('Category Two'); - expect(mergedConfig.customer.topics[0].name).to.equal('Topic One Modified'); - expect(mergedConfig.customer.brands[0].prompts[0].prompt).to.equal('Q1 Updated'); - expect(mergedConfig.customer.brands[1].name).to.equal('Brand Two'); - }); - - it('should handle availableVerticals preservation', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - availableVerticals: ['Technology', 'Finance', 'Healthcare'], - }, - }; - - const updates = { - customer: { - brands: [ - { id: 'brand-1', name: 'Brand One', status: 'active' }, - ], - }, - }; - - const { mergedConfig } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.availableVerticals).to.deep.equal(['Technology', 'Finance', 'Healthcare']); - }); - - // Additional edge case tests for metadata preservation - it('should handle null comparisons in deepEqual', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - description: null, - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - description: null, - }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.brands.modified).to.equal(0); - }); - - it('should detect when object keys differ', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - newField: 'new value', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.brands.modified).to.equal(1); - expect(mergedConfig.customer.brands[0].newField).to.equal('new value'); - }); - - it('should handle primitive types in deepEqual', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - priority: 1, - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - priority: 2, - }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.brands.modified).to.equal(1); - }); - - it('should handle comparing non-objects', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - active: true, - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - active: false, - }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.brands.modified).to.equal(1); - }); - - it('should handle array vs non-array comparison', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - tags: ['tag1', 'tag2'], - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - tags: 'tag1', - }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.brands.modified).to.equal(1); - }); - - it('should handle key existence check', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - metadata: { key1: 'value1', key2: 'value2' }, - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - metadata: { key1: 'value1', key3: 'value3' }, - }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.brands.modified).to.equal(1); - }); - - it('should handle items without id field', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - categories: [ - { name: 'Category One', status: 'active' }, - ], - }, - }; - - const updates = { - customer: { - categories: [ - { name: 'Category Two', status: 'active' }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.categories.total).to.equal(1); - expect(stats.categories.modified).to.equal(1); - }); - - it('should handle identical primitive values in arrays', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - region: ['US'], - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - region: ['US'], - }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.brands.modified).to.equal(0); - }); - - it('should handle typeof checks for non-objects', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - count: 5, - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - count: '5', - }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.brands.modified).to.equal(1); - }); - - it('should handle brands with existing prompts being updated', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { - id: 'p1', - prompt: 'Old Question', - status: 'active', - }, - ], - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { - id: 'p1', - prompt: 'New Question', - status: 'active', - }, - ], - }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.prompts.modified).to.equal(1); - expect(stats.brands.modified).to.equal(0); - }); - - it('should handle new brand without prompts field in existing config', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.brands.total).to.equal(1); - expect(stats.brands.modified).to.equal(1); - // Should not have prompts field unless explicitly added - expect(mergedConfig.customer.brands[0].prompts).to.be.undefined; - }); - - it('should update topics without categories', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - topics: [ - { - id: 'topic-1', - name: 'Topic One', - categoryId: 'cat-1', - status: 'active', - }, - ], - }, - }; - - const updates = { - customer: { - topics: [ - { - id: 'topic-1', - name: 'Topic One Updated', - categoryId: 'cat-1', - status: 'active', - }, - { - id: 'topic-2', - name: 'Topic Two', - categoryId: 'cat-1', - status: 'active', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.topics).to.have.lengthOf(2); - expect(stats.topics.total).to.equal(2); - expect(stats.topics.modified).to.equal(2); - }); - - it('should handle empty existing config with full updates', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - }, - }; - - const updates = { - customer: { - categories: [ - { id: 'cat-1', name: 'Category One', status: 'active' }, - ], - topics: [ - { - id: 'topic-1', name: 'Topic One', categoryId: 'cat-1', status: 'active', - }, - ], - brands: [ - { id: 'brand-1', name: 'Brand One', status: 'active' }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.categories.total).to.equal(1); - expect(stats.topics.total).to.equal(1); - expect(stats.brands.total).to.equal(1); - expect(mergedConfig.customer.customerName).to.equal('Test Customer'); - }); - - it('should handle object with different number of keys', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - extraField: 'some value', - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.brands.modified).to.equal(1); - }); - - it('should handle updates with null values in customer object', () => { - const existingConfig = null; - - const updates = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - categories: null, - }, - }; - - const { mergedConfig } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.customerName).to.equal('Test Customer'); - }); - - it('should handle prompts array with forEach iteration', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { id: 'p1', prompt: 'Q1', status: 'active' }, - { id: 'p2', prompt: 'Q2', status: 'active' }, - { id: 'p3', prompt: 'Q3', status: 'active' }, - ], - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { id: 'p1', prompt: 'Q1', status: 'active' }, - { id: 'p2', prompt: 'Q2 Updated', status: 'active' }, - { id: 'p4', prompt: 'Q4', status: 'active' }, - ], - }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.prompts.total).to.equal(3); - expect(stats.prompts.modified).to.equal(2); - }); - - it('should handle categories forEach with existing old items', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - categories: [ - { id: 'cat-1', name: 'Category One', status: 'active' }, - { id: 'cat-2', name: 'Category Two', status: 'active' }, - { id: 'cat-3', name: 'Category Three', status: 'active' }, - ], - }, - }; - - const updates = { - customer: { - categories: [ - { id: 'cat-1', name: 'Category One', status: 'active' }, - { id: 'cat-2', name: 'Category Two Updated', status: 'active' }, - { id: 'cat-4', name: 'Category Four', status: 'active' }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.categories.total).to.equal(3); - expect(stats.categories.modified).to.equal(2); - }); - - it('should handle topics forEach with map building', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - topics: [ - { - id: 'topic-1', name: 'Topic One', categoryId: 'cat-1', status: 'active', - }, - { - id: 'topic-2', name: 'Topic Two', categoryId: 'cat-1', status: 'active', - }, - ], - }, - }; - - const updates = { - customer: { - topics: [ - { - id: 'topic-1', name: 'Topic One', categoryId: 'cat-1', status: 'active', - }, - { - id: 'topic-3', name: 'Topic Three', categoryId: 'cat-2', status: 'active', - }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.topics.total).to.equal(2); - expect(stats.topics.modified).to.equal(1); - }); - - it('should handle brands forEach with map building', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { id: 'brand-1', name: 'Brand One', status: 'active' }, - { id: 'brand-2', name: 'Brand Two', status: 'active' }, - { id: 'brand-3', name: 'Brand Three', status: 'active' }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { id: 'brand-1', name: 'Brand One', status: 'active' }, - { id: 'brand-2', name: 'Brand Two Modified', status: 'active' }, - { id: 'brand-4', name: 'Brand Four', status: 'active' }, - ], - }, - }; - - const { stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.brands.total).to.equal(3); - expect(stats.brands.modified).to.equal(2); - }); - - it('should handle undefined oldConfig topics triggering || [] fallback', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - // topics field is completely missing (undefined) - }, - }; - - const updates = { - customer: { - topics: [ - { - id: 'topic-1', name: 'Topic One', categoryId: 'cat-1', status: 'active', - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.topics.total).to.equal(1); - expect(stats.topics.modified).to.equal(1); - expect(mergedConfig.customer.topics).to.have.lengthOf(1); - }); - - it('should handle brand with undefined prompts triggering || [] fallback', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - // prompts field completely missing (undefined) - }, - ], - }, - }; - - const updates = { - customer: { - brands: [ - { - id: 'brand-1', - name: 'Brand One', - status: 'active', - prompts: [ - { id: 'p1', prompt: 'New Prompt', status: 'active' }, - ], - }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(mergedConfig.customer.brands[0].prompts).to.have.lengthOf(1); - expect(stats.prompts.total).to.equal(1); - expect(stats.prompts.modified).to.equal(1); - }); - - it('should handle undefined oldBrands triggering || [] fallback', () => { - const existingConfig = { - customer: { - customerName: 'Test Customer', - imsOrgID: 'TEST123@AdobeOrg', - // brands field completely missing (undefined) - }, - }; - - const updates = { - customer: { - brands: [ - { id: 'brand-1', name: 'Brand One', status: 'active' }, - ], - }, - }; - - const { mergedConfig, stats } = mergeCustomerConfigV2(updates, existingConfig, userId); - - expect(stats.brands.total).to.equal(1); - expect(stats.brands.modified).to.equal(1); - expect(mergedConfig.customer.brands).to.have.lengthOf(1); - }); - }); - - describe('stripMetadata (exported for testing)', () => { - it('should return null when input is null', async () => { - const { stripMetadata } = await import('../../src/support/customer-config-v2-metadata.js'); - const result = stripMetadata(null); - expect(result).to.be.null; - }); - - it('should return undefined when input is undefined', async () => { - const { stripMetadata } = await import('../../src/support/customer-config-v2-metadata.js'); - const result = stripMetadata(undefined); - expect(result).to.be.undefined; - }); - - it('should return primitive values as-is', async () => { - const { stripMetadata } = await import('../../src/support/customer-config-v2-metadata.js'); - expect(stripMetadata(42)).to.equal(42); - expect(stripMetadata('string')).to.equal('string'); - expect(stripMetadata(true)).to.equal(true); - }); - - it('should strip metadata from objects', async () => { - const { stripMetadata } = await import('../../src/support/customer-config-v2-metadata.js'); - const obj = { - id: 'test-id', - name: 'Test', - updatedBy: 'user@example.com', - updatedAt: '2024-01-01T00:00:00.000Z', - status: 'active', - }; - const result = stripMetadata(obj); - expect(result).to.deep.equal({ id: 'test-id', name: 'Test' }); - }); - }); -});