From 7fdec22c3fb052d5c5519abbbe68b76e2402f190 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 14 Jan 2026 13:25:36 +0100 Subject: [PATCH 01/54] feat(tickets): implement createTicket method --- .../src/modules/tickets/tickets.request.ts | 1 + .../modules/tickets/zendesk-ticket.service.ts | 118 +++++++++++++++++- 2 files changed, 116 insertions(+), 3 deletions(-) diff --git a/packages/framework/src/modules/tickets/tickets.request.ts b/packages/framework/src/modules/tickets/tickets.request.ts index 22efff5c9..b10e9e526 100644 --- a/packages/framework/src/modules/tickets/tickets.request.ts +++ b/packages/framework/src/modules/tickets/tickets.request.ts @@ -9,6 +9,7 @@ export class GetTicketParams { export class PostTicketBody { title!: string; description!: string; + topic!: string; } export class GetTicketListQuery extends PaginationQuery { diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index 19a06e394..09f9d24e1 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException, NotImplementedException } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { Observable, catchError, firstValueFrom, from, map, of, switchMap, throwError } from 'rxjs'; import { Tickets, Users } from '@o2s/framework/modules'; @@ -9,8 +9,10 @@ import { type TicketCommentObject, type TicketObject, type UserObject, + createTicket, listSearchResults, listTicketComments, + searchUsers, showTicket, showUser, } from '@/generated/zendesk'; @@ -159,8 +161,97 @@ export class ZendeskTicketService extends Tickets.Service { ); } - createTicket(_data: Tickets.Request.PostTicketBody, _authorization?: string): Observable { - return throwError(() => new NotImplementedException('Creating tickets in Zendesk is not implemented')); + createTicket(data: Tickets.Request.PostTicketBody, authorization?: string): Observable { + // Validate input data + if (!data.title || !data.description || !data.topic) { + return throwError(() => new Error('Title, description and topic are required')); + } + + return this.usersService.getCurrentUser(authorization).pipe( + switchMap((user) => { + if (!user?.email) { + return throwError(() => new NotFoundException('User email not found')); + } + + // Find corresponding Zendesk user by email so that requester/submitter match the logged-in portal user + return this.findZendeskUserByEmail(user.email).pipe( + switchMap((zendeskUser) => { + const topicFieldId = Number(process.env.ZENDESK_TOPIC_FIELD_ID || 0); + const customFields: Array<{ id: number; value: string }> = []; + + // Add topic as custom field if provided and ZENDESK_TOPIC_FIELD_ID is configured + if (data.topic && topicFieldId) { + customFields.push({ + id: topicFieldId, + value: data.topic, + }); + } + + return from( + createTicket({ + body: { + ticket: { + subject: data.title, + comment: { + body: data.description, + }, + ...(zendeskUser?.id && { + requester_id: zendeskUser.id, + submitter_id: zendeskUser.id, + }), + // Add custom fields if any (e.g., topic) + // Note: Zendesk API accepts {id, value} structure for custom_fields + // TypeScript types require full CustomFieldObject, but API accepts simpler structure + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...(customFields.length > 0 && { custom_fields: customFields as any }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + }, + }), + ).pipe( + switchMap((response) => { + if (!response.data?.ticket) { + return throwError(() => new Error('Failed to create ticket in Zendesk')); + } + + const createdTicket = response.data.ticket; + + // Fetch ticket comments to ensure complete data + return this.fetchTicketComments(createdTicket.id!.toString()).pipe( + switchMap((comments) => { + // Get unique author IDs from comments + const authorIds = [ + ...new Set(comments.map((c) => c.author_id).filter(Boolean)), + ] as number[]; + + if (authorIds.length === 0) { + return of(mapTicketToModel(createdTicket, comments)); + } + + // Fetch all comment authors + return from( + Promise.all(authorIds.map((id) => firstValueFrom(this.fetchUser(id)))), + ).pipe( + map((authors) => { + const authorMap = new Map( + authors.filter((a) => a !== undefined).map((a) => [a!.id!, a!]), + ); + return mapTicketToModel(createdTicket, comments, authorMap); + }), + ); + }), + ); + }), + catchError((error) => { + return throwError( + () => new Error(`Failed to create ticket: ${error.message || error}`), + ); + }), + ); + }), + ); + }), + ); } private fetchTicket(id: string): Observable { @@ -239,4 +330,25 @@ export class ZendeskTicketService extends Tickets.Service { }), ); } + + private findZendeskUserByEmail(email: string): Observable { + return from( + searchUsers({ + query: { + query: email, + }, + }), + ).pipe( + map((response) => { + // Search for exact email match + const users = response.data?.users || []; + const matchedUser = users.find((u) => u.email === email); + return matchedUser; + }), + catchError(() => { + // If search fails, return undefined (ticket will be created without requester/submitter ids) + return of(undefined); + }), + ); + } } From 11a0a79d67d793749c8cd9b2d3c18293cff1c5e1 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Fri, 16 Jan 2026 12:05:04 +0100 Subject: [PATCH 02/54] feat(tickets): add support for ticket attachments in ZendeskTicketService --- .../src/modules/tickets/tickets.request.ts | 7 + .../modules/tickets/zendesk-ticket.service.ts | 206 ++++++++++++------ 2 files changed, 142 insertions(+), 71 deletions(-) diff --git a/packages/framework/src/modules/tickets/tickets.request.ts b/packages/framework/src/modules/tickets/tickets.request.ts index b10e9e526..0ee0d29ed 100644 --- a/packages/framework/src/modules/tickets/tickets.request.ts +++ b/packages/framework/src/modules/tickets/tickets.request.ts @@ -6,10 +6,17 @@ export class GetTicketParams { locale?: string; } +export class TicketAttachmentInput { + filename!: string; + content!: string; // base64 encoded content + contentType!: string; // e.g., 'application/pdf' +} + export class PostTicketBody { title!: string; description!: string; topic!: string; + attachments?: TicketAttachmentInput[]; } export class GetTicketListQuery extends PaginationQuery { diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index 09f9d24e1..692597977 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -1,5 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { Observable, catchError, firstValueFrom, from, map, of, switchMap, throwError } from 'rxjs'; +import axios from 'axios'; +import { Observable, catchError, firstValueFrom, forkJoin, from, map, of, switchMap, throwError } from 'rxjs'; import { Tickets, Users } from '@o2s/framework/modules'; @@ -33,19 +34,22 @@ interface ZendeskSearchQuery { @Injectable() export class ZendeskTicketService extends Tickets.Service { + private readonly baseUrl: string; + private readonly authToken: string; + constructor(private readonly usersService: Users.Service) { super(); - const baseUrl = process.env.ZENDESK_API_URL; - const token = process.env.ZENDESK_API_TOKEN; + this.baseUrl = process.env.ZENDESK_API_URL!; + this.authToken = process.env.ZENDESK_API_TOKEN!; - if (!baseUrl || !token) { + if (!this.baseUrl || !this.authToken) { throw new Error('Missing required environment variables: ZENDESK_API_URL and ZENDESK_API_TOKEN'); } client.setConfig({ - baseUrl, - headers: { Authorization: `Basic ${token}` }, + baseUrl: this.baseUrl, + headers: { Authorization: `Basic ${this.authToken}` }, }); } @@ -173,82 +177,104 @@ export class ZendeskTicketService extends Tickets.Service { return throwError(() => new NotFoundException('User email not found')); } - // Find corresponding Zendesk user by email so that requester/submitter match the logged-in portal user - return this.findZendeskUserByEmail(user.email).pipe( - switchMap((zendeskUser) => { - const topicFieldId = Number(process.env.ZENDESK_TOPIC_FIELD_ID || 0); - const customFields: Array<{ id: number; value: string }> = []; - - // Add topic as custom field if provided and ZENDESK_TOPIC_FIELD_ID is configured - if (data.topic && topicFieldId) { - customFields.push({ - id: topicFieldId, - value: data.topic, - }); - } + // Upload attachments first if they exist + const uploadTokens = + data.attachments && data.attachments.length > 0 + ? forkJoin( + data.attachments.map((attachment) => + this.uploadAttachment( + attachment.filename, + Buffer.from(attachment.content, 'base64'), + attachment.contentType, + ), + ), + ) + : of([]); - return from( - createTicket({ - body: { - ticket: { - subject: data.title, - comment: { - body: data.description, - }, - ...(zendeskUser?.id && { - requester_id: zendeskUser.id, - submitter_id: zendeskUser.id, - }), - // Add custom fields if any (e.g., topic) - // Note: Zendesk API accepts {id, value} structure for custom_fields - // TypeScript types require full CustomFieldObject, but API accepts simpler structure - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...(customFields.length > 0 && { custom_fields: customFields as any }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, - }, - }), - ).pipe( - switchMap((response) => { - if (!response.data?.ticket) { - return throwError(() => new Error('Failed to create ticket in Zendesk')); + // Find corresponding Zendesk user by email so that requester/submitter match the logged-in portal user + return uploadTokens.pipe( + switchMap((uploadTokens) => + this.findZendeskUserByEmail(user.email!).pipe( + switchMap((zendeskUser) => { + const topicFieldId = Number(process.env.ZENDESK_TOPIC_FIELD_ID || 0); + const customFields: Array<{ id: number; value: string }> = []; + + // Add topic as custom field if provided and ZENDESK_TOPIC_FIELD_ID is configured + if (data.topic && topicFieldId) { + customFields.push({ + id: topicFieldId, + value: data.topic, + }); } - const createdTicket = response.data.ticket; - - // Fetch ticket comments to ensure complete data - return this.fetchTicketComments(createdTicket.id!.toString()).pipe( - switchMap((comments) => { - // Get unique author IDs from comments - const authorIds = [ - ...new Set(comments.map((c) => c.author_id).filter(Boolean)), - ] as number[]; - - if (authorIds.length === 0) { - return of(mapTicketToModel(createdTicket, comments)); + return from( + createTicket({ + body: { + ticket: { + subject: data.title, + comment: { + body: data.description, + ...(uploadTokens.length > 0 && { uploads: uploadTokens }), + }, + ...(zendeskUser?.id && { + requester_id: zendeskUser.id, + submitter_id: zendeskUser.id, + }), + // Add custom fields if any (e.g., topic) + // Note: Zendesk API accepts {id, value} structure for custom_fields + // TypeScript types require full CustomFieldObject, but API accepts simpler structure + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...(customFields.length > 0 && { custom_fields: customFields as any }), + }, + }, + }), + ).pipe( + switchMap((response) => { + if (!response.data?.ticket) { + return throwError(() => new Error('Failed to create ticket in Zendesk')); } - // Fetch all comment authors - return from( - Promise.all(authorIds.map((id) => firstValueFrom(this.fetchUser(id)))), - ).pipe( - map((authors) => { - const authorMap = new Map( - authors.filter((a) => a !== undefined).map((a) => [a!.id!, a!]), + const createdTicket = response.data.ticket; + + // Fetch ticket comments to ensure complete data + return this.fetchTicketComments(createdTicket.id!.toString()).pipe( + switchMap((comments) => { + // Get unique author IDs from comments + const authorIds = [ + ...new Set(comments.map((c) => c.author_id).filter(Boolean)), + ] as number[]; + + if (authorIds.length === 0) { + return of(mapTicketToModel(createdTicket, comments)); + } + + // Fetch all comment authors + return from( + Promise.all( + authorIds.map((id) => firstValueFrom(this.fetchUser(id))), + ), + ).pipe( + map((authors) => { + const authorMap = new Map( + authors + .filter((a) => a !== undefined) + .map((a) => [a!.id!, a!]), + ); + return mapTicketToModel(createdTicket, comments, authorMap); + }), ); - return mapTicketToModel(createdTicket, comments, authorMap); }), ); }), + catchError((error) => { + return throwError( + () => new Error(`Failed to create ticket: ${error.message || error}`), + ); + }), ); }), - catchError((error) => { - return throwError( - () => new Error(`Failed to create ticket: ${error.message || error}`), - ); - }), - ); - }), + ), + ), ); }), ); @@ -351,4 +377,42 @@ export class ZendeskTicketService extends Tickets.Service { }), ); } + + /** + * Uploads an attachment to Zendesk using direct HTTP request. + * The generated SDK doesn't handle binary uploads properly, so we use axios directly. + * + * @param filename - Name of the file to upload + * @param content - Binary content of the file as Buffer + * @param contentType - MIME type of the file (e.g., 'application/pdf') + * @returns Observable with the upload token from Zendesk API + */ + private uploadAttachment(filename: string, content: Buffer, contentType: string): Observable { + const uploadUrl = `${this.baseUrl}/api/v2/uploads?filename=${encodeURIComponent(filename)}`; + + return from( + axios.post(uploadUrl, content, { + headers: { + Authorization: `Basic ${this.authToken}`, + 'Content-Type': contentType, + }, + responseType: 'json', + }), + ).pipe( + map((response) => { + if (!response.data?.upload?.token) { + throw new Error('Upload token not received from Zendesk API'); + } + return response.data.upload.token; + }), + catchError((error) => { + const errorMessage = + error.response?.data?.error?.description || + error.response?.data?.description || + error.message || + 'Unknown error during file upload'; + return throwError(() => new Error(`Failed to upload attachment: ${errorMessage}`)); + }), + ); + } } From a373092b69a87d99c13f40e06cb56d7b97644cea Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Fri, 16 Jan 2026 12:14:17 +0100 Subject: [PATCH 03/54] feat(tickets): add priority and type fields to PostTicketBody --- packages/framework/src/modules/tickets/tickets.request.ts | 5 +++++ .../zendesk/src/modules/tickets/zendesk-ticket.service.ts | 2 ++ 2 files changed, 7 insertions(+) diff --git a/packages/framework/src/modules/tickets/tickets.request.ts b/packages/framework/src/modules/tickets/tickets.request.ts index 0ee0d29ed..dde941a4e 100644 --- a/packages/framework/src/modules/tickets/tickets.request.ts +++ b/packages/framework/src/modules/tickets/tickets.request.ts @@ -12,11 +12,16 @@ export class TicketAttachmentInput { contentType!: string; // e.g., 'application/pdf' } +export type TicketPriority = 'urgent' | 'high' | 'normal' | 'low'; +export type TicketType = 'question' | 'problem' | 'incident' | 'task'; + export class PostTicketBody { title!: string; description!: string; topic!: string; attachments?: TicketAttachmentInput[]; + priority?: TicketPriority; + type?: TicketType; } export class GetTicketListQuery extends PaginationQuery { diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index 692597977..57f71f59e 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -216,6 +216,8 @@ export class ZendeskTicketService extends Tickets.Service { body: data.description, ...(uploadTokens.length > 0 && { uploads: uploadTokens }), }, + ...(data.priority && { priority: data.priority }), + ...(data.type && { type: data.type }), ...(zendeskUser?.id && { requester_id: zendeskUser.id, submitter_id: zendeskUser.id, From f6136af8b8c0758e0c19d412a31ff617d44e1f69 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Fri, 16 Jan 2026 12:48:26 +0100 Subject: [PATCH 04/54] refactor(ProductDetails, RecommendedProducts): fixing linter warnings --- .../src/frontend/ProductDetails.server.tsx | 26 ++++++++++--------- .../frontend/RecommendedProducts.server.tsx | 24 +++++++++-------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/blocks/product-details/src/frontend/ProductDetails.server.tsx b/packages/blocks/product-details/src/frontend/ProductDetails.server.tsx index dbe1331e5..404309fc6 100644 --- a/packages/blocks/product-details/src/frontend/ProductDetails.server.tsx +++ b/packages/blocks/product-details/src/frontend/ProductDetails.server.tsx @@ -1,6 +1,7 @@ import dynamic from 'next/dynamic'; import React from 'react'; +import type { Model } from '../api-harmonization/product-details.client'; import { sdk } from '../sdk'; import { ProductDetailsProps } from './ProductDetails.types'; @@ -16,21 +17,22 @@ export const ProductDetails: React.FC = async ({ routing, hasPriority, }) => { + let data: Model.ProductDetailsBlock; try { - const data = await sdk.blocks.getProductDetails({ id: productId }, { id, locale }, { 'x-locale': locale }); - - return ( - - ); + data = await sdk.blocks.getProductDetails({ id: productId }, { id, locale }, { 'x-locale': locale }); } catch (error) { console.error('Error fetching ProductDetails block', error); return null; } + + return ( + + ); }; diff --git a/packages/blocks/recommended-products/src/frontend/RecommendedProducts.server.tsx b/packages/blocks/recommended-products/src/frontend/RecommendedProducts.server.tsx index e5d154d14..08d2c7839 100644 --- a/packages/blocks/recommended-products/src/frontend/RecommendedProducts.server.tsx +++ b/packages/blocks/recommended-products/src/frontend/RecommendedProducts.server.tsx @@ -1,6 +1,7 @@ import dynamic from 'next/dynamic'; import React from 'react'; +import type { Model } from '../api-harmonization/recommended-products.client'; import { sdk } from '../sdk'; import { RecommendedProductsProps } from './RecommendedProducts.types'; @@ -15,26 +16,27 @@ export const RecommendedProducts: React.FC = async ({ locale, routing, }) => { + let data: Model.RecommendedProductsBlock; try { - const data = await sdk.blocks.getRecommendedProducts( + data = await sdk.blocks.getRecommendedProducts( { id }, { excludeProductId, }, { 'x-locale': locale }, ); - - return ( - - ); } catch (error) { console.error('Error fetching RecommendedProducts block', error); return null; } + + return ( + + ); }; From 3c7466080530bfe21d958b0811a2dbfc976313fa Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Fri, 16 Jan 2026 12:53:36 +0100 Subject: [PATCH 05/54] feat(tickets): enforce ZENDESK_TOPIC_FIELD_ID requirement and improve email matching --- .../src/modules/tickets/zendesk-ticket.service.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index 57f71f59e..94c89b180 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -199,6 +199,12 @@ export class ZendeskTicketService extends Tickets.Service { const topicFieldId = Number(process.env.ZENDESK_TOPIC_FIELD_ID || 0); const customFields: Array<{ id: number; value: string }> = []; + if (data.topic && !topicFieldId) { + return throwError( + () => new Error('ZENDESK_TOPIC_FIELD_ID is required to persist ticket topic'), + ); + } + // Add topic as custom field if provided and ZENDESK_TOPIC_FIELD_ID is configured if (data.topic && topicFieldId) { customFields.push({ @@ -368,9 +374,10 @@ export class ZendeskTicketService extends Tickets.Service { }), ).pipe( map((response) => { - // Search for exact email match + // Search for exact email match (case-insensitive) const users = response.data?.users || []; - const matchedUser = users.find((u) => u.email === email); + const normalizedEmail = email.toLowerCase(); + const matchedUser = users.find((u) => u.email?.toLowerCase() === normalizedEmail); return matchedUser; }), catchError(() => { From e0dcebc4b19b04ace48dafc81e4831c4bea0a715 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Fri, 16 Jan 2026 14:01:52 +0100 Subject: [PATCH 06/54] refactor(tickets): update types in PostTicketBody and update uploadAttachment to use httpClient --- .../src/modules/tickets/tickets.request.ts | 7 +-- .../modules/tickets/zendesk-ticket.service.ts | 57 +++++++++++-------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/packages/framework/src/modules/tickets/tickets.request.ts b/packages/framework/src/modules/tickets/tickets.request.ts index dde941a4e..5afb8bc61 100644 --- a/packages/framework/src/modules/tickets/tickets.request.ts +++ b/packages/framework/src/modules/tickets/tickets.request.ts @@ -12,16 +12,13 @@ export class TicketAttachmentInput { contentType!: string; // e.g., 'application/pdf' } -export type TicketPriority = 'urgent' | 'high' | 'normal' | 'low'; -export type TicketType = 'question' | 'problem' | 'incident' | 'task'; - export class PostTicketBody { title!: string; description!: string; topic!: string; attachments?: TicketAttachmentInput[]; - priority?: TicketPriority; - type?: TicketType; + priority?: string; + type?: string; } export class GetTicketListQuery extends PaginationQuery { diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index 94c89b180..89b3d931e 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -1,5 +1,5 @@ +import { HttpService } from '@nestjs/axios'; import { Injectable, NotFoundException } from '@nestjs/common'; -import axios from 'axios'; import { Observable, catchError, firstValueFrom, forkJoin, from, map, of, switchMap, throwError } from 'rxjs'; import { Tickets, Users } from '@o2s/framework/modules'; @@ -9,6 +9,7 @@ import { type SearchResultObject, type TicketCommentObject, type TicketObject, + type TicketUpdateInputWritable, type UserObject, createTicket, listSearchResults, @@ -37,7 +38,10 @@ export class ZendeskTicketService extends Tickets.Service { private readonly baseUrl: string; private readonly authToken: string; - constructor(private readonly usersService: Users.Service) { + constructor( + private readonly usersService: Users.Service, + private readonly httpClient: HttpService, + ) { super(); this.baseUrl = process.env.ZENDESK_API_URL!; @@ -222,8 +226,12 @@ export class ZendeskTicketService extends Tickets.Service { body: data.description, ...(uploadTokens.length > 0 && { uploads: uploadTokens }), }, - ...(data.priority && { priority: data.priority }), - ...(data.type && { type: data.type }), + ...(data.priority && { + priority: data.priority as TicketUpdateInputWritable['priority'], + }), + ...(data.type && { + type: data.type as TicketUpdateInputWritable['type'], + }), ...(zendeskUser?.id && { requester_id: zendeskUser.id, submitter_id: zendeskUser.id, @@ -389,7 +397,7 @@ export class ZendeskTicketService extends Tickets.Service { /** * Uploads an attachment to Zendesk using direct HTTP request. - * The generated SDK doesn't handle binary uploads properly, so we use axios directly. + * The generated SDK doesn't handle binary uploads properly, so we use HttpService directly. * * @param filename - Name of the file to upload * @param content - Binary content of the file as Buffer @@ -399,29 +407,28 @@ export class ZendeskTicketService extends Tickets.Service { private uploadAttachment(filename: string, content: Buffer, contentType: string): Observable { const uploadUrl = `${this.baseUrl}/api/v2/uploads?filename=${encodeURIComponent(filename)}`; - return from( - axios.post(uploadUrl, content, { + return this.httpClient + .post(uploadUrl, content, { headers: { Authorization: `Basic ${this.authToken}`, 'Content-Type': contentType, }, - responseType: 'json', - }), - ).pipe( - map((response) => { - if (!response.data?.upload?.token) { - throw new Error('Upload token not received from Zendesk API'); - } - return response.data.upload.token; - }), - catchError((error) => { - const errorMessage = - error.response?.data?.error?.description || - error.response?.data?.description || - error.message || - 'Unknown error during file upload'; - return throwError(() => new Error(`Failed to upload attachment: ${errorMessage}`)); - }), - ); + }) + .pipe( + map((response) => { + if (!response.data?.upload?.token) { + throw new Error('Upload token not received from Zendesk API'); + } + return response.data.upload.token; + }), + catchError((error) => { + const errorMessage = + error.response?.data?.error?.description || + error.response?.data?.description || + error.message || + 'Unknown error during file upload'; + return throwError(() => new Error(`Failed to upload attachment: ${errorMessage}`)); + }), + ); } } From ddd3f2ef652db8cb485cc5cc6a506328e3e29aef Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Fri, 16 Jan 2026 15:40:07 +0100 Subject: [PATCH 07/54] refactor(tickets): improve error handling in ZendeskTicketService by using specific exceptions --- .../modules/tickets/zendesk-ticket.service.ts | 46 ++++++------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index 89b3d931e..1a9c102af 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -1,5 +1,5 @@ import { HttpService } from '@nestjs/axios'; -import { Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { Observable, catchError, firstValueFrom, forkJoin, from, map, of, switchMap, throwError } from 'rxjs'; import { Tickets, Users } from '@o2s/framework/modules'; @@ -172,7 +172,7 @@ export class ZendeskTicketService extends Tickets.Service { createTicket(data: Tickets.Request.PostTicketBody, authorization?: string): Observable { // Validate input data if (!data.title || !data.description || !data.topic) { - return throwError(() => new Error('Title, description and topic are required')); + return throwError(() => new BadRequestException('Title, description and topic are required')); } return this.usersService.getCurrentUser(authorization).pipe( @@ -247,44 +247,24 @@ export class ZendeskTicketService extends Tickets.Service { ).pipe( switchMap((response) => { if (!response.data?.ticket) { - return throwError(() => new Error('Failed to create ticket in Zendesk')); + return throwError( + () => + new InternalServerErrorException( + 'Failed to create ticket in Zendesk', + ), + ); } const createdTicket = response.data.ticket; - // Fetch ticket comments to ensure complete data - return this.fetchTicketComments(createdTicket.id!.toString()).pipe( - switchMap((comments) => { - // Get unique author IDs from comments - const authorIds = [ - ...new Set(comments.map((c) => c.author_id).filter(Boolean)), - ] as number[]; - - if (authorIds.length === 0) { - return of(mapTicketToModel(createdTicket, comments)); - } - - // Fetch all comment authors - return from( - Promise.all( - authorIds.map((id) => firstValueFrom(this.fetchUser(id))), - ), - ).pipe( - map((authors) => { - const authorMap = new Map( - authors - .filter((a) => a !== undefined) - .map((a) => [a!.id!, a!]), - ); - return mapTicketToModel(createdTicket, comments, authorMap); - }), - ); - }), - ); + return of(mapTicketToModel(createdTicket)); }), catchError((error) => { return throwError( - () => new Error(`Failed to create ticket: ${error.message || error}`), + () => + new InternalServerErrorException( + `Failed to create ticket: ${error.message || error}`, + ), ); }), ); From aafa3723b0c219e2b83a0efba2f8698c1bd7de46 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Mon, 19 Jan 2026 15:07:56 +0100 Subject: [PATCH 08/54] feat(tickets): add create ticket functionality and corresponding page mappings --- .../blocks/cms.surveyjs-block.mapper.ts | 24 +++- .../modules/cms/mappers/cms.page.mapper.ts | 10 ++ .../modules/cms/mappers/cms.survey.mapper.ts | 11 +- .../mocks/pages/surveyjs-forms.page.ts | 104 +++++++++++++++++- .../blocks/cms.surveyjs-block.mapper.ts | 24 +++- .../mappers/blocks/cms.ticket-list.mapper.ts | 15 +++ .../modules/cms/mappers/cms.page.mapper.ts | 10 ++ .../modules/cms/mappers/cms.survey.mapper.ts | 11 +- .../mocks/pages/surveyjs-forms.page.ts | 104 +++++++++++++++++- .../src/api-harmonization/surveyjs.mapper.ts | 35 ++++++ .../src/api-harmonization/surveyjs.module.ts | 11 +- .../src/api-harmonization/surveyjs.service.ts | 28 ++++- 12 files changed, 374 insertions(+), 13 deletions(-) diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts index 7b92e2e83..e3f81b571 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts @@ -54,23 +54,41 @@ const MOCK_SURVEYJS_BLOCK_3_DE: CMS.Model.SurveyJsBlock.SurveyJsBlock = { code: 'request-device-maintenance', }; +const MOCK_SURVEYJS_BLOCK_4_EN: CMS.Model.SurveyJsBlock.SurveyJsBlock = { + id: 'survey-4', + title: 'Create ticket', + code: 'create-ticket', +}; + +const MOCK_SURVEYJS_BLOCK_4_PL: CMS.Model.SurveyJsBlock.SurveyJsBlock = { + id: 'survey-4', + title: 'Utwórz zgłoszenie', + code: 'create-ticket', +}; + +const MOCK_SURVEYJS_BLOCK_4_DE: CMS.Model.SurveyJsBlock.SurveyJsBlock = { + id: 'survey-4', + title: 'Ticket erstellen', + code: 'create-ticket', +}; + export const mapSurveyJsBlock = (locale: string, id: string): CMS.Model.SurveyJsBlock.SurveyJsBlock => { switch (locale) { case 'en': return ( - [MOCK_SURVEYJS_BLOCK_1_EN, MOCK_SURVEYJS_BLOCK_2_EN, MOCK_SURVEYJS_BLOCK_3_EN].find( + [MOCK_SURVEYJS_BLOCK_1_EN, MOCK_SURVEYJS_BLOCK_2_EN, MOCK_SURVEYJS_BLOCK_3_EN, MOCK_SURVEYJS_BLOCK_4_EN].find( (block) => block.id === id, ) || MOCK_SURVEYJS_BLOCK_1_EN ); case 'de': return ( - [MOCK_SURVEYJS_BLOCK_1_DE, MOCK_SURVEYJS_BLOCK_2_DE, MOCK_SURVEYJS_BLOCK_3_DE].find( + [MOCK_SURVEYJS_BLOCK_1_DE, MOCK_SURVEYJS_BLOCK_2_DE, MOCK_SURVEYJS_BLOCK_3_DE, MOCK_SURVEYJS_BLOCK_4_DE].find( (block) => block.id === id, ) || MOCK_SURVEYJS_BLOCK_1_DE ); case 'pl': return ( - [MOCK_SURVEYJS_BLOCK_1_PL, MOCK_SURVEYJS_BLOCK_2_PL, MOCK_SURVEYJS_BLOCK_3_PL].find( + [MOCK_SURVEYJS_BLOCK_1_PL, MOCK_SURVEYJS_BLOCK_2_PL, MOCK_SURVEYJS_BLOCK_3_PL, MOCK_SURVEYJS_BLOCK_4_PL].find( (block) => block.id === id, ) || MOCK_SURVEYJS_BLOCK_1_PL ); diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.page.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.page.mapper.ts index 267529b1e..c037c2f8c 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.page.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.page.mapper.ts @@ -60,6 +60,9 @@ import { PAGE_CONTACT_US_DE, PAGE_CONTACT_US_EN, PAGE_CONTACT_US_PL, + PAGE_CREATE_TICKET_DE, + PAGE_CREATE_TICKET_EN, + PAGE_CREATE_TICKET_PL, PAGE_REQUEST_DEVICE_MAINTENANCE_DE, PAGE_REQUEST_DEVICE_MAINTENANCE_EN, PAGE_REQUEST_DEVICE_MAINTENANCE_PL, @@ -218,6 +221,13 @@ export const mapMockPage = (slug: string, locale: string): CMS.Model.Page.Page | case '/zglos-naprawe-urzadzenia': return PAGE_REQUEST_DEVICE_MAINTENANCE_PL; + case '/create-ticket': + return PAGE_CREATE_TICKET_EN; + case '/erstelle-ticket': + return PAGE_CREATE_TICKET_DE; + case '/utworz-zgloszenie': + return PAGE_CREATE_TICKET_PL; + case '/help-and-support': return PAGE_HELP_AND_SUPPORT_EN; case '/hilfe-und-support': diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts index 5ced09fb8..3fc3ecd66 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts @@ -27,7 +27,16 @@ const MOCK_SURVEY_3: CMS.Model.Survey.Survey = { postId: '17931fe3-2492-408c-8f91-8fc062606604', }; -const MOCK_SURVEYS = [MOCK_SURVEY_1, MOCK_SURVEY_2, MOCK_SURVEY_3]; +const MOCK_SURVEY_4: CMS.Model.Survey.Survey = { + code: 'create-ticket', + surveyId: 'bf251bfa-8f6a-4e2b-a79c-554e3d45ec41', + surveyType: 'survey', + submitDestination: ['tickets'], + requiredRoles: ['selfservice_org_user'], + postId: 'f6798232-c45b-4378-9fe5-838bdc12ca88', +}; + +const MOCK_SURVEYS = [MOCK_SURVEY_1, MOCK_SURVEY_2, MOCK_SURVEY_3, MOCK_SURVEY_4]; export const mapSurvey = (code: string): CMS.Model.Survey.Survey => { return MOCK_SURVEYS.find((survey) => survey.code === code) ?? MOCK_SURVEY_1; diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts index bedd0fc82..50d4966dd 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts @@ -1,4 +1,4 @@ -import { CMS } from '@o2s/framework/modules'; +import { Auth, CMS } from '@o2s/framework/modules'; export const PAGE_CONTACT_US_EN: CMS.Model.Page.Page = { id: '9', @@ -305,3 +305,105 @@ export const PAGE_REQUEST_DEVICE_MAINTENANCE_PL: CMS.Model.Page.Page = { updatedAt: '2025-01-01', createdAt: '2025-01-01', }; + +export const PAGE_CREATE_TICKET_EN: CMS.Model.Page.Page = { + id: '13', + slug: '/create-ticket', + locale: 'en', + seo: { + noIndex: false, + noFollow: false, + title: 'Create ticket', + description: 'Create a new support ticket', + keywords: ['ticket', 'support', 'create'], + image: { + url: 'https://picsum.photos/150', + width: 150, + height: 150, + alt: 'Placeholder', + }, + }, + permissions: [Auth.Constants.Roles.ORG_USER], + hasOwnTitle: false, + template: { + __typename: 'OneColumnTemplate', + slots: { + main: [ + { + __typename: 'SurveyJsBlock', + id: 'survey-4', + }, + ], + }, + }, + updatedAt: '2025-01-01', + createdAt: '2025-01-01', +}; + +export const PAGE_CREATE_TICKET_DE: CMS.Model.Page.Page = { + id: '13', + slug: '/erstelle-ticket', + locale: 'de', + seo: { + noIndex: false, + noFollow: false, + title: 'Ticket erstellen', + description: 'Erstellen Sie ein neues Support-Ticket', + keywords: ['ticket', 'support', 'erstellen'], + image: { + url: 'https://picsum.photos/150', + width: 150, + height: 150, + alt: 'Placeholder', + }, + }, + permissions: [Auth.Constants.Roles.ORG_USER], + hasOwnTitle: false, + template: { + __typename: 'OneColumnTemplate', + slots: { + main: [ + { + __typename: 'SurveyJsBlock', + id: 'survey-4', + }, + ], + }, + }, + updatedAt: '2025-01-01', + createdAt: '2025-01-01', +}; + +export const PAGE_CREATE_TICKET_PL: CMS.Model.Page.Page = { + id: '13', + slug: '/utworz-zgloszenie', + locale: 'pl', + seo: { + noIndex: false, + noFollow: false, + title: 'Utwórz zgłoszenie', + description: 'Utwórz nowe zgłoszenie wsparcia', + keywords: ['zgłoszenie', 'wsparcie', 'utworz'], + image: { + url: 'https://picsum.photos/150', + width: 150, + height: 150, + alt: 'Placeholder', + }, + }, + permissions: [Auth.Constants.Roles.ORG_USER], + hasOwnTitle: false, + template: { + __typename: 'OneColumnTemplate', + slots: { + main: [ + { + __typename: 'SurveyJsBlock', + id: 'survey-4', + }, + ], + }, + }, + updatedAt: '2025-01-01', + createdAt: '2025-01-01', +}; diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts index 7b92e2e83..e3f81b571 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts @@ -54,23 +54,41 @@ const MOCK_SURVEYJS_BLOCK_3_DE: CMS.Model.SurveyJsBlock.SurveyJsBlock = { code: 'request-device-maintenance', }; +const MOCK_SURVEYJS_BLOCK_4_EN: CMS.Model.SurveyJsBlock.SurveyJsBlock = { + id: 'survey-4', + title: 'Create ticket', + code: 'create-ticket', +}; + +const MOCK_SURVEYJS_BLOCK_4_PL: CMS.Model.SurveyJsBlock.SurveyJsBlock = { + id: 'survey-4', + title: 'Utwórz zgłoszenie', + code: 'create-ticket', +}; + +const MOCK_SURVEYJS_BLOCK_4_DE: CMS.Model.SurveyJsBlock.SurveyJsBlock = { + id: 'survey-4', + title: 'Ticket erstellen', + code: 'create-ticket', +}; + export const mapSurveyJsBlock = (locale: string, id: string): CMS.Model.SurveyJsBlock.SurveyJsBlock => { switch (locale) { case 'en': return ( - [MOCK_SURVEYJS_BLOCK_1_EN, MOCK_SURVEYJS_BLOCK_2_EN, MOCK_SURVEYJS_BLOCK_3_EN].find( + [MOCK_SURVEYJS_BLOCK_1_EN, MOCK_SURVEYJS_BLOCK_2_EN, MOCK_SURVEYJS_BLOCK_3_EN, MOCK_SURVEYJS_BLOCK_4_EN].find( (block) => block.id === id, ) || MOCK_SURVEYJS_BLOCK_1_EN ); case 'de': return ( - [MOCK_SURVEYJS_BLOCK_1_DE, MOCK_SURVEYJS_BLOCK_2_DE, MOCK_SURVEYJS_BLOCK_3_DE].find( + [MOCK_SURVEYJS_BLOCK_1_DE, MOCK_SURVEYJS_BLOCK_2_DE, MOCK_SURVEYJS_BLOCK_3_DE, MOCK_SURVEYJS_BLOCK_4_DE].find( (block) => block.id === id, ) || MOCK_SURVEYJS_BLOCK_1_DE ); case 'pl': return ( - [MOCK_SURVEYJS_BLOCK_1_PL, MOCK_SURVEYJS_BLOCK_2_PL, MOCK_SURVEYJS_BLOCK_3_PL].find( + [MOCK_SURVEYJS_BLOCK_1_PL, MOCK_SURVEYJS_BLOCK_2_PL, MOCK_SURVEYJS_BLOCK_3_PL, MOCK_SURVEYJS_BLOCK_4_PL].find( (block) => block.id === id, ) || MOCK_SURVEYJS_BLOCK_1_PL ); diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts index 936c9265c..70d846283 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts @@ -4,6 +4,11 @@ const MOCK_TICKET_LIST_BLOCK_EN: CMS.Model.TicketListBlock.TicketListBlock = { id: 'ticket-list-1', title: 'Your recent cases', forms: [ + { + label: 'Create ticket', + url: '/create-ticket', + icon: 'Plus', + }, { label: 'Submit complaint', url: '/submit-complaint', @@ -182,6 +187,11 @@ const MOCK_TICKET_LIST_BLOCK_DE: CMS.Model.TicketListBlock.TicketListBlock = { id: 'ticket-list-1', title: 'Ihre neuesten Fälle', forms: [ + { + label: 'Ticket erstellen', + url: '/erstelle-ticket', + icon: 'Plus', + }, { label: 'Beschwerde einreichen', url: '/submit-complaint', @@ -361,6 +371,11 @@ const MOCK_TICKET_LIST_BLOCK_PL: CMS.Model.TicketListBlock.TicketListBlock = { id: 'ticket-list-1', title: 'Twoje ostatnie zgłoszenia', forms: [ + { + label: 'Utwórz zgłoszenie', + url: '/utworz-zgloszenie', + icon: 'Plus', + }, { label: 'Zgłoś błąd', url: '/submit-complaint', diff --git a/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts index cc90f912e..f3a5fcd33 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts @@ -55,6 +55,9 @@ import { PAGE_CONTACT_US_DE, PAGE_CONTACT_US_EN, PAGE_CONTACT_US_PL, + PAGE_CREATE_TICKET_DE, + PAGE_CREATE_TICKET_EN, + PAGE_CREATE_TICKET_PL, PAGE_REQUEST_DEVICE_MAINTENANCE_DE, PAGE_REQUEST_DEVICE_MAINTENANCE_EN, PAGE_REQUEST_DEVICE_MAINTENANCE_PL, @@ -239,6 +242,13 @@ export const mapPage = (slug: string, locale: string): CMS.Model.Page.Page | und case '/zglos-naprawe-urzadzenia': return PAGE_REQUEST_DEVICE_MAINTENANCE_PL; + case '/create-ticket': + return PAGE_CREATE_TICKET_EN; + case '/erstelle-ticket': + return PAGE_CREATE_TICKET_DE; + case '/utworz-zgloszenie': + return PAGE_CREATE_TICKET_PL; + case '/help-and-support': return PAGE_HELP_AND_SUPPORT_EN; case '/hilfe-und-support': diff --git a/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts index 5ced09fb8..3fc3ecd66 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts @@ -27,7 +27,16 @@ const MOCK_SURVEY_3: CMS.Model.Survey.Survey = { postId: '17931fe3-2492-408c-8f91-8fc062606604', }; -const MOCK_SURVEYS = [MOCK_SURVEY_1, MOCK_SURVEY_2, MOCK_SURVEY_3]; +const MOCK_SURVEY_4: CMS.Model.Survey.Survey = { + code: 'create-ticket', + surveyId: 'bf251bfa-8f6a-4e2b-a79c-554e3d45ec41', + surveyType: 'survey', + submitDestination: ['tickets'], + requiredRoles: ['selfservice_org_user'], + postId: 'f6798232-c45b-4378-9fe5-838bdc12ca88', +}; + +const MOCK_SURVEYS = [MOCK_SURVEY_1, MOCK_SURVEY_2, MOCK_SURVEY_3, MOCK_SURVEY_4]; export const mapSurvey = (code: string): CMS.Model.Survey.Survey => { return MOCK_SURVEYS.find((survey) => survey.code === code) ?? MOCK_SURVEY_1; diff --git a/packages/integrations/mocked/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts b/packages/integrations/mocked/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts index bedd0fc82..50d4966dd 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts @@ -1,4 +1,4 @@ -import { CMS } from '@o2s/framework/modules'; +import { Auth, CMS } from '@o2s/framework/modules'; export const PAGE_CONTACT_US_EN: CMS.Model.Page.Page = { id: '9', @@ -305,3 +305,105 @@ export const PAGE_REQUEST_DEVICE_MAINTENANCE_PL: CMS.Model.Page.Page = { updatedAt: '2025-01-01', createdAt: '2025-01-01', }; + +export const PAGE_CREATE_TICKET_EN: CMS.Model.Page.Page = { + id: '13', + slug: '/create-ticket', + locale: 'en', + seo: { + noIndex: false, + noFollow: false, + title: 'Create ticket', + description: 'Create a new support ticket', + keywords: ['ticket', 'support', 'create'], + image: { + url: 'https://picsum.photos/150', + width: 150, + height: 150, + alt: 'Placeholder', + }, + }, + permissions: [Auth.Constants.Roles.ORG_USER], + hasOwnTitle: false, + template: { + __typename: 'OneColumnTemplate', + slots: { + main: [ + { + __typename: 'SurveyJsBlock', + id: 'survey-4', + }, + ], + }, + }, + updatedAt: '2025-01-01', + createdAt: '2025-01-01', +}; + +export const PAGE_CREATE_TICKET_DE: CMS.Model.Page.Page = { + id: '13', + slug: '/erstelle-ticket', + locale: 'de', + seo: { + noIndex: false, + noFollow: false, + title: 'Ticket erstellen', + description: 'Erstellen Sie ein neues Support-Ticket', + keywords: ['ticket', 'support', 'erstellen'], + image: { + url: 'https://picsum.photos/150', + width: 150, + height: 150, + alt: 'Placeholder', + }, + }, + permissions: [Auth.Constants.Roles.ORG_USER], + hasOwnTitle: false, + template: { + __typename: 'OneColumnTemplate', + slots: { + main: [ + { + __typename: 'SurveyJsBlock', + id: 'survey-4', + }, + ], + }, + }, + updatedAt: '2025-01-01', + createdAt: '2025-01-01', +}; + +export const PAGE_CREATE_TICKET_PL: CMS.Model.Page.Page = { + id: '13', + slug: '/utworz-zgloszenie', + locale: 'pl', + seo: { + noIndex: false, + noFollow: false, + title: 'Utwórz zgłoszenie', + description: 'Utwórz nowe zgłoszenie wsparcia', + keywords: ['zgłoszenie', 'wsparcie', 'utworz'], + image: { + url: 'https://picsum.photos/150', + width: 150, + height: 150, + alt: 'Placeholder', + }, + }, + permissions: [Auth.Constants.Roles.ORG_USER], + hasOwnTitle: false, + template: { + __typename: 'OneColumnTemplate', + slots: { + main: [ + { + __typename: 'SurveyJsBlock', + id: 'survey-4', + }, + ], + }, + }, + updatedAt: '2025-01-01', + createdAt: '2025-01-01', +}; diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts index 3ec615768..0accdd329 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts @@ -1,3 +1,5 @@ +import { Tickets } from '@o2s/framework/modules'; + import { Page, Panelbase, SurveyJSLibraryJsonSchema, SurveyJs, SurveyJsRequest, SurveyResult } from './surveyjs.model'; export const mapSurveyJsRequest = ( @@ -76,3 +78,36 @@ const mapData = (element: Panelbase): Panelbase => { itemComponent: getItemComponent(element.type as string, element.itemComponent), }; }; + +/** + * Maps Survey.js form payload to Ticket creation data. + * + * Survey.js returns files in format: { name: string, type: string (MIME), content: string (base64) } + * + * Expected Survey.js configuration (after fix): + * - topic: value = "TOOL_REPAIR" (uppercase) - maps to custom field in Zendesk + * - priority: value = "urgent" (lowercase) - Zendesk API format + * - type: value = "problem" (lowercase) - Zendesk API format + */ +export const mapSurveyToTicket = (surveyPayload: SurveyResult): Tickets.Request.PostTicketBody => { + // Map attachments from Survey.js format to Tickets format + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const attachments = surveyPayload.attachments + ? (Array.isArray(surveyPayload.attachments) ? surveyPayload.attachments : [surveyPayload.attachments]) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .map((file: any) => ({ + filename: file.name, + content: file.content, // base64 encoded + contentType: file.type, // MIME type + })) + : undefined; + + return { + title: surveyPayload.title as string, + description: surveyPayload.description as string, + topic: surveyPayload.topic as string, // TOOL_REPAIR, FLEET_EXCHANGE, etc. (uppercase) + priority: surveyPayload.priority as string | undefined, // urgent, high, normal, low (lowercase - Zendesk API) + type: surveyPayload.type as string | undefined, // problem, incident, question, task (lowercase - Zendesk API) + attachments, + }; +}; diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.module.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.module.ts index dd58755a2..b16d4e372 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.module.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.module.ts @@ -3,7 +3,7 @@ import { DynamicModule, Module, Type } from '@nestjs/common'; import { LoggerModule } from '@o2s/utils.logger'; -import { ApiConfig, CMS } from '@o2s/framework/modules'; +import { ApiConfig, CMS, Tickets } from '@o2s/framework/modules'; import { SurveyjsController } from './surveyjs.controller'; import { SurveyjsService } from './surveyjs.service'; @@ -12,6 +12,7 @@ import { SurveyjsService } from './surveyjs.service'; export class SurveyjsModule { static register(config: ApiConfig): DynamicModule { const cmsService = config.integrations.cms.service; + const ticketsService = config.integrations.tickets?.service; return { module: SurveyjsModule, imports: [LoggerModule, HttpModule], @@ -21,6 +22,14 @@ export class SurveyjsModule { provide: CMS.Service, useClass: cmsService as Type, }, + ...(ticketsService + ? [ + { + provide: Tickets.Service, + useClass: ticketsService as Type, + }, + ] + : []), ], controllers: [SurveyjsController], exports: [SurveyjsService], diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts index 2c5bd9726..69c481a83 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts @@ -11,9 +11,9 @@ import { SurveyModel } from 'survey-core'; import { Utils } from '@o2s/utils.api-harmonization'; import { LoggerService } from '@o2s/utils.logger'; -import { Auth, CMS } from '@o2s/framework/modules'; +import { Auth, CMS, Tickets } from '@o2s/framework/modules'; -import { mapSurveyJS, mapSurveyJsRequest } from './surveyjs.mapper'; +import { mapSurveyJS, mapSurveyJsRequest, mapSurveyToTicket } from './surveyjs.mapper'; import { SurveyJSLibraryJsonSchema, SurveyJs, SurveyResult } from './surveyjs.model'; import { SurveyJsQuery, SurveyJsSubmitPayload } from './surveyjs.request'; @@ -25,6 +25,7 @@ export class SurveyjsService { protected httpClient: HttpService, private readonly config: ConfigService, private readonly cmsService: CMS.Service, + private readonly ticketsService: Tickets.Service, @Inject(LoggerService) protected readonly logger: LoggerService, ) { this.surveyjsHost = this.config.get('API_SURVEYJS_BASE_URL') || ''; @@ -89,6 +90,9 @@ export class SurveyjsService { this.submitToSurveyJs(payload.surveyPayload, survey.postId, userEmail), ); break; + case 'tickets': + submissions.push(this.submitToTickets(payload.surveyPayload, authorization)); + break; } } @@ -129,6 +133,26 @@ export class SurveyjsService { ); } + private submitToTickets(surveyPayload: SurveyResult, authorization?: string): Observable { + try { + const ticketData = mapSurveyToTicket(surveyPayload); + + return this.ticketsService.createTicket(ticketData, authorization).pipe( + map(() => { + this.logger.info('Ticket created successfully from survey', 'SURVEYJS'); + return undefined; + }), + catchError((error) => { + this.logger.error(`Error occurred while creating ticket from survey: ${error.message}`, 'SURVEYJS'); + throw new BadRequestException('Error occurred while creating ticket from survey.'); + }), + ); + } catch (error) { + this.logger.error(`Error mapping survey to ticket: ${(error as Error).message}`, 'SURVEYJS'); + throw new BadRequestException('Invalid survey data for ticket creation.'); + } + } + private hasAccess(requiredRoles: string[], decodedToken?: Auth.Model.Jwt | undefined): boolean { const userRoles: string[] = []; if (decodedToken) { From d5bae10f7e89db382b2c356aab88a1a4b5645971 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Tue, 20 Jan 2026 16:33:15 +0100 Subject: [PATCH 09/54] refactor(tickets): update attachment handling in ZendeskTicketService --- .../zendesk/src/modules/tickets/zendesk-ticket.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index 89b3d931e..f5221b0f7 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -188,7 +188,7 @@ export class ZendeskTicketService extends Tickets.Service { data.attachments.map((attachment) => this.uploadAttachment( attachment.filename, - Buffer.from(attachment.content, 'base64'), + attachment.content, attachment.contentType, ), ), From b26bbf0a0445d2a1ecd9d9a5d6235e80a42589bf Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 21 Jan 2026 08:52:39 +0100 Subject: [PATCH 10/54] feat(deps): add @types/multer dependency --- apps/api-harmonization/package.json | 1 + package-lock.json | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/apps/api-harmonization/package.json b/apps/api-harmonization/package.json index 3ee9338e3..a824237ed 100644 --- a/apps/api-harmonization/package.json +++ b/apps/api-harmonization/package.json @@ -96,6 +96,7 @@ "@types/express": "^5.0.6", "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", "@types/node": "^24.10.8", "@types/string-template": "^1.0.7", "@types/supertest": "^6.0.3", diff --git a/package-lock.json b/package-lock.json index f4941bd3e..01dbe39ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -140,6 +140,7 @@ "@types/express": "^5.0.6", "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", "@types/node": "^24.10.8", "@types/string-template": "^1.0.7", "@types/supertest": "^6.0.3", @@ -19495,6 +19496,16 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "25.0.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.8.tgz", From 3345a27412b1bfeb8e2b5dd0d01cf68045db1573 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 21 Jan 2026 08:53:37 +0100 Subject: [PATCH 11/54] refactor(tickets): change content type in TicketAttachmentInput from string to Buffer --- packages/framework/src/modules/tickets/tickets.request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/framework/src/modules/tickets/tickets.request.ts b/packages/framework/src/modules/tickets/tickets.request.ts index 5afb8bc61..fdb04d855 100644 --- a/packages/framework/src/modules/tickets/tickets.request.ts +++ b/packages/framework/src/modules/tickets/tickets.request.ts @@ -8,7 +8,7 @@ export class GetTicketParams { export class TicketAttachmentInput { filename!: string; - content!: string; // base64 encoded content + content!: Buffer; contentType!: string; // e.g., 'application/pdf' } From 023c478516f599b8ab928e31d9810f66daa5e2f4 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 21 Jan 2026 10:34:12 +0100 Subject: [PATCH 12/54] feat(surveyjs): implement unified survey submission handling for JSON and multipart/form-data --- .../api-harmonization/surveyjs.controller.ts | 26 +++++- .../src/api-harmonization/surveyjs.mapper.ts | 33 ++++---- .../src/api-harmonization/surveyjs.service.ts | 82 +++++++++++++++++-- .../modules/surveyjs/src/frontend/Survey.tsx | 69 ++++++++++++++-- packages/modules/surveyjs/src/sdk/surveyjs.ts | 67 +++++++++++++-- 5 files changed, 230 insertions(+), 47 deletions(-) diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.controller.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.controller.ts index 0b503d737..e994efd6b 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.controller.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.controller.ts @@ -1,4 +1,5 @@ -import { Body, Controller, Get, Headers, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, Headers, Post, Query, UploadedFiles, UseInterceptors } from '@nestjs/common'; +import { FileFieldsInterceptor } from '@nestjs/platform-express'; import { Observable } from 'rxjs'; import { Models as ApiModels } from '@o2s/utils.api-harmonization'; @@ -17,8 +18,27 @@ export class SurveyjsController { return this.surveyjsService.getSurvey(query); } + /** + * Unified endpoint for submitting surveys. + * Supports both JSON (application/json) and multipart/form-data submissions. + * + * @param files - Array of uploaded files from 'attachments' field (multipart only) + * @param body - Survey payload (JSON) or form data (multipart) + * @param headers - Request headers including authorization + */ @Post() - submitSurvey(@Body() payload: SurveyJsSubmitPayload, @Headers() headers: ApiModels.Headers.AppHeaders) { - return this.surveyjsService.submitSurvey(payload, headers['authorization']); + @UseInterceptors( + FileFieldsInterceptor([{ name: 'attachments', maxCount: 10 }], { + limits: { + fileSize: 10 * 1024 * 1024, // 10MB per file + }, + }), + ) + submitSurvey( + @UploadedFiles() files: { attachments?: Express.Multer.File[] }, + @Body() body: SurveyJsSubmitPayload | (Record & { code: string }), + @Headers() headers: ApiModels.Headers.AppHeaders, + ) { + return this.surveyjsService.submitSurvey(body, headers['authorization'], files?.attachments); } } diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts index 0accdd329..5966f360d 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts @@ -79,35 +79,30 @@ const mapData = (element: Panelbase): Panelbase => { }; }; -/** - * Maps Survey.js form payload to Ticket creation data. - * - * Survey.js returns files in format: { name: string, type: string (MIME), content: string (base64) } - * - * Expected Survey.js configuration (after fix): - * - topic: value = "TOOL_REPAIR" (uppercase) - maps to custom field in Zendesk - * - priority: value = "urgent" (lowercase) - Zendesk API format - * - type: value = "problem" (lowercase) - Zendesk API format - */ export const mapSurveyToTicket = (surveyPayload: SurveyResult): Tickets.Request.PostTicketBody => { // Map attachments from Survey.js format to Tickets format - // eslint-disable-next-line @typescript-eslint/no-explicit-any const attachments = surveyPayload.attachments ? (Array.isArray(surveyPayload.attachments) ? surveyPayload.attachments : [surveyPayload.attachments]) // eslint-disable-next-line @typescript-eslint/no-explicit-any - .map((file: any) => ({ - filename: file.name, - content: file.content, // base64 encoded - contentType: file.type, // MIME type - })) + .map((file: any) => { + // Convert base64 string to Buffer + // Survey.js may include data URI prefix (data:image/png;base64,...) + const base64Data = file.content.includes(',') ? file.content.split(',')[1] : file.content; + + return { + filename: file.name, + content: Buffer.from(base64Data, 'base64'), + contentType: file.type, // MIME type + }; + }) : undefined; return { title: surveyPayload.title as string, description: surveyPayload.description as string, - topic: surveyPayload.topic as string, // TOOL_REPAIR, FLEET_EXCHANGE, etc. (uppercase) - priority: surveyPayload.priority as string | undefined, // urgent, high, normal, low (lowercase - Zendesk API) - type: surveyPayload.type as string | undefined, // problem, incident, question, task (lowercase - Zendesk API) + topic: surveyPayload.topic as string, + priority: surveyPayload.priority as string | undefined, + type: surveyPayload.type as string | undefined, attachments, }; }; diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts index 69c481a83..9e0dd7c4c 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts @@ -66,15 +66,48 @@ export class SurveyjsService { ); } - public submitSurvey(payload: SurveyJsSubmitPayload, authorization?: string): Observable { - return this.cmsService.getSurvey({ code: payload.code }).pipe( + /** + * Unified submit survey method that handles both JSON and multipart/form-data submissions. + * + * @param payload - Survey payload (JSON format) or form data (multipart format) + * @param authorization - Authorization token + * @param files - Optional array of Multer files (for multipart/form-data submissions) + */ + public submitSurvey( + payload: SurveyJsSubmitPayload | (Record & { code: string }), + authorization?: string, + files?: Express.Multer.File[], + ): Observable { + // Detect submission format + const isMultipartSubmission = files && files.length > 0; + const code = payload.code; + + return this.cmsService.getSurvey({ code }).pipe( switchMap((survey) => { const decodedToken = authorization ? Utils.Auth.decodeAuthorizationToken(authorization) : undefined; if (!this.hasAccess(survey.requiredRoles, decodedToken)) { this.logger.info('User does not have access to survey'); throw new UnauthorizedException('User does not have access to survey'); } - return this.validateSurvey(survey.code, payload.surveyPayload).pipe( + + // For multipart submissions with files going to tickets - handle directly + if (isMultipartSubmission && survey.submitDestination.includes('tickets')) { + return this.handleMultipartTicketSubmission( + payload as Record, + files, + authorization, + ); + } + + // For JSON submissions - validate payload format + if (!('surveyPayload' in payload)) { + throw new BadRequestException('Invalid payload format for JSON submission'); + } + + const jsonPayload = payload as SurveyJsSubmitPayload; + const surveyPayload = jsonPayload.surveyPayload; + + return this.validateSurvey(code, surveyPayload).pipe( concatMap((validationResult) => { if (!validationResult) { this.logger.error('Survey payload is not valid.'); @@ -86,18 +119,16 @@ export class SurveyjsService { for (const destination of survey.submitDestination) { switch (destination) { case 'surveyjs': - submissions.push( - this.submitToSurveyJs(payload.surveyPayload, survey.postId, userEmail), - ); + submissions.push(this.submitToSurveyJs(surveyPayload, survey.postId, userEmail)); break; case 'tickets': - submissions.push(this.submitToTickets(payload.surveyPayload, authorization)); + submissions.push(this.submitToTickets(surveyPayload, authorization)); break; } } if (!submissions.length) { - this.logger.info(`No submit destinations specified for survey with code ${payload.code}`); + this.logger.info(`No submit destinations specified for survey with code ${code}`); return of(undefined); } @@ -133,6 +164,41 @@ export class SurveyjsService { ); } + private handleMultipartTicketSubmission( + formData: Record, + files: Express.Multer.File[], + authorization?: string, + ): Observable { + const { code, ...surveyData } = formData; + + // Convert Multer files to TicketAttachmentInput format + const attachments = files.map((file) => ({ + filename: file.originalname, + content: file.buffer, + contentType: file.mimetype, + })); + + const ticketData: Tickets.Request.PostTicketBody = { + title: surveyData.title || '', + description: surveyData.description || '', + topic: surveyData.topic || '', + priority: surveyData.priority, + type: surveyData.type, + attachments, + }; + + return this.ticketsService.createTicket(ticketData, authorization).pipe( + map(() => { + this.logger.info('Ticket created successfully from survey with files', 'SURVEYJS'); + return undefined; + }), + catchError((error) => { + this.logger.error(`Error occurred while creating ticket from survey: ${error.message}`, 'SURVEYJS'); + throw new BadRequestException('Error occurred while creating ticket from survey.'); + }), + ); + } + private submitToTickets(surveyPayload: SurveyResult, authorization?: string): Observable { try { const ticketData = mapSurveyToTicket(surveyPayload); diff --git a/packages/modules/surveyjs/src/frontend/Survey.tsx b/packages/modules/surveyjs/src/frontend/Survey.tsx index fff10d321..6d1d0b02b 100644 --- a/packages/modules/surveyjs/src/frontend/Survey.tsx +++ b/packages/modules/surveyjs/src/frontend/Survey.tsx @@ -110,14 +110,66 @@ export const Survey: React.FC = ({ code, labels, locale, accessToke const handleSubmit = async (data: Model.SurveyResult) => { startTransition(async () => { try { - await sdk.modules.submitSurvey( - { - code, - surveyPayload: data, - }, - { 'x-locale': locale }, - accessToken, - ); + // Detect file fields from survey schema + const fileFieldNames = new Set(); + + if (state.model) { + state.model.getAllQuestions().forEach((question) => { + if (question.getType() === 'file') { + fileFieldNames.add(question.name); + } + }); + } + + // Separate files from regular data based on schema + const files: File[] = []; + const cleanedData: Record = {}; + + for (const [key, value] of Object.entries(data)) { + if (fileFieldNames.has(key) && value) { + // This is a file field - convert Survey.js format to File objects + const attachments = Array.isArray(value) ? value : [value]; + + for (const attachment of attachments) { + if (attachment && typeof attachment === 'object' && 'content' in attachment) { + // Decode base64 to Blob + const base64Data = attachment.content.split(',')[1] || attachment.content; + const byteCharacters = atob(base64Data); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + const blob = new Blob([byteArray], { type: attachment.type }); + const file = new File([blob], attachment.name, { type: attachment.type }); + files.push(file); + } + } + } else { + // Regular field - include in cleaned data + cleanedData[key] = value; + } + } + + // Use multipart/form-data if files were detected + if (files.length > 0) { + await sdk.modules.submitSurvey( + { code, ...cleanedData } as Record & { code: string }, + { 'x-locale': locale }, + accessToken, + files, + ); + } else { + // Use regular JSON submission for forms without files + await sdk.modules.submitSurvey( + { + code, + surveyPayload: data, + }, + { 'x-locale': locale }, + accessToken, + ); + } } catch (error) { handleError(error, labels); } @@ -151,6 +203,7 @@ export const Survey: React.FC = ({ code, labels, locale, accessToke }; loadSurvey(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, code, locale, accessToken, labels]); return ( diff --git a/packages/modules/surveyjs/src/sdk/surveyjs.ts b/packages/modules/surveyjs/src/sdk/surveyjs.ts index 99bfd8fab..12cae9b6a 100644 --- a/packages/modules/surveyjs/src/sdk/surveyjs.ts +++ b/packages/modules/surveyjs/src/sdk/surveyjs.ts @@ -29,24 +29,73 @@ export const surveyjs = (sdk: Sdk) => ({ params: params, }), + /** + * Unified submit survey method that handles both JSON and multipart/form-data submissions. + * + * - If files are provided: uses multipart/form-data (efficient for large files) + * - If no files: uses application/json (standard submission) + * + * @param params - Survey payload (JSON format) or code with data (for multipart) + * @param headers - Request headers + * @param authorization - Authorization token + * @param files - Optional array of File objects (triggers multipart/form-data) + */ submitSurvey: ( - params: Request.SurveyJsSubmitPayload, + params: Request.SurveyJsSubmitPayload | (Record & { code: string }), headers: ApiModels.Headers.AppHeaders, authorization?: string, - ): Promise => - sdk.makeRequest({ + files?: File[], + ): Promise => { + // If files are provided, use multipart/form-data + if (files && files.length > 0) { + const formData = new FormData(); + + // Handle both payload formats + if ('surveyPayload' in params) { + // JSON format: extract code and surveyPayload fields + formData.append('code', params.code); + Object.entries(params.surveyPayload).forEach(([key, value]) => { + if (value !== undefined && value !== null && key !== 'attachments') { + formData.append(key, String(value)); + } + }); + } else { + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + formData.append(key, String(value)); + } + }); + } + + // Add files + files.forEach((file) => { + formData.append('attachments', file); + }); + + return sdk.makeRequest({ + method: 'post', + url: API_URL, + headers: { + ...Utils.Headers.getApiHeaders(), + ...headers, + ...(authorization ? { Authorization: `Bearer ${authorization}` } : {}), + // Don't set Content-Type - browser will set it with boundary for multipart + }, + data: formData, + }); + } + + // No files: use standard JSON submission + return sdk.makeRequest({ method: 'post', url: API_URL, headers: { ...Utils.Headers.getApiHeaders(), ...headers, - ...(authorization - ? { - Authorization: `Bearer ${authorization}`, - } - : {}), + ...(authorization ? { Authorization: `Bearer ${authorization}` } : {}), }, data: params, - }), + }); + }, }, }); From 5d43c6a9307cdea7ae8c8f4865752ac04aee37d5 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 21 Jan 2026 10:36:42 +0100 Subject: [PATCH 13/54] chore(deps): update package versions in package-lock.json --- package-lock.json | 88 +++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index 161c1263a..23785779b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ }, "apps/api-harmonization": { "name": "@o2s/api-harmonization", - "version": "1.12.1", + "version": "1.13.0", "license": "MIT", "dependencies": { "@nestjs/axios": "^4.0.1", @@ -506,7 +506,7 @@ }, "apps/docs": { "name": "@o2s/docs", - "version": "1.5.1", + "version": "1.6.0", "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/plugin-google-gtag": "^3.9.2", @@ -588,7 +588,7 @@ }, "apps/frontend": { "name": "@o2s/frontend", - "version": "1.13.0", + "version": "1.14.0", "dependencies": { "@contentful/live-preview": "^4.9.0", "@o2s/blocks.article": "*", @@ -49233,7 +49233,7 @@ }, "packages/blocks/article": { "name": "@o2s/blocks.article", - "version": "1.2.2", + "version": "1.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49273,7 +49273,7 @@ }, "packages/blocks/article-list": { "name": "@o2s/blocks.article-list", - "version": "1.2.2", + "version": "1.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49313,7 +49313,7 @@ }, "packages/blocks/article-search": { "name": "@o2s/blocks.article-search", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49355,7 +49355,7 @@ }, "packages/blocks/bento-grid": { "name": "@o2s/blocks.bento-grid", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49394,7 +49394,7 @@ }, "packages/blocks/category": { "name": "@o2s/blocks.category", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49434,7 +49434,7 @@ }, "packages/blocks/category-list": { "name": "@o2s/blocks.category-list", - "version": "1.2.2", + "version": "1.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49474,7 +49474,7 @@ }, "packages/blocks/cta-section": { "name": "@o2s/blocks.cta-section", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49513,7 +49513,7 @@ }, "packages/blocks/document-list": { "name": "@o2s/blocks.document-list", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49552,7 +49552,7 @@ }, "packages/blocks/faq": { "name": "@o2s/blocks.faq", - "version": "1.2.2", + "version": "1.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49592,7 +49592,7 @@ }, "packages/blocks/feature-section": { "name": "@o2s/blocks.feature-section", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49631,7 +49631,7 @@ }, "packages/blocks/feature-section-grid": { "name": "@o2s/blocks.feature-section-grid", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49670,7 +49670,7 @@ }, "packages/blocks/featured-service-list": { "name": "@o2s/blocks.featured-service-list", - "version": "1.1.2", + "version": "1.2.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49710,7 +49710,7 @@ }, "packages/blocks/hero-section": { "name": "@o2s/blocks.hero-section", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49749,7 +49749,7 @@ }, "packages/blocks/invoice-list": { "name": "@o2s/blocks.invoice-list", - "version": "1.3.0", + "version": "1.4.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49789,7 +49789,7 @@ }, "packages/blocks/media-section": { "name": "@o2s/blocks.media-section", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49828,7 +49828,7 @@ }, "packages/blocks/notification-details": { "name": "@o2s/blocks.notification-details", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49868,7 +49868,7 @@ }, "packages/blocks/notification-list": { "name": "@o2s/blocks.notification-list", - "version": "1.3.0", + "version": "1.4.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49908,7 +49908,7 @@ }, "packages/blocks/notification-summary": { "name": "@o2s/blocks.notification-summary", - "version": "1.0.1", + "version": "1.1.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49947,7 +49947,7 @@ }, "packages/blocks/order-details": { "name": "@o2s/blocks.order-details", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -49987,7 +49987,7 @@ }, "packages/blocks/order-list": { "name": "@o2s/blocks.order-list", - "version": "1.3.1", + "version": "1.4.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50027,7 +50027,7 @@ }, "packages/blocks/orders-summary": { "name": "@o2s/blocks.orders-summary", - "version": "1.2.1", + "version": "1.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50067,7 +50067,7 @@ }, "packages/blocks/payments-history": { "name": "@o2s/blocks.payments-history", - "version": "1.1.2", + "version": "1.2.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50107,7 +50107,7 @@ }, "packages/blocks/payments-summary": { "name": "@o2s/blocks.payments-summary", - "version": "1.1.3", + "version": "1.2.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50147,7 +50147,7 @@ }, "packages/blocks/pricing-section": { "name": "@o2s/blocks.pricing-section", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50228,7 +50228,7 @@ }, "packages/blocks/product-list": { "name": "@o2s/blocks.product-list", - "version": "0.1.1", + "version": "0.2.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50267,7 +50267,7 @@ }, "packages/blocks/quick-links": { "name": "@o2s/blocks.quick-links", - "version": "1.2.2", + "version": "1.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50347,7 +50347,7 @@ }, "packages/blocks/service-details": { "name": "@o2s/blocks.service-details", - "version": "1.1.2", + "version": "1.2.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50387,7 +50387,7 @@ }, "packages/blocks/service-list": { "name": "@o2s/blocks.service-list", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50427,7 +50427,7 @@ }, "packages/blocks/surveyjs-form": { "name": "@o2s/blocks.surveyjs-form", - "version": "1.1.2", + "version": "1.2.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50470,7 +50470,7 @@ }, "packages/blocks/ticket-details": { "name": "@o2s/blocks.ticket-details", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50510,7 +50510,7 @@ }, "packages/blocks/ticket-list": { "name": "@o2s/blocks.ticket-list", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50550,7 +50550,7 @@ }, "packages/blocks/ticket-recent": { "name": "@o2s/blocks.ticket-recent", - "version": "1.1.2", + "version": "1.2.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50590,7 +50590,7 @@ }, "packages/blocks/ticket-summary": { "name": "@o2s/blocks.ticket-summary", - "version": "1.0.1", + "version": "1.1.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50629,7 +50629,7 @@ }, "packages/blocks/user-account": { "name": "@o2s/blocks.user-account", - "version": "1.1.2", + "version": "1.2.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -50714,7 +50714,7 @@ }, "packages/configs/integrations": { "name": "@o2s/configs.integrations", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "dependencies": { "@o2s/framework": "*", @@ -50796,7 +50796,7 @@ }, "packages/framework": { "name": "@o2s/framework", - "version": "1.14.0", + "version": "1.15.0", "license": "MIT", "dependencies": { "@o2s/utils.logger": "*", @@ -51002,7 +51002,7 @@ }, "packages/integrations/mocked": { "name": "@o2s/integrations.mocked", - "version": "1.15.0", + "version": "1.16.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -51167,7 +51167,7 @@ }, "packages/modules/surveyjs": { "name": "@o2s/modules.surveyjs", - "version": "0.2.3", + "version": "0.3.0", "license": "MIT", "dependencies": { "@o2s/configs.integrations": "*", @@ -51473,7 +51473,7 @@ }, "packages/ui": { "name": "@o2s/ui", - "version": "1.7.1", + "version": "1.8.0", "license": "MIT", "dependencies": { "@o2s/framework": "*", @@ -51551,7 +51551,7 @@ }, "packages/utils/api-harmonization": { "name": "@o2s/utils.api-harmonization", - "version": "0.1.3", + "version": "0.2.0", "license": "MIT", "dependencies": { "@o2s/framework": "*" @@ -51574,7 +51574,7 @@ }, "packages/utils/frontend": { "name": "@o2s/utils.frontend", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "dependencies": { "@o2s/framework": "*" From 140650d2dee05eca87d13da9e527a22d2b19b22c Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 21 Jan 2026 12:27:44 +0100 Subject: [PATCH 14/54] fix(surveyjs): ensure tickets service is always provided in SurveyjsModule --- .../src/api-harmonization/surveyjs.module.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.module.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.module.ts index b16d4e372..e6b136b05 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.module.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.module.ts @@ -12,7 +12,7 @@ import { SurveyjsService } from './surveyjs.service'; export class SurveyjsModule { static register(config: ApiConfig): DynamicModule { const cmsService = config.integrations.cms.service; - const ticketsService = config.integrations.tickets?.service; + const ticketsService = config.integrations.tickets.service; return { module: SurveyjsModule, imports: [LoggerModule, HttpModule], @@ -22,14 +22,10 @@ export class SurveyjsModule { provide: CMS.Service, useClass: cmsService as Type, }, - ...(ticketsService - ? [ - { - provide: Tickets.Service, - useClass: ticketsService as Type, - }, - ] - : []), + { + provide: Tickets.Service, + useClass: ticketsService as Type, + }, ], controllers: [SurveyjsController], exports: [SurveyjsService], From 18506a9f83de7b19a0cbf6cd7223ffb9081f2479 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 21 Jan 2026 12:52:57 +0100 Subject: [PATCH 15/54] feat(cms): add mappings for create ticket form pages in multiple locales --- .../src/modules/cms/mappers/cms.page.mapper.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.page.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.page.mapper.ts index c037c2f8c..45e02d18b 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.page.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.page.mapper.ts @@ -291,6 +291,7 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_CONTACT_US_PL, PAGE_COMPLAINT_FORM_PL, PAGE_REQUEST_DEVICE_MAINTENANCE_PL, + PAGE_CREATE_TICKET_PL, PAGE_ORDER_LIST_PL, PAGE_ORDER_DETAILS_PL, PAGE_WARRANTY_AND_REPAIR_PL, @@ -312,6 +313,7 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_CONTACT_US_DE, PAGE_COMPLAINT_FORM_DE, PAGE_REQUEST_DEVICE_MAINTENANCE_DE, + PAGE_CREATE_TICKET_DE, PAGE_ORDER_LIST_DE, PAGE_ORDER_DETAILS_DE, PAGE_WARRANTY_AND_REPAIR_DE, @@ -333,6 +335,7 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_CONTACT_US_EN, PAGE_COMPLAINT_FORM_EN, PAGE_REQUEST_DEVICE_MAINTENANCE_EN, + PAGE_CREATE_TICKET_EN, PAGE_ORDER_LIST_EN, PAGE_ORDER_DETAILS_EN, PAGE_WARRANTY_AND_REPAIR_EN, @@ -383,6 +386,9 @@ export const getAlternativePages = (id: string, slug: string, locale: string): C PAGE_REQUEST_DEVICE_MAINTENANCE_EN, PAGE_REQUEST_DEVICE_MAINTENANCE_DE, PAGE_REQUEST_DEVICE_MAINTENANCE_PL, + PAGE_CREATE_TICKET_EN, + PAGE_CREATE_TICKET_DE, + PAGE_CREATE_TICKET_PL, PAGE_ORDER_LIST_EN, PAGE_ORDER_LIST_DE, PAGE_ORDER_LIST_PL, From 7803964f792f3f6d9118c79176a43d0619b7c9a6 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 21 Jan 2026 13:02:47 +0100 Subject: [PATCH 16/54] feat(cms): extend page mappings to include create ticket pages for additional locales --- .../mocked/src/modules/cms/mappers/cms.page.mapper.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts index f3a5fcd33..d2dca960e 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts @@ -314,6 +314,7 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_CONTACT_US_PL, PAGE_COMPLAINT_FORM_PL, PAGE_REQUEST_DEVICE_MAINTENANCE_PL, + PAGE_CREATE_TICKET_PL, PAGE_ORDER_LIST_PL, PAGE_ORDER_DETAILS_PL, PAGE_WARRANTY_AND_REPAIR_PL, @@ -337,6 +338,7 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_CONTACT_US_DE, PAGE_COMPLAINT_FORM_DE, PAGE_REQUEST_DEVICE_MAINTENANCE_DE, + PAGE_CREATE_TICKET_DE, PAGE_ORDER_LIST_DE, PAGE_ORDER_DETAILS_DE, PAGE_WARRANTY_AND_REPAIR_DE, @@ -360,6 +362,7 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_CONTACT_US_EN, PAGE_COMPLAINT_FORM_EN, PAGE_REQUEST_DEVICE_MAINTENANCE_EN, + PAGE_CREATE_TICKET_EN, PAGE_ORDER_LIST_EN, PAGE_ORDER_DETAILS_EN, PAGE_WARRANTY_AND_REPAIR_EN, @@ -413,6 +416,9 @@ export const getAlternativePages = (id: string, slug: string, locale: string): C PAGE_REQUEST_DEVICE_MAINTENANCE_EN, PAGE_REQUEST_DEVICE_MAINTENANCE_DE, PAGE_REQUEST_DEVICE_MAINTENANCE_PL, + PAGE_CREATE_TICKET_EN, + PAGE_CREATE_TICKET_DE, + PAGE_CREATE_TICKET_PL, PAGE_ORDER_LIST_EN, PAGE_ORDER_LIST_DE, PAGE_ORDER_LIST_PL, From d428fdd0c0451afce17fac7ed025a88dc499c969 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 21 Jan 2026 13:12:32 +0100 Subject: [PATCH 17/54] feat(surveyjs): add validation for required fields in ticket creation --- .../src/api-harmonization/surveyjs.mapper.ts | 4 ++++ .../src/api-harmonization/surveyjs.service.ts | 22 ++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts index 5966f360d..071d5d34c 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts @@ -79,6 +79,10 @@ const mapData = (element: Panelbase): Panelbase => { }; }; +/** + * Maps Survey.js payload to ticket creation format. + * Note: Caller must validate that title, description, and topic are non-empty before calling this function. + */ export const mapSurveyToTicket = (surveyPayload: SurveyResult): Tickets.Request.PostTicketBody => { // Map attachments from Survey.js format to Tickets format const attachments = surveyPayload.attachments diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts index 9e0dd7c4c..ad3ec7a6c 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts @@ -171,6 +171,14 @@ export class SurveyjsService { ): Observable { const { code, ...surveyData } = formData; + // Validate required fields for ticket creation + if (!surveyData.title || !surveyData.description || !surveyData.topic) { + this.logger.error( + 'Missing required fields for ticket creation: title, description, and topic are required', + ); + throw new BadRequestException('Title, description, and topic are required to create a ticket'); + } + // Convert Multer files to TicketAttachmentInput format const attachments = files.map((file) => ({ filename: file.originalname, @@ -179,9 +187,9 @@ export class SurveyjsService { })); const ticketData: Tickets.Request.PostTicketBody = { - title: surveyData.title || '', - description: surveyData.description || '', - topic: surveyData.topic || '', + title: surveyData.title, + description: surveyData.description, + topic: surveyData.topic, priority: surveyData.priority, type: surveyData.type, attachments, @@ -201,6 +209,14 @@ export class SurveyjsService { private submitToTickets(surveyPayload: SurveyResult, authorization?: string): Observable { try { + // Validate required fields before mapping + if (!surveyPayload.title || !surveyPayload.description || !surveyPayload.topic) { + this.logger.error( + 'Missing required fields for ticket creation: title, description, and topic are required', + ); + throw new BadRequestException('Title, description, and topic are required to create a ticket'); + } + const ticketData = mapSurveyToTicket(surveyPayload); return this.ticketsService.createTicket(ticketData, authorization).pipe( From 05fdbb41d86c21456b14a2b59f17343ebc448ed8 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 21 Jan 2026 16:06:01 +0100 Subject: [PATCH 18/54] refactor(surveyjs): simplify survey submission by removing multipart handling --- .../api-harmonization/surveyjs.controller.ts | 26 +------ .../src/api-harmonization/surveyjs.service.ts | 77 +------------------ .../modules/surveyjs/src/frontend/Survey.tsx | 69 ++--------------- packages/modules/surveyjs/src/sdk/surveyjs.ts | 54 +------------ 4 files changed, 14 insertions(+), 212 deletions(-) diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.controller.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.controller.ts index e994efd6b..e7d2d1426 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.controller.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.controller.ts @@ -1,5 +1,4 @@ -import { Body, Controller, Get, Headers, Post, Query, UploadedFiles, UseInterceptors } from '@nestjs/common'; -import { FileFieldsInterceptor } from '@nestjs/platform-express'; +import { Body, Controller, Get, Headers, Post, Query } from '@nestjs/common'; import { Observable } from 'rxjs'; import { Models as ApiModels } from '@o2s/utils.api-harmonization'; @@ -18,27 +17,8 @@ export class SurveyjsController { return this.surveyjsService.getSurvey(query); } - /** - * Unified endpoint for submitting surveys. - * Supports both JSON (application/json) and multipart/form-data submissions. - * - * @param files - Array of uploaded files from 'attachments' field (multipart only) - * @param body - Survey payload (JSON) or form data (multipart) - * @param headers - Request headers including authorization - */ @Post() - @UseInterceptors( - FileFieldsInterceptor([{ name: 'attachments', maxCount: 10 }], { - limits: { - fileSize: 10 * 1024 * 1024, // 10MB per file - }, - }), - ) - submitSurvey( - @UploadedFiles() files: { attachments?: Express.Multer.File[] }, - @Body() body: SurveyJsSubmitPayload | (Record & { code: string }), - @Headers() headers: ApiModels.Headers.AppHeaders, - ) { - return this.surveyjsService.submitSurvey(body, headers['authorization'], files?.attachments); + submitSurvey(@Body() body: SurveyJsSubmitPayload, @Headers() headers: ApiModels.Headers.AppHeaders) { + return this.surveyjsService.submitSurvey(body, headers['authorization']); } } diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts index ad3ec7a6c..26bcc19d2 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts @@ -66,21 +66,8 @@ export class SurveyjsService { ); } - /** - * Unified submit survey method that handles both JSON and multipart/form-data submissions. - * - * @param payload - Survey payload (JSON format) or form data (multipart format) - * @param authorization - Authorization token - * @param files - Optional array of Multer files (for multipart/form-data submissions) - */ - public submitSurvey( - payload: SurveyJsSubmitPayload | (Record & { code: string }), - authorization?: string, - files?: Express.Multer.File[], - ): Observable { - // Detect submission format - const isMultipartSubmission = files && files.length > 0; - const code = payload.code; + public submitSurvey(payload: SurveyJsSubmitPayload, authorization?: string): Observable { + const { code, surveyPayload } = payload; return this.cmsService.getSurvey({ code }).pipe( switchMap((survey) => { @@ -90,23 +77,6 @@ export class SurveyjsService { throw new UnauthorizedException('User does not have access to survey'); } - // For multipart submissions with files going to tickets - handle directly - if (isMultipartSubmission && survey.submitDestination.includes('tickets')) { - return this.handleMultipartTicketSubmission( - payload as Record, - files, - authorization, - ); - } - - // For JSON submissions - validate payload format - if (!('surveyPayload' in payload)) { - throw new BadRequestException('Invalid payload format for JSON submission'); - } - - const jsonPayload = payload as SurveyJsSubmitPayload; - const surveyPayload = jsonPayload.surveyPayload; - return this.validateSurvey(code, surveyPayload).pipe( concatMap((validationResult) => { if (!validationResult) { @@ -164,49 +134,6 @@ export class SurveyjsService { ); } - private handleMultipartTicketSubmission( - formData: Record, - files: Express.Multer.File[], - authorization?: string, - ): Observable { - const { code, ...surveyData } = formData; - - // Validate required fields for ticket creation - if (!surveyData.title || !surveyData.description || !surveyData.topic) { - this.logger.error( - 'Missing required fields for ticket creation: title, description, and topic are required', - ); - throw new BadRequestException('Title, description, and topic are required to create a ticket'); - } - - // Convert Multer files to TicketAttachmentInput format - const attachments = files.map((file) => ({ - filename: file.originalname, - content: file.buffer, - contentType: file.mimetype, - })); - - const ticketData: Tickets.Request.PostTicketBody = { - title: surveyData.title, - description: surveyData.description, - topic: surveyData.topic, - priority: surveyData.priority, - type: surveyData.type, - attachments, - }; - - return this.ticketsService.createTicket(ticketData, authorization).pipe( - map(() => { - this.logger.info('Ticket created successfully from survey with files', 'SURVEYJS'); - return undefined; - }), - catchError((error) => { - this.logger.error(`Error occurred while creating ticket from survey: ${error.message}`, 'SURVEYJS'); - throw new BadRequestException('Error occurred while creating ticket from survey.'); - }), - ); - } - private submitToTickets(surveyPayload: SurveyResult, authorization?: string): Observable { try { // Validate required fields before mapping diff --git a/packages/modules/surveyjs/src/frontend/Survey.tsx b/packages/modules/surveyjs/src/frontend/Survey.tsx index 6d1d0b02b..fff10d321 100644 --- a/packages/modules/surveyjs/src/frontend/Survey.tsx +++ b/packages/modules/surveyjs/src/frontend/Survey.tsx @@ -110,66 +110,14 @@ export const Survey: React.FC = ({ code, labels, locale, accessToke const handleSubmit = async (data: Model.SurveyResult) => { startTransition(async () => { try { - // Detect file fields from survey schema - const fileFieldNames = new Set(); - - if (state.model) { - state.model.getAllQuestions().forEach((question) => { - if (question.getType() === 'file') { - fileFieldNames.add(question.name); - } - }); - } - - // Separate files from regular data based on schema - const files: File[] = []; - const cleanedData: Record = {}; - - for (const [key, value] of Object.entries(data)) { - if (fileFieldNames.has(key) && value) { - // This is a file field - convert Survey.js format to File objects - const attachments = Array.isArray(value) ? value : [value]; - - for (const attachment of attachments) { - if (attachment && typeof attachment === 'object' && 'content' in attachment) { - // Decode base64 to Blob - const base64Data = attachment.content.split(',')[1] || attachment.content; - const byteCharacters = atob(base64Data); - const byteNumbers = new Array(byteCharacters.length); - for (let i = 0; i < byteCharacters.length; i++) { - byteNumbers[i] = byteCharacters.charCodeAt(i); - } - const byteArray = new Uint8Array(byteNumbers); - const blob = new Blob([byteArray], { type: attachment.type }); - const file = new File([blob], attachment.name, { type: attachment.type }); - files.push(file); - } - } - } else { - // Regular field - include in cleaned data - cleanedData[key] = value; - } - } - - // Use multipart/form-data if files were detected - if (files.length > 0) { - await sdk.modules.submitSurvey( - { code, ...cleanedData } as Record & { code: string }, - { 'x-locale': locale }, - accessToken, - files, - ); - } else { - // Use regular JSON submission for forms without files - await sdk.modules.submitSurvey( - { - code, - surveyPayload: data, - }, - { 'x-locale': locale }, - accessToken, - ); - } + await sdk.modules.submitSurvey( + { + code, + surveyPayload: data, + }, + { 'x-locale': locale }, + accessToken, + ); } catch (error) { handleError(error, labels); } @@ -203,7 +151,6 @@ export const Survey: React.FC = ({ code, labels, locale, accessToke }; loadSurvey(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, code, locale, accessToken, labels]); return ( diff --git a/packages/modules/surveyjs/src/sdk/surveyjs.ts b/packages/modules/surveyjs/src/sdk/surveyjs.ts index 12cae9b6a..d2649d3e8 100644 --- a/packages/modules/surveyjs/src/sdk/surveyjs.ts +++ b/packages/modules/surveyjs/src/sdk/surveyjs.ts @@ -29,63 +29,11 @@ export const surveyjs = (sdk: Sdk) => ({ params: params, }), - /** - * Unified submit survey method that handles both JSON and multipart/form-data submissions. - * - * - If files are provided: uses multipart/form-data (efficient for large files) - * - If no files: uses application/json (standard submission) - * - * @param params - Survey payload (JSON format) or code with data (for multipart) - * @param headers - Request headers - * @param authorization - Authorization token - * @param files - Optional array of File objects (triggers multipart/form-data) - */ submitSurvey: ( - params: Request.SurveyJsSubmitPayload | (Record & { code: string }), + params: Request.SurveyJsSubmitPayload, headers: ApiModels.Headers.AppHeaders, authorization?: string, - files?: File[], ): Promise => { - // If files are provided, use multipart/form-data - if (files && files.length > 0) { - const formData = new FormData(); - - // Handle both payload formats - if ('surveyPayload' in params) { - // JSON format: extract code and surveyPayload fields - formData.append('code', params.code); - Object.entries(params.surveyPayload).forEach(([key, value]) => { - if (value !== undefined && value !== null && key !== 'attachments') { - formData.append(key, String(value)); - } - }); - } else { - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - formData.append(key, String(value)); - } - }); - } - - // Add files - files.forEach((file) => { - formData.append('attachments', file); - }); - - return sdk.makeRequest({ - method: 'post', - url: API_URL, - headers: { - ...Utils.Headers.getApiHeaders(), - ...headers, - ...(authorization ? { Authorization: `Bearer ${authorization}` } : {}), - // Don't set Content-Type - browser will set it with boundary for multipart - }, - data: formData, - }); - } - - // No files: use standard JSON submission return sdk.makeRequest({ method: 'post', url: API_URL, From 490c7708c1a02a5967cdc2521954890c4093b971 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Thu, 22 Jan 2026 15:24:35 +0100 Subject: [PATCH 19/54] feat(tickets): update ticket creation to use ticketFormId and customFields --- .../src/modules/tickets/tickets.request.ts | 5 +- .../modules/tickets/zendesk-field.mapper.ts | 93 +++++++++++++++++++ .../modules/tickets/zendesk-ticket.service.ts | 33 ++----- .../src/api-harmonization/surveyjs.mapper.ts | 23 +++-- .../src/api-harmonization/surveyjs.service.ts | 6 +- 5 files changed, 119 insertions(+), 41 deletions(-) create mode 100644 packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts diff --git a/packages/framework/src/modules/tickets/tickets.request.ts b/packages/framework/src/modules/tickets/tickets.request.ts index fdb04d855..a84ff6cd0 100644 --- a/packages/framework/src/modules/tickets/tickets.request.ts +++ b/packages/framework/src/modules/tickets/tickets.request.ts @@ -15,10 +15,9 @@ export class TicketAttachmentInput { export class PostTicketBody { title!: string; description!: string; - topic!: string; + ticketFormId!: number; attachments?: TicketAttachmentInput[]; - priority?: string; - type?: string; + customFields?: Record; } export class GetTicketListQuery extends PaginationQuery { diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts new file mode 100644 index 000000000..85c588293 --- /dev/null +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts @@ -0,0 +1,93 @@ +/** + * Zendesk Custom Field Mapper + * Maps Survey.js form fields to Zendesk custom fields using environment variables + */ + +export interface ZendeskCustomField { + id: number; + value: string | number | boolean; +} + +/** + * Mapper for converting Survey.js fields to Zendesk custom fields. + * Field IDs are configured via environment variables (ZENDESK_*_FIELD_ID). + * + * To add new custom field mapping: + * 1. Add environment variable (e.g., ZENDESK_NEW_FIELD_ID=123456) + * 2. Add mapping to fieldMap: newField: Number(process.env.ZENDESK_NEW_FIELD_ID) || undefined + * 3. Include the field in Survey.js form with matching name + */ +export class ZendeskFieldMapper { + // Static mapping of Survey.js field names to Zendesk custom field IDs from environment variables + private static readonly fieldMap: Record = { + topic: process.env.ZENDESK_TOPIC_FIELD_ID ? Number(process.env.ZENDESK_TOPIC_FIELD_ID) : undefined, + // Add more field mappings here as needed + // example: priority: process.env.ZENDESK_PRIORITY_FIELD_ID ? Number(process.env.ZENDESK_PRIORITY_FIELD_ID) : undefined, + }; + + /** + * Converts a record of field values to Zendesk custom fields format. + * Only includes fields that: + * - Have a mapping in fieldMap + * - Have a valid environment variable configured + * - Have non-null/undefined values + * + * @param data - Object with field names as keys and their values + * @returns Array of Zendesk custom field objects with id and value + */ + static toCustomFields(data: Record): ZendeskCustomField[] { + const customFields: ZendeskCustomField[] = []; + + for (const [fieldName, fieldValue] of Object.entries(data)) { + // Skip if value is null or undefined + if (fieldValue === null || fieldValue === undefined) { + continue; + } + + // Get field ID from mapping + const fieldId = this.fieldMap[fieldName]; + + // Skip if field is not mapped or environment variable is not configured + if (!fieldId || isNaN(fieldId)) { + continue; + } + + // Validate and convert value to supported types + const validatedValue = this.validateAndConvertValue(fieldValue); + if (validatedValue !== null) { + customFields.push({ + id: fieldId, + value: validatedValue, + }); + } + } + + return customFields; + } + + /** + * Validates and converts field value to Zendesk-supported types. + * Zendesk API accepts: string, number, boolean + * + * @param value - The value to validate and convert + * @returns Validated value or null if invalid + */ + private static validateAndConvertValue(value: unknown): string | number | boolean | null { + // Handle primitives directly + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + // Convert arrays and objects to JSON strings + if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { + try { + return JSON.stringify(value); + } catch { + return null; + } + } + + // Invalid type + return null; + } +} diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index d8ba51b8d..beb570d49 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -9,7 +9,6 @@ import { type SearchResultObject, type TicketCommentObject, type TicketObject, - type TicketUpdateInputWritable, type UserObject, createTicket, listSearchResults, @@ -20,6 +19,7 @@ import { } from '@/generated/zendesk'; import { client } from '@/generated/zendesk/client.gen'; +import { ZendeskFieldMapper } from './zendesk-field.mapper'; import { mapTicketToModel } from './zendesk-ticket.mapper'; type ZendeskTicket = TicketObject; @@ -171,8 +171,8 @@ export class ZendeskTicketService extends Tickets.Service { createTicket(data: Tickets.Request.PostTicketBody, authorization?: string): Observable { // Validate input data - if (!data.title || !data.description || !data.topic) { - return throwError(() => new BadRequestException('Title, description and topic are required')); + if (!data.title || !data.description || !data.ticketFormId) { + return throwError(() => new BadRequestException('Title, description and ticketFormId are required')); } return this.usersService.getCurrentUser(authorization).pipe( @@ -200,22 +200,8 @@ export class ZendeskTicketService extends Tickets.Service { switchMap((uploadTokens) => this.findZendeskUserByEmail(user.email!).pipe( switchMap((zendeskUser) => { - const topicFieldId = Number(process.env.ZENDESK_TOPIC_FIELD_ID || 0); - const customFields: Array<{ id: number; value: string }> = []; - - if (data.topic && !topicFieldId) { - return throwError( - () => new Error('ZENDESK_TOPIC_FIELD_ID is required to persist ticket topic'), - ); - } - - // Add topic as custom field if provided and ZENDESK_TOPIC_FIELD_ID is configured - if (data.topic && topicFieldId) { - customFields.push({ - id: topicFieldId, - value: data.topic, - }); - } + // Map custom fields from Survey.js to Zendesk format using field mapper + const customFields = ZendeskFieldMapper.toCustomFields(data.customFields || {}); return from( createTicket({ @@ -226,17 +212,12 @@ export class ZendeskTicketService extends Tickets.Service { body: data.description, ...(uploadTokens.length > 0 && { uploads: uploadTokens }), }, - ...(data.priority && { - priority: data.priority as TicketUpdateInputWritable['priority'], - }), - ...(data.type && { - type: data.type as TicketUpdateInputWritable['type'], - }), + ticket_form_id: data.ticketFormId, ...(zendeskUser?.id && { requester_id: zendeskUser.id, submitter_id: zendeskUser.id, }), - // Add custom fields if any (e.g., topic) + // Add custom fields if any // Note: Zendesk API accepts {id, value} structure for custom_fields // TypeScript types require full CustomFieldObject, but API accepts simpler structure // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts index 071d5d34c..fbf8b7631 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts @@ -81,12 +81,18 @@ const mapData = (element: Panelbase): Panelbase => { /** * Maps Survey.js payload to ticket creation format. - * Note: Caller must validate that title, description, and topic are non-empty before calling this function. + * Extracts standard fields (title, description, ticketFormId, attachments) and passes + * all other fields as customFields to be mapped by ZendeskFieldMapper. + * + * Note: Caller must validate that title, description, and ticketFormId are non-empty before calling this function. */ export const mapSurveyToTicket = (surveyPayload: SurveyResult): Tickets.Request.PostTicketBody => { + // Extract standard fields + const { title, description, ticketFormId, attachments, ...customFields } = surveyPayload; + // Map attachments from Survey.js format to Tickets format - const attachments = surveyPayload.attachments - ? (Array.isArray(surveyPayload.attachments) ? surveyPayload.attachments : [surveyPayload.attachments]) + const mappedAttachments = attachments + ? (Array.isArray(attachments) ? attachments : [attachments]) // eslint-disable-next-line @typescript-eslint/no-explicit-any .map((file: any) => { // Convert base64 string to Buffer @@ -102,11 +108,10 @@ export const mapSurveyToTicket = (surveyPayload: SurveyResult): Tickets.Request. : undefined; return { - title: surveyPayload.title as string, - description: surveyPayload.description as string, - topic: surveyPayload.topic as string, - priority: surveyPayload.priority as string | undefined, - type: surveyPayload.type as string | undefined, - attachments, + title: title as string, + description: description as string, + ticketFormId: ticketFormId as number, + attachments: mappedAttachments, + customFields, }; }; diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts index 26bcc19d2..e85d2afc7 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts @@ -137,11 +137,11 @@ export class SurveyjsService { private submitToTickets(surveyPayload: SurveyResult, authorization?: string): Observable { try { // Validate required fields before mapping - if (!surveyPayload.title || !surveyPayload.description || !surveyPayload.topic) { + if (!surveyPayload.title || !surveyPayload.description || !surveyPayload.ticketFormId) { this.logger.error( - 'Missing required fields for ticket creation: title, description, and topic are required', + 'Missing required fields for ticket creation: title, description, and ticketFormId are required', ); - throw new BadRequestException('Title, description, and topic are required to create a ticket'); + throw new BadRequestException('Title, description, and ticketFormId are required to create a ticket'); } const ticketData = mapSurveyToTicket(surveyPayload); From eab0016336853f7137915ac125f916de14a62fd7 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Fri, 23 Jan 2026 14:32:39 +0100 Subject: [PATCH 20/54] refactor(surveyjs): remove unused create ticket page mocks --- .../blocks/cms.surveyjs-block.mapper.ts | 24 +--- .../modules/cms/mappers/cms.page.mapper.ts | 16 --- .../modules/cms/mappers/cms.survey.mapper.ts | 11 +- .../mocks/pages/surveyjs-forms.page.ts | 104 +----------------- .../blocks/cms.surveyjs-block.mapper.ts | 24 +--- .../mappers/blocks/cms.ticket-list.mapper.ts | 15 --- .../modules/cms/mappers/cms.page.mapper.ts | 16 --- .../modules/cms/mappers/cms.survey.mapper.ts | 11 +- .../mocks/pages/surveyjs-forms.page.ts | 104 +----------------- 9 files changed, 10 insertions(+), 315 deletions(-) diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts index e3f81b571..7b92e2e83 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts @@ -54,41 +54,23 @@ const MOCK_SURVEYJS_BLOCK_3_DE: CMS.Model.SurveyJsBlock.SurveyJsBlock = { code: 'request-device-maintenance', }; -const MOCK_SURVEYJS_BLOCK_4_EN: CMS.Model.SurveyJsBlock.SurveyJsBlock = { - id: 'survey-4', - title: 'Create ticket', - code: 'create-ticket', -}; - -const MOCK_SURVEYJS_BLOCK_4_PL: CMS.Model.SurveyJsBlock.SurveyJsBlock = { - id: 'survey-4', - title: 'Utwórz zgłoszenie', - code: 'create-ticket', -}; - -const MOCK_SURVEYJS_BLOCK_4_DE: CMS.Model.SurveyJsBlock.SurveyJsBlock = { - id: 'survey-4', - title: 'Ticket erstellen', - code: 'create-ticket', -}; - export const mapSurveyJsBlock = (locale: string, id: string): CMS.Model.SurveyJsBlock.SurveyJsBlock => { switch (locale) { case 'en': return ( - [MOCK_SURVEYJS_BLOCK_1_EN, MOCK_SURVEYJS_BLOCK_2_EN, MOCK_SURVEYJS_BLOCK_3_EN, MOCK_SURVEYJS_BLOCK_4_EN].find( + [MOCK_SURVEYJS_BLOCK_1_EN, MOCK_SURVEYJS_BLOCK_2_EN, MOCK_SURVEYJS_BLOCK_3_EN].find( (block) => block.id === id, ) || MOCK_SURVEYJS_BLOCK_1_EN ); case 'de': return ( - [MOCK_SURVEYJS_BLOCK_1_DE, MOCK_SURVEYJS_BLOCK_2_DE, MOCK_SURVEYJS_BLOCK_3_DE, MOCK_SURVEYJS_BLOCK_4_DE].find( + [MOCK_SURVEYJS_BLOCK_1_DE, MOCK_SURVEYJS_BLOCK_2_DE, MOCK_SURVEYJS_BLOCK_3_DE].find( (block) => block.id === id, ) || MOCK_SURVEYJS_BLOCK_1_DE ); case 'pl': return ( - [MOCK_SURVEYJS_BLOCK_1_PL, MOCK_SURVEYJS_BLOCK_2_PL, MOCK_SURVEYJS_BLOCK_3_PL, MOCK_SURVEYJS_BLOCK_4_PL].find( + [MOCK_SURVEYJS_BLOCK_1_PL, MOCK_SURVEYJS_BLOCK_2_PL, MOCK_SURVEYJS_BLOCK_3_PL].find( (block) => block.id === id, ) || MOCK_SURVEYJS_BLOCK_1_PL ); diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.page.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.page.mapper.ts index 45e02d18b..267529b1e 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.page.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.page.mapper.ts @@ -60,9 +60,6 @@ import { PAGE_CONTACT_US_DE, PAGE_CONTACT_US_EN, PAGE_CONTACT_US_PL, - PAGE_CREATE_TICKET_DE, - PAGE_CREATE_TICKET_EN, - PAGE_CREATE_TICKET_PL, PAGE_REQUEST_DEVICE_MAINTENANCE_DE, PAGE_REQUEST_DEVICE_MAINTENANCE_EN, PAGE_REQUEST_DEVICE_MAINTENANCE_PL, @@ -221,13 +218,6 @@ export const mapMockPage = (slug: string, locale: string): CMS.Model.Page.Page | case '/zglos-naprawe-urzadzenia': return PAGE_REQUEST_DEVICE_MAINTENANCE_PL; - case '/create-ticket': - return PAGE_CREATE_TICKET_EN; - case '/erstelle-ticket': - return PAGE_CREATE_TICKET_DE; - case '/utworz-zgloszenie': - return PAGE_CREATE_TICKET_PL; - case '/help-and-support': return PAGE_HELP_AND_SUPPORT_EN; case '/hilfe-und-support': @@ -291,7 +281,6 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_CONTACT_US_PL, PAGE_COMPLAINT_FORM_PL, PAGE_REQUEST_DEVICE_MAINTENANCE_PL, - PAGE_CREATE_TICKET_PL, PAGE_ORDER_LIST_PL, PAGE_ORDER_DETAILS_PL, PAGE_WARRANTY_AND_REPAIR_PL, @@ -313,7 +302,6 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_CONTACT_US_DE, PAGE_COMPLAINT_FORM_DE, PAGE_REQUEST_DEVICE_MAINTENANCE_DE, - PAGE_CREATE_TICKET_DE, PAGE_ORDER_LIST_DE, PAGE_ORDER_DETAILS_DE, PAGE_WARRANTY_AND_REPAIR_DE, @@ -335,7 +323,6 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_CONTACT_US_EN, PAGE_COMPLAINT_FORM_EN, PAGE_REQUEST_DEVICE_MAINTENANCE_EN, - PAGE_CREATE_TICKET_EN, PAGE_ORDER_LIST_EN, PAGE_ORDER_DETAILS_EN, PAGE_WARRANTY_AND_REPAIR_EN, @@ -386,9 +373,6 @@ export const getAlternativePages = (id: string, slug: string, locale: string): C PAGE_REQUEST_DEVICE_MAINTENANCE_EN, PAGE_REQUEST_DEVICE_MAINTENANCE_DE, PAGE_REQUEST_DEVICE_MAINTENANCE_PL, - PAGE_CREATE_TICKET_EN, - PAGE_CREATE_TICKET_DE, - PAGE_CREATE_TICKET_PL, PAGE_ORDER_LIST_EN, PAGE_ORDER_LIST_DE, PAGE_ORDER_LIST_PL, diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts index 3fc3ecd66..5ced09fb8 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts @@ -27,16 +27,7 @@ const MOCK_SURVEY_3: CMS.Model.Survey.Survey = { postId: '17931fe3-2492-408c-8f91-8fc062606604', }; -const MOCK_SURVEY_4: CMS.Model.Survey.Survey = { - code: 'create-ticket', - surveyId: 'bf251bfa-8f6a-4e2b-a79c-554e3d45ec41', - surveyType: 'survey', - submitDestination: ['tickets'], - requiredRoles: ['selfservice_org_user'], - postId: 'f6798232-c45b-4378-9fe5-838bdc12ca88', -}; - -const MOCK_SURVEYS = [MOCK_SURVEY_1, MOCK_SURVEY_2, MOCK_SURVEY_3, MOCK_SURVEY_4]; +const MOCK_SURVEYS = [MOCK_SURVEY_1, MOCK_SURVEY_2, MOCK_SURVEY_3]; export const mapSurvey = (code: string): CMS.Model.Survey.Survey => { return MOCK_SURVEYS.find((survey) => survey.code === code) ?? MOCK_SURVEY_1; diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts index 50d4966dd..bedd0fc82 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts @@ -1,4 +1,4 @@ -import { Auth, CMS } from '@o2s/framework/modules'; +import { CMS } from '@o2s/framework/modules'; export const PAGE_CONTACT_US_EN: CMS.Model.Page.Page = { id: '9', @@ -305,105 +305,3 @@ export const PAGE_REQUEST_DEVICE_MAINTENANCE_PL: CMS.Model.Page.Page = { updatedAt: '2025-01-01', createdAt: '2025-01-01', }; - -export const PAGE_CREATE_TICKET_EN: CMS.Model.Page.Page = { - id: '13', - slug: '/create-ticket', - locale: 'en', - seo: { - noIndex: false, - noFollow: false, - title: 'Create ticket', - description: 'Create a new support ticket', - keywords: ['ticket', 'support', 'create'], - image: { - url: 'https://picsum.photos/150', - width: 150, - height: 150, - alt: 'Placeholder', - }, - }, - permissions: [Auth.Constants.Roles.ORG_USER], - hasOwnTitle: false, - template: { - __typename: 'OneColumnTemplate', - slots: { - main: [ - { - __typename: 'SurveyJsBlock', - id: 'survey-4', - }, - ], - }, - }, - updatedAt: '2025-01-01', - createdAt: '2025-01-01', -}; - -export const PAGE_CREATE_TICKET_DE: CMS.Model.Page.Page = { - id: '13', - slug: '/erstelle-ticket', - locale: 'de', - seo: { - noIndex: false, - noFollow: false, - title: 'Ticket erstellen', - description: 'Erstellen Sie ein neues Support-Ticket', - keywords: ['ticket', 'support', 'erstellen'], - image: { - url: 'https://picsum.photos/150', - width: 150, - height: 150, - alt: 'Placeholder', - }, - }, - permissions: [Auth.Constants.Roles.ORG_USER], - hasOwnTitle: false, - template: { - __typename: 'OneColumnTemplate', - slots: { - main: [ - { - __typename: 'SurveyJsBlock', - id: 'survey-4', - }, - ], - }, - }, - updatedAt: '2025-01-01', - createdAt: '2025-01-01', -}; - -export const PAGE_CREATE_TICKET_PL: CMS.Model.Page.Page = { - id: '13', - slug: '/utworz-zgloszenie', - locale: 'pl', - seo: { - noIndex: false, - noFollow: false, - title: 'Utwórz zgłoszenie', - description: 'Utwórz nowe zgłoszenie wsparcia', - keywords: ['zgłoszenie', 'wsparcie', 'utworz'], - image: { - url: 'https://picsum.photos/150', - width: 150, - height: 150, - alt: 'Placeholder', - }, - }, - permissions: [Auth.Constants.Roles.ORG_USER], - hasOwnTitle: false, - template: { - __typename: 'OneColumnTemplate', - slots: { - main: [ - { - __typename: 'SurveyJsBlock', - id: 'survey-4', - }, - ], - }, - }, - updatedAt: '2025-01-01', - createdAt: '2025-01-01', -}; diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts index e3f81b571..7b92e2e83 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.surveyjs-block.mapper.ts @@ -54,41 +54,23 @@ const MOCK_SURVEYJS_BLOCK_3_DE: CMS.Model.SurveyJsBlock.SurveyJsBlock = { code: 'request-device-maintenance', }; -const MOCK_SURVEYJS_BLOCK_4_EN: CMS.Model.SurveyJsBlock.SurveyJsBlock = { - id: 'survey-4', - title: 'Create ticket', - code: 'create-ticket', -}; - -const MOCK_SURVEYJS_BLOCK_4_PL: CMS.Model.SurveyJsBlock.SurveyJsBlock = { - id: 'survey-4', - title: 'Utwórz zgłoszenie', - code: 'create-ticket', -}; - -const MOCK_SURVEYJS_BLOCK_4_DE: CMS.Model.SurveyJsBlock.SurveyJsBlock = { - id: 'survey-4', - title: 'Ticket erstellen', - code: 'create-ticket', -}; - export const mapSurveyJsBlock = (locale: string, id: string): CMS.Model.SurveyJsBlock.SurveyJsBlock => { switch (locale) { case 'en': return ( - [MOCK_SURVEYJS_BLOCK_1_EN, MOCK_SURVEYJS_BLOCK_2_EN, MOCK_SURVEYJS_BLOCK_3_EN, MOCK_SURVEYJS_BLOCK_4_EN].find( + [MOCK_SURVEYJS_BLOCK_1_EN, MOCK_SURVEYJS_BLOCK_2_EN, MOCK_SURVEYJS_BLOCK_3_EN].find( (block) => block.id === id, ) || MOCK_SURVEYJS_BLOCK_1_EN ); case 'de': return ( - [MOCK_SURVEYJS_BLOCK_1_DE, MOCK_SURVEYJS_BLOCK_2_DE, MOCK_SURVEYJS_BLOCK_3_DE, MOCK_SURVEYJS_BLOCK_4_DE].find( + [MOCK_SURVEYJS_BLOCK_1_DE, MOCK_SURVEYJS_BLOCK_2_DE, MOCK_SURVEYJS_BLOCK_3_DE].find( (block) => block.id === id, ) || MOCK_SURVEYJS_BLOCK_1_DE ); case 'pl': return ( - [MOCK_SURVEYJS_BLOCK_1_PL, MOCK_SURVEYJS_BLOCK_2_PL, MOCK_SURVEYJS_BLOCK_3_PL, MOCK_SURVEYJS_BLOCK_4_PL].find( + [MOCK_SURVEYJS_BLOCK_1_PL, MOCK_SURVEYJS_BLOCK_2_PL, MOCK_SURVEYJS_BLOCK_3_PL].find( (block) => block.id === id, ) || MOCK_SURVEYJS_BLOCK_1_PL ); diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts index 70d846283..936c9265c 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts @@ -4,11 +4,6 @@ const MOCK_TICKET_LIST_BLOCK_EN: CMS.Model.TicketListBlock.TicketListBlock = { id: 'ticket-list-1', title: 'Your recent cases', forms: [ - { - label: 'Create ticket', - url: '/create-ticket', - icon: 'Plus', - }, { label: 'Submit complaint', url: '/submit-complaint', @@ -187,11 +182,6 @@ const MOCK_TICKET_LIST_BLOCK_DE: CMS.Model.TicketListBlock.TicketListBlock = { id: 'ticket-list-1', title: 'Ihre neuesten Fälle', forms: [ - { - label: 'Ticket erstellen', - url: '/erstelle-ticket', - icon: 'Plus', - }, { label: 'Beschwerde einreichen', url: '/submit-complaint', @@ -371,11 +361,6 @@ const MOCK_TICKET_LIST_BLOCK_PL: CMS.Model.TicketListBlock.TicketListBlock = { id: 'ticket-list-1', title: 'Twoje ostatnie zgłoszenia', forms: [ - { - label: 'Utwórz zgłoszenie', - url: '/utworz-zgloszenie', - icon: 'Plus', - }, { label: 'Zgłoś błąd', url: '/submit-complaint', diff --git a/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts index d2dca960e..cc90f912e 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/cms.page.mapper.ts @@ -55,9 +55,6 @@ import { PAGE_CONTACT_US_DE, PAGE_CONTACT_US_EN, PAGE_CONTACT_US_PL, - PAGE_CREATE_TICKET_DE, - PAGE_CREATE_TICKET_EN, - PAGE_CREATE_TICKET_PL, PAGE_REQUEST_DEVICE_MAINTENANCE_DE, PAGE_REQUEST_DEVICE_MAINTENANCE_EN, PAGE_REQUEST_DEVICE_MAINTENANCE_PL, @@ -242,13 +239,6 @@ export const mapPage = (slug: string, locale: string): CMS.Model.Page.Page | und case '/zglos-naprawe-urzadzenia': return PAGE_REQUEST_DEVICE_MAINTENANCE_PL; - case '/create-ticket': - return PAGE_CREATE_TICKET_EN; - case '/erstelle-ticket': - return PAGE_CREATE_TICKET_DE; - case '/utworz-zgloszenie': - return PAGE_CREATE_TICKET_PL; - case '/help-and-support': return PAGE_HELP_AND_SUPPORT_EN; case '/hilfe-und-support': @@ -314,7 +304,6 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_CONTACT_US_PL, PAGE_COMPLAINT_FORM_PL, PAGE_REQUEST_DEVICE_MAINTENANCE_PL, - PAGE_CREATE_TICKET_PL, PAGE_ORDER_LIST_PL, PAGE_ORDER_DETAILS_PL, PAGE_WARRANTY_AND_REPAIR_PL, @@ -338,7 +327,6 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_CONTACT_US_DE, PAGE_COMPLAINT_FORM_DE, PAGE_REQUEST_DEVICE_MAINTENANCE_DE, - PAGE_CREATE_TICKET_DE, PAGE_ORDER_LIST_DE, PAGE_ORDER_DETAILS_DE, PAGE_WARRANTY_AND_REPAIR_DE, @@ -362,7 +350,6 @@ export const getAllPages = (locale: string): CMS.Model.Page.Page[] => { PAGE_CONTACT_US_EN, PAGE_COMPLAINT_FORM_EN, PAGE_REQUEST_DEVICE_MAINTENANCE_EN, - PAGE_CREATE_TICKET_EN, PAGE_ORDER_LIST_EN, PAGE_ORDER_DETAILS_EN, PAGE_WARRANTY_AND_REPAIR_EN, @@ -416,9 +403,6 @@ export const getAlternativePages = (id: string, slug: string, locale: string): C PAGE_REQUEST_DEVICE_MAINTENANCE_EN, PAGE_REQUEST_DEVICE_MAINTENANCE_DE, PAGE_REQUEST_DEVICE_MAINTENANCE_PL, - PAGE_CREATE_TICKET_EN, - PAGE_CREATE_TICKET_DE, - PAGE_CREATE_TICKET_PL, PAGE_ORDER_LIST_EN, PAGE_ORDER_LIST_DE, PAGE_ORDER_LIST_PL, diff --git a/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts index 3fc3ecd66..5ced09fb8 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts @@ -27,16 +27,7 @@ const MOCK_SURVEY_3: CMS.Model.Survey.Survey = { postId: '17931fe3-2492-408c-8f91-8fc062606604', }; -const MOCK_SURVEY_4: CMS.Model.Survey.Survey = { - code: 'create-ticket', - surveyId: 'bf251bfa-8f6a-4e2b-a79c-554e3d45ec41', - surveyType: 'survey', - submitDestination: ['tickets'], - requiredRoles: ['selfservice_org_user'], - postId: 'f6798232-c45b-4378-9fe5-838bdc12ca88', -}; - -const MOCK_SURVEYS = [MOCK_SURVEY_1, MOCK_SURVEY_2, MOCK_SURVEY_3, MOCK_SURVEY_4]; +const MOCK_SURVEYS = [MOCK_SURVEY_1, MOCK_SURVEY_2, MOCK_SURVEY_3]; export const mapSurvey = (code: string): CMS.Model.Survey.Survey => { return MOCK_SURVEYS.find((survey) => survey.code === code) ?? MOCK_SURVEY_1; diff --git a/packages/integrations/mocked/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts b/packages/integrations/mocked/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts index 50d4966dd..bedd0fc82 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/mocks/pages/surveyjs-forms.page.ts @@ -1,4 +1,4 @@ -import { Auth, CMS } from '@o2s/framework/modules'; +import { CMS } from '@o2s/framework/modules'; export const PAGE_CONTACT_US_EN: CMS.Model.Page.Page = { id: '9', @@ -305,105 +305,3 @@ export const PAGE_REQUEST_DEVICE_MAINTENANCE_PL: CMS.Model.Page.Page = { updatedAt: '2025-01-01', createdAt: '2025-01-01', }; - -export const PAGE_CREATE_TICKET_EN: CMS.Model.Page.Page = { - id: '13', - slug: '/create-ticket', - locale: 'en', - seo: { - noIndex: false, - noFollow: false, - title: 'Create ticket', - description: 'Create a new support ticket', - keywords: ['ticket', 'support', 'create'], - image: { - url: 'https://picsum.photos/150', - width: 150, - height: 150, - alt: 'Placeholder', - }, - }, - permissions: [Auth.Constants.Roles.ORG_USER], - hasOwnTitle: false, - template: { - __typename: 'OneColumnTemplate', - slots: { - main: [ - { - __typename: 'SurveyJsBlock', - id: 'survey-4', - }, - ], - }, - }, - updatedAt: '2025-01-01', - createdAt: '2025-01-01', -}; - -export const PAGE_CREATE_TICKET_DE: CMS.Model.Page.Page = { - id: '13', - slug: '/erstelle-ticket', - locale: 'de', - seo: { - noIndex: false, - noFollow: false, - title: 'Ticket erstellen', - description: 'Erstellen Sie ein neues Support-Ticket', - keywords: ['ticket', 'support', 'erstellen'], - image: { - url: 'https://picsum.photos/150', - width: 150, - height: 150, - alt: 'Placeholder', - }, - }, - permissions: [Auth.Constants.Roles.ORG_USER], - hasOwnTitle: false, - template: { - __typename: 'OneColumnTemplate', - slots: { - main: [ - { - __typename: 'SurveyJsBlock', - id: 'survey-4', - }, - ], - }, - }, - updatedAt: '2025-01-01', - createdAt: '2025-01-01', -}; - -export const PAGE_CREATE_TICKET_PL: CMS.Model.Page.Page = { - id: '13', - slug: '/utworz-zgloszenie', - locale: 'pl', - seo: { - noIndex: false, - noFollow: false, - title: 'Utwórz zgłoszenie', - description: 'Utwórz nowe zgłoszenie wsparcia', - keywords: ['zgłoszenie', 'wsparcie', 'utworz'], - image: { - url: 'https://picsum.photos/150', - width: 150, - height: 150, - alt: 'Placeholder', - }, - }, - permissions: [Auth.Constants.Roles.ORG_USER], - hasOwnTitle: false, - template: { - __typename: 'OneColumnTemplate', - slots: { - main: [ - { - __typename: 'SurveyJsBlock', - id: 'survey-4', - }, - ], - }, - }, - updatedAt: '2025-01-01', - createdAt: '2025-01-01', -}; From f5464901c8d3cd65407114112d44e57b63c3287a Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Sat, 24 Jan 2026 16:19:41 +0100 Subject: [PATCH 21/54] feat(zendesk): enhance field mapping --- .../modules/tickets/zendesk-field.mapper.ts | 79 +++++++++++++++++-- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts index 85c588293..b2470e249 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts @@ -18,12 +18,64 @@ export interface ZendeskCustomField { * 3. Include the field in Survey.js form with matching name */ export class ZendeskFieldMapper { - // Static mapping of Survey.js field names to Zendesk custom field IDs from environment variables - private static readonly fieldMap: Record = { - topic: process.env.ZENDESK_TOPIC_FIELD_ID ? Number(process.env.ZENDESK_TOPIC_FIELD_ID) : undefined, - // Add more field mappings here as needed - // example: priority: process.env.ZENDESK_PRIORITY_FIELD_ID ? Number(process.env.ZENDESK_PRIORITY_FIELD_ID) : undefined, - }; + /** + * Gets the field map dynamically from environment variables. + * Using a getter ensures environment variables are read at runtime, not during module initialization. + */ + private static get fieldMap(): Record { + return { + machineName: process.env.ZENDESK_DEVICE_NAME_FIELD_ID + ? Number(process.env.ZENDESK_DEVICE_NAME_FIELD_ID) + : undefined, + serialNumber: process.env.ZENDESK_SERIAL_NUMBER_FIELD_ID + ? Number(process.env.ZENDESK_SERIAL_NUMBER_FIELD_ID) + : undefined, + maintenanceType: process.env.ZENDESK_MAINTENANCE_TYPE_FIELD_ID + ? Number(process.env.ZENDESK_MAINTENANCE_TYPE_FIELD_ID) + : undefined, + preferredDate: process.env.ZENDESK_MAINTENANCE_PREFERRED_DATE_FIELD_ID + ? Number(process.env.ZENDESK_MAINTENANCE_PREFERRED_DATE_FIELD_ID) + : undefined, + additionalNotes: process.env.ZENDESK_ADDITIONAL_NOTES_FIELD_ID + ? Number(process.env.ZENDESK_ADDITIONAL_NOTES_FIELD_ID) + : undefined, + contactInformation: process.env.ZENDESK_CONTACT_FIELD_ID + ? Number(process.env.ZENDESK_CONTACT_FIELD_ID) + : undefined, + + issueDate: process.env.ZENDESK_ISSUE_DATE_FIELD_ID + ? Number(process.env.ZENDESK_ISSUE_DATE_FIELD_ID) + : undefined, + organizationName: process.env.ZENDESK_COMPANY_NAME_FIELD_ID + ? Number(process.env.ZENDESK_COMPANY_NAME_FIELD_ID) + : undefined, + firstName: process.env.ZENDESK_FIRST_NAME_FIELD_ID + ? Number(process.env.ZENDESK_FIRST_NAME_FIELD_ID) + : undefined, + lastName: process.env.ZENDESK_LAST_NAME_FIELD_ID + ? Number(process.env.ZENDESK_LAST_NAME_FIELD_ID) + : undefined, + email: process.env.ZENDESK_EMAIL_FIELD_ID ? Number(process.env.ZENDESK_EMAIL_FIELD_ID) : undefined, + phone: process.env.ZENDESK_PHONE_FIELD_ID ? Number(process.env.ZENDESK_PHONE_FIELD_ID) : undefined, + invoiceNumber: process.env.ZENDESK_INVOICE_NUMBER_FIELD_ID + ? Number(process.env.ZENDESK_INVOICE_NUMBER_FIELD_ID) + : undefined, + + address: process.env.ZENDESK_ADDRESS_FIELD_ID ? Number(process.env.ZENDESK_ADDRESS_FIELD_ID) : undefined, + topic: process.env.ZENDESK_TOPIC_FIELD_ID ? Number(process.env.ZENDESK_TOPIC_FIELD_ID) : undefined, + inquiryType: process.env.ZENDESK_INQUIRY_TYPE_FIELD_ID + ? Number(process.env.ZENDESK_INQUIRY_TYPE_FIELD_ID) + : undefined, + productCategory: process.env.ZENDESK_PRODUCT_CATEGORY_FIELD_ID + ? Number(process.env.ZENDESK_PRODUCT_CATEGORY_FIELD_ID) + : undefined, + preferredContactMethod: process.env.ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID + ? Number(process.env.ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID) + : undefined, + // Add more field mappings here as needed + // example: priority: process.env.ZENDESK_PRIORITY_FIELD_ID ? Number(process.env.ZENDESK_PRIORITY_FIELD_ID) : undefined, + }; + } /** * Converts a record of field values to Zendesk custom fields format. @@ -54,6 +106,7 @@ export class ZendeskFieldMapper { // Validate and convert value to supported types const validatedValue = this.validateAndConvertValue(fieldValue); + if (validatedValue !== null) { customFields.push({ id: fieldId, @@ -68,13 +121,23 @@ export class ZendeskFieldMapper { /** * Validates and converts field value to Zendesk-supported types. * Zendesk API accepts: string, number, boolean + * Date fields must be in YYYY-MM-DD format * * @param value - The value to validate and convert * @returns Validated value or null if invalid */ private static validateAndConvertValue(value: unknown): string | number | boolean | null { - // Handle primitives directly - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + // Handle primitives + if (typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (typeof value === 'string') { + // Convert date strings to YYYY-MM-DD format for date fields + if (/\d{4}-\d{2}-\d{2}/.test(value)) { + const date = new Date(value); + return isNaN(date.getTime()) ? value : (date.toISOString().split('T')[0] ?? value); + } return value; } From 8464a4c6e714cb1ac7fde12668e0817b4c3fcd38 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Mon, 26 Jan 2026 10:22:55 +0100 Subject: [PATCH 22/54] feat(zendesk): add additional Zendesk fields and update survey submission destinations --- apps/api-harmonization/turbo.json | 50 +++++++++++++++++- .../modules/cms/mappers/cms.survey.mapper.ts | 6 +-- .../modules/cms/mappers/cms.survey.mapper.ts | 6 +-- .../modules/tickets/zendesk-field.mapper.ts | 12 ++++- .../modules/tickets/zendesk-ticket.service.ts | 2 +- packages/integrations/zendesk/turbo.json | 52 ++++++++++++++++++- .../src/api-harmonization/surveyjs.service.ts | 2 +- 7 files changed, 116 insertions(+), 14 deletions(-) diff --git a/apps/api-harmonization/turbo.json b/apps/api-harmonization/turbo.json index a9c951037..e91b58d9b 100644 --- a/apps/api-harmonization/turbo.json +++ b/apps/api-harmonization/turbo.json @@ -41,7 +41,30 @@ "CF_PREVIEW_TOKEN", "CF_SPACE_ID", "CF_ENV", - "CF_MANAGEMENT_TOKEN" + "CF_MANAGEMENT_TOKEN", + "ZENDESK_API_URL", + "ZENDESK_API_TOKEN", + "ZENDESK_TOPIC_FIELD_ID", + "ZENDESK_DEVICE_NAME_FIELD_ID", + "ZENDESK_SERIAL_NUMBER_FIELD_ID", + "ZENDESK_MAINTENANCE_TYPE_FIELD_ID", + "ZENDESK_MAINTENANCE_PREFERRED_DATE_FIELD_ID", + "ZENDESK_ADDITIONAL_NOTES_FIELD_ID", + "ZENDESK_CONTACT_FIELD_ID", + "ZENDESK_ISSUE_DATE_FIELD_ID", + "ZENDESK_COMPANY_NAME_FIELD_ID", + "ZENDESK_FIRST_NAME_FIELD_ID", + "ZENDESK_LAST_NAME_FIELD_ID", + "ZENDESK_EMAIL_FIELD_ID", + "ZENDESK_PHONE_FIELD_ID", + "ZENDESK_INVOICE_NUMBER_FIELD_ID", + "ZENDESK_ADDRESS_FIELD_ID", + "ZENDESK_INQUIRY_TYPE_FIELD_ID", + "ZENDESK_PRODUCT_CATEGORY_FIELD_ID", + "ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID", + "ZENDESK_TERMS_ACCEPTANCE_FIELD_ID", + "ZENDESK_NEWSLETTER_CONSENT_FIELD_ID", + "ZENDESK_MARKETING_CONSENT_FIELD_ID" ] }, "start": { @@ -77,7 +100,30 @@ "CF_PREVIEW_TOKEN", "CF_SPACE_ID", "CF_ENV", - "CF_MANAGEMENT_TOKEN" + "CF_MANAGEMENT_TOKEN", + "ZENDESK_API_URL", + "ZENDESK_API_TOKEN", + "ZENDESK_TOPIC_FIELD_ID", + "ZENDESK_DEVICE_NAME_FIELD_ID", + "ZENDESK_SERIAL_NUMBER_FIELD_ID", + "ZENDESK_MAINTENANCE_TYPE_FIELD_ID", + "ZENDESK_MAINTENANCE_PREFERRED_DATE_FIELD_ID", + "ZENDESK_ADDITIONAL_NOTES_FIELD_ID", + "ZENDESK_CONTACT_FIELD_ID", + "ZENDESK_ISSUE_DATE_FIELD_ID", + "ZENDESK_COMPANY_NAME_FIELD_ID", + "ZENDESK_FIRST_NAME_FIELD_ID", + "ZENDESK_LAST_NAME_FIELD_ID", + "ZENDESK_EMAIL_FIELD_ID", + "ZENDESK_PHONE_FIELD_ID", + "ZENDESK_INVOICE_NUMBER_FIELD_ID", + "ZENDESK_ADDRESS_FIELD_ID", + "ZENDESK_INQUIRY_TYPE_FIELD_ID", + "ZENDESK_PRODUCT_CATEGORY_FIELD_ID", + "ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID", + "ZENDESK_TERMS_ACCEPTANCE_FIELD_ID", + "ZENDESK_NEWSLETTER_CONSENT_FIELD_ID", + "ZENDESK_MARKETING_CONSENT_FIELD_ID" ] } } diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts index 5ced09fb8..a70dfb9b8 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts @@ -4,7 +4,7 @@ const MOCK_SURVEY_1: CMS.Model.Survey.Survey = { code: 'contact-us', surveyId: '72c90a02-6bfe-4e83-ba48-01f11752c234', surveyType: 'survey', - submitDestination: ['surveyjs'], + submitDestination: ['tickets'], requiredRoles: [], postId: 'a91349b1-0c4c-4b7a-b712-91f04a1e6e99', }; @@ -13,7 +13,7 @@ const MOCK_SURVEY_2: CMS.Model.Survey.Survey = { code: 'complaint-form', surveyId: '3897de9c-279b-4c50-b359-09f5c73a3c49', surveyType: 'survey', - submitDestination: ['surveyjs'], + submitDestination: ['tickets'], requiredRoles: ['selfservice_org_user'], postId: 'e0f1b26b-a434-44ab-9608-c49dcd0658ec', }; @@ -22,7 +22,7 @@ const MOCK_SURVEY_3: CMS.Model.Survey.Survey = { code: 'request-device-maintenance', surveyId: 'd93ccc83-4aff-418b-9e9b-c9c3447908cf', surveyType: 'survey', - submitDestination: ['surveyjs'], + submitDestination: ['tickets'], requiredRoles: ['selfservice_org_user'], postId: '17931fe3-2492-408c-8f91-8fc062606604', }; diff --git a/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts index 5ced09fb8..a70dfb9b8 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts @@ -4,7 +4,7 @@ const MOCK_SURVEY_1: CMS.Model.Survey.Survey = { code: 'contact-us', surveyId: '72c90a02-6bfe-4e83-ba48-01f11752c234', surveyType: 'survey', - submitDestination: ['surveyjs'], + submitDestination: ['tickets'], requiredRoles: [], postId: 'a91349b1-0c4c-4b7a-b712-91f04a1e6e99', }; @@ -13,7 +13,7 @@ const MOCK_SURVEY_2: CMS.Model.Survey.Survey = { code: 'complaint-form', surveyId: '3897de9c-279b-4c50-b359-09f5c73a3c49', surveyType: 'survey', - submitDestination: ['surveyjs'], + submitDestination: ['tickets'], requiredRoles: ['selfservice_org_user'], postId: 'e0f1b26b-a434-44ab-9608-c49dcd0658ec', }; @@ -22,7 +22,7 @@ const MOCK_SURVEY_3: CMS.Model.Survey.Survey = { code: 'request-device-maintenance', surveyId: 'd93ccc83-4aff-418b-9e9b-c9c3447908cf', surveyType: 'survey', - submitDestination: ['surveyjs'], + submitDestination: ['tickets'], requiredRoles: ['selfservice_org_user'], postId: '17931fe3-2492-408c-8f91-8fc062606604', }; diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts index b2470e249..c4daf36cc 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts @@ -72,8 +72,16 @@ export class ZendeskFieldMapper { preferredContactMethod: process.env.ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID ? Number(process.env.ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID) : undefined, - // Add more field mappings here as needed - // example: priority: process.env.ZENDESK_PRIORITY_FIELD_ID ? Number(process.env.ZENDESK_PRIORITY_FIELD_ID) : undefined, + termsAcceptance: process.env.ZENDESK_TERMS_ACCEPTANCE_FIELD_ID + ? Number(process.env.ZENDESK_TERMS_ACCEPTANCE_FIELD_ID) + : undefined, + newsletterConsent: process.env.ZENDESK_NEWSLETTER_CONSENT_FIELD_ID + ? Number(process.env.ZENDESK_NEWSLETTER_CONSENT_FIELD_ID) + : undefined, + marketingConsent: process.env.ZENDESK_MARKETING_CONSENT_FIELD_ID + ? Number(process.env.ZENDESK_MARKETING_CONSENT_FIELD_ID) + : undefined, + // Add more custom fields mappings here as needed }; } diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index beb570d49..c3c7ca767 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -171,7 +171,7 @@ export class ZendeskTicketService extends Tickets.Service { createTicket(data: Tickets.Request.PostTicketBody, authorization?: string): Observable { // Validate input data - if (!data.title || !data.description || !data.ticketFormId) { + if (!data.description || !data.ticketFormId) { return throwError(() => new BadRequestException('Title, description and ticketFormId are required')); } diff --git a/packages/integrations/zendesk/turbo.json b/packages/integrations/zendesk/turbo.json index 7fa953d58..008839217 100644 --- a/packages/integrations/zendesk/turbo.json +++ b/packages/integrations/zendesk/turbo.json @@ -3,11 +3,59 @@ "tasks": { "dev": { "dependsOn": ["@o2s/utils.logger#build", "@o2s/framework#build"], - "env": ["ZENDESK_API_URL", "ZENDESK_API_TOKEN", "ZENDESK_TOPIC_FIELD_ID"] + "env": [ + "ZENDESK_API_URL", + "ZENDESK_API_TOKEN", + "ZENDESK_TOPIC_FIELD_ID", + "ZENDESK_DEVICE_NAME_FIELD_ID", + "ZENDESK_SERIAL_NUMBER_FIELD_ID", + "ZENDESK_MAINTENANCE_TYPE_FIELD_ID", + "ZENDESK_MAINTENANCE_PREFERRED_DATE_FIELD_ID", + "ZENDESK_ADDITIONAL_NOTES_FIELD_ID", + "ZENDESK_CONTACT_FIELD_ID", + "ZENDESK_ISSUE_DATE_FIELD_ID", + "ZENDESK_COMPANY_NAME_FIELD_ID", + "ZENDESK_FIRST_NAME_FIELD_ID", + "ZENDESK_LAST_NAME_FIELD_ID", + "ZENDESK_EMAIL_FIELD_ID", + "ZENDESK_PHONE_FIELD_ID", + "ZENDESK_INVOICE_NUMBER_FIELD_ID", + "ZENDESK_ADDRESS_FIELD_ID", + "ZENDESK_INQUIRY_TYPE_FIELD_ID", + "ZENDESK_PRODUCT_CATEGORY_FIELD_ID", + "ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID", + "ZENDESK_TERMS_ACCEPTANCE_FIELD_ID", + "ZENDESK_NEWSLETTER_CONSENT_FIELD_ID", + "ZENDESK_MARKETING_CONSENT_FIELD_ID" + ] }, "build": { "dependsOn": ["@o2s/utils.logger#build", "@o2s/framework#build"], - "env": ["ZENDESK_API_URL", "ZENDESK_API_TOKEN", "ZENDESK_TOPIC_FIELD_ID"] + "env": [ + "ZENDESK_API_URL", + "ZENDESK_API_TOKEN", + "ZENDESK_TOPIC_FIELD_ID", + "ZENDESK_DEVICE_NAME_FIELD_ID", + "ZENDESK_SERIAL_NUMBER_FIELD_ID", + "ZENDESK_MAINTENANCE_TYPE_FIELD_ID", + "ZENDESK_MAINTENANCE_PREFERRED_DATE_FIELD_ID", + "ZENDESK_ADDITIONAL_NOTES_FIELD_ID", + "ZENDESK_CONTACT_FIELD_ID", + "ZENDESK_ISSUE_DATE_FIELD_ID", + "ZENDESK_COMPANY_NAME_FIELD_ID", + "ZENDESK_FIRST_NAME_FIELD_ID", + "ZENDESK_LAST_NAME_FIELD_ID", + "ZENDESK_EMAIL_FIELD_ID", + "ZENDESK_PHONE_FIELD_ID", + "ZENDESK_INVOICE_NUMBER_FIELD_ID", + "ZENDESK_ADDRESS_FIELD_ID", + "ZENDESK_INQUIRY_TYPE_FIELD_ID", + "ZENDESK_PRODUCT_CATEGORY_FIELD_ID", + "ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID", + "ZENDESK_TERMS_ACCEPTANCE_FIELD_ID", + "ZENDESK_NEWSLETTER_CONSENT_FIELD_ID", + "ZENDESK_MARKETING_CONSENT_FIELD_ID" + ] } } } diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts index e85d2afc7..adb42e78d 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts @@ -137,7 +137,7 @@ export class SurveyjsService { private submitToTickets(surveyPayload: SurveyResult, authorization?: string): Observable { try { // Validate required fields before mapping - if (!surveyPayload.title || !surveyPayload.description || !surveyPayload.ticketFormId) { + if (!surveyPayload.description || !surveyPayload.ticketFormId) { this.logger.error( 'Missing required fields for ticket creation: title, description, and ticketFormId are required', ); From 7dd33638c84ca937c5303bc72243088d968a0297 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Tue, 27 Jan 2026 10:08:43 +0100 Subject: [PATCH 23/54] docs(zendesk): updated custom fields mapping documentation --- .../integrations/tickets/zendesk/features.md | 66 +++++++++++++++++-- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/apps/docs/docs/integrations/tickets/zendesk/features.md b/apps/docs/docs/integrations/tickets/zendesk/features.md index 8a955b278..ffec70d53 100644 --- a/apps/docs/docs/integrations/tickets/zendesk/features.md +++ b/apps/docs/docs/integrations/tickets/zendesk/features.md @@ -110,7 +110,7 @@ The integration maps Zendesk ticket data to the standard ticket model with the f | status | status | Mapped according to status mapping | | subject | properties | Added as property with id 'subject' | | description | properties | Added as property with id 'description' | -| custom_fields | properties | Each field added with id pattern 'custom_field_X' where X is the Zendesk custom field ID | +| custom_fields | properties | Mapped using `ZendeskFieldMapper` to readable names (see Custom Fields section below) | | comments | comments | Mapped with author information | | comments.attachments | attachments | Extracted from comments | @@ -123,13 +123,65 @@ The integration maps Zendesk ticket data to the standard ticket model with the f | new, open | OPEN | Default status | | (other) | OPEN | Fallback for unknown statuses | -### Topic Handling -The integration can map a custom field to the ticket topic: - -1. Set the `ZENDESK_TOPIC_FIELD_ID` environment variable to the ID of the custom field -2. The value of this field will be used as the ticket topic (converted to uppercase) -3. If not set or field not found, the default topic is "GENERAL" +### Custom Fields Mapping + +Custom fields from Zendesk are mapped to readable names using the `ZendeskFieldMapper`. This provides a consistent, maintainable way to work with custom fields throughout the application. + +**How it works:** + +1. **Field Mapping Configuration**: Custom fields are defined in `ZendeskFieldMapper` with readable names and environment variable IDs: + ```typescript + // In zendesk-field.mapper.ts + fieldMap = { + machineName: process.env.ZENDESK_DEVICE_NAME_FIELD_ID, + serialNumber: process.env.ZENDESK_SERIAL_NUMBER_FIELD_ID, + maintenanceType: process.env.ZENDESK_MAINTENANCE_TYPE_FIELD_ID, + // ... more fields + } + ``` + +2. **Reading Tickets**: When a ticket is retrieved from Zendesk, custom fields are automatically mapped to their readable names: + - Custom field with ID `123456` → `machineName` (if configured in `ZendeskFieldMapper`) + - Only fields with mappings in `ZendeskFieldMapper` are included + - Fields without mappings are skipped + +3. **CMS Integration**: To display custom fields in ticket details, add mappings in CMS: + ```typescript + // In CMS mapper (e.g., mocked, contentful, strapi) + properties: { + // ... standard fields + machineName: 'Machine Name', + serialNumber: 'Serial Number', + } + ``` + +**Adding a new custom field:** + +To add support for a new custom field: + +1. **Add environment variable**: + ``` + ZENDESK_NEW_FIELD_ID=789012 + ``` + +2. **Add to ZendeskFieldMapper** in `zendesk-field.mapper.ts`: + ```typescript + fieldMap = { + // ... existing fields + newField: process.env.ZENDESK_NEW_FIELD_ID + ? Number(process.env.ZENDESK_NEW_FIELD_ID) + : undefined, + } + ``` + +3. **Add CMS mappings** for all supported locales (in mocked, contentful, strapi mappers): + ```typescript + properties: { + // ... existing fields + newField: 'New Field Label', // Add for each locale + } + ``` ### Default Values and Fallbacks From b9a0a84e15c2d0fe0c738ba631cb51bf2bb85ae7 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Tue, 27 Jan 2026 17:20:10 +0100 Subject: [PATCH 24/54] feat(zendesk): add Zendesk form IDs to turbo config files --- apps/api-harmonization/turbo.json | 6 ++++++ packages/integrations/zendesk/turbo.json | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/apps/api-harmonization/turbo.json b/apps/api-harmonization/turbo.json index e91b58d9b..15d842cd3 100644 --- a/apps/api-harmonization/turbo.json +++ b/apps/api-harmonization/turbo.json @@ -44,6 +44,9 @@ "CF_MANAGEMENT_TOKEN", "ZENDESK_API_URL", "ZENDESK_API_TOKEN", + "ZENDESK_CONTACT_FORM_ID", + "ZENDESK_COMPLAINT_FORM_ID", + "ZENDESK_REQUEST_DEVICE_MAINTENANCE_FORM_ID", "ZENDESK_TOPIC_FIELD_ID", "ZENDESK_DEVICE_NAME_FIELD_ID", "ZENDESK_SERIAL_NUMBER_FIELD_ID", @@ -103,6 +106,9 @@ "CF_MANAGEMENT_TOKEN", "ZENDESK_API_URL", "ZENDESK_API_TOKEN", + "ZENDESK_CONTACT_FORM_ID", + "ZENDESK_COMPLAINT_FORM_ID", + "ZENDESK_REQUEST_DEVICE_MAINTENANCE_FORM_ID", "ZENDESK_TOPIC_FIELD_ID", "ZENDESK_DEVICE_NAME_FIELD_ID", "ZENDESK_SERIAL_NUMBER_FIELD_ID", diff --git a/packages/integrations/zendesk/turbo.json b/packages/integrations/zendesk/turbo.json index 008839217..c14a13c39 100644 --- a/packages/integrations/zendesk/turbo.json +++ b/packages/integrations/zendesk/turbo.json @@ -6,6 +6,9 @@ "env": [ "ZENDESK_API_URL", "ZENDESK_API_TOKEN", + "ZENDESK_CONTACT_FORM_ID", + "ZENDESK_COMPLAINT_FORM_ID", + "ZENDESK_REQUEST_DEVICE_MAINTENANCE_FORM_ID", "ZENDESK_TOPIC_FIELD_ID", "ZENDESK_DEVICE_NAME_FIELD_ID", "ZENDESK_SERIAL_NUMBER_FIELD_ID", @@ -34,6 +37,9 @@ "env": [ "ZENDESK_API_URL", "ZENDESK_API_TOKEN", + "ZENDESK_CONTACT_FORM_ID", + "ZENDESK_COMPLAINT_FORM_ID", + "ZENDESK_REQUEST_DEVICE_MAINTENANCE_FORM_ID", "ZENDESK_TOPIC_FIELD_ID", "ZENDESK_DEVICE_NAME_FIELD_ID", "ZENDESK_SERIAL_NUMBER_FIELD_ID", From 2e75c7a6eaab69589edf269c1c2d29cd21fa7234 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Tue, 27 Jan 2026 21:06:08 +0100 Subject: [PATCH 25/54] docs(zendesk): update ticket documentation --- .../integrations/tickets/zendesk/features.md | 47 +++++++++++++++++-- .../core-model-tickets.md | 1 - 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/apps/docs/docs/integrations/tickets/zendesk/features.md b/apps/docs/docs/integrations/tickets/zendesk/features.md index ffec70d53..e31ee408d 100644 --- a/apps/docs/docs/integrations/tickets/zendesk/features.md +++ b/apps/docs/docs/integrations/tickets/zendesk/features.md @@ -106,7 +106,6 @@ The integration maps Zendesk ticket data to the standard ticket model with the f | id | id | Converted to string | | created_at | createdAt | ISO date string | | updated_at | updatedAt | ISO date string | -| priority | type | Converted to uppercase (default: NORMAL) | | status | status | Mapped according to status mapping | | subject | properties | Added as property with id 'subject' | | description | properties | Added as property with id 'description' | @@ -183,13 +182,54 @@ To add support for a new custom field: } ``` +### Topic Field Mapping + +The `topic` field is automatically set during ticket creation based on the `ticketFormId` provided in the ticket data. This ensures consistent categorization across different form types. + +**How it works:** + +When creating a ticket via the Zendesk integration: + +1. The system compares the `ticketFormId` with configured environment variables: + - `ZENDESK_CONTACT_FORM_ID` → topic value: `CONTACT_US` + - `ZENDESK_COMPLAINT_FORM_ID` → topic value: `COMPLAINT` + - `ZENDESK_REQUEST_DEVICE_MAINTENANCE_FORM_ID` → topic value: `REQUEST_DEVICE_MAINTENANCE` + +2. The matching topic value is automatically added to the ticket's custom fields + +3. The topic is then stored in Zendesk using the `ZENDESK_TOPIC_FIELD_ID` custom field + +**Example:** + +```typescript +// Survey.js sends ticketFormId +{ + ticketFormId: 33406700504221, // Matches ZENDESK_CONTACT_FORM_ID + customFields: { ... } +} + +// Service automatically adds topic +{ + ticketFormId: 33406700504221, + customFields: { + topic: 'CONTACT_US', // Automatically set + ... + } +} +``` + +**Important**: If the `ticketFormId` doesn't match any configured form ID, the ticket creation will fail with a `BadRequestException`. This ensures that all tickets are properly categorized. + +**Supported topic values:** +- `CONTACT_US` - General contact inquiries +- `COMPLAINT` - Customer complaints +- `REQUEST_DEVICE_MAINTENANCE` - Device maintenance requests + ### Default Values and Fallbacks The integration handles missing data with the following defaults: - **Status**: `OPEN` (if status is unknown or missing) -- **Topic**: `GENERAL` (if topic field is not configured or not found) -- **Type**: `NORMAL` (if priority is missing, converted from Zendesk priority) - **Empty strings**: Used for missing string values (subject, description, etc.) - **Comments**: `undefined` if no comments exist (not an empty array) - **Attachments**: `undefined` if no attachments exist (not an empty array) @@ -223,7 +263,6 @@ The integration converts framework filter parameters to Zendesk Search API queri | Framework Parameter | Zendesk Search Query | Notes | |---------------------|----------------------|------------------------------------------| | status | `status:{value}` | Converted to lowercase | -| type | `priority:{value}` | Note: maps to priority, not type | | topic | `tag:{value}` | Maps to Zendesk tags | | dateFrom | `created>={iso_date}` | Converted to ISO format | | dateTo | `created<={iso_date}` | Converted to ISO format | diff --git a/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md b/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md index 45c3258ed..6e343c40c 100644 --- a/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md +++ b/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md @@ -72,7 +72,6 @@ getTicketList( | offset | number | Number of items to skip | | limit | number | Maximum number of items to return | | topic | string | Filter by ticket topic | -| type | string | Filter by ticket type | | status | TicketStatus | Filter by ticket status | | dateFrom | Date | Filter by creation date (from) | | dateTo | Date | Filter by creation date (to) | From 23890872bf879b446429bd04753e403b8af50163 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Tue, 27 Jan 2026 21:08:16 +0100 Subject: [PATCH 26/54] refactor(ticket-details): make ticket type optional and improve mapping logic --- .../src/api-harmonization/ticket-details.mapper.ts | 14 +++++++++----- .../src/api-harmonization/ticket-details.model.ts | 2 +- .../src/frontend/TicketDetails.client.tsx | 10 +--------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/blocks/ticket-details/src/api-harmonization/ticket-details.mapper.ts b/packages/blocks/ticket-details/src/api-harmonization/ticket-details.mapper.ts index 208d307f9..1e43529bc 100644 --- a/packages/blocks/ticket-details/src/api-harmonization/ticket-details.mapper.ts +++ b/packages/blocks/ticket-details/src/api-harmonization/ticket-details.mapper.ts @@ -34,11 +34,15 @@ export const mapTicket = ( title: cms.properties?.topic as string, value: ticket.topic, }, - type: { - label: cms.fieldMapping.type?.[ticket.type] || ticket.type, - title: cms.properties?.type as string, - value: ticket.type, - }, + ...(ticket.type && cms.properties?.type && cms.fieldMapping.type + ? { + type: { + label: cms.fieldMapping.type[ticket.type] || ticket.type, + title: cms.properties.type as string, + value: ticket.type, + }, + } + : {}), status: { label: cms.fieldMapping.status?.[ticket.status] || ticket.status, title: cms.properties?.status as string, diff --git a/packages/blocks/ticket-details/src/api-harmonization/ticket-details.model.ts b/packages/blocks/ticket-details/src/api-harmonization/ticket-details.model.ts index 0e713034d..e9bc4ade9 100644 --- a/packages/blocks/ticket-details/src/api-harmonization/ticket-details.model.ts +++ b/packages/blocks/ticket-details/src/api-harmonization/ticket-details.model.ts @@ -18,7 +18,7 @@ export class Ticket { title: string; label: string; }; - type!: { + type?: { value: Tickets.Model.Ticket['type']; title: string; label: string; diff --git a/packages/blocks/ticket-details/src/frontend/TicketDetails.client.tsx b/packages/blocks/ticket-details/src/frontend/TicketDetails.client.tsx index 8c3b5dfca..1fd9d1509 100644 --- a/packages/blocks/ticket-details/src/frontend/TicketDetails.client.tsx +++ b/packages/blocks/ticket-details/src/frontend/TicketDetails.client.tsx @@ -27,7 +27,7 @@ export const TicketDetailsPure: React.FC> = ({ return (
-
+

{ticket.topic.label}

@@ -50,14 +50,6 @@ export const TicketDetailsPure: React.FC> = ({
    - - - - Date: Tue, 27 Jan 2026 21:10:27 +0100 Subject: [PATCH 27/54] refactor(tickets): make ticket type optional in the Ticket model --- packages/framework/src/modules/tickets/tickets.model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/framework/src/modules/tickets/tickets.model.ts b/packages/framework/src/modules/tickets/tickets.model.ts index 74daa5536..95fb9556b 100644 --- a/packages/framework/src/modules/tickets/tickets.model.ts +++ b/packages/framework/src/modules/tickets/tickets.model.ts @@ -5,7 +5,7 @@ export class Ticket { createdAt!: string; updatedAt!: string; topic!: string; - type!: string; + type?: string; status!: TicketStatus; properties!: TicketProperty[]; attachments?: TicketAttachment[]; From aa7d8de32dd0278027770e1ad93cb4f92c45bb2b Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Tue, 27 Jan 2026 21:15:46 +0100 Subject: [PATCH 28/54] refactor(ticket-details): update ticket properties and field mappings --- .../blocks/cms.ticket-details.mapper.ts | 131 +++++++------ .../blocks/cms.ticket-details.mapper.ts | 131 +++++++------ .../mappers/blocks/cms.ticket-list.mapper.ts | 184 +++--------------- .../src/modules/tickets/tickets.mocks.ts | 87 +++------ 4 files changed, 206 insertions(+), 327 deletions(-) diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts index 2ea369836..cfdca395e 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts @@ -7,27 +7,34 @@ const MOCK_TICKET_DETAILS_BLOCK_EN: CMS.Model.TicketDetailsBlock.TicketDetailsBl attachmentsTitle: 'Attachments', properties: { id: 'Case ID', - topic: 'Topic', - type: 'Case type', status: 'Status', - description: 'Additional notes', - address: 'Service address', - contact: 'Form of contact', + // Custom fields from Zendesk (using readable names from ZendeskFieldMapper) + machineName: 'Machine Name', + serialNumber: 'Serial Number', + maintenanceType: 'Maintenance Type', + preferredDate: 'Preferred Date', + additionalNotes: 'Additional Notes', + contactInformation: 'Contact Information', + issueDate: 'Issue Date', + organizationName: 'Organization Name', + firstName: 'First Name', + lastName: 'Last Name', + email: 'Email', + phone: 'Phone', + invoiceNumber: 'Invoice Number', + address: 'Address', + inquiryType: 'Inquiry Type', + productCategory: 'Product Category', + preferredContactMethod: 'Preferred Contact Method', + termsAcceptance: 'Terms Acceptance', + newsletterConsent: 'Newsletter Consent', + marketingConsent: 'Marketing Consent', }, fieldMapping: { topic: { - TOOL_REPAIR: 'Tool Repair', - FLEET_EXCHANGE: 'Fleet Exchange', - CALIBRATION: 'Calibration', - THEFT_REPORT: 'Theft Report', - SOFTWARE_SUPPORT: 'Software Support', - RENTAL_REQUEST: 'Rental Request', - TRAINING_REQUEST: 'Training Request', - }, - type: { - URGENT: 'Urgent', - STANDARD: 'Standard', - LOW_PRIORITY: 'Low Priority', + CONTACT_US: 'Contact Form', + REQUEST_DEVICE_MAINTENANCE: 'Device Maintenance', + COMPLAINT: 'Complaint', }, status: { OPEN: 'Under consideration', @@ -45,33 +52,39 @@ const MOCK_TICKET_DETAILS_BLOCK_EN: CMS.Model.TicketDetailsBlock.TicketDetailsBl const MOCK_TICKET_DETAILS_BLOCK_PL: CMS.Model.TicketDetailsBlock.TicketDetailsBlock = { id: 'ticket-list-1', - title: 'Szczegóły sprawy', + title: 'Szczegóły zgłoszenia', commentsTitle: 'Komentarze', attachmentsTitle: 'Załączniki', properties: { - id: 'ID sprawy', - topic: 'Temat', - type: 'Typ sprawy', + id: 'ID zgłoszenia', status: 'Status', - description: 'Dodatkowe notatki', - address: 'Adres serwisowy', - contact: 'Forma kontaktu', + // Custom fields from Zendesk (using readable names from ZendeskFieldMapper) + machineName: 'Nazwa urządzenia', + serialNumber: 'Numer seryjny', + maintenanceType: 'Typ konserwacji', + preferredDate: 'Preferowana data', + additionalNotes: 'Dodatkowe uwagi', + contactInformation: 'Informacje kontaktowe', + issueDate: 'Data wystąpienia problemu', + organizationName: 'Nazwa organizacji', + firstName: 'Imię', + lastName: 'Nazwisko', + email: 'Email', + phone: 'Telefon', + invoiceNumber: 'Numer faktury', + address: 'Adres', + inquiryType: 'Typ zapytania', + productCategory: 'Kategoria produktu', + preferredContactMethod: 'Preferowana forma kontaktu', + termsAcceptance: 'Akceptacja regulaminu', + newsletterConsent: 'Zgoda na newsletter', + marketingConsent: 'Zgoda marketingowa', }, fieldMapping: { topic: { - ALL: 'Wszystko', - TOOL_REPAIR: 'Naprawa narzędzia', - FLEET_EXCHANGE: 'Wymiana floty', - CALIBRATION: 'Kalibracja', - THEFT_REPORT: 'Zgłoszenie kradzieży', - SOFTWARE_SUPPORT: 'Wsparcie oprogramowania', - RENTAL_REQUEST: 'Prośba o wynajem', - TRAINING_REQUEST: 'Prośba o szkolenie', - }, - type: { - URGENT: 'Pilne', - STANDARD: 'Standardowe', - LOW_PRIORITY: 'Niski priorytet', + CONTACT_US: 'Formularz kontaktowy', + REQUEST_DEVICE_MAINTENANCE: 'Konserwacja urządzenia', + COMPLAINT: 'Reklamacja', }, status: { OPEN: 'W rozpatrzeniu', @@ -80,7 +93,7 @@ const MOCK_TICKET_DETAILS_BLOCK_PL: CMS.Model.TicketDetailsBlock.TicketDetailsBl }, }, labels: { - showMore: 'Pokaż szczegóły sprawy', + showMore: 'Pokaż szczegóły zgłoszenia', showLess: 'Pokaż mniej szczegółów', today: 'Dzisiaj', yesterday: 'Wczoraj', @@ -94,28 +107,34 @@ const MOCK_TICKET_DETAILS_BLOCK_DE: CMS.Model.TicketDetailsBlock.TicketDetailsBl attachmentsTitle: 'Anhänge', properties: { id: 'Fall-ID', - topic: 'Thema', - type: 'Falltyp', status: 'Status', - description: 'Zusätzliche Notizen', - address: 'Serviceadresse', - contact: 'Kontaktform', + // Custom fields from Zendesk (using readable names from ZendeskFieldMapper) + machineName: 'Gerätename', + serialNumber: 'Seriennummer', + maintenanceType: 'Wartungstyp', + preferredDate: 'Bevorzugtes Datum', + additionalNotes: 'Zusätzliche Hinweise', + contactInformation: 'Kontaktinformationen', + issueDate: 'Problemdatum', + organizationName: 'Organisationsname', + firstName: 'Vorname', + lastName: 'Nachname', + email: 'E-Mail', + phone: 'Telefon', + invoiceNumber: 'Rechnungsnummer', + address: 'Adresse', + inquiryType: 'Anfragetyp', + productCategory: 'Produktkategorie', + preferredContactMethod: 'Bevorzugte Kontaktmethode', + termsAcceptance: 'AGB-Akzeptanz', + newsletterConsent: 'Newsletter-Zustimmung', + marketingConsent: 'Marketing-Zustimmung', }, fieldMapping: { topic: { - ALL: 'Alle', - TOOL_REPAIR: 'Werkzeugreparatur', - FLEET_EXCHANGE: 'Flottenaustausch', - CALIBRATION: 'Kalibrierung', - THEFT_REPORT: 'Diebstahlmeldung', - SOFTWARE_SUPPORT: 'Software-Support', - RENTAL_REQUEST: 'Mietanfrage', - TRAINING_REQUEST: 'Schulungsanfrage', - }, - type: { - URGENT: 'Dringend', - STANDARD: 'Standard', - LOW_PRIORITY: 'Niedrige Priorität', + CONTACT_US: 'Kontaktformular', + REQUEST_DEVICE_MAINTENANCE: 'Gerätewartung', + COMPLAINT: 'Beschwerde', }, status: { OPEN: 'In Bearbeitung', diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts index 2ea369836..cfdca395e 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts @@ -7,27 +7,34 @@ const MOCK_TICKET_DETAILS_BLOCK_EN: CMS.Model.TicketDetailsBlock.TicketDetailsBl attachmentsTitle: 'Attachments', properties: { id: 'Case ID', - topic: 'Topic', - type: 'Case type', status: 'Status', - description: 'Additional notes', - address: 'Service address', - contact: 'Form of contact', + // Custom fields from Zendesk (using readable names from ZendeskFieldMapper) + machineName: 'Machine Name', + serialNumber: 'Serial Number', + maintenanceType: 'Maintenance Type', + preferredDate: 'Preferred Date', + additionalNotes: 'Additional Notes', + contactInformation: 'Contact Information', + issueDate: 'Issue Date', + organizationName: 'Organization Name', + firstName: 'First Name', + lastName: 'Last Name', + email: 'Email', + phone: 'Phone', + invoiceNumber: 'Invoice Number', + address: 'Address', + inquiryType: 'Inquiry Type', + productCategory: 'Product Category', + preferredContactMethod: 'Preferred Contact Method', + termsAcceptance: 'Terms Acceptance', + newsletterConsent: 'Newsletter Consent', + marketingConsent: 'Marketing Consent', }, fieldMapping: { topic: { - TOOL_REPAIR: 'Tool Repair', - FLEET_EXCHANGE: 'Fleet Exchange', - CALIBRATION: 'Calibration', - THEFT_REPORT: 'Theft Report', - SOFTWARE_SUPPORT: 'Software Support', - RENTAL_REQUEST: 'Rental Request', - TRAINING_REQUEST: 'Training Request', - }, - type: { - URGENT: 'Urgent', - STANDARD: 'Standard', - LOW_PRIORITY: 'Low Priority', + CONTACT_US: 'Contact Form', + REQUEST_DEVICE_MAINTENANCE: 'Device Maintenance', + COMPLAINT: 'Complaint', }, status: { OPEN: 'Under consideration', @@ -45,33 +52,39 @@ const MOCK_TICKET_DETAILS_BLOCK_EN: CMS.Model.TicketDetailsBlock.TicketDetailsBl const MOCK_TICKET_DETAILS_BLOCK_PL: CMS.Model.TicketDetailsBlock.TicketDetailsBlock = { id: 'ticket-list-1', - title: 'Szczegóły sprawy', + title: 'Szczegóły zgłoszenia', commentsTitle: 'Komentarze', attachmentsTitle: 'Załączniki', properties: { - id: 'ID sprawy', - topic: 'Temat', - type: 'Typ sprawy', + id: 'ID zgłoszenia', status: 'Status', - description: 'Dodatkowe notatki', - address: 'Adres serwisowy', - contact: 'Forma kontaktu', + // Custom fields from Zendesk (using readable names from ZendeskFieldMapper) + machineName: 'Nazwa urządzenia', + serialNumber: 'Numer seryjny', + maintenanceType: 'Typ konserwacji', + preferredDate: 'Preferowana data', + additionalNotes: 'Dodatkowe uwagi', + contactInformation: 'Informacje kontaktowe', + issueDate: 'Data wystąpienia problemu', + organizationName: 'Nazwa organizacji', + firstName: 'Imię', + lastName: 'Nazwisko', + email: 'Email', + phone: 'Telefon', + invoiceNumber: 'Numer faktury', + address: 'Adres', + inquiryType: 'Typ zapytania', + productCategory: 'Kategoria produktu', + preferredContactMethod: 'Preferowana forma kontaktu', + termsAcceptance: 'Akceptacja regulaminu', + newsletterConsent: 'Zgoda na newsletter', + marketingConsent: 'Zgoda marketingowa', }, fieldMapping: { topic: { - ALL: 'Wszystko', - TOOL_REPAIR: 'Naprawa narzędzia', - FLEET_EXCHANGE: 'Wymiana floty', - CALIBRATION: 'Kalibracja', - THEFT_REPORT: 'Zgłoszenie kradzieży', - SOFTWARE_SUPPORT: 'Wsparcie oprogramowania', - RENTAL_REQUEST: 'Prośba o wynajem', - TRAINING_REQUEST: 'Prośba o szkolenie', - }, - type: { - URGENT: 'Pilne', - STANDARD: 'Standardowe', - LOW_PRIORITY: 'Niski priorytet', + CONTACT_US: 'Formularz kontaktowy', + REQUEST_DEVICE_MAINTENANCE: 'Konserwacja urządzenia', + COMPLAINT: 'Reklamacja', }, status: { OPEN: 'W rozpatrzeniu', @@ -80,7 +93,7 @@ const MOCK_TICKET_DETAILS_BLOCK_PL: CMS.Model.TicketDetailsBlock.TicketDetailsBl }, }, labels: { - showMore: 'Pokaż szczegóły sprawy', + showMore: 'Pokaż szczegóły zgłoszenia', showLess: 'Pokaż mniej szczegółów', today: 'Dzisiaj', yesterday: 'Wczoraj', @@ -94,28 +107,34 @@ const MOCK_TICKET_DETAILS_BLOCK_DE: CMS.Model.TicketDetailsBlock.TicketDetailsBl attachmentsTitle: 'Anhänge', properties: { id: 'Fall-ID', - topic: 'Thema', - type: 'Falltyp', status: 'Status', - description: 'Zusätzliche Notizen', - address: 'Serviceadresse', - contact: 'Kontaktform', + // Custom fields from Zendesk (using readable names from ZendeskFieldMapper) + machineName: 'Gerätename', + serialNumber: 'Seriennummer', + maintenanceType: 'Wartungstyp', + preferredDate: 'Bevorzugtes Datum', + additionalNotes: 'Zusätzliche Hinweise', + contactInformation: 'Kontaktinformationen', + issueDate: 'Problemdatum', + organizationName: 'Organisationsname', + firstName: 'Vorname', + lastName: 'Nachname', + email: 'E-Mail', + phone: 'Telefon', + invoiceNumber: 'Rechnungsnummer', + address: 'Adresse', + inquiryType: 'Anfragetyp', + productCategory: 'Produktkategorie', + preferredContactMethod: 'Bevorzugte Kontaktmethode', + termsAcceptance: 'AGB-Akzeptanz', + newsletterConsent: 'Newsletter-Zustimmung', + marketingConsent: 'Marketing-Zustimmung', }, fieldMapping: { topic: { - ALL: 'Alle', - TOOL_REPAIR: 'Werkzeugreparatur', - FLEET_EXCHANGE: 'Flottenaustausch', - CALIBRATION: 'Kalibrierung', - THEFT_REPORT: 'Diebstahlmeldung', - SOFTWARE_SUPPORT: 'Software-Support', - RENTAL_REQUEST: 'Mietanfrage', - TRAINING_REQUEST: 'Schulungsanfrage', - }, - type: { - URGENT: 'Dringend', - STANDARD: 'Standard', - LOW_PRIORITY: 'Niedrige Priorität', + CONTACT_US: 'Kontaktformular', + REQUEST_DEVICE_MAINTENANCE: 'Gerätewartung', + COMPLAINT: 'Beschwerde', }, status: { OPEN: 'In Bearbeitung', diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts index 936c9265c..e7a90f37e 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts @@ -22,8 +22,7 @@ const MOCK_TICKET_LIST_BLOCK_EN: CMS.Model.TicketListBlock.TicketListBlock = { ], table: { columns: [ - { id: 'topic', title: 'Topic' }, - { id: 'type', title: 'Case type' }, + { id: 'topic', title: 'Case Type' }, { id: 'status', title: 'Status' }, { id: 'updatedAt', title: 'Date' }, ], @@ -39,18 +38,9 @@ const MOCK_TICKET_LIST_BLOCK_EN: CMS.Model.TicketListBlock.TicketListBlock = { }, fieldMapping: { topic: { - TOOL_REPAIR: 'Tool Repair', - FLEET_EXCHANGE: 'Fleet Exchange', - CALIBRATION: 'Calibration', - THEFT_REPORT: 'Theft Report', - SOFTWARE_SUPPORT: 'Software Support', - RENTAL_REQUEST: 'Rental Request', - TRAINING_REQUEST: 'Training Request', - }, - type: { - URGENT: 'Urgent', - STANDARD: 'Standard', - LOW_PRIORITY: 'Low Priority', + CONTACT_US: 'Contact Form', + REQUEST_DEVICE_MAINTENANCE: 'Device Maintenance', + COMPLAINT: 'Complaint', }, status: { OPEN: 'Under consideration', @@ -93,10 +83,8 @@ const MOCK_TICKET_LIST_BLOCK_EN: CMS.Model.TicketListBlock.TicketListBlock = { label: 'Sort by', allowMultiple: false, options: [ - { label: 'Topic (ascending)', value: 'topic_ASC' }, - { label: 'Topic (descending)', value: 'topic_DESC' }, - { label: 'Type (ascending)', value: 'type_ASC' }, - { label: 'Type (descending)', value: 'type_DESC' }, + { label: 'Case Type (ascending)', value: 'topic_ASC' }, + { label: 'Case Type (descending)', value: 'topic_DESC' }, { label: 'Status (ascending)', value: 'status_ASC' }, { label: 'Status (descending)', value: 'status_DESC' }, { label: 'Updated (ascending)', value: 'updatedAt_ASC' }, @@ -106,42 +94,14 @@ const MOCK_TICKET_LIST_BLOCK_EN: CMS.Model.TicketListBlock.TicketListBlock = { { __typename: 'FilterSelect', id: 'topic', - label: 'Topic', + label: 'Case Type', allowMultiple: false, isLeading: false, options: [ { label: 'All', value: 'ALL' }, - { label: 'Tool Repair', value: 'TOOL_REPAIR' }, - { label: 'Fleet Exchange', value: 'FLEET_EXCHANGE' }, - { label: 'Calibration', value: 'CALIBRATION' }, - { label: 'Theft Report', value: 'THEFT_REPORT' }, - { label: 'Software Support', value: 'SOFTWARE_SUPPORT' }, - { label: 'Rental Request', value: 'RENTAL_REQUEST' }, - { label: 'Training Request', value: 'TRAINING_REQUEST' }, - ], - }, - { - __typename: 'FilterSelect', - id: 'type', - label: 'Case type', - allowMultiple: true, - isLeading: false, - options: [ - { label: 'Urgent', value: 'URGENT' }, - { label: 'Standard', value: 'STANDARD' }, - { label: 'Low Priority', value: 'LOW_PRIORITY' }, - ], - }, - { - __typename: 'FilterSelect', - id: 'priority', - label: 'Priority', - allowMultiple: false, - isLeading: false, - options: [ - { label: 'High', value: 'HIGH' }, - { label: 'Medium', value: 'MEDIUM' }, - { label: 'Low', value: 'LOW' }, + { label: 'Contact Form', value: 'CONTACT_US' }, + { label: 'Device Maintenance', value: 'REQUEST_DEVICE_MAINTENANCE' }, + { label: 'Complaint', value: 'COMPLAINT' }, ], }, { @@ -200,8 +160,7 @@ const MOCK_TICKET_LIST_BLOCK_DE: CMS.Model.TicketListBlock.TicketListBlock = { ], table: { columns: [ - { id: 'topic', title: 'Thema' }, - { id: 'type', title: 'Falltyp' }, + { id: 'topic', title: 'Falltyp' }, { id: 'status', title: 'Status' }, { id: 'updatedAt', title: 'Datum' }, ], @@ -217,19 +176,9 @@ const MOCK_TICKET_LIST_BLOCK_DE: CMS.Model.TicketListBlock.TicketListBlock = { }, fieldMapping: { topic: { - ALL: 'Alle', - TOOL_REPAIR: 'Werkzeugreparatur', - FLEET_EXCHANGE: 'Flottenaustausch', - CALIBRATION: 'Kalibrierung', - THEFT_REPORT: 'Diebstahlmeldung', - SOFTWARE_SUPPORT: 'Software-Support', - RENTAL_REQUEST: 'Mietanfrage', - TRAINING_REQUEST: 'Schulungsanfrage', - }, - type: { - URGENT: 'Dringend', - STANDARD: 'Standard', - LOW_PRIORITY: 'Niedrige Priorität', + CONTACT_US: 'Kontaktformular', + REQUEST_DEVICE_MAINTENANCE: 'Gerätewartung', + COMPLAINT: 'Beschwerde', }, status: { OPEN: 'In Bearbeitung', @@ -272,10 +221,8 @@ const MOCK_TICKET_LIST_BLOCK_DE: CMS.Model.TicketListBlock.TicketListBlock = { label: 'Sortieren nach', allowMultiple: false, options: [ - { label: 'Thema aufsteigend', value: 'topic_ASC' }, - { label: 'Thema absteigend', value: 'topic_DESC' }, - { label: 'Typ aufsteigend', value: 'type_ASC' }, - { label: 'Typ absteigend', value: 'type_DESC' }, + { label: 'Falltyp aufsteigend', value: 'topic_ASC' }, + { label: 'Falltyp absteigend', value: 'topic_DESC' }, { label: 'Status aufsteigend', value: 'status_ASC' }, { label: 'Status absteigend', value: 'status_DESC' }, { label: 'Aktualisiert aufsteigend', value: 'updatedAt_ASC' }, @@ -285,42 +232,14 @@ const MOCK_TICKET_LIST_BLOCK_DE: CMS.Model.TicketListBlock.TicketListBlock = { { __typename: 'FilterSelect', id: 'topic', - label: 'Thema', - allowMultiple: false, - isLeading: false, - options: [ - { label: 'Alle', value: 'ALL' }, - { label: 'Werkzeugreparatur', value: 'TOOL_REPAIR' }, - { label: 'Flottenaustausch', value: 'FLEET_EXCHANGE' }, - { label: 'Kalibrierung', value: 'CALIBRATION' }, - { label: 'Diebstahlmeldung', value: 'THEFT_REPORT' }, - { label: 'Software-Support', value: 'SOFTWARE_SUPPORT' }, - { label: 'Mietanfrage', value: 'RENTAL_REQUEST' }, - { label: 'Schulungsanfrage', value: 'TRAINING_REQUEST' }, - ], - }, - { - __typename: 'FilterSelect', - id: 'type', label: 'Falltyp', - allowMultiple: true, - isLeading: false, - options: [ - { label: 'Dringend', value: 'URGENT' }, - { label: 'Standard', value: 'STANDARD' }, - { label: 'Niedrige Priorität', value: 'LOW_PRIORITY' }, - ], - }, - { - __typename: 'FilterSelect', - id: 'priority', - label: 'Priorität', allowMultiple: false, isLeading: false, options: [ - { label: 'Hoch', value: 'HIGH' }, - { label: 'Mittel', value: 'MEDIUM' }, - { label: 'Niedrig', value: 'LOW' }, + { label: 'Alle', value: 'ALL' }, + { label: 'Kontaktformular', value: 'CONTACT_US' }, + { label: 'Gerätewartung', value: 'REQUEST_DEVICE_MAINTENANCE' }, + { label: 'Beschwerde', value: 'COMPLAINT' }, ], }, { @@ -380,10 +299,9 @@ const MOCK_TICKET_LIST_BLOCK_PL: CMS.Model.TicketListBlock.TicketListBlock = { table: { columns: [ - { id: 'topic', title: 'Temat' }, - { id: 'type', title: 'Typ zgłoszenia' }, + { id: 'topic', title: 'Typ zgłoszenia' }, { id: 'status', title: 'Status' }, - { id: 'updatedAt', title: 'Data' }, + { id: 'updatedAt', title: 'Data utworzenia' }, ], actions: { title: 'Akcja', @@ -397,19 +315,9 @@ const MOCK_TICKET_LIST_BLOCK_PL: CMS.Model.TicketListBlock.TicketListBlock = { }, fieldMapping: { topic: { - ALL: 'Wszystko', - TOOL_REPAIR: 'Naprawa narzędzi', - FLEET_EXCHANGE: 'Wymiana floty', - CALIBRATION: 'Kalibracja', - THEFT_REPORT: 'Zgłoszenie kradzieży', - SOFTWARE_SUPPORT: 'Wsparcie oprogramowania', - RENTAL_REQUEST: 'Wniosek o wynajem', - TRAINING_REQUEST: 'Wniosek o szkolenie', - }, - type: { - URGENT: 'Pilne', - STANDARD: 'Standardowe', - LOW_PRIORITY: 'Niski priorytet', + CONTACT_US: 'Formularz kontaktowy', + REQUEST_DEVICE_MAINTENANCE: 'Konserwacja urządzenia', + COMPLAINT: 'Reklamacja', }, status: { OPEN: 'W rozpatrzeniu', @@ -453,10 +361,8 @@ const MOCK_TICKET_LIST_BLOCK_PL: CMS.Model.TicketListBlock.TicketListBlock = { label: 'Sortuj według', allowMultiple: false, options: [ - { label: 'Temat rosnąco', value: 'topic_ASC' }, - { label: 'Temat malejąco', value: 'topic_DESC' }, - { label: 'Typ rosnąco', value: 'type_ASC' }, - { label: 'Typ malejąco', value: 'type_DESC' }, + { label: 'Typ zgłoszenia rosnąco', value: 'topic_ASC' }, + { label: 'Typ zgłoszenia malejąco', value: 'topic_DESC' }, { label: 'Status rosnąco', value: 'status_ASC' }, { label: 'Status malejąco', value: 'status_DESC' }, { label: 'Aktualizacja rosnąco', value: 'updatedAt_ASC' }, @@ -466,42 +372,14 @@ const MOCK_TICKET_LIST_BLOCK_PL: CMS.Model.TicketListBlock.TicketListBlock = { { __typename: 'FilterSelect', id: 'topic', - label: 'Temat', - allowMultiple: false, - isLeading: false, - options: [ - { label: 'Wszystko', value: 'ALL' }, - { label: 'Naprawa narzędzi', value: 'TOOL_REPAIR' }, - { label: 'Wymiana floty', value: 'FLEET_EXCHANGE' }, - { label: 'Kalibracja', value: 'CALIBRATION' }, - { label: 'Zgłoszenie kradzieży', value: 'THEFT_REPORT' }, - { label: 'Wsparcie oprogramowania', value: 'SOFTWARE_SUPPORT' }, - { label: 'Wniosek o wynajem', value: 'RENTAL_REQUEST' }, - { label: 'Wniosek o szkolenie', value: 'TRAINING_REQUEST' }, - ], - }, - { - __typename: 'FilterSelect', - id: 'type', label: 'Typ zgłoszenia', - allowMultiple: true, - isLeading: false, - options: [ - { label: 'Pilne', value: 'URGENT' }, - { label: 'Standardowe', value: 'STANDARD' }, - { label: 'Niski priorytet', value: 'LOW_PRIORITY' }, - ], - }, - { - __typename: 'FilterSelect', - id: 'priority', - label: 'Priorytet', allowMultiple: false, isLeading: false, options: [ - { label: 'Wysoki', value: 'HIGH' }, - { label: 'Średni', value: 'MEDIUM' }, - { label: 'Niski', value: 'LOW' }, + { label: 'Wszystko', value: 'ALL' }, + { label: 'Formularz kontaktowy', value: 'CONTACT_US' }, + { label: 'Konserwacja urządzenia', value: 'REQUEST_DEVICE_MAINTENANCE' }, + { label: 'Reklamacja', value: 'COMPLAINT' }, ], }, { diff --git a/packages/integrations/mocked/src/modules/tickets/tickets.mocks.ts b/packages/integrations/mocked/src/modules/tickets/tickets.mocks.ts index 07b3e0c58..fde7544eb 100644 --- a/packages/integrations/mocked/src/modules/tickets/tickets.mocks.ts +++ b/packages/integrations/mocked/src/modules/tickets/tickets.mocks.ts @@ -9,8 +9,7 @@ const MOCK_TICKET_1_EN: Tickets.Model.Ticket = { id: 'EL-465-920-678', createdAt: dateToday.toISOString(), updatedAt: dateToday.toISOString(), - topic: 'TOOL_REPAIR', - type: 'URGENT', + topic: 'REQUEST_DEVICE_MAINTENANCE', status: 'OPEN', attachments: [ { @@ -64,8 +63,7 @@ const MOCK_TICKET_2_EN: Tickets.Model.Ticket = { id: 'EL-465-920-677', createdAt: dateYesterday.toISOString(), updatedAt: dateYesterday.toISOString(), - topic: 'FLEET_EXCHANGE', - type: 'STANDARD', + topic: 'CONTACT_US', status: 'CLOSED', properties: [ { @@ -120,8 +118,7 @@ const MOCK_TICKET_3_EN: Tickets.Model.Ticket = { id: 'EL-465-920-676', createdAt: '2024-12-12T10:00:00', updatedAt: '2024-12-14T16:00:00', - topic: 'CALIBRATION', - type: 'STANDARD', + topic: 'COMPLAINT', status: 'IN_PROGRESS', properties: [ { @@ -184,8 +181,7 @@ const MOCK_TICKET_4_EN: Tickets.Model.Ticket = { id: 'EL-465-920-675', createdAt: '2024-12-10T10:00:00', updatedAt: '2024-12-12T16:00:00', - topic: 'THEFT_REPORT', - type: 'URGENT', + topic: 'CONTACT_US', status: 'OPEN', properties: [ { @@ -215,8 +211,7 @@ const MOCK_TICKET_5_EN: Tickets.Model.Ticket = { id: 'EL-465-920-674', createdAt: '2024-12-10T10:00:00', updatedAt: '2024-12-12T16:00:00', - topic: 'SOFTWARE_SUPPORT', - type: 'STANDARD', + topic: 'COMPLAINT', status: 'OPEN', properties: [ { @@ -280,8 +275,7 @@ const MOCK_TICKET_1_PL: Tickets.Model.Ticket = { id: 'EL-465-920-678', createdAt: dateToday.toISOString(), updatedAt: dateToday.toISOString(), - topic: 'TOOL_REPAIR', - type: 'URGENT', + topic: 'REQUEST_DEVICE_MAINTENANCE', status: 'OPEN', attachments: [ { @@ -335,8 +329,7 @@ const MOCK_TICKET_2_PL: Tickets.Model.Ticket = { id: 'EL-465-920-677', createdAt: dateYesterday.toISOString(), updatedAt: dateYesterday.toISOString(), - topic: 'FLEET_EXCHANGE', - type: 'STANDARD', + topic: 'CONTACT_US', status: 'CLOSED', properties: [ { @@ -398,8 +391,7 @@ const MOCK_TICKET_3_PL: Tickets.Model.Ticket = { id: 'EL-465-920-676', createdAt: '2024-12-12T10:00:00', updatedAt: '2024-12-14T16:00:00', - topic: 'CALIBRATION', - type: 'STANDARD', + topic: 'COMPLAINT', status: 'IN_PROGRESS', properties: [ { @@ -462,8 +454,7 @@ const MOCK_TICKET_4_PL: Tickets.Model.Ticket = { id: 'EL-465-920-675', createdAt: '2024-12-10T10:00:00', updatedAt: '2024-12-12T16:00:00', - topic: 'THEFT_REPORT', - type: 'URGENT', + topic: 'CONTACT_US', status: 'OPEN', properties: [ { @@ -493,8 +484,7 @@ const MOCK_TICKET_5_PL: Tickets.Model.Ticket = { id: 'EL-465-920-674', createdAt: '2024-12-10T10:00:00', updatedAt: '2024-12-12T16:00:00', - topic: 'SOFTWARE_SUPPORT', - type: 'STANDARD', + topic: 'COMPLAINT', status: 'OPEN', properties: [ { @@ -558,8 +548,7 @@ const MOCK_TICKET_1_DE: Tickets.Model.Ticket = { id: 'EL-465-920-678', createdAt: dateToday.toISOString(), updatedAt: dateToday.toISOString(), - topic: 'TOOL_REPAIR', - type: 'URGENT', + topic: 'REQUEST_DEVICE_MAINTENANCE', status: 'OPEN', attachments: [ { @@ -613,8 +602,8 @@ const MOCK_TICKET_2_DE: Tickets.Model.Ticket = { id: 'EL-465-920-677', createdAt: dateYesterday.toISOString(), updatedAt: dateYesterday.toISOString(), - topic: 'FLEET_EXCHANGE', - type: 'STANDARD', + topic: 'CONTACT_US', + type: 'NORMAL', status: 'CLOSED', properties: [ { @@ -676,8 +665,7 @@ const MOCK_TICKET_3_DE: Tickets.Model.Ticket = { id: 'EL-465-920-676', createdAt: '2024-12-12T10:00:00', updatedAt: '2024-12-14T16:00:00', - topic: 'CALIBRATION', - type: 'STANDARD', + topic: 'COMPLAINT', status: 'IN_PROGRESS', properties: [ { @@ -740,7 +728,7 @@ const MOCK_TICKET_4_DE: Tickets.Model.Ticket = { id: 'EL-465-920-675', createdAt: '2024-12-10T10:00:00', updatedAt: '2024-12-12T16:00:00', - topic: 'THEFT_REPORT', + topic: 'CONTACT_US', type: 'URGENT', status: 'OPEN', properties: [ @@ -771,8 +759,7 @@ const MOCK_TICKET_5_DE: Tickets.Model.Ticket = { id: 'EL-465-920-674', createdAt: '2024-12-10T10:00:00', updatedAt: '2024-12-12T16:00:00', - topic: 'SOFTWARE_SUPPORT', - type: 'STANDARD', + topic: 'COMPLAINT', status: 'OPEN', properties: [ { @@ -834,24 +821,16 @@ Lassen Sie uns wissen, wenn Sie weitere Unterstützung benötigen. const generateRandomTicketsPL = (): Tickets.Model.Ticket[] => { return Array.from({ length: 100 }, (_, index) => { - const ticketType = ['URGENT', 'STANDARD', 'LOW_PRIORITY'][Math.floor(Math.random() * 3)] as string; const status = ['OPEN', 'CLOSED', 'IN_PROGRESS'][Math.floor(Math.random() * 3)] as Tickets.Model.TicketStatus; - const topic = [ - 'TOOL_REPAIR', - 'FLEET_EXCHANGE', - 'CALIBRATION', - 'THEFT_REPORT', - 'SOFTWARE_SUPPORT', - 'RENTAL_REQUEST', - 'TRAINING_REQUEST', - ][Math.floor(Math.random() * 7)] as string; + const topic = ['CONTACT_US', 'REQUEST_DEVICE_MAINTENANCE', 'COMPLAINT'][ + Math.floor(Math.random() * 3) + ] as string; return { id: `EL-465-920-${573 - index}`, createdAt: new Date(2024, 11, Math.floor(Math.random() * 31) + 1).toISOString(), updatedAt: new Date(2024, 11, Math.floor(Math.random() * 31) + 1).toISOString(), topic, - type: ticketType, status, properties: [ { @@ -912,24 +891,16 @@ const generateRandomTicketsPL = (): Tickets.Model.Ticket[] => { const generateRandomTicketsDE = (): Tickets.Model.Ticket[] => { return Array.from({ length: 100 }, (_, index) => { - const ticketType = ['URGENT', 'STANDARD', 'LOW_PRIORITY'][Math.floor(Math.random() * 3)] as string; const status = ['OPEN', 'CLOSED', 'IN_PROGRESS'][Math.floor(Math.random() * 3)] as Tickets.Model.TicketStatus; - const topic = [ - 'TOOL_REPAIR', - 'FLEET_EXCHANGE', - 'CALIBRATION', - 'THEFT_REPORT', - 'SOFTWARE_SUPPORT', - 'RENTAL_REQUEST', - 'TRAINING_REQUEST', - ][Math.floor(Math.random() * 7)] as string; + const topic = ['CONTACT_US', 'REQUEST_DEVICE_MAINTENANCE', 'COMPLAINT'][ + Math.floor(Math.random() * 3) + ] as string; return { id: `EL-465-920-${573 - index}`, createdAt: new Date(2024, 11, Math.floor(Math.random() * 31) + 1).toISOString(), updatedAt: new Date(2024, 11, Math.floor(Math.random() * 31) + 1).toISOString(), topic, - type: ticketType, status, properties: [ { @@ -990,24 +961,16 @@ const generateRandomTicketsDE = (): Tickets.Model.Ticket[] => { const generateRandomTicketsEN = (): Tickets.Model.Ticket[] => { return Array.from({ length: 100 }, (_, index) => { - const ticketType = ['URGENT', 'STANDARD', 'LOW_PRIORITY'][Math.floor(Math.random() * 3)] as string; const status = ['OPEN', 'CLOSED', 'IN_PROGRESS'][Math.floor(Math.random() * 3)] as Tickets.Model.TicketStatus; - const topic = [ - 'TOOL_REPAIR', - 'FLEET_EXCHANGE', - 'CALIBRATION', - 'THEFT_REPORT', - 'SOFTWARE_SUPPORT', - 'RENTAL_REQUEST', - 'TRAINING_REQUEST', - ][Math.floor(Math.random() * 7)] as string; + const topic = ['CONTACT_US', 'REQUEST_DEVICE_MAINTENANCE', 'COMPLAINT'][ + Math.floor(Math.random() * 3) + ] as string; return { id: `EL-465-920-${573 - index}`, createdAt: new Date(2024, 11, Math.floor(Math.random() * 31) + 1).toISOString(), updatedAt: new Date(2024, 11, Math.floor(Math.random() * 31) + 1).toISOString(), topic, - type: ticketType, status, properties: [ { From 52e381f13315fc5f9c94ca91b2b26f8cb06f4a90 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Tue, 27 Jan 2026 21:18:34 +0100 Subject: [PATCH 29/54] feat(zendesk): add method to retrieve field key by Zendesk field ID for reverse mapping --- .../src/modules/tickets/zendesk-field.mapper.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts index c4daf36cc..ab7298484 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts @@ -85,6 +85,22 @@ export class ZendeskFieldMapper { }; } + /** + * Gets the field key (name) by its Zendesk field ID. + * Used for reverse mapping when reading tickets from Zendesk. + * + * @param fieldId - Zendesk custom field ID + * @returns Field key (e.g., 'machineName', 'serialNumber') or undefined if not found + */ + static getFieldKeyById(fieldId: number): string | undefined { + for (const [key, id] of Object.entries(this.fieldMap)) { + if (id === fieldId) { + return key; + } + } + return undefined; + } + /** * Converts a record of field values to Zendesk custom fields format. * Only includes fields that: From fba71f4e2642787739bb40a7993256793c9f2dd3 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Tue, 27 Jan 2026 21:19:56 +0100 Subject: [PATCH 30/54] feat(zendesk): enhance topic mapping logic based on ticket form IDs --- .../modules/tickets/zendesk-ticket.service.ts | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index c3c7ca767..8396973c2 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -129,10 +129,6 @@ export class ZendeskTicketService extends Tickets.Service { searchQuery += ` status:${options.status.toLowerCase()}`; } - if (options.type) { - searchQuery += ` priority:${options.type.toLowerCase()}`; - } - if (options.topic) { searchQuery += ` tag:${options.topic.toLowerCase()}`; } @@ -200,8 +196,47 @@ export class ZendeskTicketService extends Tickets.Service { switchMap((uploadTokens) => this.findZendeskUserByEmail(user.email!).pipe( switchMap((zendeskUser) => { + // Map ticketFormId to topic value + // Ensure ticketFormId is a number for comparison + const ticketFormIdNum = + typeof data.ticketFormId === 'string' + ? Number(data.ticketFormId) + : data.ticketFormId; + + let topicValue: string; + const contactFormId = process.env.ZENDESK_CONTACT_FORM_ID + ? Number(process.env.ZENDESK_CONTACT_FORM_ID) + : undefined; + const complaintFormId = process.env.ZENDESK_COMPLAINT_FORM_ID + ? Number(process.env.ZENDESK_COMPLAINT_FORM_ID) + : undefined; + const deviceMaintenanceFormId = process.env.ZENDESK_REQUEST_DEVICE_MAINTENANCE_FORM_ID + ? Number(process.env.ZENDESK_REQUEST_DEVICE_MAINTENANCE_FORM_ID) + : undefined; + + if (ticketFormIdNum === contactFormId) { + topicValue = 'CONTACT_US'; + } else if (ticketFormIdNum === complaintFormId) { + topicValue = 'COMPLAINT'; + } else if (ticketFormIdNum === deviceMaintenanceFormId) { + topicValue = 'REQUEST_DEVICE_MAINTENANCE'; + } else { + return throwError( + () => + new BadRequestException( + `Invalid ticketFormId: ${data.ticketFormId}. Must match one of the configured form IDs.`, + ), + ); + } + + // Add topic to customFields before mapping + const customFieldsWithTopic = { + ...(data.customFields || {}), + topic: topicValue, + }; + // Map custom fields from Survey.js to Zendesk format using field mapper - const customFields = ZendeskFieldMapper.toCustomFields(data.customFields || {}); + const customFields = ZendeskFieldMapper.toCustomFields(customFieldsWithTopic); return from( createTicket({ From 6d0957253ba67c882dfe6d92fd0653783988faf8 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Tue, 27 Jan 2026 21:31:35 +0100 Subject: [PATCH 31/54] feat(zendesk): improved zendesk-ticket mapper --- .../modules/tickets/zendesk-ticket.mapper.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts index e683ecae0..2604f9711 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts @@ -2,6 +2,8 @@ import { Tickets } from '@o2s/framework/modules'; import { type TicketCommentObject, type TicketObject, type UserObject } from '@/generated/zendesk'; +import { ZendeskFieldMapper } from './zendesk-field.mapper'; + type ZendeskTicket = TicketObject; export function mapTicketToModel( @@ -23,21 +25,31 @@ export function mapTicketToModel( status = 'OPEN'; } - let topic = 'GENERAL'; + // Determine topic from custom field if configured + let topic: string = 'GENERAL'; + const topicFieldId = process.env.ZENDESK_TOPIC_FIELD_ID ? Number(process.env.ZENDESK_TOPIC_FIELD_ID) : undefined; + + if (topicFieldId && ticket.custom_fields) { + const topicField = ticket.custom_fields.find((field) => field.id === topicFieldId); + if (topicField?.value) { + topic = String(topicField.value).toUpperCase(); + } + } + const properties: Tickets.Model.TicketProperty[] = [ { id: 'subject', value: ticket.subject || '' }, { id: 'description', value: ticket.description || '' }, ]; + // Map custom fields to properties using readable names from ZendeskFieldMapper if (ticket.custom_fields) { - const topicFieldId = Number(process.env.ZENDESK_TOPIC_FIELD_ID || 0); ticket.custom_fields.forEach((field) => { if (field.value !== null && field.value !== undefined) { - if (topicFieldId && field.id === topicFieldId) { - topic = String(field.value).toUpperCase(); - } else { + const fieldKey = ZendeskFieldMapper.getFieldKeyById(field.id!); + + if (fieldKey) { properties.push({ - id: `custom_field_${field.id}`, + id: fieldKey, value: String(field.value), }); } @@ -82,7 +94,6 @@ export function mapTicketToModel( createdAt: ticket.created_at || '', updatedAt: ticket.updated_at || '', topic, - type: (ticket.priority || 'NORMAL').toUpperCase(), status, properties, comments: mappedComments.length > 0 ? mappedComments : undefined, From a72a013bbbd498986ef064bebfcc76a94f180dcd Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Tue, 27 Jan 2026 21:51:29 +0100 Subject: [PATCH 32/54] feat(ticket-details): enhance ticket mapping with new field mappings and localized labels --- .../ticket-details.mapper.ts | 9 ++- .../cms/models/blocks/ticket-details.model.ts | 6 +- .../blocks/cms.ticket-details.mapper.ts | 66 +++++++++++++++++++ .../blocks/cms.ticket-details.mapper.ts | 66 +++++++++++++++++++ 4 files changed, 145 insertions(+), 2 deletions(-) diff --git a/packages/blocks/ticket-details/src/api-harmonization/ticket-details.mapper.ts b/packages/blocks/ticket-details/src/api-harmonization/ticket-details.mapper.ts index 1e43529bc..7e13e82ab 100644 --- a/packages/blocks/ticket-details/src/api-harmonization/ticket-details.mapper.ts +++ b/packages/blocks/ticket-details/src/api-harmonization/ticket-details.mapper.ts @@ -57,11 +57,18 @@ export const mapTicket = ( return prev; } + // Check if there's a fieldMapping for this property to translate the value + const fieldMapping = cms.fieldMapping[property.id as keyof typeof cms.fieldMapping]; + const mappedValue = + fieldMapping && typeof fieldMapping === 'object' + ? (fieldMapping as Record)[property.value] || property.value + : property.value; + return [ ...prev, { id: property.id, - value: property.value, + value: mappedValue, label: field, }, ]; diff --git a/packages/framework/src/modules/cms/models/blocks/ticket-details.model.ts b/packages/framework/src/modules/cms/models/blocks/ticket-details.model.ts index 22a78adf4..ffacea7a2 100644 --- a/packages/framework/src/modules/cms/models/blocks/ticket-details.model.ts +++ b/packages/framework/src/modules/cms/models/blocks/ticket-details.model.ts @@ -9,7 +9,11 @@ export class TicketDetailsBlock extends Block.Block { properties?: { [key: string]: string; }; - fieldMapping!: Mapping.Mapping; + fieldMapping!: Mapping.Mapping & { + [key: string]: { + [key: string]: string; + }; + }; labels!: { showMore: string; showLess: string; diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts index cfdca395e..d879c6198 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts @@ -41,6 +41,28 @@ const MOCK_TICKET_DETAILS_BLOCK_EN: CMS.Model.TicketDetailsBlock.TicketDetailsBl CLOSED: 'Resolved', IN_PROGRESS: 'New response', }, + inquiryType: { + product_inquiries: 'Product Inquiries', + feedback_suggestions: 'Feedback & Suggestions', + partnerships_collaborations: 'Partnerships & Collaborations', + training_resources: 'Training & Resources', + compliance_regulations: 'Compliance & Regulations', + other: 'Other', + }, + productCategory: { + raw_materials: 'Raw Materials', + semi_finished_products: 'Semi-finished Products', + components: 'Components', + machinery: 'Machinery', + tools: 'Tools', + spare_parts: 'Spare Parts', + other_product_category: 'Other', + }, + maintenanceType: { + scheduled_maintenance: 'Scheduled Maintenance', + preventive_maintenance: 'Preventive Maintenance', + corrective_maintenance: 'Corrective Maintenance', + }, }, labels: { showMore: 'Show case details', @@ -91,6 +113,28 @@ const MOCK_TICKET_DETAILS_BLOCK_PL: CMS.Model.TicketDetailsBlock.TicketDetailsBl CLOSED: 'Rozwiązane', IN_PROGRESS: 'Nowa odpowiedź', }, + inquiryType: { + product_inquiries: 'Zapytania dotyczące produktów', + feedback_suggestions: 'Informacje zwrotne i sugestie', + partnerships_collaborations: 'Partnerstwo i współpraca', + training_resources: 'Szkolenia i zasoby', + compliance_regulations: 'Zgodność z przepisami i regulacje', + other: 'Inny', + }, + productCategory: { + raw_materials: 'Surowce', + semi_finished_products: 'Półprodukty', + components: 'Składniki', + machinery: 'Maszyny', + tools: 'Narzędzia', + spare_parts: 'Części zamienne', + other_product_category: 'Inny', + }, + maintenanceType: { + scheduled_maintenance: 'Konserwacja zaplanowana', + preventive_maintenance: 'Konserwacja zapobiegawcza', + corrective_maintenance: 'Konserwacja naprawcza', + }, }, labels: { showMore: 'Pokaż szczegóły zgłoszenia', @@ -141,6 +185,28 @@ const MOCK_TICKET_DETAILS_BLOCK_DE: CMS.Model.TicketDetailsBlock.TicketDetailsBl CLOSED: 'Gelöst', IN_PROGRESS: 'Neue Antwort', }, + inquiryType: { + product_inquiries: 'Produktanfragen', + feedback_suggestions: 'Feedback & Vorschläge', + partnerships_collaborations: 'Partnerschaften & Kooperationen', + training_resources: 'Schulungen & Ressourcen', + compliance_regulations: 'Compliance & Vorschriften', + other: 'Sonstiges', + }, + productCategory: { + raw_materials: 'Rohmaterialien', + semi_finished_products: 'Halbfertigprodukte', + components: 'Komponenten', + machinery: 'Maschinen', + tools: 'Werkzeuge', + spare_parts: 'Ersatzteile', + other_product_category: 'Sonstiges', + }, + maintenanceType: { + scheduled_maintenance: 'Planmäßige Wartung', + preventive_maintenance: 'Vorbeugende Wartung', + corrective_maintenance: 'Korrigierende Wartung', + }, }, labels: { showMore: 'Falldetails anzeigen', diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts index cfdca395e..d879c6198 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts @@ -41,6 +41,28 @@ const MOCK_TICKET_DETAILS_BLOCK_EN: CMS.Model.TicketDetailsBlock.TicketDetailsBl CLOSED: 'Resolved', IN_PROGRESS: 'New response', }, + inquiryType: { + product_inquiries: 'Product Inquiries', + feedback_suggestions: 'Feedback & Suggestions', + partnerships_collaborations: 'Partnerships & Collaborations', + training_resources: 'Training & Resources', + compliance_regulations: 'Compliance & Regulations', + other: 'Other', + }, + productCategory: { + raw_materials: 'Raw Materials', + semi_finished_products: 'Semi-finished Products', + components: 'Components', + machinery: 'Machinery', + tools: 'Tools', + spare_parts: 'Spare Parts', + other_product_category: 'Other', + }, + maintenanceType: { + scheduled_maintenance: 'Scheduled Maintenance', + preventive_maintenance: 'Preventive Maintenance', + corrective_maintenance: 'Corrective Maintenance', + }, }, labels: { showMore: 'Show case details', @@ -91,6 +113,28 @@ const MOCK_TICKET_DETAILS_BLOCK_PL: CMS.Model.TicketDetailsBlock.TicketDetailsBl CLOSED: 'Rozwiązane', IN_PROGRESS: 'Nowa odpowiedź', }, + inquiryType: { + product_inquiries: 'Zapytania dotyczące produktów', + feedback_suggestions: 'Informacje zwrotne i sugestie', + partnerships_collaborations: 'Partnerstwo i współpraca', + training_resources: 'Szkolenia i zasoby', + compliance_regulations: 'Zgodność z przepisami i regulacje', + other: 'Inny', + }, + productCategory: { + raw_materials: 'Surowce', + semi_finished_products: 'Półprodukty', + components: 'Składniki', + machinery: 'Maszyny', + tools: 'Narzędzia', + spare_parts: 'Części zamienne', + other_product_category: 'Inny', + }, + maintenanceType: { + scheduled_maintenance: 'Konserwacja zaplanowana', + preventive_maintenance: 'Konserwacja zapobiegawcza', + corrective_maintenance: 'Konserwacja naprawcza', + }, }, labels: { showMore: 'Pokaż szczegóły zgłoszenia', @@ -141,6 +185,28 @@ const MOCK_TICKET_DETAILS_BLOCK_DE: CMS.Model.TicketDetailsBlock.TicketDetailsBl CLOSED: 'Gelöst', IN_PROGRESS: 'Neue Antwort', }, + inquiryType: { + product_inquiries: 'Produktanfragen', + feedback_suggestions: 'Feedback & Vorschläge', + partnerships_collaborations: 'Partnerschaften & Kooperationen', + training_resources: 'Schulungen & Ressourcen', + compliance_regulations: 'Compliance & Vorschriften', + other: 'Sonstiges', + }, + productCategory: { + raw_materials: 'Rohmaterialien', + semi_finished_products: 'Halbfertigprodukte', + components: 'Komponenten', + machinery: 'Maschinen', + tools: 'Werkzeuge', + spare_parts: 'Ersatzteile', + other_product_category: 'Sonstiges', + }, + maintenanceType: { + scheduled_maintenance: 'Planmäßige Wartung', + preventive_maintenance: 'Vorbeugende Wartung', + corrective_maintenance: 'Korrigierende Wartung', + }, }, labels: { showMore: 'Falldetails anzeigen', From c9a72757d4a282aded22b63e70649654a903f300 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Tue, 27 Jan 2026 22:04:52 +0100 Subject: [PATCH 33/54] feat(ticket-list): add ticketId field to models and enhance UI with ticket ID display --- .../src/api-harmonization/ticket-list.model.ts | 1 + .../ticket-list/src/frontend/TicketList.client.tsx | 11 ++++++++--- .../modules/cms/models/blocks/ticket-list.model.ts | 2 ++ .../cms/mappers/blocks/cms.ticket-list.mapper.ts | 2 ++ .../cms/mappers/blocks/cms.ticket-list.mapper.ts | 3 +++ 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts b/packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts index f9bf37e23..3b6a6a52b 100644 --- a/packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts +++ b/packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts @@ -26,6 +26,7 @@ export class TicketListBlock extends ApiModels.Block.Block { showMoreFilters?: string; hideMoreFilters?: string; noActiveFilters?: string; + ticketId?: string; }; initialFilters?: Partial; meta?: CMS.Model.TicketListBlock.TicketListBlock['meta']; diff --git a/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx b/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx index 537b70017..8b508d177 100644 --- a/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx +++ b/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx @@ -117,9 +117,14 @@ export const TicketListPure: React.FC = ({ locale, accessTo type: 'custom', cellClassName: 'max-w-[200px] lg:max-w-md', render: (_value: unknown, ticket: Model.Ticket) => ( - +
    + + + {data.labels.ticketId}: {ticket.id} + +
    ), }; case 'status': diff --git a/packages/framework/src/modules/cms/models/blocks/ticket-list.model.ts b/packages/framework/src/modules/cms/models/blocks/ticket-list.model.ts index 77fb496a0..22140abe6 100644 --- a/packages/framework/src/modules/cms/models/blocks/ticket-list.model.ts +++ b/packages/framework/src/modules/cms/models/blocks/ticket-list.model.ts @@ -24,6 +24,7 @@ export class TicketListBlock extends Block.Block { showMoreFilters?: string; hideMoreFilters?: string; noActiveFilters?: string; + ticketId?: string; }; detailsUrl!: string; forms?: Link[]; @@ -56,6 +57,7 @@ export class Meta { showMoreFilters?: string; hideMoreFilters?: string; noActiveFilters?: string; + ticketId?: string; }; detailsUrl!: string; } diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts index 068a270a0..cd55311aa 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts @@ -36,6 +36,7 @@ export const mapTicketListBlock = ({ yesterday: 'Yesterday', showMore: 'Show more', clickToSelect: 'Click to select', + ticketId: 'Ticket ID', }, detailsUrl: data.detailsUrl || '', meta: isPreview @@ -59,6 +60,7 @@ export const mapTicketListBlock = ({ yesterday: 'yesterday', showMore: 'showMore', clickToSelect: 'clickToSelect', + ticketId: 'ticketId', }, detailsUrl: 'detailsUrl', } diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts index e7a90f37e..2b04cb2ef 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts @@ -134,6 +134,7 @@ const MOCK_TICKET_LIST_BLOCK_EN: CMS.Model.TicketListBlock.TicketListBlock = { showMoreFilters: 'Show more filters', hideMoreFilters: 'Hide more filters', noActiveFilters: 'No active filters', + ticketId: 'Ticket ID', }, detailsUrl: '/cases/{id}', }; @@ -272,6 +273,7 @@ const MOCK_TICKET_LIST_BLOCK_DE: CMS.Model.TicketListBlock.TicketListBlock = { showMoreFilters: 'Mehr Filter anzeigen', hideMoreFilters: 'Weniger Filter anzeigen', noActiveFilters: 'Keine aktiven Filter', + ticketId: 'Fall-ID', }, detailsUrl: '/faelle/{id}', }; @@ -411,6 +413,7 @@ const MOCK_TICKET_LIST_BLOCK_PL: CMS.Model.TicketListBlock.TicketListBlock = { clickToSelect: 'Kliknij, aby wybrać', showMoreFilters: 'Pokaż więcej filtrów', hideMoreFilters: 'Ukryj więcej filtrów', + ticketId: 'ID zgłoszenia', noActiveFilters: 'Brak aktywnych filtrów', }, detailsUrl: '/zgloszenia/{id}', From 69f440c6c4c591b3ee9299ed64dd17f34f2e5071 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 09:54:28 +0100 Subject: [PATCH 34/54] refactor(ticket-list): make ticket type optional and enhance mapping logic --- .../ticket-list.mapper.spec.ts | 17 ++++++++++++++--- .../src/api-harmonization/ticket-list.mapper.ts | 10 ++++++---- .../src/api-harmonization/ticket-list.model.ts | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/blocks/ticket-list/src/api-harmonization/ticket-list.mapper.spec.ts b/packages/blocks/ticket-list/src/api-harmonization/ticket-list.mapper.spec.ts index 75a5dccfe..f4dea1d8f 100644 --- a/packages/blocks/ticket-list/src/api-harmonization/ticket-list.mapper.spec.ts +++ b/packages/blocks/ticket-list/src/api-harmonization/ticket-list.mapper.spec.ts @@ -85,8 +85,8 @@ describe('ticket-list.mapper', () => { expect(result.topic.label).toBe('Network Problem'); expect(result.topic.value).toBe('NETWORK'); - expect(result.type.label).toBe('Incident Report'); - expect(result.type.value).toBe('INCIDENT'); + expect(result.type?.label).toBe('Incident Report'); + expect(result.type?.value).toBe('INCIDENT'); expect(result.status.label).toBe('Open Ticket'); expect(result.status.value).toBe('OPEN'); }); @@ -104,9 +104,20 @@ describe('ticket-list.mapper', () => { const result = mapTicket(ticket, cms, 'en', 'UTC'); expect(result.topic.label).toBe('UNKNOWN_TOPIC'); - expect(result.type.label).toBe('UNKNOWN_TYPE'); + expect(result.type?.label).toBe('UNKNOWN_TYPE'); expect(result.status.label).toBe('UNKNOWN_STATUS'); }); + + it('should handle undefined type', () => { + const ticket = createMockTicket({ type: undefined }); + const cms = createMockCms(); + + const result = mapTicket(ticket, cms, 'en', 'UTC'); + + expect(result.type).toBeUndefined(); + expect(result.topic).toBeDefined(); + expect(result.status).toBeDefined(); + }); }); describe('date formatting', () => { diff --git a/packages/blocks/ticket-list/src/api-harmonization/ticket-list.mapper.ts b/packages/blocks/ticket-list/src/api-harmonization/ticket-list.mapper.ts index adc5659a1..79f19b28b 100644 --- a/packages/blocks/ticket-list/src/api-harmonization/ticket-list.mapper.ts +++ b/packages/blocks/ticket-list/src/api-harmonization/ticket-list.mapper.ts @@ -44,10 +44,12 @@ export const mapTicket = ( label: cms.fieldMapping.topic?.[ticket.topic] || ticket.topic, value: ticket.topic, }, - type: { - label: cms.fieldMapping.type?.[ticket.type] || ticket.type, - value: ticket.type, - }, + type: ticket.type + ? { + label: cms.fieldMapping.type?.[ticket.type] || ticket.type, + value: ticket.type, + } + : undefined, status: { label: cms.fieldMapping.status?.[ticket.status] || ticket.status, value: ticket.status, diff --git a/packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts b/packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts index 3b6a6a52b..c75bc35b9 100644 --- a/packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts +++ b/packages/blocks/ticket-list/src/api-harmonization/ticket-list.model.ts @@ -44,7 +44,7 @@ export class Ticket { value: Tickets.Model.Ticket['topic']; label: string; }; - type!: { + type?: { value: Tickets.Model.Ticket['type']; label: string; }; From d928907e60c4fb9b53a306dfb85bfc87a030fcb3 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 10:05:30 +0100 Subject: [PATCH 35/54] refactor(cms.fieldMapping): enhance field mapping return type to include additional key-value structure --- .../src/modules/cms/mappers/cms.fieldMapping.mapper.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/integrations/strapi-cms/src/modules/cms/mappers/cms.fieldMapping.mapper.ts b/packages/integrations/strapi-cms/src/modules/cms/mappers/cms.fieldMapping.mapper.ts index 589c1637d..302416b48 100644 --- a/packages/integrations/strapi-cms/src/modules/cms/mappers/cms.fieldMapping.mapper.ts +++ b/packages/integrations/strapi-cms/src/modules/cms/mappers/cms.fieldMapping.mapper.ts @@ -2,7 +2,9 @@ import { Models } from '@o2s/framework/modules'; import { FieldMappingFragment } from '@/generated/strapi'; -export const mapFields = (component: FieldMappingFragment[]): Models.Mapping.Mapping => { +export const mapFields = ( + component: FieldMappingFragment[], +): Models.Mapping.Mapping & { [key: string]: { [key: string]: string } } => { return component.reduce( (acc, field) => ({ ...acc, @@ -14,6 +16,6 @@ export const mapFields = (component: FieldMappingFragment[]): Models.Mapping. {} as { [key: string]: string }, ), }), - {} as Models.Mapping.Mapping, + {} as Models.Mapping.Mapping & { [key: string]: { [key: string]: string } }, ); }; From cfd9d36d1d299e2d6a7fab06f5a7f2cd3239c1ef Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 12:31:27 +0100 Subject: [PATCH 36/54] fix(surveyjs.mapper): improve attachment mapping by filtering invalid files --- .../surveyjs/src/api-harmonization/surveyjs.mapper.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts index fbf8b7631..a9e0b35f8 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts @@ -93,6 +93,11 @@ export const mapSurveyToTicket = (surveyPayload: SurveyResult): Tickets.Request. // Map attachments from Survey.js format to Tickets format const mappedAttachments = attachments ? (Array.isArray(attachments) ? attachments : [attachments]) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .filter((file: any) => { + // Guard against invalid attachments to prevent runtime errors + return file && typeof file.content === 'string' && file.name; + }) // eslint-disable-next-line @typescript-eslint/no-explicit-any .map((file: any) => { // Convert base64 string to Buffer @@ -107,11 +112,14 @@ export const mapSurveyToTicket = (surveyPayload: SurveyResult): Tickets.Request. }) : undefined; + // Convert empty array to undefined for cleaner API + const finalAttachments = mappedAttachments && mappedAttachments.length > 0 ? mappedAttachments : undefined; + return { title: title as string, description: description as string, ticketFormId: ticketFormId as number, - attachments: mappedAttachments, + attachments: finalAttachments, customFields, }; }; From 9b3b572e58986953ec2f1375d4b50cdb772c59f7 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 12:42:36 +0100 Subject: [PATCH 37/54] refactor(zendesk-ticket.service): removed unnecessary conversion of ticketFormId --- .../src/modules/tickets/zendesk-ticket.service.ts | 12 +++--------- .../src/api-harmonization/surveyjs.mapper.ts | 5 ++++- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index 8396973c2..8ea5b859f 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -197,12 +197,6 @@ export class ZendeskTicketService extends Tickets.Service { this.findZendeskUserByEmail(user.email!).pipe( switchMap((zendeskUser) => { // Map ticketFormId to topic value - // Ensure ticketFormId is a number for comparison - const ticketFormIdNum = - typeof data.ticketFormId === 'string' - ? Number(data.ticketFormId) - : data.ticketFormId; - let topicValue: string; const contactFormId = process.env.ZENDESK_CONTACT_FORM_ID ? Number(process.env.ZENDESK_CONTACT_FORM_ID) @@ -214,11 +208,11 @@ export class ZendeskTicketService extends Tickets.Service { ? Number(process.env.ZENDESK_REQUEST_DEVICE_MAINTENANCE_FORM_ID) : undefined; - if (ticketFormIdNum === contactFormId) { + if (data.ticketFormId === contactFormId) { topicValue = 'CONTACT_US'; - } else if (ticketFormIdNum === complaintFormId) { + } else if (data.ticketFormId === complaintFormId) { topicValue = 'COMPLAINT'; - } else if (ticketFormIdNum === deviceMaintenanceFormId) { + } else if (data.ticketFormId === deviceMaintenanceFormId) { topicValue = 'REQUEST_DEVICE_MAINTENANCE'; } else { return throwError( diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts index a9e0b35f8..794763ac9 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts @@ -90,6 +90,9 @@ export const mapSurveyToTicket = (surveyPayload: SurveyResult): Tickets.Request. // Extract standard fields const { title, description, ticketFormId, attachments, ...customFields } = surveyPayload; + // Ensure ticketFormId is a number (Survey.js may send it as string) + const ticketFormIdNum = typeof ticketFormId === 'string' ? Number(ticketFormId) : (ticketFormId as number); + // Map attachments from Survey.js format to Tickets format const mappedAttachments = attachments ? (Array.isArray(attachments) ? attachments : [attachments]) @@ -118,7 +121,7 @@ export const mapSurveyToTicket = (surveyPayload: SurveyResult): Tickets.Request. return { title: title as string, description: description as string, - ticketFormId: ticketFormId as number, + ticketFormId: ticketFormIdNum, attachments: finalAttachments, customFields, }; From c7dad1ad00bb99531fabcd8f2ace1026203946ab Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 12:53:23 +0100 Subject: [PATCH 38/54] fix(zendesk-ticket.mapper): ensure topic field is present and throw error if missing --- .../zendesk/src/modules/tickets/zendesk-ticket.mapper.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts index 2604f9711..14f750e79 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts @@ -26,9 +26,9 @@ export function mapTicketToModel( } // Determine topic from custom field if configured - let topic: string = 'GENERAL'; const topicFieldId = process.env.ZENDESK_TOPIC_FIELD_ID ? Number(process.env.ZENDESK_TOPIC_FIELD_ID) : undefined; + let topic: string | undefined; if (topicFieldId && ticket.custom_fields) { const topicField = ticket.custom_fields.find((field) => field.id === topicFieldId); if (topicField?.value) { @@ -36,6 +36,13 @@ export function mapTicketToModel( } } + if (!topic) { + throw new Error( + `Topic field not found or empty for ticket ${ticket.id}. ` + + `Ensure ZENDESK_TOPIC_FIELD_ID is configured and ticket has a topic value.`, + ); + } + const properties: Tickets.Model.TicketProperty[] = [ { id: 'subject', value: ticket.subject || '' }, { id: 'description', value: ticket.description || '' }, From 5004678a4bda50956c9bb4e86ddb6c39da627c5c Mon Sep 17 00:00:00 2001 From: Marcin Krasowski Date: Wed, 28 Jan 2026 13:48:31 +0100 Subject: [PATCH 39/54] feat: add changeset validation for framework/integrations/modules/blocks changes - Introduced a new `requires_changeset` output in the `changed-packages` workflow. - Added a step to determine if a changeset is required based on modified paths. - Updated CI/CD logic to validate changeset presence for specific package changes. --- .github/CI_CD_README.md | 7 ++++- .github/actions/changed-packages/action.yaml | 22 ++++++++++++++ .github/workflows/pull-request.yaml | 30 ++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/.github/CI_CD_README.md b/.github/CI_CD_README.md index 7865217c6..73e79ba36 100644 --- a/.github/CI_CD_README.md +++ b/.github/CI_CD_README.md @@ -11,6 +11,9 @@ graph TD A[PR Opened/Synchronized] --> B[skip-duplicate-check] B --> C[changed-packages] B --> D[build] + C --> O{Requires Changeset?} + O -->|Yes| P[check-changeset] + O -->|No| I[End] C --> E{docs package changed?} C --> L{stories changed?} D --> F[lint] @@ -18,7 +21,7 @@ graph TD F --> H[deploy-docs-preview] G --> H E -->|Yes| H - E -->|No| I[End] + E -->|No| I H --> J[Prepare Environment] J --> K[Deploy Docs to Vercel Preview] @@ -162,6 +165,7 @@ Runs code quality checks on PRs and deploys preview environments. - `skip-duplicate-check`: Prevents duplicate workflow runs - `changed-packages`: Determines which packages/stories changed +- `check-changeset`: Checks if a changeset exists when required packages are modified - `build`: Builds the project - `lint`: Lints the code - `test`: Runs tests @@ -205,6 +209,7 @@ Determines which packages have changed using Turborepo and detects Storybook cha - `package_changed`: JSON string containing changed packages information - `stories_changed`: `true` if any `*.stories.tsx` or `.storybook/` files changed +- `requires_changeset`: `true` if changes require a changeset (framework/integrations/modules/blocks) ### `deploy-vercel` diff --git a/.github/actions/changed-packages/action.yaml b/.github/actions/changed-packages/action.yaml index 64476a67d..62724854d 100644 --- a/.github/actions/changed-packages/action.yaml +++ b/.github/actions/changed-packages/action.yaml @@ -22,6 +22,9 @@ outputs: stories_changed: description: 'True if any storybook files or config changed' value: ${{ steps.storybook.outputs.stories_changed }} + requires_changeset: + description: 'True if changes require a changeset (framework/integrations/modules/blocks)' + value: ${{ steps.changeset-required.outputs.requires_changeset }} runs: using: 'composite' @@ -69,3 +72,22 @@ runs: echo "stories_changed=false" >> $GITHUB_OUTPUT echo "No storybook changes detected." fi + + - name: Determine if changeset is required + id: changeset-required + shell: bash + env: + TURBO_REF_FILTER: ${{ inputs.event-name == 'pull_request' && inputs.base-sha || 'HEAD^' }} + run: | + # Check if changes involve framework, integrations, modules, or blocks + CHANGESET_REQUIRED_FILES=$(git diff --name-only $TURBO_REF_FILTER | grep -E "^packages/(framework|integrations|modules|blocks)/" || true) + + if [ -n "$CHANGESET_REQUIRED_FILES" ]; then + echo "requires_changeset=true" >> $GITHUB_OUTPUT + echo "### Changes requiring changeset detected ###" + echo "$CHANGESET_REQUIRED_FILES" + echo "############################################" + else + echo "requires_changeset=false" >> $GITHUB_OUTPUT + echo "No changes requiring changeset detected." + fi diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 312c79151..cabc424cf 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -42,6 +42,7 @@ jobs: outputs: package_changed: ${{ steps.determine-changes.outputs.package_changed }} stories_changed: ${{ steps.determine-changes.outputs.stories_changed }} + requires_changeset: ${{ steps.determine-changes.outputs.requires_changeset }} steps: - name: Checkout repository uses: actions/checkout@v6 @@ -57,6 +58,35 @@ jobs: base-sha: ${{ github.event.pull_request.base.sha }} fetch-depth: '0' + check-changeset: + needs: [skip-duplicate-check, changed-packages] + if: | + needs.skip-duplicate-check.outputs.should_skip != 'true' && + needs.changed-packages.outputs.requires_changeset == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check for changeset + shell: bash + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + # Check if any .md file in .changeset (excluding README.md) has been modified/added in this PR + CHANGESET_DIFF=$(git diff --name-only $BASE_SHA | grep -E "^\.changeset/.*\.md$" | grep -v "README.md" || true) + + if [ -z "$CHANGESET_DIFF" ]; then + echo "::error::No changeset found in this PR! You modified packages in framework, integrations, modules, or blocks." + echo "::error::Please run 'npm run changeset' to add a changeset describing your changes." + exit 1 + else + echo "Changeset found in PR:" + echo "$CHANGESET_DIFF" + fi + build: needs: skip-duplicate-check if: needs.skip-duplicate-check.outputs.should_skip != 'true' From fc9980206a818b8d6cf3c1c5ac864f7bb77964ab Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 14:22:53 +0100 Subject: [PATCH 40/54] refactor(tickets): updated PostTicketBody class --- .../integrations/tickets/zendesk/features.md | 18 +++++------ .../core-model-tickets.md | 11 ++++--- .../src/modules/tickets/tickets.request.ts | 8 ++--- .../modules/tickets/zendesk-ticket.service.ts | 30 ++++++++++--------- .../src/api-harmonization/surveyjs.mapper.ts | 23 ++++++-------- .../src/api-harmonization/surveyjs.service.ts | 6 ++-- 6 files changed, 47 insertions(+), 49 deletions(-) diff --git a/apps/docs/docs/integrations/tickets/zendesk/features.md b/apps/docs/docs/integrations/tickets/zendesk/features.md index e806e43d5..3b0a7d092 100644 --- a/apps/docs/docs/integrations/tickets/zendesk/features.md +++ b/apps/docs/docs/integrations/tickets/zendesk/features.md @@ -184,41 +184,41 @@ To add support for a new custom field: ### Topic Field Mapping -The `topic` field is automatically set during ticket creation based on the `ticketFormId` provided in the ticket data. This ensures consistent categorization across different form types. +The `topic` field is automatically set during ticket creation based on the `type` field provided in the ticket data. This ensures consistent categorization across different form types. **How it works:** When creating a ticket via the Zendesk integration: -1. The system compares the `ticketFormId` with configured environment variables: +1. The system compares the `type` (ticket form ID) with configured environment variables: - `ZENDESK_CONTACT_FORM_ID` → topic value: `CONTACT_US` - `ZENDESK_COMPLAINT_FORM_ID` → topic value: `COMPLAINT` - `ZENDESK_REQUEST_DEVICE_MAINTENANCE_FORM_ID` → topic value: `REQUEST_DEVICE_MAINTENANCE` -2. The matching topic value is automatically added to the ticket's custom fields +2. The matching topic value is automatically added to the ticket's fields 3. The topic is then stored in Zendesk using the `ZENDESK_TOPIC_FIELD_ID` custom field **Example:** ```typescript -// Survey.js sends ticketFormId +// Survey.js sends type (ticket form ID) { - ticketFormId: 33406700504221, // Matches ZENDESK_CONTACT_FORM_ID - customFields: { ... } + type: 33406700504221, // Matches ZENDESK_CONTACT_FORM_ID + fields: { ... } } // Service automatically adds topic { - ticketFormId: 33406700504221, - customFields: { + type: 33406700504221, + fields: { topic: 'CONTACT_US', // Automatically set ... } } ``` -**Important**: If the `ticketFormId` doesn't match any configured form ID, the ticket creation will fail with a `BadRequestException`. This ensures that all tickets are properly categorized. +**Important**: If the `type` doesn't match any configured form ID, the ticket creation will fail with a `BadRequestException`. This ensures that all tickets are properly categorized. **Supported topic values:** - `CONTACT_US` - General contact inquiries diff --git a/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md b/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md index 6e343c40c..f36a54d7f 100644 --- a/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md +++ b/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md @@ -116,10 +116,13 @@ createTicket( #### Body Parameters -| Parameter | Type | Description | -| ----------- | ------ | --------------------------------- | -| title | string | Title or subject of the ticket | -| description | string | Detailed description of the issue | +| Parameter | Type | Required | Description | +| ----------- | ------------------------- | -------- | -------------------------------------------------------------- | +| title | string | No | Title or subject of the ticket | +| description | string | No | Detailed description of the issue | +| type | number | No | Ticket type identifier (e.g., form ID in ticket systems) | +| attachments | TicketAttachmentInput[] | No | Array of file attachments | +| fields | Record | No | Additional custom fields specific to the integration | #### Returns diff --git a/packages/framework/src/modules/tickets/tickets.request.ts b/packages/framework/src/modules/tickets/tickets.request.ts index a84ff6cd0..4588e032a 100644 --- a/packages/framework/src/modules/tickets/tickets.request.ts +++ b/packages/framework/src/modules/tickets/tickets.request.ts @@ -13,11 +13,11 @@ export class TicketAttachmentInput { } export class PostTicketBody { - title!: string; - description!: string; - ticketFormId!: number; + title?: string; + description?: string; + type?: number; attachments?: TicketAttachmentInput[]; - customFields?: Record; + fields?: Record; } export class GetTicketListQuery extends PaginationQuery { diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index 8ea5b859f..d4a9b9ba0 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -167,8 +167,9 @@ export class ZendeskTicketService extends Tickets.Service { createTicket(data: Tickets.Request.PostTicketBody, authorization?: string): Observable { // Validate input data - if (!data.description || !data.ticketFormId) { - return throwError(() => new BadRequestException('Title, description and ticketFormId are required')); + // Note: subject (title) is optional in Zendesk API, but description (as first comment body) and type (ticket form ID) are required + if (!data.description || !data.type) { + return throwError(() => new BadRequestException('Description and type are required')); } return this.usersService.getCurrentUser(authorization).pipe( @@ -196,7 +197,7 @@ export class ZendeskTicketService extends Tickets.Service { switchMap((uploadTokens) => this.findZendeskUserByEmail(user.email!).pipe( switchMap((zendeskUser) => { - // Map ticketFormId to topic value + // Map type (ticket form ID) to topic value let topicValue: string; const contactFormId = process.env.ZENDESK_CONTACT_FORM_ID ? Number(process.env.ZENDESK_CONTACT_FORM_ID) @@ -208,40 +209,41 @@ export class ZendeskTicketService extends Tickets.Service { ? Number(process.env.ZENDESK_REQUEST_DEVICE_MAINTENANCE_FORM_ID) : undefined; - if (data.ticketFormId === contactFormId) { + if (data.type === contactFormId) { topicValue = 'CONTACT_US'; - } else if (data.ticketFormId === complaintFormId) { + } else if (data.type === complaintFormId) { topicValue = 'COMPLAINT'; - } else if (data.ticketFormId === deviceMaintenanceFormId) { + } else if (data.type === deviceMaintenanceFormId) { topicValue = 'REQUEST_DEVICE_MAINTENANCE'; } else { return throwError( () => new BadRequestException( - `Invalid ticketFormId: ${data.ticketFormId}. Must match one of the configured form IDs.`, + `Invalid type: ${data.type}. Must match one of the configured form IDs.`, ), ); } - // Add topic to customFields before mapping - const customFieldsWithTopic = { - ...(data.customFields || {}), + // Add topic to fields before mapping + const fieldsWithTopic = { + ...(data.fields || {}), topic: topicValue, }; - // Map custom fields from Survey.js to Zendesk format using field mapper - const customFields = ZendeskFieldMapper.toCustomFields(customFieldsWithTopic); + // Map fields to Zendesk custom fields format using field mapper + const customFields = ZendeskFieldMapper.toCustomFields(fieldsWithTopic); return from( createTicket({ body: { ticket: { - subject: data.title, + // Subject is optional in Zendesk API + ...(data.title && { subject: data.title }), comment: { body: data.description, ...(uploadTokens.length > 0 && { uploads: uploadTokens }), }, - ticket_form_id: data.ticketFormId, + ticket_form_id: data.type, ...(zendeskUser?.id && { requester_id: zendeskUser.id, submitter_id: zendeskUser.id, diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts index 794763ac9..392608316 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts @@ -79,19 +79,14 @@ const mapData = (element: Panelbase): Panelbase => { }; }; -/** - * Maps Survey.js payload to ticket creation format. - * Extracts standard fields (title, description, ticketFormId, attachments) and passes - * all other fields as customFields to be mapped by ZendeskFieldMapper. - * - * Note: Caller must validate that title, description, and ticketFormId are non-empty before calling this function. - */ export const mapSurveyToTicket = (surveyPayload: SurveyResult): Tickets.Request.PostTicketBody => { - // Extract standard fields - const { title, description, ticketFormId, attachments, ...customFields } = surveyPayload; + const { title, description, ticketFormId, attachments, ...fields } = surveyPayload; - // Ensure ticketFormId is a number (Survey.js may send it as string) + // Ensure type is a number (Survey.js may send it as string) const ticketFormIdNum = typeof ticketFormId === 'string' ? Number(ticketFormId) : (ticketFormId as number); + if (!Number.isFinite(ticketFormIdNum)) { + throw new Error('Invalid ticketFormId: must be a valid number'); + } // Map attachments from Survey.js format to Tickets format const mappedAttachments = attachments @@ -119,10 +114,10 @@ export const mapSurveyToTicket = (surveyPayload: SurveyResult): Tickets.Request. const finalAttachments = mappedAttachments && mappedAttachments.length > 0 ? mappedAttachments : undefined; return { - title: title as string, - description: description as string, - ticketFormId: ticketFormIdNum, + title: title as string | undefined, + description: description as string | undefined, + type: ticketFormIdNum, attachments: finalAttachments, - customFields, + fields, }; }; diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts index db35edbe5..9e3c589d6 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.service.ts @@ -138,10 +138,8 @@ export class SurveyjsService { try { // Validate required fields before mapping if (!surveyPayload.description || !surveyPayload.ticketFormId) { - this.logger.error( - 'Missing required fields for ticket creation: title, description, and ticketFormId are required', - ); - throw new BadRequestException('Title, description, and ticketFormId are required to create a ticket'); + this.logger.error('Missing required fields for ticket creation: description and type are required'); + throw new BadRequestException('Description and type are required to create a ticket'); } const ticketData = mapSurveyToTicket(surveyPayload); From 25c7cb6526513607e51eecc2ca4ff5d653a7cb8b Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 14:46:18 +0100 Subject: [PATCH 41/54] docs(zendesk.features): correct formatting for environment variables --- apps/docs/docs/integrations/tickets/zendesk/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/docs/integrations/tickets/zendesk/features.md b/apps/docs/docs/integrations/tickets/zendesk/features.md index 3b0a7d092..0215cc326 100644 --- a/apps/docs/docs/integrations/tickets/zendesk/features.md +++ b/apps/docs/docs/integrations/tickets/zendesk/features.md @@ -160,7 +160,7 @@ Custom fields from Zendesk are mapped to readable names using the `ZendeskFieldM To add support for a new custom field: 1. **Add environment variable**: - ``` + ```env ZENDESK_NEW_FIELD_ID=789012 ``` From b168499b41d7422856c4182017e3d04bdd2f634b Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 14:57:34 +0100 Subject: [PATCH 42/54] fix(ticket-list): conditionally render ticket ID label based on availability --- .../blocks/ticket-list/src/frontend/TicketList.client.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx b/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx index 8b508d177..1b0eb31c6 100644 --- a/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx +++ b/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx @@ -121,9 +121,11 @@ export const TicketListPure: React.FC = ({ locale, accessTo - - {data.labels.ticketId}: {ticket.id} - + {data.labels.ticketId && ( + + {data.labels.ticketId}: {ticket.id} + + )}
), }; From 26ce47a3cf7a89bf4d9b72043b3d1755d9867f72 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 14:58:30 +0100 Subject: [PATCH 43/54] fix(ticket-recent.mapper): update type field to be conditionally defined --- .../src/api-harmonization/ticket-recent.mapper.ts | 8 +++++--- .../src/api-harmonization/ticket-recent.model.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/blocks/ticket-recent/src/api-harmonization/ticket-recent.mapper.ts b/packages/blocks/ticket-recent/src/api-harmonization/ticket-recent.mapper.ts index 218d14957..330d33cf3 100644 --- a/packages/blocks/ticket-recent/src/api-harmonization/ticket-recent.mapper.ts +++ b/packages/blocks/ticket-recent/src/api-harmonization/ticket-recent.mapper.ts @@ -36,9 +36,11 @@ export const mapTicket = ( topic: { value: ticket.topic, }, - type: { - value: ticket.type, - }, + type: ticket.type + ? { + value: ticket.type, + } + : undefined, status: { value: ticket.status, }, diff --git a/packages/blocks/ticket-recent/src/api-harmonization/ticket-recent.model.ts b/packages/blocks/ticket-recent/src/api-harmonization/ticket-recent.model.ts index 4a46e31ba..18c25b3fd 100644 --- a/packages/blocks/ticket-recent/src/api-harmonization/ticket-recent.model.ts +++ b/packages/blocks/ticket-recent/src/api-harmonization/ticket-recent.model.ts @@ -23,7 +23,7 @@ export class Ticket { topic!: { value: Tickets.Model.Ticket['topic']; }; - type!: { + type?: { value: Tickets.Model.Ticket['type']; }; status!: { From 42612b2b48a704508d40cb8d8931372cf5512307 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 15:00:08 +0100 Subject: [PATCH 44/54] fix(ticket-list.mapper): update label for updatedAt field --- .../src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts index 2b04cb2ef..16ea8e23e 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts @@ -303,7 +303,7 @@ const MOCK_TICKET_LIST_BLOCK_PL: CMS.Model.TicketListBlock.TicketListBlock = { columns: [ { id: 'topic', title: 'Typ zgłoszenia' }, { id: 'status', title: 'Status' }, - { id: 'updatedAt', title: 'Data utworzenia' }, + { id: 'updatedAt', title: 'Data aktualizacji' }, ], actions: { title: 'Akcja', From d8b75c2c033940af6524c85461dbee603cefcecb Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 15:23:52 +0100 Subject: [PATCH 45/54] refactor(zendesk-field.mapper): convert class methods to standalone functions --- .../modules/tickets/zendesk-field.mapper.ts | 293 +++++++++--------- .../modules/tickets/zendesk-ticket.mapper.ts | 8 +- .../modules/tickets/zendesk-ticket.service.ts | 29 +- 3 files changed, 172 insertions(+), 158 deletions(-) diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts index ab7298484..63dc9b5d1 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts @@ -9,172 +9,165 @@ export interface ZendeskCustomField { } /** - * Mapper for converting Survey.js fields to Zendesk custom fields. - * Field IDs are configured via environment variables (ZENDESK_*_FIELD_ID). + * Gets the field map dynamically from environment variables. * * To add new custom field mapping: * 1. Add environment variable (e.g., ZENDESK_NEW_FIELD_ID=123456) - * 2. Add mapping to fieldMap: newField: Number(process.env.ZENDESK_NEW_FIELD_ID) || undefined + * 2. Add mapping to this function: newField: Number(process.env.ZENDESK_NEW_FIELD_ID) || undefined * 3. Include the field in Survey.js form with matching name */ -export class ZendeskFieldMapper { - /** - * Gets the field map dynamically from environment variables. - * Using a getter ensures environment variables are read at runtime, not during module initialization. - */ - private static get fieldMap(): Record { - return { - machineName: process.env.ZENDESK_DEVICE_NAME_FIELD_ID - ? Number(process.env.ZENDESK_DEVICE_NAME_FIELD_ID) - : undefined, - serialNumber: process.env.ZENDESK_SERIAL_NUMBER_FIELD_ID - ? Number(process.env.ZENDESK_SERIAL_NUMBER_FIELD_ID) - : undefined, - maintenanceType: process.env.ZENDESK_MAINTENANCE_TYPE_FIELD_ID - ? Number(process.env.ZENDESK_MAINTENANCE_TYPE_FIELD_ID) - : undefined, - preferredDate: process.env.ZENDESK_MAINTENANCE_PREFERRED_DATE_FIELD_ID - ? Number(process.env.ZENDESK_MAINTENANCE_PREFERRED_DATE_FIELD_ID) - : undefined, - additionalNotes: process.env.ZENDESK_ADDITIONAL_NOTES_FIELD_ID - ? Number(process.env.ZENDESK_ADDITIONAL_NOTES_FIELD_ID) - : undefined, - contactInformation: process.env.ZENDESK_CONTACT_FIELD_ID - ? Number(process.env.ZENDESK_CONTACT_FIELD_ID) - : undefined, - - issueDate: process.env.ZENDESK_ISSUE_DATE_FIELD_ID - ? Number(process.env.ZENDESK_ISSUE_DATE_FIELD_ID) - : undefined, - organizationName: process.env.ZENDESK_COMPANY_NAME_FIELD_ID - ? Number(process.env.ZENDESK_COMPANY_NAME_FIELD_ID) - : undefined, - firstName: process.env.ZENDESK_FIRST_NAME_FIELD_ID - ? Number(process.env.ZENDESK_FIRST_NAME_FIELD_ID) - : undefined, - lastName: process.env.ZENDESK_LAST_NAME_FIELD_ID - ? Number(process.env.ZENDESK_LAST_NAME_FIELD_ID) - : undefined, - email: process.env.ZENDESK_EMAIL_FIELD_ID ? Number(process.env.ZENDESK_EMAIL_FIELD_ID) : undefined, - phone: process.env.ZENDESK_PHONE_FIELD_ID ? Number(process.env.ZENDESK_PHONE_FIELD_ID) : undefined, - invoiceNumber: process.env.ZENDESK_INVOICE_NUMBER_FIELD_ID - ? Number(process.env.ZENDESK_INVOICE_NUMBER_FIELD_ID) - : undefined, - - address: process.env.ZENDESK_ADDRESS_FIELD_ID ? Number(process.env.ZENDESK_ADDRESS_FIELD_ID) : undefined, - topic: process.env.ZENDESK_TOPIC_FIELD_ID ? Number(process.env.ZENDESK_TOPIC_FIELD_ID) : undefined, - inquiryType: process.env.ZENDESK_INQUIRY_TYPE_FIELD_ID - ? Number(process.env.ZENDESK_INQUIRY_TYPE_FIELD_ID) - : undefined, - productCategory: process.env.ZENDESK_PRODUCT_CATEGORY_FIELD_ID - ? Number(process.env.ZENDESK_PRODUCT_CATEGORY_FIELD_ID) - : undefined, - preferredContactMethod: process.env.ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID - ? Number(process.env.ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID) - : undefined, - termsAcceptance: process.env.ZENDESK_TERMS_ACCEPTANCE_FIELD_ID - ? Number(process.env.ZENDESK_TERMS_ACCEPTANCE_FIELD_ID) - : undefined, - newsletterConsent: process.env.ZENDESK_NEWSLETTER_CONSENT_FIELD_ID - ? Number(process.env.ZENDESK_NEWSLETTER_CONSENT_FIELD_ID) - : undefined, - marketingConsent: process.env.ZENDESK_MARKETING_CONSENT_FIELD_ID - ? Number(process.env.ZENDESK_MARKETING_CONSENT_FIELD_ID) - : undefined, - // Add more custom fields mappings here as needed - }; - } +const getFieldMap = (): Record => { + return { + machineName: process.env.ZENDESK_DEVICE_NAME_FIELD_ID + ? Number(process.env.ZENDESK_DEVICE_NAME_FIELD_ID) + : undefined, + serialNumber: process.env.ZENDESK_SERIAL_NUMBER_FIELD_ID + ? Number(process.env.ZENDESK_SERIAL_NUMBER_FIELD_ID) + : undefined, + maintenanceType: process.env.ZENDESK_MAINTENANCE_TYPE_FIELD_ID + ? Number(process.env.ZENDESK_MAINTENANCE_TYPE_FIELD_ID) + : undefined, + preferredDate: process.env.ZENDESK_MAINTENANCE_PREFERRED_DATE_FIELD_ID + ? Number(process.env.ZENDESK_MAINTENANCE_PREFERRED_DATE_FIELD_ID) + : undefined, + additionalNotes: process.env.ZENDESK_ADDITIONAL_NOTES_FIELD_ID + ? Number(process.env.ZENDESK_ADDITIONAL_NOTES_FIELD_ID) + : undefined, + contactInformation: process.env.ZENDESK_CONTACT_FIELD_ID + ? Number(process.env.ZENDESK_CONTACT_FIELD_ID) + : undefined, + + issueDate: process.env.ZENDESK_ISSUE_DATE_FIELD_ID + ? Number(process.env.ZENDESK_ISSUE_DATE_FIELD_ID) + : undefined, + organizationName: process.env.ZENDESK_COMPANY_NAME_FIELD_ID + ? Number(process.env.ZENDESK_COMPANY_NAME_FIELD_ID) + : undefined, + firstName: process.env.ZENDESK_FIRST_NAME_FIELD_ID + ? Number(process.env.ZENDESK_FIRST_NAME_FIELD_ID) + : undefined, + lastName: process.env.ZENDESK_LAST_NAME_FIELD_ID ? Number(process.env.ZENDESK_LAST_NAME_FIELD_ID) : undefined, + email: process.env.ZENDESK_EMAIL_FIELD_ID ? Number(process.env.ZENDESK_EMAIL_FIELD_ID) : undefined, + phone: process.env.ZENDESK_PHONE_FIELD_ID ? Number(process.env.ZENDESK_PHONE_FIELD_ID) : undefined, + invoiceNumber: process.env.ZENDESK_INVOICE_NUMBER_FIELD_ID + ? Number(process.env.ZENDESK_INVOICE_NUMBER_FIELD_ID) + : undefined, + + address: process.env.ZENDESK_ADDRESS_FIELD_ID ? Number(process.env.ZENDESK_ADDRESS_FIELD_ID) : undefined, + topic: process.env.ZENDESK_TOPIC_FIELD_ID ? Number(process.env.ZENDESK_TOPIC_FIELD_ID) : undefined, + inquiryType: process.env.ZENDESK_INQUIRY_TYPE_FIELD_ID + ? Number(process.env.ZENDESK_INQUIRY_TYPE_FIELD_ID) + : undefined, + productCategory: process.env.ZENDESK_PRODUCT_CATEGORY_FIELD_ID + ? Number(process.env.ZENDESK_PRODUCT_CATEGORY_FIELD_ID) + : undefined, + preferredContactMethod: process.env.ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID + ? Number(process.env.ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID) + : undefined, + termsAcceptance: process.env.ZENDESK_TERMS_ACCEPTANCE_FIELD_ID + ? Number(process.env.ZENDESK_TERMS_ACCEPTANCE_FIELD_ID) + : undefined, + newsletterConsent: process.env.ZENDESK_NEWSLETTER_CONSENT_FIELD_ID + ? Number(process.env.ZENDESK_NEWSLETTER_CONSENT_FIELD_ID) + : undefined, + marketingConsent: process.env.ZENDESK_MARKETING_CONSENT_FIELD_ID + ? Number(process.env.ZENDESK_MARKETING_CONSENT_FIELD_ID) + : undefined, + // Add more custom fields mappings here as needed + }; +}; - /** - * Gets the field key (name) by its Zendesk field ID. - * Used for reverse mapping when reading tickets from Zendesk. - * - * @param fieldId - Zendesk custom field ID - * @returns Field key (e.g., 'machineName', 'serialNumber') or undefined if not found - */ - static getFieldKeyById(fieldId: number): string | undefined { - for (const [key, id] of Object.entries(this.fieldMap)) { - if (id === fieldId) { - return key; - } +/** + * Gets the field key (name) by its Zendesk field ID. + * Used for reverse mapping when reading tickets from Zendesk. + * + * @param fieldId - Zendesk custom field ID + * @returns Field key (e.g., 'machineName', 'serialNumber') or undefined if not found + */ +export const getFieldKeyById = (fieldId: number): string | undefined => { + const fieldMap = getFieldMap(); + for (const [key, id] of Object.entries(fieldMap)) { + if (id === fieldId) { + return key; } - return undefined; } + return undefined; +}; - /** - * Converts a record of field values to Zendesk custom fields format. - * Only includes fields that: - * - Have a mapping in fieldMap - * - Have a valid environment variable configured - * - Have non-null/undefined values - * - * @param data - Object with field names as keys and their values - * @returns Array of Zendesk custom field objects with id and value - */ - static toCustomFields(data: Record): ZendeskCustomField[] { - const customFields: ZendeskCustomField[] = []; - - for (const [fieldName, fieldValue] of Object.entries(data)) { - // Skip if value is null or undefined - if (fieldValue === null || fieldValue === undefined) { - continue; - } - - // Get field ID from mapping - const fieldId = this.fieldMap[fieldName]; - - // Skip if field is not mapped or environment variable is not configured - if (!fieldId || isNaN(fieldId)) { - continue; - } - - // Validate and convert value to supported types - const validatedValue = this.validateAndConvertValue(fieldValue); - - if (validatedValue !== null) { - customFields.push({ - id: fieldId, - value: validatedValue, - }); - } - } +/** + * Validates and converts field value to Zendesk-supported types. + * Zendesk API accepts: string, number, boolean + * Date fields must be in YYYY-MM-DD format + * + * @param value - The value to validate and convert + * @returns Validated value or null if invalid + */ +const validateAndConvertValue = (value: unknown): string | number | boolean | null => { + // Handle primitives + if (typeof value === 'number' || typeof value === 'boolean') { + return value; + } - return customFields; + if (typeof value === 'string') { + // Convert date strings to YYYY-MM-DD format for date fields + if (/\d{4}-\d{2}-\d{2}/.test(value)) { + const date = new Date(value); + return isNaN(date.getTime()) ? value : (date.toISOString().split('T')[0] ?? value); + } + return value; } - /** - * Validates and converts field value to Zendesk-supported types. - * Zendesk API accepts: string, number, boolean - * Date fields must be in YYYY-MM-DD format - * - * @param value - The value to validate and convert - * @returns Validated value or null if invalid - */ - private static validateAndConvertValue(value: unknown): string | number | boolean | null { - // Handle primitives - if (typeof value === 'number' || typeof value === 'boolean') { - return value; + // Convert arrays and objects to JSON strings + if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { + try { + return JSON.stringify(value); + } catch { + return null; } + } + + // Invalid type + return null; +}; - if (typeof value === 'string') { - // Convert date strings to YYYY-MM-DD format for date fields - if (/\d{4}-\d{2}-\d{2}/.test(value)) { - const date = new Date(value); - return isNaN(date.getTime()) ? value : (date.toISOString().split('T')[0] ?? value); - } - return value; +/** + * Converts a record of field values to Zendesk custom fields format. + * Only includes fields that: + * - Have a mapping in fieldMap + * - Have a valid environment variable configured + * - Have non-null/undefined values + * + * @param data - Object with field names as keys and their values + * @returns Array of Zendesk custom field objects with id and value + */ +export const toCustomFields = (data: Record): ZendeskCustomField[] => { + const customFields: ZendeskCustomField[] = []; + const fieldMap = getFieldMap(); + + for (const [fieldName, fieldValue] of Object.entries(data)) { + // Skip if value is null or undefined + if (fieldValue === null || fieldValue === undefined) { + continue; } - // Convert arrays and objects to JSON strings - if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { - try { - return JSON.stringify(value); - } catch { - return null; - } + // Get field ID from mapping + const fieldId = fieldMap[fieldName]; + + // Skip if field is not mapped or environment variable is not configured + if (!fieldId || isNaN(fieldId)) { + continue; } - // Invalid type - return null; + // Validate and convert value to supported types + const validatedValue = validateAndConvertValue(fieldValue); + + if (validatedValue !== null) { + customFields.push({ + id: fieldId, + value: validatedValue, + }); + } } -} + + return customFields; +}; diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts index 14f750e79..8a494b29b 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts @@ -2,7 +2,7 @@ import { Tickets } from '@o2s/framework/modules'; import { type TicketCommentObject, type TicketObject, type UserObject } from '@/generated/zendesk'; -import { ZendeskFieldMapper } from './zendesk-field.mapper'; +import { getFieldKeyById } from './zendesk-field.mapper'; type ZendeskTicket = TicketObject; @@ -52,9 +52,11 @@ export function mapTicketToModel( if (ticket.custom_fields) { ticket.custom_fields.forEach((field) => { if (field.value !== null && field.value !== undefined) { - const fieldKey = ZendeskFieldMapper.getFieldKeyById(field.id!); + const fieldKey = getFieldKeyById(field.id!); - if (fieldKey) { + // Skip 'topic' field as it's already set as top-level property from custom_fields + // to avoid duplicate/conflicting entries + if (fieldKey && fieldKey.toLowerCase() !== 'topic') { properties.push({ id: fieldKey, value: String(field.value), diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index d4a9b9ba0..0b5d9a583 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -19,7 +19,7 @@ import { } from '@/generated/zendesk'; import { client } from '@/generated/zendesk/client.gen'; -import { ZendeskFieldMapper } from './zendesk-field.mapper'; +import { toCustomFields } from './zendesk-field.mapper'; import { mapTicketToModel } from './zendesk-ticket.mapper'; type ZendeskTicket = TicketObject; @@ -231,7 +231,22 @@ export class ZendeskTicketService extends Tickets.Service { }; // Map fields to Zendesk custom fields format using field mapper - const customFields = ZendeskFieldMapper.toCustomFields(fieldsWithTopic); + const customFields = toCustomFields(fieldsWithTopic); + + // Validate that topic custom field was successfully mapped + // This ensures ZENDESK_TOPIC_FIELD_ID is configured and prevents creating tickets + // that cannot be read back (mapTicketToModel requires topic field) + const topicFieldId = process.env.ZENDESK_TOPIC_FIELD_ID + ? Number(process.env.ZENDESK_TOPIC_FIELD_ID) + : undefined; + if (!topicFieldId || !customFields.some((f) => f.id === topicFieldId)) { + return throwError( + () => + new InternalServerErrorException( + 'ZENDESK_TOPIC_FIELD_ID is required to map topic.', + ), + ); + } return from( createTicket({ @@ -380,9 +395,13 @@ export class ZendeskTicketService extends Tickets.Service { const matchedUser = users.find((u) => u.email?.toLowerCase() === normalizedEmail); return matchedUser; }), - catchError(() => { - // If search fails, return undefined (ticket will be created without requester/submitter ids) - return of(undefined); + catchError((error) => { + // Treat 404 as "user not found" (return undefined so ticket is created without requester/submitter ids) + if (error?.status === 404 || error?.message?.includes('404')) { + return of(undefined); + } + // Propagate other errors (network, auth, rate-limit, etc.) + return throwError(() => new Error(`Failed to search users: ${error.message || error}`)); }), ); } From ed7d5648ba118a003122bb25f1f21738bfa26bf4 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 17:00:49 +0100 Subject: [PATCH 46/54] feat(zendesk-field.mapper): add consent field handling and improve value validation --- .../modules/tickets/zendesk-field.mapper.ts | 45 +++++++++++++++++-- .../modules/tickets/zendesk-ticket.mapper.ts | 22 +++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts index 63dc9b5d1..15b54d45b 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts @@ -94,15 +94,34 @@ export const getFieldKeyById = (fieldId: number): string | undefined => { return undefined; }; +/** + * Checks if a field name represents a consent/checkbox field. + * These fields expect array values from SurveyJS that need to be converted to boolean. + * Only checks fields that have configured environment variables. + * + * @param fieldName - The name of the field to check + * @returns true if field is a consent field with configured env variable + */ +const isConsentField = (fieldName: string): boolean => { + const consentFieldMap: Record = { + termsAcceptance: process.env.ZENDESK_TERMS_ACCEPTANCE_FIELD_ID, + newsletterConsent: process.env.ZENDESK_NEWSLETTER_CONSENT_FIELD_ID, + marketingConsent: process.env.ZENDESK_MARKETING_CONSENT_FIELD_ID, + }; + + return fieldName in consentFieldMap && !!consentFieldMap[fieldName]; +}; + /** * Validates and converts field value to Zendesk-supported types. * Zendesk API accepts: string, number, boolean * Date fields must be in YYYY-MM-DD format * + * @param fieldName - The name of the field being converted * @param value - The value to validate and convert * @returns Validated value or null if invalid */ -const validateAndConvertValue = (value: unknown): string | number | boolean | null => { +const validateAndConvertValue = (fieldName: string, value: unknown): string | number | boolean | null => { // Handle primitives if (typeof value === 'number' || typeof value === 'boolean') { return value; @@ -117,8 +136,26 @@ const validateAndConvertValue = (value: unknown): string | number | boolean | nu return value; } - // Convert arrays and objects to JSON strings - if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { + // Handle arrays ONLY for consent fields - SurveyJS sends checkbox values as arrays + if (Array.isArray(value)) { + if (isConsentField(fieldName)) { + // For consent checkbox fields: non-empty array = true, empty array = false + // SurveyJS checkbox values: ['accepted'], ['subscribed'], etc. + if (value.length > 0) { + return true; + } + return false; + } + // For non-consent array fields, convert to JSON string + try { + return JSON.stringify(value); + } catch { + return null; + } + } + + // Convert objects to JSON strings + if (typeof value === 'object' && value !== null) { try { return JSON.stringify(value); } catch { @@ -159,7 +196,7 @@ export const toCustomFields = (data: Record): ZendeskCustomFiel } // Validate and convert value to supported types - const validatedValue = validateAndConvertValue(fieldValue); + const validatedValue = validateAndConvertValue(fieldName, fieldValue); if (validatedValue !== null) { customFields.push({ diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts index 8a494b29b..1ba3c6194 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts @@ -48,6 +48,19 @@ export function mapTicketToModel( { id: 'description', value: ticket.description || '' }, ]; + // Get consent field IDs from environment variables + const consentFieldIds = [ + process.env.ZENDESK_TERMS_ACCEPTANCE_FIELD_ID ? Number(process.env.ZENDESK_TERMS_ACCEPTANCE_FIELD_ID) : null, + process.env.ZENDESK_NEWSLETTER_CONSENT_FIELD_ID + ? Number(process.env.ZENDESK_NEWSLETTER_CONSENT_FIELD_ID) + : null, + process.env.ZENDESK_MARKETING_CONSENT_FIELD_ID ? Number(process.env.ZENDESK_MARKETING_CONSENT_FIELD_ID) : null, + ].filter((id): id is number => id !== null); + + // Check if this is CONTACT_US form by comparing ticket_form_id + const contactFormId = process.env.ZENDESK_CONTACT_FORM_ID ? Number(process.env.ZENDESK_CONTACT_FORM_ID) : undefined; + const isContactUsForm = ticket.ticket_form_id === contactFormId; + // Map custom fields to properties using readable names from ZendeskFieldMapper if (ticket.custom_fields) { ticket.custom_fields.forEach((field) => { @@ -57,6 +70,15 @@ export function mapTicketToModel( // Skip 'topic' field as it's already set as top-level property from custom_fields // to avoid duplicate/conflicting entries if (fieldKey && fieldKey.toLowerCase() !== 'topic') { + // For CONTACT_US form: show consent fields even if false + // For other forms: skip boolean fields with false value (unchecked checkboxes) + const isConsentField = consentFieldIds.includes(field.id!); + if (typeof field.value === 'boolean' && field.value === false) { + if (!isContactUsForm || !isConsentField) { + return; + } + } + properties.push({ id: fieldKey, value: String(field.value), From 2f80a738b65f47f69a4e0a48b291335141377a47 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 17:09:49 +0100 Subject: [PATCH 47/54] feat(ticket-details.mapper): added cms mappers for consents --- .../blocks/cms.ticket-details.mapper.ts | 36 +++++++++++++++++++ .../blocks/cms.ticket-details.mapper.ts | 36 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts index d879c6198..3e60df883 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts @@ -63,6 +63,18 @@ const MOCK_TICKET_DETAILS_BLOCK_EN: CMS.Model.TicketDetailsBlock.TicketDetailsBl preventive_maintenance: 'Preventive Maintenance', corrective_maintenance: 'Corrective Maintenance', }, + termsAcceptance: { + true: 'Yes', + false: 'No', + }, + newsletterConsent: { + true: 'Yes', + false: 'No', + }, + marketingConsent: { + true: 'Yes', + false: 'No', + }, }, labels: { showMore: 'Show case details', @@ -135,6 +147,18 @@ const MOCK_TICKET_DETAILS_BLOCK_PL: CMS.Model.TicketDetailsBlock.TicketDetailsBl preventive_maintenance: 'Konserwacja zapobiegawcza', corrective_maintenance: 'Konserwacja naprawcza', }, + termsAcceptance: { + true: 'Tak', + false: 'Nie', + }, + newsletterConsent: { + true: 'Tak', + false: 'Nie', + }, + marketingConsent: { + true: 'Tak', + false: 'Nie', + }, }, labels: { showMore: 'Pokaż szczegóły zgłoszenia', @@ -207,6 +231,18 @@ const MOCK_TICKET_DETAILS_BLOCK_DE: CMS.Model.TicketDetailsBlock.TicketDetailsBl preventive_maintenance: 'Vorbeugende Wartung', corrective_maintenance: 'Korrigierende Wartung', }, + termsAcceptance: { + true: 'Ja', + false: 'Nein', + }, + newsletterConsent: { + true: 'Ja', + false: 'Nein', + }, + marketingConsent: { + true: 'Ja', + false: 'Nein', + }, }, labels: { showMore: 'Falldetails anzeigen', diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts index d879c6198..3e60df883 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts @@ -63,6 +63,18 @@ const MOCK_TICKET_DETAILS_BLOCK_EN: CMS.Model.TicketDetailsBlock.TicketDetailsBl preventive_maintenance: 'Preventive Maintenance', corrective_maintenance: 'Corrective Maintenance', }, + termsAcceptance: { + true: 'Yes', + false: 'No', + }, + newsletterConsent: { + true: 'Yes', + false: 'No', + }, + marketingConsent: { + true: 'Yes', + false: 'No', + }, }, labels: { showMore: 'Show case details', @@ -135,6 +147,18 @@ const MOCK_TICKET_DETAILS_BLOCK_PL: CMS.Model.TicketDetailsBlock.TicketDetailsBl preventive_maintenance: 'Konserwacja zapobiegawcza', corrective_maintenance: 'Konserwacja naprawcza', }, + termsAcceptance: { + true: 'Tak', + false: 'Nie', + }, + newsletterConsent: { + true: 'Tak', + false: 'Nie', + }, + marketingConsent: { + true: 'Tak', + false: 'Nie', + }, }, labels: { showMore: 'Pokaż szczegóły zgłoszenia', @@ -207,6 +231,18 @@ const MOCK_TICKET_DETAILS_BLOCK_DE: CMS.Model.TicketDetailsBlock.TicketDetailsBl preventive_maintenance: 'Vorbeugende Wartung', corrective_maintenance: 'Korrigierende Wartung', }, + termsAcceptance: { + true: 'Ja', + false: 'Nein', + }, + newsletterConsent: { + true: 'Ja', + false: 'Nein', + }, + marketingConsent: { + true: 'Ja', + false: 'Nein', + }, }, labels: { showMore: 'Falldetails anzeigen', From 13ecd36518df1d09bd271e033b8ad2cc40c7c933 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 17:15:17 +0100 Subject: [PATCH 48/54] fix(zendesk-ticket.mapper): ensure correct identification of CONTACT_US --- .../zendesk/src/modules/tickets/zendesk-ticket.mapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts index 1ba3c6194..8f50c0a9d 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts @@ -59,7 +59,7 @@ export function mapTicketToModel( // Check if this is CONTACT_US form by comparing ticket_form_id const contactFormId = process.env.ZENDESK_CONTACT_FORM_ID ? Number(process.env.ZENDESK_CONTACT_FORM_ID) : undefined; - const isContactUsForm = ticket.ticket_form_id === contactFormId; + const isContactUsForm = contactFormId !== undefined && ticket.ticket_form_id === contactFormId; // Map custom fields to properties using readable names from ZendeskFieldMapper if (ticket.custom_fields) { From 91c77c403ab2db4242d0bc77c7e6c9c2d7daf091 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 18:16:05 +0100 Subject: [PATCH 49/54] fix(zendesk-ticket.service): correct tag syntax in search query --- .../zendesk/src/modules/tickets/zendesk-ticket.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index 0b5d9a583..d02d1f066 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -130,7 +130,7 @@ export class ZendeskTicketService extends Tickets.Service { } if (options.topic) { - searchQuery += ` tag:${options.topic.toLowerCase()}`; + searchQuery += ` tags:${options.topic.toLowerCase()}`; } if (options.dateFrom) { From e1d21c546a80ac862f2e9284eeaf252cdf766872 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 18:40:11 +0100 Subject: [PATCH 50/54] feat(tickets.request): allow status to accept multiple values as an array --- .../src/modules/tickets/tickets.request.ts | 2 +- .../blocks/cms.ticket-details.mapper.ts | 12 +++++++++ .../blocks/cms.ticket-details.mapper.ts | 12 +++++++++ .../mappers/blocks/cms.ticket-list.mapper.ts | 3 --- .../modules/tickets/zendesk-ticket.service.ts | 27 ++++++++++++++++++- 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/framework/src/modules/tickets/tickets.request.ts b/packages/framework/src/modules/tickets/tickets.request.ts index 4588e032a..74787e1f9 100644 --- a/packages/framework/src/modules/tickets/tickets.request.ts +++ b/packages/framework/src/modules/tickets/tickets.request.ts @@ -23,7 +23,7 @@ export class PostTicketBody { export class GetTicketListQuery extends PaginationQuery { topic?: string; type?: string; - status?: TicketStatus; + status?: TicketStatus | TicketStatus[]; dateFrom?: Date; dateTo?: Date; sort?: string; diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts index 3e60df883..e62c71dc6 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts @@ -63,6 +63,10 @@ const MOCK_TICKET_DETAILS_BLOCK_EN: CMS.Model.TicketDetailsBlock.TicketDetailsBl preventive_maintenance: 'Preventive Maintenance', corrective_maintenance: 'Corrective Maintenance', }, + preferredContactMethod: { + phone: 'Phone', + email: 'Email', + }, termsAcceptance: { true: 'Yes', false: 'No', @@ -147,6 +151,10 @@ const MOCK_TICKET_DETAILS_BLOCK_PL: CMS.Model.TicketDetailsBlock.TicketDetailsBl preventive_maintenance: 'Konserwacja zapobiegawcza', corrective_maintenance: 'Konserwacja naprawcza', }, + preferredContactMethod: { + phone: 'Telefon', + email: 'Email', + }, termsAcceptance: { true: 'Tak', false: 'Nie', @@ -231,6 +239,10 @@ const MOCK_TICKET_DETAILS_BLOCK_DE: CMS.Model.TicketDetailsBlock.TicketDetailsBl preventive_maintenance: 'Vorbeugende Wartung', corrective_maintenance: 'Korrigierende Wartung', }, + preferredContactMethod: { + phone: 'Telefon', + email: 'E-Mail', + }, termsAcceptance: { true: 'Ja', false: 'Nein', diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts index 3e60df883..e62c71dc6 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts @@ -63,6 +63,10 @@ const MOCK_TICKET_DETAILS_BLOCK_EN: CMS.Model.TicketDetailsBlock.TicketDetailsBl preventive_maintenance: 'Preventive Maintenance', corrective_maintenance: 'Corrective Maintenance', }, + preferredContactMethod: { + phone: 'Phone', + email: 'Email', + }, termsAcceptance: { true: 'Yes', false: 'No', @@ -147,6 +151,10 @@ const MOCK_TICKET_DETAILS_BLOCK_PL: CMS.Model.TicketDetailsBlock.TicketDetailsBl preventive_maintenance: 'Konserwacja zapobiegawcza', corrective_maintenance: 'Konserwacja naprawcza', }, + preferredContactMethod: { + phone: 'Telefon', + email: 'Email', + }, termsAcceptance: { true: 'Tak', false: 'Nie', @@ -231,6 +239,10 @@ const MOCK_TICKET_DETAILS_BLOCK_DE: CMS.Model.TicketDetailsBlock.TicketDetailsBl preventive_maintenance: 'Vorbeugende Wartung', corrective_maintenance: 'Korrigierende Wartung', }, + preferredContactMethod: { + phone: 'Telefon', + email: 'E-Mail', + }, termsAcceptance: { true: 'Ja', false: 'Nein', diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts index 16ea8e23e..d33eec662 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts @@ -98,7 +98,6 @@ const MOCK_TICKET_LIST_BLOCK_EN: CMS.Model.TicketListBlock.TicketListBlock = { allowMultiple: false, isLeading: false, options: [ - { label: 'All', value: 'ALL' }, { label: 'Contact Form', value: 'CONTACT_US' }, { label: 'Device Maintenance', value: 'REQUEST_DEVICE_MAINTENANCE' }, { label: 'Complaint', value: 'COMPLAINT' }, @@ -237,7 +236,6 @@ const MOCK_TICKET_LIST_BLOCK_DE: CMS.Model.TicketListBlock.TicketListBlock = { allowMultiple: false, isLeading: false, options: [ - { label: 'Alle', value: 'ALL' }, { label: 'Kontaktformular', value: 'CONTACT_US' }, { label: 'Gerätewartung', value: 'REQUEST_DEVICE_MAINTENANCE' }, { label: 'Beschwerde', value: 'COMPLAINT' }, @@ -378,7 +376,6 @@ const MOCK_TICKET_LIST_BLOCK_PL: CMS.Model.TicketListBlock.TicketListBlock = { allowMultiple: false, isLeading: false, options: [ - { label: 'Wszystko', value: 'ALL' }, { label: 'Formularz kontaktowy', value: 'CONTACT_US' }, { label: 'Konserwacja urządzenia', value: 'REQUEST_DEVICE_MAINTENANCE' }, { label: 'Reklamacja', value: 'COMPLAINT' }, diff --git a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts index d02d1f066..63c2f2b8c 100644 --- a/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.service.ts @@ -125,8 +125,33 @@ export class ZendeskTicketService extends Tickets.Service { let searchQuery = `type:ticket requester:${user.email}`; + // Map internal status values to Zendesk status values (reverse of zendesk-ticket.mapper.ts) if (options.status) { - searchQuery += ` status:${options.status.toLowerCase()}`; + const statusArray = Array.isArray(options.status) ? options.status : [options.status]; + const zendeskStatuses: string[] = []; + + statusArray.forEach((internalStatus) => { + switch (internalStatus) { + case 'CLOSED': + // Map to both 'solved' and 'closed' + zendeskStatuses.push('solved', 'closed'); + break; + case 'IN_PROGRESS': + // Map to both 'pending' and 'hold' + zendeskStatuses.push('pending', 'hold'); + break; + case 'OPEN': + // Map to both 'new' and 'open' + zendeskStatuses.push('new', 'open'); + break; + } + }); + + // Add all Zendesk statuses to query + if (zendeskStatuses.length > 0) { + const statusQuery = zendeskStatuses.map((s) => `status:${s}`).join(' '); + searchQuery += ` ${statusQuery}`; + } } if (options.topic) { From 4092ffd40558647eae7fed650e6253225cf0b91f Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 19:05:21 +0100 Subject: [PATCH 51/54] fix(survey.mapper): update submitDestination from 'tickets' to 'surveyjs' for mock surveys --- .../src/modules/cms/mappers/cms.survey.mapper.ts | 6 +++--- .../mocked/src/modules/cms/mappers/cms.survey.mapper.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts index a70dfb9b8..5ced09fb8 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/cms.survey.mapper.ts @@ -4,7 +4,7 @@ const MOCK_SURVEY_1: CMS.Model.Survey.Survey = { code: 'contact-us', surveyId: '72c90a02-6bfe-4e83-ba48-01f11752c234', surveyType: 'survey', - submitDestination: ['tickets'], + submitDestination: ['surveyjs'], requiredRoles: [], postId: 'a91349b1-0c4c-4b7a-b712-91f04a1e6e99', }; @@ -13,7 +13,7 @@ const MOCK_SURVEY_2: CMS.Model.Survey.Survey = { code: 'complaint-form', surveyId: '3897de9c-279b-4c50-b359-09f5c73a3c49', surveyType: 'survey', - submitDestination: ['tickets'], + submitDestination: ['surveyjs'], requiredRoles: ['selfservice_org_user'], postId: 'e0f1b26b-a434-44ab-9608-c49dcd0658ec', }; @@ -22,7 +22,7 @@ const MOCK_SURVEY_3: CMS.Model.Survey.Survey = { code: 'request-device-maintenance', surveyId: 'd93ccc83-4aff-418b-9e9b-c9c3447908cf', surveyType: 'survey', - submitDestination: ['tickets'], + submitDestination: ['surveyjs'], requiredRoles: ['selfservice_org_user'], postId: '17931fe3-2492-408c-8f91-8fc062606604', }; diff --git a/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts index a70dfb9b8..5ced09fb8 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/cms.survey.mapper.ts @@ -4,7 +4,7 @@ const MOCK_SURVEY_1: CMS.Model.Survey.Survey = { code: 'contact-us', surveyId: '72c90a02-6bfe-4e83-ba48-01f11752c234', surveyType: 'survey', - submitDestination: ['tickets'], + submitDestination: ['surveyjs'], requiredRoles: [], postId: 'a91349b1-0c4c-4b7a-b712-91f04a1e6e99', }; @@ -13,7 +13,7 @@ const MOCK_SURVEY_2: CMS.Model.Survey.Survey = { code: 'complaint-form', surveyId: '3897de9c-279b-4c50-b359-09f5c73a3c49', surveyType: 'survey', - submitDestination: ['tickets'], + submitDestination: ['surveyjs'], requiredRoles: ['selfservice_org_user'], postId: 'e0f1b26b-a434-44ab-9608-c49dcd0658ec', }; @@ -22,7 +22,7 @@ const MOCK_SURVEY_3: CMS.Model.Survey.Survey = { code: 'request-device-maintenance', surveyId: 'd93ccc83-4aff-418b-9e9b-c9c3447908cf', surveyType: 'survey', - submitDestination: ['tickets'], + submitDestination: ['surveyjs'], requiredRoles: ['selfservice_org_user'], postId: '17931fe3-2492-408c-8f91-8fc062606604', }; From fb4e1e6d640a55537fe6f2ee7faa8024f912fa2f Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Wed, 28 Jan 2026 19:47:59 +0100 Subject: [PATCH 52/54] docs(tickets): updated zendesk, tickets and survey js documentation --- .../integrations/forms/surveyjs/features.md | 16 +++-- .../docs/integrations/forms/surveyjs/usage.md | 4 +- .../integrations/tickets/zendesk/features.md | 11 ++-- .../tickets/zendesk/how-to-setup.md | 60 +++++++++++++++++-- .../integrations/tickets/zendesk/usage.md | 57 ++++++++++++++++++ .../core-model-tickets.md | 14 ++--- 6 files changed, 135 insertions(+), 27 deletions(-) diff --git a/apps/docs/docs/integrations/forms/surveyjs/features.md b/apps/docs/docs/integrations/forms/surveyjs/features.md index 48cfc4290..6a734d60c 100644 --- a/apps/docs/docs/integrations/forms/surveyjs/features.md +++ b/apps/docs/docs/integrations/forms/surveyjs/features.md @@ -21,7 +21,7 @@ This document provides an overview of features supported by the SurveyJS integra | [Localization](#localization) | ✅ | Multi-language support via locale prop | | [Custom UI Components](#custom-ui-components) | ✅ | Custom React components for question types | | [Block Integration](#block-integration) | ✅ | Integration with `@o2s/blocks.surveyjs-form` block | -| [Ticket System Integration](#ticket-system-integration) | 📋 | Planned feature for ticket submission forms | +| [Ticket System Integration](#ticket-system-integration) | ✅ | Submit surveys as tickets to ticket systems | ## Feature details @@ -155,13 +155,11 @@ Integration with the `@o2s/blocks.surveyjs-form` block: ### Ticket System Integration {#ticket-system-integration} -:::info Planned -This is a planned feature and is not yet implemented. -::: +The SurveyJS module supports ticket submission through the `submitDestination` configuration: -The SurveyJS module is planned to support ticket submission: +- **Ticket forms**: Create dynamic ticket submission forms using SurveyJS schemas +- **Form validation**: Automatic validation of required fields (`description`, `ticketFormId`) +- **Integration**: Works with ticket systems implementing the framework's Tickets service (e.g., Zendesk) +- **Custom workflows**: Configure submission destinations in CMS via `submitDestination: ['tickets']` -- **Ticket forms**: Create dynamic ticket submission forms -- **Form validation**: Validate ticket data before submission -- **Integration**: Can be integrated with ticket systems (e.g., Zendesk) -- **Custom workflows**: Configure custom submission workflows +Required survey fields for ticket submission: `description` (string) and `ticketFormId` (number). diff --git a/apps/docs/docs/integrations/forms/surveyjs/usage.md b/apps/docs/docs/integrations/forms/surveyjs/usage.md index d1a0f9ea5..6eaa8282a 100644 --- a/apps/docs/docs/integrations/forms/surveyjs/usage.md +++ b/apps/docs/docs/integrations/forms/surveyjs/usage.md @@ -43,7 +43,7 @@ First, create a survey entry in your CMS (e.g., Strapi) with the following requi - **surveyId** - SurveyJS survey ID from your SurveyJS service - **postId** - SurveyJS post ID for submissions - **surveyType** - Type of survey (typically `"survey"`) -- **submitDestination** - Array of destinations (e.g., `["surveyjs"]`) +- **submitDestination** - Array of destinations: `["surveyjs"]` for SurveyJS backend, `["tickets"]` for ticket system - **requiredRoles** - Array of required roles (can be empty `[]` for public surveys) ### Step 3: Add block to page in CMS @@ -68,6 +68,8 @@ The block automatically handles everything: No frontend code changes are needed - the form will be automatically rendered on the page. +**Ticket submission:** To submit surveys as tickets, set `submitDestination: ["tickets"]` and ensure your survey includes `description` (string) and `ticketFormId` (number) fields. + ## Direct component usage (advanced) If you need to use the survey component directly without the CMS block system: diff --git a/apps/docs/docs/integrations/tickets/zendesk/features.md b/apps/docs/docs/integrations/tickets/zendesk/features.md index 0215cc326..da91ec5fa 100644 --- a/apps/docs/docs/integrations/tickets/zendesk/features.md +++ b/apps/docs/docs/integrations/tickets/zendesk/features.md @@ -16,6 +16,7 @@ The Zendesk integration provides: - **Viewing individual tickets** - Retrieve full ticket details including comments and attachments - **Listing tickets** - Get a list of tickets with filtering options (status, type, topic, date range) +- **Creating tickets** - Create new tickets with attachments and custom fields - **Access to ticket comments** - View conversation history for each ticket - **Attachment handling** - Access attachments from ticket comments - **User-specific ticket access** - Users can only see their own tickets (matched by email) @@ -25,11 +26,11 @@ The Zendesk integration provides: The following table shows which methods from the base TicketService are currently supported by the Zendesk integration: -| Method | Description | Supported | -| ------------- | ------------------------------------------------- | ----------- | -| getTicket | Retrieve a single ticket by ID | ✓ | -| getTicketList | Retrieve a list of tickets with filtering options | ✓ | -| createTicket | Create a new ticket | ✗ (planned) | +| Method | Description | Supported | +| ------------- | ------------------------------------------------- | --------- | +| getTicket | Retrieve a single ticket by ID | ✓ | +| getTicketList | Retrieve a list of tickets with filtering options | ✓ | +| createTicket | Create a new ticket | ✓ | ## Module Structure diff --git a/apps/docs/docs/integrations/tickets/zendesk/how-to-setup.md b/apps/docs/docs/integrations/tickets/zendesk/how-to-setup.md index 4536aa014..b50b2087d 100644 --- a/apps/docs/docs/integrations/tickets/zendesk/how-to-setup.md +++ b/apps/docs/docs/integrations/tickets/zendesk/how-to-setup.md @@ -64,11 +64,34 @@ After configuring the integration, you need to set up environment variables that Configure the following environment variables in your API Harmonization server: -| name | type | description | required | default | -| ---------------------- | ------ | ------------------------------------------------------------------------ | -------- | ------- | -| ZENDESK_API_URL | string | Your Zendesk API URL (e.g., `https://your-subdomain.zendesk.com/api/v2`) | yes | - | -| ZENDESK_API_TOKEN | string | Base64-encoded authentication token | yes | - | -| ZENDESK_TOPIC_FIELD_ID | number | ID of the custom field that contains the ticket topic (optional) | no | - | +| name | type | description | required | default | +| ------------------------------------------- | ------ | ------------------------------------------------------------------------ | -------- | ------- | +| ZENDESK_API_URL | string | Your Zendesk API URL (e.g., `https://your-subdomain.zendesk.com/api/v2`) | yes | - | +| ZENDESK_API_TOKEN | string | Base64-encoded authentication token | yes | - | +| ZENDESK_TOPIC_FIELD_ID | number | Custom field ID for ticket topic | yes | - | +| ZENDESK_CONTACT_FORM_ID | number | Ticket form ID for contact inquiries | yes | - | +| ZENDESK_COMPLAINT_FORM_ID | number | Ticket form ID for complaints | yes | - | +| ZENDESK_REQUEST_DEVICE_MAINTENANCE_FORM_ID | number | Ticket form ID for device maintenance requests | yes | - | +| ZENDESK_DEVICE_NAME_FIELD_ID | number | Custom field ID for device/machine name | yes | - | +| ZENDESK_SERIAL_NUMBER_FIELD_ID | number | Custom field ID for serial number | yes | - | +| ZENDESK_MAINTENANCE_TYPE_FIELD_ID | number | Custom field ID for maintenance type | yes | - | +| ZENDESK_MAINTENANCE_PREFERRED_DATE_FIELD_ID | number | Custom field ID for preferred maintenance date | yes | - | +| ZENDESK_ADDITIONAL_NOTES_FIELD_ID | number | Custom field ID for additional notes | yes | - | +| ZENDESK_CONTACT_FIELD_ID | number | Custom field ID for contact information | yes | - | +| ZENDESK_ISSUE_DATE_FIELD_ID | number | Custom field ID for issue date | yes | - | +| ZENDESK_COMPANY_NAME_FIELD_ID | number | Custom field ID for company/organization name | yes | - | +| ZENDESK_FIRST_NAME_FIELD_ID | number | Custom field ID for first name | yes | - | +| ZENDESK_LAST_NAME_FIELD_ID | number | Custom field ID for last name | yes | - | +| ZENDESK_EMAIL_FIELD_ID | number | Custom field ID for email address | yes | - | +| ZENDESK_PHONE_FIELD_ID | number | Custom field ID for phone number | yes | - | +| ZENDESK_INVOICE_NUMBER_FIELD_ID | number | Custom field ID for invoice number | yes | - | +| ZENDESK_ADDRESS_FIELD_ID | number | Custom field ID for address | yes | - | +| ZENDESK_INQUIRY_TYPE_FIELD_ID | number | Custom field ID for inquiry type | yes | - | +| ZENDESK_PRODUCT_CATEGORY_FIELD_ID | number | Custom field ID for product category | yes | - | +| ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID | number | Custom field ID for preferred contact method | yes | - | +| ZENDESK_TERMS_ACCEPTANCE_FIELD_ID | number | Custom field ID for terms acceptance | yes | - | +| ZENDESK_NEWSLETTER_CONSENT_FIELD_ID | number | Custom field ID for newsletter consent | yes | - | +| ZENDESK_MARKETING_CONSENT_FIELD_ID | number | Custom field ID for marketing consent | yes | - | **Important notes:** @@ -95,7 +118,34 @@ Configure the following environment variables in your API Harmonization server: ```env ZENDESK_API_URL=https://your-subdomain.zendesk.com/api/v2 ZENDESK_API_TOKEN=base64_encoded_token_here + +# Form IDs +ZENDESK_CONTACT_FORM_ID=789012 +ZENDESK_COMPLAINT_FORM_ID=345678 +ZENDESK_REQUEST_DEVICE_MAINTENANCE_FORM_ID=901234 + +# Custom field mappings for forms ZENDESK_TOPIC_FIELD_ID=123456 +ZENDESK_DEVICE_NAME_FIELD_ID=111111 +ZENDESK_SERIAL_NUMBER_FIELD_ID=222222 +ZENDESK_MAINTENANCE_TYPE_FIELD_ID=333333 +ZENDESK_MAINTENANCE_PREFERRED_DATE_FIELD_ID=444444 +ZENDESK_ADDITIONAL_NOTES_FIELD_ID=555555 +ZENDESK_CONTACT_FIELD_ID=666666 +ZENDESK_ISSUE_DATE_FIELD_ID=777777 +ZENDESK_COMPANY_NAME_FIELD_ID=888888 +ZENDESK_FIRST_NAME_FIELD_ID=999999 +ZENDESK_LAST_NAME_FIELD_ID=101010 +ZENDESK_EMAIL_FIELD_ID=111111 +ZENDESK_PHONE_FIELD_ID=121212 +ZENDESK_INVOICE_NUMBER_FIELD_ID=131313 +ZENDESK_ADDRESS_FIELD_ID=141414 +ZENDESK_INQUIRY_TYPE_FIELD_ID=151515 +ZENDESK_PRODUCT_CATEGORY_FIELD_ID=161616 +ZENDESK_PREFERRED_CONTACT_METHOD_FIELD_ID=171717 +ZENDESK_TERMS_ACCEPTANCE_FIELD_ID=181818 +ZENDESK_NEWSLETTER_CONSENT_FIELD_ID=191919 +ZENDESK_MARKETING_CONSENT_FIELD_ID=202020 ``` Make sure to set these variables in your environment configuration file (e.g., `.env`) or your deployment platform's environment variable settings. diff --git a/apps/docs/docs/integrations/tickets/zendesk/usage.md b/apps/docs/docs/integrations/tickets/zendesk/usage.md index 61d0bafac..3b959f02b 100644 --- a/apps/docs/docs/integrations/tickets/zendesk/usage.md +++ b/apps/docs/docs/integrations/tickets/zendesk/usage.md @@ -149,6 +149,63 @@ GET /tickets/12345 } ``` +### Create Ticket + +Create a new ticket with attachments and custom fields. + +**Endpoint:** `POST /tickets` + +**Body Parameters:** + +| Parameter | Type | Required | Description | +| ----------- | ----------------------- | -------- | ---------------------------------------------- | +| title | string | No | Subject of the ticket | +| description | string | Yes | Detailed description (first comment body) | +| type | number | Yes | Ticket form ID (must match configured form ID) | +| attachments | TicketAttachmentInput[] | No | Array of file attachments | +| fields | object | No | Custom fields for the ticket | + +**Example Request:** + +```bash +POST /tickets +Authorization: Bearer {token} +Content-Type: application/json + +{ + "title": "Device maintenance request", + "description": "My device needs servicing", + "type": 789012, + "fields": { + "machineName": "Device-001", + "maintenanceType": "Repair" + } +} +``` + +**Example Response:** + +```json +{ + "id": "54321", + "createdAt": "2024-01-20T10:00:00Z", + "updatedAt": "2024-01-20T10:00:00Z", + "topic": "CONTACT_US", + "type": "NORMAL", + "status": "OPEN", + "properties": [ + { + "id": "subject", + "value": "Device maintenance request" + }, + { + "id": "description", + "value": "My device needs servicing" + } + ] +} +``` + ## Authentication All ticket endpoints require authentication. The integration uses the `Authorization` header to identify the current user. diff --git a/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md b/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md index f36a54d7f..bb6c16a55 100644 --- a/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md +++ b/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md @@ -116,13 +116,13 @@ createTicket( #### Body Parameters -| Parameter | Type | Required | Description | -| ----------- | ------------------------- | -------- | -------------------------------------------------------------- | -| title | string | No | Title or subject of the ticket | -| description | string | No | Detailed description of the issue | -| type | number | No | Ticket type identifier (e.g., form ID in ticket systems) | -| attachments | TicketAttachmentInput[] | No | Array of file attachments | -| fields | Record | No | Additional custom fields specific to the integration | +| Parameter | Type | Required | Description | +| ----------- | ----------------------- | -------- | ------------------------------------------------------------ | +| title | string | No | Title or subject of the ticket | +| description | string | No | Detailed description of the issue | +| type | number | No | Ticket type identifier (e.g., form ID in ticket systems) | +| attachments | TicketAttachmentInput[] | No | Array of file attachments | +| fields | object | No | Additional custom fields specific to the integration | #### Returns From d3e111a2d66077480aecc08779b90466be8c143f Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Thu, 29 Jan 2026 08:37:42 +0100 Subject: [PATCH 53/54] chore: added changeset --- .changeset/cuddly-teams-flash.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .changeset/cuddly-teams-flash.md diff --git a/.changeset/cuddly-teams-flash.md b/.changeset/cuddly-teams-flash.md new file mode 100644 index 000000000..3f63b2766 --- /dev/null +++ b/.changeset/cuddly-teams-flash.md @@ -0,0 +1,15 @@ +--- +'@o2s/integrations.contentful-cms': minor +'@o2s/integrations.strapi-cms': minor +'@o2s/blocks.ticket-details': minor +'@o2s/blocks.ticket-recent': minor +'@o2s/integrations.zendesk': minor +'@o2s/integrations.mocked': minor +'@o2s/blocks.ticket-list': minor +'@o2s/modules.surveyjs': minor +'@o2s/api-harmonization': minor +'@o2s/framework': minor +'@o2s/docs': minor +--- + +Added ticket creation functionality to the Zendesk integration. Users can now create tickets via POST /tickets with attachments and custom fields. Added custom field mapping from Survey.js format to Zendesk custom fields via new zendesk-field.mapper. Updated table columns on TicketList component to display: ticket type (topic), status, and last updated date. Added display of custom field values from ticket properties on TicketDetails. Updated mapper mocks in cms From e01d4f535073c04d716d97372fdbdf7e8908d000 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Thu, 29 Jan 2026 10:47:06 +0100 Subject: [PATCH 54/54] docs(tickets): clarify ticket creation requirements --- apps/docs/docs/integrations/forms/surveyjs/features.md | 6 ++++-- .../normalized-data-model/core-model-tickets.md | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/docs/docs/integrations/forms/surveyjs/features.md b/apps/docs/docs/integrations/forms/surveyjs/features.md index 6a734d60c..bc530131d 100644 --- a/apps/docs/docs/integrations/forms/surveyjs/features.md +++ b/apps/docs/docs/integrations/forms/surveyjs/features.md @@ -87,8 +87,10 @@ Each question type has custom React components for consistent UI styling. Flexible submission handling: -- **Current target**: Currently, the only submission target is the SurveyJS backend service -- **Extensible architecture**: The submission system can be extended to support multiple destinations: +- **Submission destinations**: Forms can be submitted to multiple destinations configured via the `submitDestination` setting: + - **SurveyJS backend service**: Default submission target for standard survey responses + - **Ticket systems**: When `submitDestination` includes `'tickets'`, submissions are routed to the framework's Tickets service (e.g., Zendesk integration) +- **Extensible architecture**: The submission system can be extended to support additional destinations: - Backend APIs (REST, GraphQL) - Message brokers (e.g., RabbitMQ) - Workflow tools (e.g., N8n) diff --git a/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md b/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md index bb6c16a55..93bc6e031 100644 --- a/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md +++ b/apps/docs/docs/main-components/harmonization-app/normalized-data-model/core-model-tickets.md @@ -124,6 +124,12 @@ createTicket( | attachments | TicketAttachmentInput[] | No | Array of file attachments | | fields | object | No | Additional custom fields specific to the integration | +> **Note**: While `description` and `type` are marked as optional in the core model, certain integrations require these fields: +> - **Zendesk**: Requires both `description` (string) and `type` (number, ticket form ID) +> - **SurveyJS**: Requires `description` (string) and `ticketFormId` (number, mapped to `type`) +> +> Implementers should check the specific integration documentation for required fields before implementing ticket creation. + #### Returns An Observable that emits the created ticket.