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 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' diff --git a/apps/api-harmonization/package.json b/apps/api-harmonization/package.json index 9e5a4e3c2..9c867391b 100644 --- a/apps/api-harmonization/package.json +++ b/apps/api-harmonization/package.json @@ -104,6 +104,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.9", "@types/string-template": "^1.0.7", "@types/supertest": "^6.0.3", @@ -123,4 +124,4 @@ "tsconfig-paths": "^4.2.0", "typescript": "^5.9.3" } -} +} \ No newline at end of file diff --git a/apps/api-harmonization/turbo.json b/apps/api-harmonization/turbo.json index 1b7887544..8e30f48ed 100644 --- a/apps/api-harmonization/turbo.json +++ b/apps/api-harmonization/turbo.json @@ -36,8 +36,34 @@ "CF_PREVIEW_TOKEN", "CF_SPACE_ID", "CF_ENV", - "CF_MANAGEMENT_TOKEN" + "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", + "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" ] } } -} +} \ No newline at end of file diff --git a/apps/docs/docs/integrations/forms/surveyjs/features.md b/apps/docs/docs/integrations/forms/surveyjs/features.md index 48cfc4290..bc530131d 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 @@ -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) @@ -155,13 +157,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 534f51943..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 @@ -101,18 +102,17 @@ The integration maps Zendesk ticket data to the standard ticket model with the f ### Field Mapping -| Zendesk Field | Normalized Field | Notes | -| -------------------- | ---------------- | ---------------------------------------------------------------------------------------- | -| 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' | -| custom_fields | properties | Each field added with id pattern 'custom_field_X' where X is the Zendesk custom field ID | -| comments | comments | Mapped with author information | -| comments.attachments | attachments | Extracted from comments | +| Zendesk Field | Normalized Field | Notes | +| -------------------- |------------------|------------------------------------------| +| id | id | Converted to string | +| created_at | createdAt | ISO date string | +| updated_at | updatedAt | ISO date string | +| 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 | Mapped using `ZendeskFieldMapper` to readable names (see Custom Fields section below) | +| comments | comments | Mapped with author information | +| comments.attachments | attachments | Extracted from comments | ### Status Mapping @@ -123,21 +123,114 @@ 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: +### Custom Fields Mapping -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 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**: + ```env + 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 + } + ``` + +### Topic Field Mapping + +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 `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 fields + +3. The topic is then stored in Zendesk using the `ZENDESK_TOPIC_FIELD_ID` custom field + +**Example:** + +```typescript +// Survey.js sends type (ticket form ID) +{ + type: 33406700504221, // Matches ZENDESK_CONTACT_FORM_ID + fields: { ... } +} + +// Service automatically adds topic +{ + type: 33406700504221, + fields: { + topic: 'CONTACT_US', // Automatically set + ... + } +} +``` + +**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 +- `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) @@ -168,15 +261,14 @@ The integration converts framework filter parameters to Zendesk Search API queri ### Parameter Mapping -| 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 | -| offset | `page` | Calculated as `Math.floor(offset / limit) + 1` | -| limit | `per_page` | Default: 10 | +| Framework Parameter | Zendesk Search Query | Notes | +|---------------------|----------------------|------------------------------------------| +| status | `status:{value}` | Converted to lowercase | +| topic | `tag:{value}` | Maps to Zendesk tags | +| dateFrom | `created>={iso_date}` | Converted to ISO format | +| dateTo | `created<={iso_date}` | Converted to ISO format | +| offset | `page` | Calculated as `Math.floor(offset / limit) + 1` | +| limit | `per_page` | Default: 10 | ### Base Query 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 45c3258ed..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 @@ -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) | @@ -117,10 +116,19 @@ 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 | 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 diff --git a/package-lock.json b/package-lock.json index f4c52fc2e..6671c4fc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -149,6 +149,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.9", "@types/string-template": "^1.0.7", "@types/supertest": "^6.0.3", @@ -19555,6 +19556,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.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", 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 ( + + ); }; 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..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 @@ -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, @@ -53,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/blocks/ticket-details/src/api-harmonization/ticket-details.model.ts b/packages/blocks/ticket-details/src/api-harmonization/ticket-details.model.ts index 7981bfbdc..d6eeb7c19 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 @@ -24,7 +24,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 36db3ed6b..0b789c800 100644 --- a/packages/blocks/ticket-details/src/frontend/TicketDetails.client.tsx +++ b/packages/blocks/ticket-details/src/frontend/TicketDetails.client.tsx @@ -28,7 +28,7 @@ export const TicketDetailsPure: React.FC> = ({ return (
-
+

{ticket.topic.label}

@@ -51,14 +51,6 @@ export const TicketDetailsPure: React.FC> = ({
    - - - - { 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 469ac326f..f499e5c24 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']; @@ -48,7 +49,7 @@ export class Ticket { value: Tickets.Model.Ticket['topic']; label: string; }; - type!: { + type?: { value: Tickets.Model.Ticket['type']; label: string; }; diff --git a/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx b/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx index 537b70017..1b0eb31c6 100644 --- a/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx +++ b/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx @@ -117,9 +117,16 @@ 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 && ( + + {data.labels.ticketId}: {ticket.id} + + )} +
    ), }; case 'status': 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!: { 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/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/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[]; diff --git a/packages/framework/src/modules/tickets/tickets.request.ts b/packages/framework/src/modules/tickets/tickets.request.ts index 22efff5c9..74787e1f9 100644 --- a/packages/framework/src/modules/tickets/tickets.request.ts +++ b/packages/framework/src/modules/tickets/tickets.request.ts @@ -6,15 +6,24 @@ export class GetTicketParams { locale?: string; } +export class TicketAttachmentInput { + filename!: string; + content!: Buffer; + contentType!: string; // e.g., 'application/pdf' +} + export class PostTicketBody { - title!: string; - description!: string; + title?: string; + description?: string; + type?: number; + attachments?: TicketAttachmentInput[]; + fields?: Record; } 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 2ea369836..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 @@ -7,33 +7,78 @@ 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', 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', + }, + preferredContactMethod: { + phone: 'Phone', + email: 'Email', + }, + termsAcceptance: { + true: 'Yes', + false: 'No', + }, + newsletterConsent: { + true: 'Yes', + false: 'No', + }, + marketingConsent: { + true: 'Yes', + false: 'No', + }, }, labels: { showMore: 'Show case details', @@ -45,42 +90,86 @@ 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', 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', + }, + preferredContactMethod: { + phone: 'Telefon', + email: 'Email', + }, + termsAcceptance: { + true: 'Tak', + false: 'Nie', + }, + newsletterConsent: { + true: 'Tak', + false: 'Nie', + }, + marketingConsent: { + true: 'Tak', + false: 'Nie', + }, }, labels: { - showMore: 'Pokaż szczegóły sprawy', + showMore: 'Pokaż szczegóły zgłoszenia', showLess: 'Pokaż mniej szczegółów', today: 'Dzisiaj', yesterday: 'Wczoraj', @@ -94,34 +183,78 @@ 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', 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', + }, + preferredContactMethod: { + phone: 'Telefon', + email: 'E-Mail', + }, + termsAcceptance: { + true: 'Ja', + false: 'Nein', + }, + newsletterConsent: { + true: 'Ja', + false: 'Nein', + }, + marketingConsent: { + true: 'Ja', + false: 'Nein', + }, }, labels: { showMore: 'Falldetails anzeigen', 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-details.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-details.mapper.ts index 2ea369836..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 @@ -7,33 +7,78 @@ 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', 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', + }, + preferredContactMethod: { + phone: 'Phone', + email: 'Email', + }, + termsAcceptance: { + true: 'Yes', + false: 'No', + }, + newsletterConsent: { + true: 'Yes', + false: 'No', + }, + marketingConsent: { + true: 'Yes', + false: 'No', + }, }, labels: { showMore: 'Show case details', @@ -45,42 +90,86 @@ 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', 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', + }, + preferredContactMethod: { + phone: 'Telefon', + email: 'Email', + }, + termsAcceptance: { + true: 'Tak', + false: 'Nie', + }, + newsletterConsent: { + true: 'Tak', + false: 'Nie', + }, + marketingConsent: { + true: 'Tak', + false: 'Nie', + }, }, labels: { - showMore: 'Pokaż szczegóły sprawy', + showMore: 'Pokaż szczegóły zgłoszenia', showLess: 'Pokaż mniej szczegółów', today: 'Dzisiaj', yesterday: 'Wczoraj', @@ -94,34 +183,78 @@ 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', 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', + }, + preferredContactMethod: { + phone: 'Telefon', + email: 'E-Mail', + }, + 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-list.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.ticket-list.mapper.ts index 936c9265c..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 @@ -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,13 @@ const MOCK_TICKET_LIST_BLOCK_EN: CMS.Model.TicketListBlock.TicketListBlock = { { __typename: 'FilterSelect', id: 'topic', - label: 'Topic', - 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', + label: 'Case Type', 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' }, ], }, { @@ -174,6 +133,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}', }; @@ -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,13 @@ 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: 'Kontaktformular', value: 'CONTACT_US' }, + { label: 'Gerätewartung', value: 'REQUEST_DEVICE_MAINTENANCE' }, + { label: 'Beschwerde', value: 'COMPLAINT' }, ], }, { @@ -353,6 +271,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}', }; @@ -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 aktualizacji' }, ], 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,13 @@ 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: 'Formularz kontaktowy', value: 'CONTACT_US' }, + { label: 'Konserwacja urządzenia', value: 'REQUEST_DEVICE_MAINTENANCE' }, + { label: 'Reklamacja', value: 'COMPLAINT' }, ], }, { @@ -533,6 +410,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}', 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: [ { 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 } }, ); }; 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..15b54d45b --- /dev/null +++ b/packages/integrations/zendesk/src/modules/tickets/zendesk-field.mapper.ts @@ -0,0 +1,210 @@ +/** + * 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; +} + +/** + * 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 this function: newField: Number(process.env.ZENDESK_NEW_FIELD_ID) || undefined + * 3. Include the field in Survey.js form with matching name + */ +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 + */ +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; +}; + +/** + * 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 = (fieldName: string, value: unknown): string | number | boolean | null => { + // 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; + } + + // 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 { + return null; + } + } + + // Invalid type + return null; +}; + +/** + * 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; + } + + // 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; + } + + // Validate and convert value to supported types + const validatedValue = validateAndConvertValue(fieldName, 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 e683ecae0..8f50c0a9d 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 { getFieldKeyById } from './zendesk-field.mapper'; + type ZendeskTicket = TicketObject; export function mapTicketToModel( @@ -23,21 +25,62 @@ export function mapTicketToModel( status = 'OPEN'; } - let topic = 'GENERAL'; + // Determine topic from custom field if configured + 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) { + topic = String(topicField.value).toUpperCase(); + } + } + + 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 || '' }, ]; + // 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 = contactFormId !== undefined && ticket.ticket_form_id === contactFormId; + + // 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 = getFieldKeyById(field.id!); + + // 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: `custom_field_${field.id}`, + id: fieldKey, value: String(field.value), }); } @@ -82,7 +125,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, 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..63c2f2b8c 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, NotImplementedException } from '@nestjs/common'; -import { Observable, catchError, firstValueFrom, from, map, of, switchMap, throwError } from 'rxjs'; +import { HttpService } from '@nestjs/axios'; +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'; @@ -9,13 +10,16 @@ import { type TicketCommentObject, type TicketObject, type UserObject, + createTicket, listSearchResults, listTicketComments, + searchUsers, showTicket, showUser, } from '@/generated/zendesk'; import { client } from '@/generated/zendesk/client.gen'; +import { toCustomFields } from './zendesk-field.mapper'; import { mapTicketToModel } from './zendesk-ticket.mapper'; type ZendeskTicket = TicketObject; @@ -31,19 +35,25 @@ interface ZendeskSearchQuery { @Injectable() export class ZendeskTicketService extends Tickets.Service { - constructor(private readonly usersService: Users.Service) { + private readonly baseUrl: string; + private readonly authToken: string; + + constructor( + private readonly usersService: Users.Service, + private readonly httpClient: HttpService, + ) { 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}` }, }); } @@ -115,16 +125,37 @@ 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; + } + }); - if (options.type) { - searchQuery += ` priority:${options.type.toLowerCase()}`; + // Add all Zendesk statuses to query + if (zendeskStatuses.length > 0) { + const statusQuery = zendeskStatuses.map((s) => `status:${s}`).join(' '); + searchQuery += ` ${statusQuery}`; + } } if (options.topic) { - searchQuery += ` tag:${options.topic.toLowerCase()}`; + searchQuery += ` tags:${options.topic.toLowerCase()}`; } if (options.dateFrom) { @@ -159,8 +190,142 @@ 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 + // 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( + switchMap((user) => { + if (!user?.email) { + return throwError(() => new NotFoundException('User email not found')); + } + + // Upload attachments first if they exist + const uploadTokens = + data.attachments && data.attachments.length > 0 + ? forkJoin( + data.attachments.map((attachment) => + this.uploadAttachment( + attachment.filename, + attachment.content, + attachment.contentType, + ), + ), + ) + : of([]); + + // 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) => { + // 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) + : 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 (data.type === contactFormId) { + topicValue = 'CONTACT_US'; + } else if (data.type === complaintFormId) { + topicValue = 'COMPLAINT'; + } else if (data.type === deviceMaintenanceFormId) { + topicValue = 'REQUEST_DEVICE_MAINTENANCE'; + } else { + return throwError( + () => + new BadRequestException( + `Invalid type: ${data.type}. Must match one of the configured form IDs.`, + ), + ); + } + + // Add topic to fields before mapping + const fieldsWithTopic = { + ...(data.fields || {}), + topic: topicValue, + }; + + // Map fields to Zendesk custom fields format using field mapper + 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({ + body: { + ticket: { + // Subject is optional in Zendesk API + ...(data.title && { subject: data.title }), + comment: { + body: data.description, + ...(uploadTokens.length > 0 && { uploads: uploadTokens }), + }, + ticket_form_id: data.type, + ...(zendeskUser?.id && { + requester_id: zendeskUser.id, + submitter_id: zendeskUser.id, + }), + // 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 + ...(customFields.length > 0 && { custom_fields: customFields as any }), + }, + }, + }), + ).pipe( + switchMap((response) => { + if (!response.data?.ticket) { + return throwError( + () => + new InternalServerErrorException( + 'Failed to create ticket in Zendesk', + ), + ); + } + + const createdTicket = response.data.ticket; + + return of(mapTicketToModel(createdTicket)); + }), + catchError((error) => { + return throwError( + () => + new InternalServerErrorException( + `Failed to create ticket: ${error.message || error}`, + ), + ); + }), + ); + }), + ), + ), + ); + }), + ); } private fetchTicket(id: string): Observable { @@ -239,4 +404,67 @@ 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 (case-insensitive) + const users = response.data?.users || []; + const normalizedEmail = email.toLowerCase(); + const matchedUser = users.find((u) => u.email?.toLowerCase() === normalizedEmail); + return matchedUser; + }), + 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}`)); + }), + ); + } + + /** + * Uploads an attachment to Zendesk using direct HTTP request. + * 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 + * @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 this.httpClient + .post(uploadUrl, content, { + headers: { + Authorization: `Basic ${this.authToken}`, + 'Content-Type': contentType, + }, + }) + .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}`)); + }), + ); + } } diff --git a/packages/integrations/zendesk/turbo.json b/packages/integrations/zendesk/turbo.json index 7fa953d58..c14a13c39 100644 --- a/packages/integrations/zendesk/turbo.json +++ b/packages/integrations/zendesk/turbo.json @@ -3,11 +3,65 @@ "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_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", + "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_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", + "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.controller.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.controller.ts index 0b503d737..e7d2d1426 100644 --- a/packages/modules/surveyjs/src/api-harmonization/surveyjs.controller.ts +++ b/packages/modules/surveyjs/src/api-harmonization/surveyjs.controller.ts @@ -18,7 +18,7 @@ export class SurveyjsController { } @Post() - submitSurvey(@Body() payload: SurveyJsSubmitPayload, @Headers() headers: ApiModels.Headers.AppHeaders) { - return this.surveyjsService.submitSurvey(payload, headers['authorization']); + 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.mapper.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.mapper.ts index 3ec615768..392608316 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,46 @@ const mapData = (element: Panelbase): Panelbase => { itemComponent: getItemComponent(element.type as string, element.itemComponent), }; }; + +export const mapSurveyToTicket = (surveyPayload: SurveyResult): Tickets.Request.PostTicketBody => { + const { title, description, ticketFormId, attachments, ...fields } = surveyPayload; + + // 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 + ? (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 + // 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; + + // Convert empty array to undefined for cleaner API + const finalAttachments = mappedAttachments && mappedAttachments.length > 0 ? mappedAttachments : undefined; + + return { + title: title as string | undefined, + description: description as string | undefined, + type: ticketFormIdNum, + attachments: finalAttachments, + fields, + }; +}; diff --git a/packages/modules/surveyjs/src/api-harmonization/surveyjs.module.ts b/packages/modules/surveyjs/src/api-harmonization/surveyjs.module.ts index dd58755a2..e6b136b05 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,10 @@ export class SurveyjsModule { provide: CMS.Service, useClass: cmsService as Type, }, + { + 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 2cfd473f6..9e3c589d6 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') || ''; @@ -66,14 +67,17 @@ export class SurveyjsService { } public submitSurvey(payload: SurveyJsSubmitPayload, authorization?: string): Observable { - return this.cmsService.getSurvey({ code: payload.code }).pipe( + const { code, surveyPayload } = payload; + + return this.cmsService.getSurvey({ code }).pipe( switchMap((survey) => { const decodedToken = authorization ? Utils.Auth.decodeToken(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( + + return this.validateSurvey(code, surveyPayload).pipe( concatMap((validationResult) => { if (!validationResult) { this.logger.error('Survey payload is not valid.'); @@ -85,15 +89,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(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); } @@ -129,6 +134,32 @@ export class SurveyjsService { ); } + private submitToTickets(surveyPayload: SurveyResult, authorization?: string): Observable { + try { + // Validate required fields before mapping + if (!surveyPayload.description || !surveyPayload.ticketFormId) { + 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); + + 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) { diff --git a/packages/modules/surveyjs/src/sdk/surveyjs.ts b/packages/modules/surveyjs/src/sdk/surveyjs.ts index 99bfd8fab..d2649d3e8 100644 --- a/packages/modules/surveyjs/src/sdk/surveyjs.ts +++ b/packages/modules/surveyjs/src/sdk/surveyjs.ts @@ -33,20 +33,17 @@ export const surveyjs = (sdk: Sdk) => ({ params: Request.SurveyJsSubmitPayload, headers: ApiModels.Headers.AppHeaders, authorization?: string, - ): Promise => - sdk.makeRequest({ + ): Promise => { + return sdk.makeRequest({ method: 'post', url: API_URL, headers: { ...Utils.Headers.getApiHeaders(), ...headers, - ...(authorization - ? { - Authorization: `Bearer ${authorization}`, - } - : {}), + ...(authorization ? { Authorization: `Bearer ${authorization}` } : {}), }, data: params, - }), + }); + }, }, });