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();