diff --git a/.github/workflows/build-preview-environment.yml b/.github/workflows/build-preview-environment.yml index b419b3d335..55ababd552 100644 --- a/.github/workflows/build-preview-environment.yml +++ b/.github/workflows/build-preview-environment.yml @@ -11,15 +11,16 @@ on: permissions: id-token: write - contents: read - pull-requests: read + contents: write + pull-requests: write + packages: write env: REF: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || github.event_name == 'pull_request' && github.event.pull_request.head.ref }} jobs: deploy-dev-pr-environment: - if: contains(github.event.pull_request.labels.*.name, 'deploy-pr') + if: contains(github.event.pull_request.labels.*.name, 'deploy-pr') || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest outputs: env_name: ${{ steps.env-name.outputs.PR_ENV_NAME }} @@ -45,7 +46,7 @@ jobs: run: | SANITIZED_BRANCH_NAME=$(echo -n "${{ steps.clean-ref.outputs.ref }}" | tr "/" "-") echo "Sanitized branch name: $SANITIZED_BRANCH_NAME" - TRIMMED_BRANCH_NAME=$(echo -n "$SANITIZED_BRANCH_NAME" | cut -c 1-8) + TRIMMED_BRANCH_NAME=$(echo -n "$SANITIZED_BRANCH_NAME" | cut -c 1-7) echo "sanitized_env_name=$SANITIZED_BRANCH_NAME" >> $GITHUB_OUTPUT; echo "trimmed_env_name=$TRIMMED_BRANCH_NAME" >> $GITHUB_OUTPUT; @@ -65,9 +66,21 @@ jobs: image_name: workflows-service ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }} tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }} + file: 'services/workflows-service/Dockerfile' + + build-wf-service-ee: + needs: [deploy-dev-pr-environment,build-wf-service] + uses: ./.github/workflows/build-push-docker-images.yml + with: + registry: ghcr.io/${{ github.repository_owner }} + context: services/workflows-service + image_name: workflows-service-ee + ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }} + tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }} + file: 'services/workflows-service/Dockerfile.ee' build-backoffice: - needs: deploy-dev-pr-environment + needs: [deploy-dev-pr-environment] uses: ./.github/workflows/build-push-docker-images.yml with: registry: ghcr.io/${{ github.repository_owner }} @@ -75,9 +88,10 @@ jobs: image_name: backoffice ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }} tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }} + file: 'apps/backoffice-v2/Dockerfile.preview' build-kyb: - needs: deploy-dev-pr-environment + needs: [deploy-dev-pr-environment] uses: ./.github/workflows/build-push-docker-images.yml with: registry: ghcr.io/${{ github.repository_owner }} @@ -85,9 +99,10 @@ jobs: image_name: kyb-app ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }} tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }} + file: 'apps/kyb-app/Dockerfile.preview' build-dashboard: - needs: deploy-dev-pr-environment + needs: [deploy-dev-pr-environment] uses: ./.github/workflows/build-push-docker-images.yml with: registry: ghcr.io/${{ github.repository_owner }} @@ -95,9 +110,48 @@ jobs: image_name: workflows-dashboard ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }} tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }} + file: 'apps/workflows-dashboard/Dockerfile.preview' + + build-unified-api: + runs-on: ubuntu-latest + needs: [deploy-dev-pr-environment] + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + with: + platforms: 'arm64,arm' + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + role-to-assume: ${{ vars.PREVIEW_OIDC_ROLE }} + aws-region: ${{ vars.PREVIEW_AWS_REGION }} + + # Access the secret + - name: Retrieve secret from Secrets Manager + id: get-secret + run: | + secret_value=$(aws secretsmanager get-secret-value --secret-id ${{ vars.PREVIEW_SECRET }} --query 'SecretString' --output text | jq -r '.SUBMODULE_SECRET') + echo "SUBMODULE_SECRET=$secret_value" >> $GITHUB_ENV + echo "SUBMODULE_SECRET=$secret_value" >> $GITHUB_OUTPUT + + - name: Log in to the container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ghcr.io/${{ github.repository_owner }} + username: ${{ github.actor }} + password: ${{ steps.get-secret.outputs.SUBMODULE_SECRET }} + + - name: Checkout repository + run: | + docker pull ghcr.io/${{ github.repository_owner }}/${{ vars.UNIFIED_IMAGE_NAME }}:latest + docker tag ghcr.io/${{ github.repository_owner }}/${{ vars.UNIFIED_IMAGE_NAME }}:latest ghcr.io/${{ github.repository_owner }}/${{ vars.UNIFIED_IMAGE_NAME }}:${{ needs.deploy-dev-pr-environment.outputs.env_name }} + docker push ghcr.io/${{ github.repository_owner }}/${{ vars.UNIFIED_IMAGE_NAME }}:${{ needs.deploy-dev-pr-environment.outputs.env_name }} deploy-preview: - needs: [deploy-dev-pr-environment,build-wf-service,build-backoffice,build-kyb,build-dashboard] + needs: [deploy-dev-pr-environment,build-wf-service,build-wf-service-ee,build-backoffice,build-kyb,build-dashboard,build-unified-api] runs-on: ubuntu-latest steps: - name: Trigger workflow in another repo diff --git a/.github/workflows/build-push-docker-images.yml b/.github/workflows/build-push-docker-images.yml index 12e75b06d0..86e4cf07d9 100644 --- a/.github/workflows/build-push-docker-images.yml +++ b/.github/workflows/build-push-docker-images.yml @@ -23,10 +23,15 @@ on: required: true description: "Tag name of the Preview Image" type: string + file: + required: true + description: "File name for the Preview Image" + type: string permissions: id-token: write contents: write + packages: write pull-requests: write jobs: @@ -40,18 +45,56 @@ jobs: ref: ${{ inputs.ref }} fetch-depth: 1 persist-credentials: false - sparse-checkout: | - ${{ inputs.context }} - sparse-checkout-cone-mode: true - - name: Get tags - run: git fetch --tags origin + - name: Configure AWS credentials + if: inputs.image_name == 'workflows-service-ee' + uses: aws-actions/configure-aws-credentials@v3 + with: + role-to-assume: ${{ vars.PREVIEW_OIDC_ROLE }} + aws-region: ${{ vars.PREVIEW_AWS_REGION }} + + # Access the secret + - name: Retrieve secret from Secrets Manager + if: inputs.image_name == 'workflows-service-ee' + id: get-secret + run: | + echo ${{ inputs.image_name }} + secret_value=$(aws secretsmanager get-secret-value --secret-id ${{ vars.PREVIEW_SECRET }} --query 'SecretString' --output text | jq -r '.SUBMODULE_SECRET') + echo "SUBMODULE_SECRET=$secret_value" >> $GITHUB_ENV + echo "SUBMODULE_SECRET=$secret_value" >> $GITHUB_OUTPUT + + - name: Checkout wf-data-migration + id: wf-migration-code + if: inputs.image_name == 'workflows-service-ee' + uses: actions/checkout@v4 + with: + repository: ballerine-io/wf-data-migration + token: ${{ steps.get-secret.outputs.SUBMODULE_SECRET }} + ref: dev + fetch-depth: 1 + path: services/workflows-service/prisma/data-migrations + + - name: Get Latest Commit ID + if: inputs.image_name == 'workflows-service-ee' + id: lastcommit + uses: nmbgeek/github-action-get-latest-commit@main + with: + owner: ${{ github.repository_owner }} + token: ${{ steps.get-secret.outputs.SUBMODULE_SECRET }} + repo: wf-data-migration + branch: dev + + # - name: Get tags + # if: ${{ inputs.image_name }} != 'workflows-service-ee' + # run: git fetch --tags origin - name: Get version - if: ${{ inputs.image_name }} == 'workflows-service' + if: ${{ inputs.image_name == 'workflows-service' }} id: version run: | - TAG=$(git tag -l "$(echo ${{ inputs.image_name }}@)*" | sort -V -r | head -n 1) + echo ${{ inputs.image_name }} + git fetch --tags origin + TAG=$(git tag -l "$(echo workflow-service@)*" | sort -V -r | head -n 1) echo "tag=$TAG" echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "TAG=$TAG" >> "$GITHUB_ENV" @@ -61,7 +104,7 @@ jobs: - name: Bump version id: bump-version - if: ${{ inputs.image_name }} == 'workflows-service' + if: ${{ inputs.image_name == 'workflows-service' }} uses: ./.github/actions/bump-version with: tag: ${{ steps.version.outputs.tag }} @@ -103,7 +146,7 @@ jobs: - name: Print docker version outputs run: | echo "Metadata: ${{ steps.docker_meta.outputs.tags }}" - if [[ "${{ inputs.image_name }}" == "workflows-service" ]]; then + if [[ "${{ inputs.image_name }}" == "workflows-service" && "${{ inputs.image_name }}" != "workflows-service-ee" ]]; then echo "sha_short: ${{ steps.version.outputs.sha_short }}" echo "bump-version-version: ${{ steps.bump-version.outputs.version }}" echo "bump-version-tag: ${{ steps.bump-version.outputs.tag }}" @@ -118,17 +161,6 @@ jobs: cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache tags: ${{ steps.docker_meta.outputs.tags }} + file: ${{ inputs.file }} build-args: | - ${{ inputs.image_name == 'workflows-service' && format('"RELEASE={0}"\n"SHORT_SHA={1}"', steps.version.outputs.tag, steps.version.outputs.sha_short) || '' }} - - - name: Scan Docker Image - uses: aquasecurity/trivy-action@master - continue-on-error: true - with: - image-ref: ${{ steps.docker_meta.outputs.tags }} - format: 'table' - ignore-unfixed: true - exit-code: 1 - vuln-type: 'os,library' - severity: 'CRITICAL,HIGH' - timeout: '5m' + ${{ (inputs.image_name == 'workflows-service' && format('"RELEASE={0}"\n"SHORT_SHA={1}"', steps.version.outputs.tag, steps.version.outputs.sha_short)) || (inputs.image_name == 'workflows-service-ee' && format('"BASE_IMAGE=ghcr.io/ballerine-io/workflows-service:{0}"', inputs.tag)) || '' }} diff --git a/.github/workflows/deploy-wf-service.yml b/.github/workflows/deploy-wf-service.yml index 086d510694..a0aae724af 100644 --- a/.github/workflows/deploy-wf-service.yml +++ b/.github/workflows/deploy-wf-service.yml @@ -111,6 +111,7 @@ jobs: docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-sb ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-${{ inputs.environment }} docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-sb ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ inputs.environment }} docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-sb ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:latest + docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:latest else docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-dev docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-dev ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}:${{ env.SHORT_HASH }}-${{ inputs.environment }} diff --git a/.github/workflows/destroy-preview-environment.yml b/.github/workflows/destroy-preview-environment.yml index ee4896cf50..d8a03ead9e 100644 --- a/.github/workflows/destroy-preview-environment.yml +++ b/.github/workflows/destroy-preview-environment.yml @@ -18,7 +18,12 @@ env: jobs: deploy-dev-pr-environment: - if: contains(github.event.pull_request.labels.*.name, 'deploy-pr') + if: | + (github.event_name == 'pull_request' && github.event.action == 'unlabeled' && github.event.label.name == 'deploy-pr') + || + (github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'deploy-pr')) + || + github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest outputs: env_name: ${{ steps.env-name.outputs.PR_ENV_NAME }} @@ -43,7 +48,7 @@ jobs: run: | SANITIZED_BRANCH_NAME=$(echo -n ${{ steps.clean-ref.outputs.ref }} | tr "/" "-") echo "Sanitized branch name: $SANITIZED_BRANCH_NAME" - TRIMMED_BRANCH_NAME=$(echo -n "$SANITIZED_BRANCH_NAME" | cut -c 1-8) + TRIMMED_BRANCH_NAME=$(echo -n "$SANITIZED_BRANCH_NAME" | cut -c 1-7) echo "sanitized_env_name=$SANITIZED_BRANCH_NAME" >> $GITHUB_OUTPUT; echo "trimmed_env_name=$TRIMMED_BRANCH_NAME" >> $GITHUB_OUTPUT; @@ -60,6 +65,8 @@ jobs: (github.event_name == 'pull_request' && github.event.action == 'unlabeled' && github.event.label.name == 'deploy-pr') || (github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'deploy-pr')) + || + github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - name: Trigger workflow in another repo diff --git a/README.md b/README.md index 535ee869b1..9cb3a7365a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Documentation · - Slack + Slack · Website · diff --git a/apps/backoffice-v2/Dockerfile.preview b/apps/backoffice-v2/Dockerfile.preview new file mode 100644 index 0000000000..56425f34f6 --- /dev/null +++ b/apps/backoffice-v2/Dockerfile.preview @@ -0,0 +1,17 @@ +FROM node:18.17.1-bullseye-slim + +WORKDIR /app + +COPY ./package.json . + +RUN npm install --legacy-peer-deps + +COPY . . + +RUN npm run build --verbose + +ENV PATH="$PATH:/app/node_modules/.bin" + +EXPOSE 5137 + +CMD ["npm", "run", "prod:next", "--host"] \ No newline at end of file diff --git a/apps/backoffice-v2/package.json b/apps/backoffice-v2/package.json index 414fcd4b3c..935a69e0f9 100644 --- a/apps/backoffice-v2/package.json +++ b/apps/backoffice-v2/package.json @@ -42,6 +42,7 @@ "start": "vite", "dev": "vite", "build": "cross-env NODE_OPTIONS=--max-old-space-size=32768 vite build", + "prod:next": "vite build && vite --host", "test": "vitest run --passWithNoTests", "test:unit": "vitest run --passWithNoTests", "test:e2e": "playwright test", diff --git a/apps/kyb-app/Dockerfile.preview b/apps/kyb-app/Dockerfile.preview new file mode 100644 index 0000000000..2367991576 --- /dev/null +++ b/apps/kyb-app/Dockerfile.preview @@ -0,0 +1,19 @@ +FROM node:18.17.1-bullseye-slim + +WORKDIR /app + +RUN apt update -y && apt install xdg-utils -y + +COPY ./package.json . + +RUN npm install --legacy-peer-deps + +COPY . . + +RUN npm run build --verbose + +ENV PATH="$PATH:./node_modules/.bin" + +EXPOSE 5201 + +CMD ["npm", "run", "prod:next", "--host"] \ No newline at end of file diff --git a/apps/kyb-app/package.json b/apps/kyb-app/package.json index f8963ddce1..8515512c61 100644 --- a/apps/kyb-app/package.json +++ b/apps/kyb-app/package.json @@ -7,6 +7,7 @@ "dev": "vite", "start": "vite", "build": "tsc && vite build", + "prod:next": "vite build && vite --host", "lint": "eslint . --fix", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/apps/workflows-dashboard/Dockerfile.preview b/apps/workflows-dashboard/Dockerfile.preview new file mode 100644 index 0000000000..85f9192ffe --- /dev/null +++ b/apps/workflows-dashboard/Dockerfile.preview @@ -0,0 +1,17 @@ +FROM node:18.17.1-bullseye-slim + +WORKDIR /app + +COPY ./package.json . + +RUN npm install --legacy-peer-deps + +COPY . . + +RUN npm run build --verbose + +ENV PATH="$PATH:/app/node_modules/.bin" + +EXPOSE 5200 + +CMD ["npm", "run", "prod:next", "--host"] \ No newline at end of file diff --git a/apps/workflows-dashboard/package.json b/apps/workflows-dashboard/package.json index 43731897ed..f999d76502 100644 --- a/apps/workflows-dashboard/package.json +++ b/apps/workflows-dashboard/package.json @@ -8,6 +8,7 @@ "dev": "vite --host", "start": "vite", "build": "tsc && vite build", + "prod:next": "vite build && vite --host", "lint": "eslint . --fix", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/services/workflows-service/package.json b/services/workflows-service/package.json index aecb414f03..876366ccb9 100644 --- a/services/workflows-service/package.json +++ b/services/workflows-service/package.json @@ -12,6 +12,7 @@ "start": "nest start", "dev": "npm run start:watch", "start:prod": "node dist/src/main", + "start:preview": "npm run db:migrate-up && npm run db:data-migration:migrate && npm run db:data-sync && npm run start:prod", "prod": "npm run db:migrate-up && npm run start:prod", "prod:next": "npm run db:migrate-up && npm run db:data-sync && npm run start:prod", "start:watch": "nest start --watch", diff --git a/services/workflows-service/src/common/types.ts b/services/workflows-service/src/common/types.ts index ef511f8ffc..618ea00f30 100644 --- a/services/workflows-service/src/common/types.ts +++ b/services/workflows-service/src/common/types.ts @@ -11,7 +11,7 @@ export type TDocumentsWithoutPageType = TDocumentWithoutPageType[]; export const SubscriptionSchema = z.discriminatedUnion('type', [ z .object({ - type: z.literal('webhook'), + type: z.enum(['webhook', 'email']), url: z.string().url(), events: z.array(z.string()), config: z diff --git a/services/workflows-service/src/events/document-changed-webhook-caller.ts b/services/workflows-service/src/events/document-changed-webhook-caller.ts index 1bb0c5280b..59dbbb62e2 100644 --- a/services/workflows-service/src/events/document-changed-webhook-caller.ts +++ b/services/workflows-service/src/events/document-changed-webhook-caller.ts @@ -101,11 +101,19 @@ export class DocumentChangedWebhookCaller { return; } - const webhooks = getWebhooks( - data.updatedRuntimeData.config, - this.configService.get('ENVIRONMENT_NAME'), - 'workflow.context.document.changed', - ); + const customer = await this.customerService.getByProjectId(data.updatedRuntimeData.projectId, { + select: { + authenticationConfiguration: true, + subscriptions: true, + }, + }); + + const webhooks = getWebhooks({ + workflowConfig: data.updatedRuntimeData.config, + customerSubscriptions: customer.subscriptions, + envName: this.configService.get('ENVIRONMENT_NAME'), + event: 'workflow.context.document.changed', + }); data.updatedRuntimeData.context.documents.forEach((doc: any) => { delete doc.propertiesSchema; @@ -137,12 +145,6 @@ export class DocumentChangedWebhookCaller { }); }); - const customer = await this.customerService.getByProjectId(data.updatedRuntimeData.projectId, { - select: { - authenticationConfiguration: true, - }, - }); - const { webhookSharedSecret } = customer.authenticationConfiguration as TAuthenticationConfiguration; diff --git a/services/workflows-service/src/events/get-webhooks.ts b/services/workflows-service/src/events/get-webhooks.ts index 2723152966..58996ca508 100644 --- a/services/workflows-service/src/events/get-webhooks.ts +++ b/services/workflows-service/src/events/get-webhooks.ts @@ -1,6 +1,7 @@ import { randomUUID } from 'crypto'; import packageJson from '../../package.json'; import { WorkflowConfig } from '@/workflow/schemas/zod-schemas'; +import { TCustomerSubscription } from '@/customer/schemas/zod-schemas'; export type Webhook = { id: string; @@ -12,12 +13,58 @@ export type Webhook = { }; }; -export const getWebhooks = ( - config: WorkflowConfig, - envName: string | undefined, - event: string, -): Webhook[] => { - return (config?.subscriptions ?? []) +export const mergeSubscriptions = ( + customerSubscriptions: TCustomerSubscription['subscriptions'], + workflowSubscriptions: TCustomerSubscription['subscriptions'], +): TCustomerSubscription['subscriptions'] => { + if (!workflowSubscriptions?.length) return customerSubscriptions ?? []; + + if (!customerSubscriptions?.length) return workflowSubscriptions ?? []; + + const workflowEvents = workflowSubscriptions.flatMap(sub => sub.events); + + const processedCustomerSubs = customerSubscriptions.reduce( + (acc, sub) => { + if (sub.events.length === 0) { + acc.push(sub); + + return acc; + } + + const remainingEvents = sub.events.filter(event => !workflowEvents.includes(event)); + + if (remainingEvents.length > 0) { + acc.push({ + ...sub, + events: remainingEvents, + }); + } + + return acc; + }, + [], + ); + + return [...processedCustomerSubs, ...workflowSubscriptions]; +}; + +export const getWebhooks = ({ + workflowConfig, + customerSubscriptions, + envName, + event, +}: { + workflowConfig: WorkflowConfig; + customerSubscriptions: TCustomerSubscription['subscriptions']; + envName: string | undefined; + event: string; +}): Webhook[] => { + const mergedSubscriptions = mergeSubscriptions( + customerSubscriptions, + workflowConfig?.subscriptions ?? [], + ); + + return mergedSubscriptions .filter(({ type, events }) => type === 'webhook' && events.includes(event)) .map( ({ url, config }): Webhook => ({ diff --git a/services/workflows-service/src/events/get-webhooks.unit.test.ts b/services/workflows-service/src/events/get-webhooks.unit.test.ts new file mode 100644 index 0000000000..099b9f3118 --- /dev/null +++ b/services/workflows-service/src/events/get-webhooks.unit.test.ts @@ -0,0 +1,193 @@ +import { TCustomerSubscription } from '@/customer/schemas/zod-schemas'; +import { mergeSubscriptions } from './get-webhooks'; + +jest.mock('crypto', () => ({ + randomUUID: jest.fn().mockReturnValue('mocked-uuid'), +})); + +describe('Webhook Functions', () => { + describe('mergeSubscriptions', () => { + it('should return customer subscriptions when workflow subscriptions are empty', () => { + // Arrange + const customerSubs = [{ type: 'webhook' as const, events: ['event1'], url: 'url1' }]; + const workflowSubs: Array = []; + + // Act + const result = mergeSubscriptions(customerSubs, workflowSubs); + + // Assert + expect(result).toEqual(customerSubs); + }); + it('should return workflow subscriptions when customer subscriptions are empty', () => { + // Arrange + const customerSubs: Array = []; + const workflowSubs = [{ type: 'webhook' as const, events: ['event1'], url: 'url1' }]; + + // Act + const result = mergeSubscriptions(customerSubs, workflowSubs); + + // Assert + expect(result).toEqual(workflowSubs); + }); + + it('should override customer subscriptions with workflow subscriptions for matching events', () => { + // Arrange + const customerSubs = [ + { + type: 'webhook' as const, + events: ['workflow.completed', 'workflow.started'], + url: 'customer-url1', + }, + { type: 'webhook' as const, events: ['workflow.completed'], url: 'customer-url2' }, + ]; + const workflowSubs = [ + { type: 'webhook' as const, events: ['workflow.completed'], url: 'workflow-url1' }, + ]; + + // Act + const result = mergeSubscriptions(customerSubs, workflowSubs); + + // Assert + expect(result).toEqual([ + { type: 'webhook', events: ['workflow.started'], url: 'customer-url1' }, + { type: 'webhook', events: ['workflow.completed'], url: 'workflow-url1' }, + ]); + }); + + it('should override customer subscriptions with workflow subscriptions for matching events regardless of type', () => { + // Arrange + const customerSubs = [ + { type: 'email' as const, events: ['workflow.completed'], url: 'customer-email' }, + { type: 'webhook' as const, events: ['workflow.completed'], url: 'customer-url' }, + ]; + const workflowSubs = [ + { type: 'webhook' as const, events: ['workflow.completed'], url: 'workflow-url' }, + { type: 'email' as const, events: ['workflow.completed'], url: 'workflow-email' }, + ]; + + // Act + const result = mergeSubscriptions(customerSubs, workflowSubs); + + // Assert + expect(result).toEqual([ + { type: 'webhook', events: ['workflow.completed'], url: 'workflow-url' }, + { type: 'email', events: ['workflow.completed'], url: 'workflow-email' }, + ]); + }); + + it('should handle multiple events in workflow subscriptions', () => { + // Arrange + const customerSubs = [ + { type: 'webhook' as const, events: ['event1', 'event2', 'event3'], url: 'customer-url1' }, + { type: 'webhook' as const, events: ['event2', 'event4'], url: 'customer-url2' }, + ]; + const workflowSubs = [ + { type: 'webhook' as const, events: ['event1', 'event2'], url: 'workflow-url' }, + ]; + + // Act + const result = mergeSubscriptions(customerSubs, workflowSubs); + + // Assert + expect(result).toEqual([ + { type: 'webhook', events: ['event3'], url: 'customer-url1' }, + { type: 'webhook', events: ['event4'], url: 'customer-url2' }, + { type: 'webhook', events: ['event1', 'event2'], url: 'workflow-url' }, + ]); + }); + + it('should remove customer subscriptions entirely if all their events are overridden', () => { + // Arrange + const customerSubs = [ + { type: 'webhook' as const, events: ['event1', 'event2'], url: 'customer-url' }, + ]; + const workflowSubs = [ + { type: 'webhook' as const, events: ['event1', 'event2'], url: 'workflow-url' }, + ]; + + // Act + const result = mergeSubscriptions(customerSubs, workflowSubs); + + // Assert + expect(result).toEqual([ + { type: 'webhook', events: ['event1', 'event2'], url: 'workflow-url' }, + ]); + }); + + it('should handle empty arrays for both customer and workflow subscriptions', () => { + // Arrange + const customerSubs: Array = []; + const workflowSubs: Array = []; + + // Act + const result = mergeSubscriptions(customerSubs, workflowSubs); + + // Assert + expect(result).toEqual([]); + }); + + it('should handle undefined customer subscriptions', () => { + // Arrange + const customerSubs = undefined; + const workflowSubs = [{ type: 'webhook' as const, events: ['event1'], url: 'workflow-url' }]; + + // Act + const result = mergeSubscriptions( + customerSubs as unknown as Array, + workflowSubs, + ); + + // Assert + expect(result).toEqual([{ type: 'webhook', events: ['event1'], url: 'workflow-url' }]); + }); + + it('should handle undefined workflow subscriptions', () => { + // Arrange + const customerSubs = [{ type: 'webhook' as const, events: ['event1'], url: 'customer-url' }]; + const workflowSubs = undefined; + + // Act + const result = mergeSubscriptions( + customerSubs as unknown as Array, + workflowSubs as unknown as Array, + ); + + // Assert + expect(result).toEqual([{ type: 'webhook', events: ['event1'], url: 'customer-url' }]); + }); + + it('should handle empty events arrays', () => { + // Arrange + const customerSubs = [{ type: 'webhook' as const, events: [], url: 'customer-url' }]; + const workflowSubs = [{ type: 'webhook' as const, events: [], url: 'workflow-url' }]; + + // Act + const result = mergeSubscriptions(customerSubs, workflowSubs); + + // Assert + expect(result).toEqual([ + { type: 'webhook', events: [], url: 'customer-url' }, + { type: 'webhook', events: [], url: 'workflow-url' }, + ]); + }); + + it('should handle duplicate events in workflow subscriptions', () => { + // Arrange + const customerSubs = [ + { type: 'webhook' as const, events: ['event1', 'event2'], url: 'customer-url' }, + ]; + const workflowSubs = [ + { type: 'webhook' as const, events: ['event1', 'event1'], url: 'workflow-url' }, + ]; + + // Act + const result = mergeSubscriptions(customerSubs, workflowSubs); + + // Assert + expect(result).toEqual([ + { type: 'webhook', events: ['event2'], url: 'customer-url' }, + { type: 'webhook', events: ['event1', 'event1'], url: 'workflow-url' }, + ]); + }); + }); +}); diff --git a/services/workflows-service/src/events/workflow-completed-webhook-caller.ts b/services/workflows-service/src/events/workflow-completed-webhook-caller.ts index 0fcbf32117..c0c926b485 100644 --- a/services/workflows-service/src/events/workflow-completed-webhook-caller.ts +++ b/services/workflows-service/src/events/workflow-completed-webhook-caller.ts @@ -49,18 +49,20 @@ export class WorkflowCompletedWebhookCaller { id: data.runtimeData.id, }); - const webhooks = getWebhooks( - data.runtimeData.config, - this.configService.get('ENVIRONMENT_NAME'), - 'workflow.completed', - ); - const customer = await this.customerService.getByProjectId(data.runtimeData.projectId, { select: { authenticationConfiguration: true, + subscriptions: true, }, }); + const webhooks = getWebhooks({ + workflowConfig: data.runtimeData.config, + customerSubscriptions: customer.subscriptions, + envName: this.configService.get('ENVIRONMENT_NAME'), + event: 'workflow.completed', + }); + const { webhookSharedSecret } = customer.authenticationConfiguration as TAuthenticationConfiguration; diff --git a/services/workflows-service/src/events/workflow-state-changed-webhook-caller.ts b/services/workflows-service/src/events/workflow-state-changed-webhook-caller.ts index 12140b27c3..372d53fa44 100644 --- a/services/workflows-service/src/events/workflow-state-changed-webhook-caller.ts +++ b/services/workflows-service/src/events/workflow-state-changed-webhook-caller.ts @@ -42,18 +42,20 @@ export class WorkflowStateChangedWebhookCaller { id: data.runtimeData.id, }); - const webhooks = getWebhooks( - data.runtimeData.config, - this.configService.get('ENVIRONMENT_NAME'), - 'workflow.state.changed', - ); - const customer = await this.customerService.getByProjectId(data.runtimeData.projectId, { select: { authenticationConfiguration: true, + subscriptions: true, }, }); + const webhooks = getWebhooks({ + workflowConfig: data.runtimeData.config, + customerSubscriptions: customer.subscriptions, + envName: this.configService.get('ENVIRONMENT_NAME'), + event: 'workflow.state.changed', + }); + const { webhookSharedSecret } = customer.authenticationConfiguration as TAuthenticationConfiguration; diff --git a/services/workflows-service/src/rule-engine/core/test/rule-engine.unit.test.ts b/services/workflows-service/src/rule-engine/core/test/rule-engine.unit.test.ts index 98c3341240..d8235788e8 100644 --- a/services/workflows-service/src/rule-engine/core/test/rule-engine.unit.test.ts +++ b/services/workflows-service/src/rule-engine/core/test/rule-engine.unit.test.ts @@ -254,6 +254,16 @@ describe('Rule Engine', () => { }; const engine = RuleEngine(ruleSetExample); + const today = new Date(); + const sixMonthsAgo = new Date(); + sixMonthsAgo.setMonth(today.getMonth() - 6); + + if (context.pluginsOutput?.businessInformation?.data?.[0]) { + context.pluginsOutput.businessInformation.data[0].establishDate = sixMonthsAgo + .toISOString() + .split('T')[0] as string; + } + let result = engine.run(context); expect(result).toBeDefined(); @@ -313,7 +323,13 @@ describe('Rule Engine', () => { const sixMonthsAgo = new Date(); sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); - const context1 = JSON.parse(JSON.stringify(context)) as any; + const context1 = { + pluginsOutput: { + businessInformation: { + data: [{ establishDate: sixMonthsAgo.toISOString() }], + }, + }, + }; let result = engine.run(context1); expect(result).toBeDefined();