Repository: kodustech/kodus-ai
Pull request #23: Fix/approve-pr-logic
Author: login: stelianok
Created: 2025-04-25T20:33:45Z | Merged: 2025-04-26T13:48:19Z

This pull request addresses the logic for automatically approving pull requests across multiple code management platforms within the `kodus-ai` repository. The changes are made in the `fix/approve-pr-logic` branch and are intended to be merged into the `main` branch.

Key updates include:

1. **Webhook Handling**: Enhancements to the webhook handling logic in `receiveWebhook.use-case.ts`, with new conditions added and existing ones improved. There are noted areas for improvement in error handling and maintainability.

2. **Interface Update**: A new method is added to `code-management.interface.ts` to determine if a pull request should be approved.

3. **Azure Repos**: 
   - A new method `checkIfPullRequestShouldBeApproved` is introduced in `azureRepos.service.ts` to automate PR approval based on reviewer status.
   - `azure-repos-request-helper.ts` now includes a method `getListOfPullRequestReviewers` to fetch reviewer details.

4. **Bitbucket**: 
   - A function `checkIfPullRequestShouldBeApproved` is added to `bitbucket.service.ts` for automatic PR approval.
   - Improvements are made to comment creation logic and filtering, with suggestions for better handling of null values, type safety, and maintainability.

5. **Cron Job**: The logic in `CheckIfPRCanBeApproved.cron.ts` is refactored to streamline PR approval checks based on resolved review comments, specifically for Azure Repos.

6. **GitHub**: 
   - `GithubService` is updated with a method `checkIfPullRequestShouldBeApproved` for automatic PR approvals.
   - Minor refinements are made to interface implementation and data mapping.

7. **GitLab**: 
   - A method `checkIfPullRequestShouldBeApproved` is added to `gitlab.service.ts` for automatic merge request approvals.
   - Code reformatting adjustments are made throughout the file.

8. **Code Management Service**: 
   - A new method `checkIfPullRequestShouldBeApproved` is added to `codeManagement.service.ts`, which delegates the approval check to the appropriate platform-specific service.

Overall, this pull request aims to enhance the logic for automatic pull request approvals across different platforms, with a focus on improving maintainability and consistency in the codebase.


Diff:
diff --git a/src/core/application/use-cases/platformIntegration/codeManagement/receiveWebhook.use-case.ts b/src/core/application/use-cases/platformIntegration/codeManagement/receiveWebhook.use-case.ts
index af34c703c..90d364104 100644
--- a/src/core/application/use-cases/platformIntegration/codeManagement/receiveWebhook.use-case.ts
+++ b/src/core/application/use-cases/platformIntegration/codeManagement/receiveWebhook.use-case.ts
@@ -36,7 +36,7 @@ export class ReceiveWebhookUseCase implements IUseCase {
         private readonly integrationConfigService: IIntegrationConfigService,
 
         private readonly bitbuckerService: BitbucketService,
-    ) {}
+    ) { }
 
     public async execute(params: {
         payload: any;
@@ -90,7 +90,7 @@ export class ReceiveWebhookUseCase implements IUseCase {
                 case 'Note Hook':
                     if (
                         params.payload?.object_attributes?.action ===
-                            'create' &&
+                        'create' &&
                         !params.payload?.object_attributes?.change_position &&
                         !params.payload?.object_attributes?.type
                     ) {
@@ -251,6 +251,14 @@ export class ReceiveWebhookUseCase implements IUseCase {
             return true;
         }
 
+        // Verify if the PR is closed.
+        if (
+            objectAttributes.state === 'closed' ||
+            objectAttributes.action === 'close'
+        ) {
+            return true;
+        }
+
         // Ignore if it's an update to the description
         if (objectAttributes.action === 'update' && changes.description) {
             return false;
@@ -322,7 +330,7 @@ export class ReceiveWebhookUseCase implements IUseCase {
                 repository.name,
             );
 
-        if (storedPR) {
+        if (storedPR && pullrequest.state === 'OPEN') {
             const prCommit = pullRequestCommits[pullRequestCommits.length - 1];
             const storedPRCommitHashes = storedPR?.commits?.map(
                 (commit) => commit.sha,
diff --git a/src/core/domain/azureRepos/entities/azureRepoExtras.type.ts b/src/core/domain/azureRepos/entities/azureRepoExtras.type.ts
index d530ce550..914102f8c 100644
--- a/src/core/domain/azureRepos/entities/azureRepoExtras.type.ts
+++ b/src/core/domain/azureRepos/entities/azureRepoExtras.type.ts
@@ -211,3 +211,31 @@ export interface AzureRepoDiffChange {
     additions?: number;
     deletions?: number;
 }
+
+/**
+ * Representa um reviewer retornado pela API
+ * IdentityRefWithVote (nome do tipo retornado)
+ */
+export interface AzureRepoReviewerWithVote {
+    _links: any;
+    descriptor: string;
+    directoryAlias?: string; // deprecated
+    displayName: string;
+    hasDeclined: boolean;
+    id: string;
+    imageUrl?: string; // deprecated
+    inactive?: boolean; // deprecated
+    isAadIdentity?: boolean; // deprecated
+    isContainer?: boolean; // deprecated
+    isDeletedInOrigin: boolean;
+    isFlagged: boolean;
+    isReapprove: boolean;
+    isRequired: boolean;
+    profileUrl?: string; // deprecated
+    reviewerUrl: string;
+    uniqueName?: string; // deprecated
+    url: string;
+    vote: number; // int16
+    votedFor: any;
+}
+
diff --git a/src/core/domain/platformIntegrations/interfaces/code-management.interface.ts b/src/core/domain/platformIntegrations/interfaces/code-management.interface.ts
index 000632db7..4d919141a 100644
--- a/src/core/domain/platformIntegrations/interfaces/code-management.interface.ts
+++ b/src/core/domain/platformIntegrations/interfaces/code-management.interface.ts
@@ -110,4 +110,10 @@ export interface ICodeManagementService
         repository: Partial<Repository>,
     }): Promise<PullRequestsWithChangesRequested[] | null>
 
+    checkIfPullRequestShouldBeApproved(params: {
+        organizationAndTeamData: OrganizationAndTeamData,
+        prNumber: number,
+        repository: { id: string; name: string };
+    }): Promise<any | null>;
+
 }
diff --git a/src/core/infrastructure/adapters/services/azureRepos.service.ts b/src/core/infrastructure/adapters/services/azureRepos.service.ts
index 60bbceda4..d9ccbf7b1 100644
--- a/src/core/infrastructure/adapters/services/azureRepos.service.ts
+++ b/src/core/infrastructure/adapters/services/azureRepos.service.ts
@@ -65,31 +65,31 @@ import {
     TranslationsCategory,
 } from '@/shared/utils/translations/translations';
 import { LanguageValue } from '@/shared/domain/enums/language-parameter.enum';
+import { KODY_CRITICAL_ISSUE_COMMENT_MARKER } from '@/shared/utils/codeManagement/codeCommentMarkers';
 
 @IntegrationServiceDecorator(PlatformType.AZURE_REPOS, 'codeManagement')
 export class AzureReposService
     implements
-        Omit<
-            ICodeManagementService,
-            | 'getOrganizations'
-            | 'getPullRequestDetails'
-            | 'getWorkflows'
-            | 'getListMembers'
-            | 'getCommitsByReleaseMode'
-            | 'getPullRequestsForRTTM'
-            | 'createCommentInPullRequest'
-            | 'getPullRequestReviewThreads'
-            | 'getListOfValidReviews'
-            | 'getPullRequestsWithChangesRequested'
-            | 'findTeamAndOrganizationIdByConfigKey'
-            | 'createResponseToComment'
-            | 'getAuthenticationOAuthToken'
-            | 'countReactions'
-            | 'getRepositoryAllFiles'
-            | 'mergePullRequest'
-            | 'getUserById'
-        >
-{
+    Omit<
+        ICodeManagementService,
+        | 'getOrganizations'
+        | 'getPullRequestDetails'
+        | 'getWorkflows'
+        | 'getListMembers'
+        | 'getCommitsByReleaseMode'
+        | 'getPullRequestsForRTTM'
+        | 'createCommentInPullRequest'
+        | 'getPullRequestReviewThreads'
+        | 'getListOfValidReviews'
+        | 'getPullRequestsWithChangesRequested'
+        | 'findTeamAndOrganizationIdByConfigKey'
+        | 'createResponseToComment'
+        | 'getAuthenticationOAuthToken'
+        | 'countReactions'
+        | 'getRepositoryAllFiles'
+        | 'mergePullRequest'
+        | 'getUserById'
+    > {
     constructor(
         @Inject(INTEGRATION_SERVICE_TOKEN)
         private readonly integrationService: IIntegrationService,
@@ -105,7 +105,8 @@ export class AzureReposService
 
         private readonly logger: PinoLoggerService,
         private readonly azureReposRequestHelper: AzureReposRequestHelper,
-    ) {}
+    ) { }
+
 
     async markReviewCommentAsResolved(params: {
         organizationAndTeamData: OrganizationAndTeamData;
@@ -156,6 +157,64 @@ export class AzureReposService
         }
     }
 
+    async checkIfPullRequestShouldBeApproved(params: {
+        organizationAndTeamData: OrganizationAndTeamData;
+        prNumber: number;
+        repository: { id: string; name: string; };
+    }): Promise<any | null> {
+        try {
+            const { organizationAndTeamData, prNumber, repository } = params;
+
+            const { orgName, token } = await this.getAuthDetails(
+                organizationAndTeamData,
+            );
+
+            const projectId = await this.getProjectIdFromRepository(
+                organizationAndTeamData,
+                repository.id,
+            );
+
+            const currentUserId =
+                await this.azureReposRequestHelper.getAuthenticatedUserId({
+                    orgName,
+                    token,
+                });
+
+            if (!currentUserId) {
+                throw new Error('Unable to identify reviewer from PAT.');
+            }
+
+            const reviewers = await this.azureReposRequestHelper.getListOfPullRequestReviewers({
+                orgName,
+                projectId,
+                repositoryId: repository.id,
+                prId: prNumber,
+                token
+            });
+
+            const isApprovedByCurrentUser = reviewers
+                ? reviewers?.some((reviewer) => (reviewer.id === currentUserId))
+                : false;
+
+            if (isApprovedByCurrentUser) {
+                return null;
+            }
+
+            const result = await this.approvePullRequest({ organizationAndTeamData, prNumber, repository });
+
+            return result;
+        } catch (error) {
+            this.logger.error({
+                message: `Error approving pull request #${params.prNumber}`,
+                context: AzureReposService.name,
+                serviceName: 'AzureReposService approvePullRequest',
+                error,
+                metadata: { params },
+            });
+            return null;
+        }
+    }
+
     async approvePullRequest(params: {
         organizationAndTeamData: OrganizationAndTeamData;
         prNumber: number;
@@ -1074,7 +1133,7 @@ export class AzureReposService
                             '## Code Review Completed! ud83dudd25',
                         ) &&
                         !comment.body.includes(
-                            '# Found critical issues please',
+                            KODY_CRITICAL_ISSUE_COMMENT_MARKER,
                         ),
                 )
                 .sort(
@@ -1567,9 +1626,8 @@ export class AzureReposService
                 queryString += `created_on >= "${filters.startDate}"`;
             }
             if (filters?.endDate) {
-                queryString += `${
-                    queryString ? ' AND ' : ''
-                }created_on <= "${filters.endDate}"`;
+                queryString += `${queryString ? ' AND ' : ''
+                    }created_on <= "${filters.endDate}"`;
             }
 
             const projectId = await this.getProjectIdFromRepository(
@@ -2766,7 +2824,7 @@ export class AzureReposService
             actionStatement,
             this.formatSub(translations.talkToKody),
             this.formatSub(translations.feedback) +
-                '<!-- kody-codereview -->&#8203;\n&#8203;',
+            '<!-- kody-codereview -->&#8203;\n&#8203;',
         ]
             .join('\n')
             .trim();
diff --git a/src/core/infrastructure/adapters/services/azureRepos/azure-repos-request-helper.ts b/src/core/infrastructure/adapters/services/azureRepos/azure-repos-request-helper.ts
index f55ad79ee..28f54d74f 100644
--- a/src/core/infrastructure/adapters/services/azureRepos/azure-repos-request-helper.ts
+++ b/src/core/infrastructure/adapters/services/azureRepos/azure-repos-request-helper.ts
@@ -11,13 +11,14 @@ import {
     AzureRepoPRThread,
     AzureRepoSubscription,
     AzureRepoCommentType,
+    AzureRepoReviewerWithVote,
 } from '@/core/domain/azureRepos/entities/azureRepoExtras.type';
 import { decrypt } from '@/shared/utils/crypto';
 import { FileChange } from '@/config/types/general/codeReview.type';
 
 @Injectable()
 export class AzureReposRequestHelper {
-    constructor() {}
+    constructor() { }
 
     async getProjects(params: {
         orgName: string;
@@ -633,6 +634,22 @@ export class AzureReposRequestHelper {
         return data;
     }
 
+    async getListOfPullRequestReviewers(params: {
+        orgName: string;
+        token: string;
+        projectId: string;
+        repositoryId: string;
+        prId: number;
+    }): Promise<AzureRepoReviewerWithVote[]> {
+        const instance = await this.azureRequest(params);
+
+        const url = `/${params.projectId}/_apis/git/repositories/${params.repositoryId}/pullRequests/${params.prId}/reviewers/?api-version=7.1`;
+
+        const { data } = await instance.get(url);
+
+        return data?.value ?? [];
+    }
+
     async getRepositoryContentFile(params: {
         orgName: string;
         token: string;
diff --git a/src/core/infrastructure/adapters/services/bitbucket/bitbucket.service.ts b/src/core/infrastructure/adapters/services/bitbucket/bitbucket.service.ts
index 1093641da..cff3ed3ea 100644
--- a/src/core/infrastructure/adapters/services/bitbucket/bitbucket.service.ts
+++ b/src/core/infrastructure/adapters/services/bitbucket/bitbucket.service.ts
@@ -63,26 +63,26 @@ import {
     REPOSITORY_MANAGER_TOKEN,
 } from '@/core/domain/repository/contracts/repository-manager.contract';
 import { IRepository } from '@/core/domain/pullRequests/interfaces/pullRequests.interface';
+import { KODY_CODE_REVIEW_COMPLETED_MARKER, KODY_CRITICAL_ISSUE_COMMENT_MARKER, KODY_START_COMMAND_MARKER } from '@/shared/utils/codeManagement/codeCommentMarkers';
 
 @Injectable()
 @IntegrationServiceDecorator(PlatformType.BITBUCKET, 'codeManagement')
 export class BitbucketService
     implements
-        IBitbucketService,
-        Omit<
-            ICodeManagementService,
-            | 'getOrganizations'
-            | 'getListOfValidReviews'
-            | 'getUserByEmailOrName'
-            | 'getPullRequestReviewThreads'
-            | 'getUserById'
-            | 'getDataForCalculateDeployFrequency'
-            | 'getCommitsByReleaseMode'
-            | 'getAuthenticationOAuthToken'
-            | 'countReactions'
-            | 'getRepositoryAllFiles'
-        >
-{
+    IBitbucketService,
+    Omit<
+        ICodeManagementService,
+        | 'getOrganizations'
+        | 'getListOfValidReviews'
+        | 'getUserByEmailOrName'
+        | 'getPullRequestReviewThreads'
+        | 'getUserById'
+        | 'getDataForCalculateDeployFrequency'
+        | 'getCommitsByReleaseMode'
+        | 'getAuthenticationOAuthToken'
+        | 'countReactions'
+        | 'getRepositoryAllFiles'
+    > {
     constructor(
         @Inject(INTEGRATION_SERVICE_TOKEN)
         private readonly integrationService: IIntegrationService,
@@ -102,7 +102,7 @@ export class BitbucketService
         private readonly promptService: PromptService,
 
         private readonly logger: PinoLoggerService,
-    ) {}
+    ) { }
 
     async getPullRequestsWithChangesRequested(params: {
         organizationAndTeamData: OrganizationAndTeamData;
@@ -1473,7 +1473,7 @@ export class BitbucketService
                             path: lineComment?.path,
                             to: this.sanitizeLine(
                                 params.lineComment.start_line ??
-                                    params.lineComment.line,
+                                params.lineComment.line,
                             ),
                         },
                     },
@@ -2817,6 +2817,70 @@ export class BitbucketService
         }
     }
 
+    async checkIfPullRequestShouldBeApproved(params: {
+        organizationAndTeamData: OrganizationAndTeamData,
+        prNumber: number,
+        repository: { id: string; name: string };
+    }) {
+        try {
+            const { organizationAndTeamData, prNumber, repository } = params;
+
+            const bitbucketAuthDetails = await this.getAuthDetails(
+                organizationAndTeamData,
+            );
+
+            const workspace = await this.getWorkspaceFromRepository(
+                organizationAndTeamData,
+                repository.id,
+            );
+
+            if (!workspace) {
+                return null;
+            }
+
+            const bitbucketAPI =
+                this.instanceBitbucketApi(bitbucketAuthDetails);
+
+            if (!bitbucketAuthDetails) {
+                this.logger.warn({
+                    message: 'Bitbucket auth details not found',
+                    context: this.checkIfPullRequestShouldBeApproved.name,
+                    metadata: { organizationAndTeamData }
+                });
+                return null;
+            }
+
+            const currentUser = (await bitbucketAPI.users.getAuthedUser({})).data;
+
+            const activities = await bitbucketAPI.pullrequests.listActivities({
+                repo_slug: `{${repository.id}}`,
+                workspace: `{${workspace}}`,
+                pull_request_id: prNumber,
+            }).then((res) => this.getPaginatedResults(bitbucketAPI, res));
+
+            const isApprovedByCurrentUser = activities
+                .find((activity: any) => activity.approval?.user?.uuid === currentUser?.uuid);
+
+            if (isApprovedByCurrentUser) {
+                return null;
+            }
+
+            await this.approvePullRequest({ organizationAndTeamData, prNumber, repository });
+
+        } catch (error) {
+            this.logger.error({
+                message: `Error to approve pull request #${params.prNumber}`,
+                context: BitbucketService.name,
+                serviceName: 'BitbucketService checkIfPullRequestShouldBeApproved',
+                error: error,
+                metadata: {
+                    params,
+                },
+            });
+            return null;
+        }
+    }
+
     async approvePullRequest(params: {
         organizationAndTeamData: OrganizationAndTeamData;
         prNumber: number;
@@ -3055,9 +3119,8 @@ export class BitbucketService
                 queryString += `created_on >= "${filters.startDate}"`;
             }
             if (filters?.endDate) {
-                queryString += `${
-                    queryString ? ' AND ' : ''
-                }created_on <= "${filters.endDate}"`;
+                queryString += `${queryString ? ' AND ' : ''
+                    }created_on <= "${filters.endDate}"`;
             }
 
             const pullRequests = await bitbucketAPI.pullrequests
@@ -3167,12 +3230,6 @@ export class BitbucketService
 
             const bitbucketAPI = this.instanceBitbucketApi(bitbucketAuthDetail);
 
-            // const activities = await bitbucketAPI.pullrequests.listActivities({
-            //     repo_slug: `{${repository.id}}`,
-            //     workspace: `{${workspace}}`,
-            //     pull_request_id: prNumber,
-            // });
-
             const comments = await bitbucketAPI.pullrequests
                 .listComments({
                     repo_slug: `{${repository.id}}`,
@@ -3186,10 +3243,13 @@ export class BitbucketService
                 .filter((comment) => {
                     return (
                         !comment?.content?.raw.includes(
-                            '## Code Review Completed! 🔥',
+                            KODY_CODE_REVIEW_COMPLETED_MARKER,
+                        ) &&
+                        !comment?.content?.raw.includes(
+                            KODY_CRITICAL_ISSUE_COMMENT_MARKER,
                         ) &&
                         !comment?.content?.raw.includes(
-                            '# Found critical issues please',
+                            KODY_START_COMMAND_MARKER,
                         )
                     ); // Exclude comments with the specific strings
                 })
diff --git a/src/core/infrastructure/adapters/services/cron/CheckIfPRCanBeApproved.cron.ts b/src/core/infrastructure/adapters/services/cron/CheckIfPRCanBeApproved.cron.ts
index 030a4cc06..d45f86ed0 100644
--- a/src/core/infrastructure/adapters/services/cron/CheckIfPRCanBeApproved.cron.ts
+++ b/src/core/infrastructure/adapters/services/cron/CheckIfPRCanBeApproved.cron.ts
@@ -26,10 +26,6 @@ import {
     CodeReviewConfigWithRepositoryInfo,
 } from '@/config/types/general/codeReview.type';
 import { PullRequestState } from '@/shared/domain/enums/pullRequestState.enum';
-import {
-    IIntegrationService,
-    INTEGRATION_SERVICE_TOKEN,
-} from '@/core/domain/integrations/contracts/integration.service.contracts';
 import { AzureRepoCommentTypeString } from '@/core/domain/azureRepos/entities/azureRepoExtras.type';
 
 const API_CRON_CHECK_IF_PR_SHOULD_BE_APPROVED =
@@ -49,11 +45,8 @@ export class CheckIfPRCanBeApprovedCronProvider {
         @Inject(PULL_REQUESTS_SERVICE_TOKEN)
         private readonly pullRequestService: IPullRequestsService,
 
-        @Inject(INTEGRATION_SERVICE_TOKEN)
-        private readonly integrationService: IIntegrationService,
-
         private readonly codeManagementService: CodeManagementService,
-    ) {}
+    ) { }
 
     @Cron(API_CRON_CHECK_IF_PR_SHOULD_BE_APPROVED, {
         name: 'CHECK IF PR SHOULD BE APPROVED',
@@ -227,28 +220,25 @@ export class CheckIfPRCanBeApprovedCronProvider {
                     );
             }
 
-            if (!reviewComments || reviewComments.length < 1) {
-                return false;
-            }
-
-            let isEveryReviewCommentResolved = null;
-
             if (platformType === PlatformType.AZURE_REPOS) {
-                isEveryReviewCommentResolved = reviewComments
+                reviewComments = reviewComments
                     .filter(
                         (comment) =>
                             comment?.commentType ===
                             AzureRepoCommentTypeString.CODE,
                     )
-                    ?.every((reviewComment) => reviewComment.isResolved);
-            } else {
-                isEveryReviewCommentResolved = reviewComments?.every(
-                    (reviewComment) => reviewComment.isResolved,
-                );
             }
 
+            if (!reviewComments || reviewComments.length < 1) {
+                return false;
+            }
+
+            const isEveryReviewCommentResolved = reviewComments?.every(
+                (reviewComment) => reviewComment.isResolved,
+            );
+
             if (isEveryReviewCommentResolved) {
-                await this.codeManagementService.approvePullRequest(
+                await this.codeManagementService.checkIfPullRequestShouldBeApproved(
                     {
                         organizationAndTeamData,
                         prNumber,
diff --git a/src/core/infrastructure/adapters/services/github/github.service.ts b/src/core/infrastructure/adapters/services/github/github.service.ts
index ff08a7d41..9989d2133 100644
--- a/src/core/infrastructure/adapters/services/github/github.service.ts
+++ b/src/core/infrastructure/adapters/services/github/github.service.ts
@@ -105,15 +105,14 @@ interface GitHubAuthResponse {
 @IntegrationServiceDecorator(PlatformType.GITHUB, 'codeManagement')
 export class GithubService
     implements
-        IGithubService,
-        Omit<
-            ICodeManagementService,
-            | 'getOrganizations'
-            | 'getUserById'
-            | 'getLanguageRepository'
-            | 'createSingleIssueComment'
-        >
-{
+    IGithubService,
+    Omit<
+        ICodeManagementService,
+        | 'getOrganizations'
+        | 'getUserById'
+        | 'getLanguageRepository'
+        | 'createSingleIssueComment'
+    > {
     private readonly MAX_RETRY_ATTEMPTS = 2;
     private readonly TTL = 50 * 60 * 1000; // 50 minutes
 
@@ -146,7 +145,8 @@ export class GithubService
 
         private readonly promptService: PromptService,
         private readonly logger: PinoLoggerService,
-    ) {}
+    ) { }
+
 
     private async handleIntegration(
         integration: any,
@@ -1902,7 +1902,7 @@ export class GithubService
                         if (
                             secondToLastDeploy &&
                             lastDeploy.teamConfig?.configValue?.type ===
-                                'deployment'
+                            'deployment'
                         ) {
                             commits = await this.getCommitsForTagName(
                                 octokit,
@@ -1913,7 +1913,7 @@ export class GithubService
                         } else if (
                             secondToLastDeploy &&
                             lastDeploy.teamConfig?.configValue?.type ===
-                                'releases'
+                            'releases'
                         ) {
                             commits = await this.getCommitsForTagName(
                                 octokit,
@@ -2420,7 +2420,7 @@ export class GithubService
             actionStatement,
             this.formatSub(translations.talkToKody),
             this.formatSub(translations.feedback) +
-                '<!-- kody-codereview -->&#8203;\n&#8203;',
+            '<!-- kody-codereview -->&#8203;\n&#8203;',
         ]
             .join('\n')
             .trim();
@@ -2631,13 +2631,13 @@ export class GithubService
                         // So we need one of them to actually mark the thread as resolved and the other to match the id we saved in the database.
                         return firstComment
                             ? {
-                                  id: firstComment.id, // Used to actually resolve the thread
-                                  threadId: reviewThread.id,
-                                  isResolved: reviewThread.isResolved,
-                                  isOutdated: reviewThread.isOutdated,
-                                  fullDatabaseId: firstComment.fullDatabaseId, // The REST API id, used to match comments saved in the database.
-                                  body: firstComment.body,
-                              }
+                                id: firstComment.id, // Used to actually resolve the thread
+                                threadId: reviewThread.id,
+                                isResolved: reviewThread.isResolved,
+                                isOutdated: reviewThread.isOutdated,
+                                fullDatabaseId: firstComment.fullDatabaseId, // The REST API id, used to match comments saved in the database.
+                                body: firstComment.body,
+                            }
                             : null;
                     })
                     .filter((comment) => comment !== null);
@@ -3213,15 +3213,15 @@ export class GithubService
                 reactions: {
                     thumbsUp: isOAuth
                         ? Math.max(
-                              0,
-                              comment.reactions[GitHubReaction.THUMBS_UP] - 1,
-                          )
+                            0,
+                            comment.reactions[GitHubReaction.THUMBS_UP] - 1,
+                        )
                         : comment.reactions[GitHubReaction.THUMBS_UP],
                     thumbsDown: isOAuth
                         ? Math.max(
-                              0,
-                              comment.reactions[GitHubReaction.THUMBS_DOWN] - 1,
-                          )
+                            0,
+                            comment.reactions[GitHubReaction.THUMBS_DOWN] - 1,
+                        )
                         : comment.reactions[GitHubReaction.THUMBS_DOWN],
                 },
                 comment: {
@@ -3418,6 +3418,77 @@ export class GithubService
         }
     }
 
+    async checkIfPullRequestShouldBeApproved(params: {
+        organizationAndTeamData: OrganizationAndTeamData;
+        prNumber: number;
+        repository: { id: string; name: string; };
+    }): Promise<any | null> {
+        const { organizationAndTeamData, prNumber, repository } = params;
+
+        const githubAuth = await this.getGithubAuthDetails(
+            organizationAndTeamData,
+        );
+
+        const octokit = await this.instanceOctokit(
+            organizationAndTeamData,
+            githubAuth,
+        );
+
+        const graphQLWithAuth = await this.instanceGraphQL(
+            organizationAndTeamData,
+        );
+
+        const query = `
+        query {
+          viewer {
+            login
+            id
+            __typename
+          }
+        }
+      `;
+
+        const variables = {};
+        const userAuth: {
+            viewer: { login: string; id: string; __typename: string };
+        } = await graphQLWithAuth(query, variables);
+
+        const { data: allReviews } = await octokit.rest.pulls.listReviews({
+            owner: githubAuth.org,
+            repo: repository.name,
+            pull_number: prNumber,
+            per_page: 100,
+        });
+
+        if (!allReviews?.length) {
+            return;
+        }
+
+        const myReviews = allReviews
+            ?.filter(
+                (reviewer) =>
+                    reviewer?.user?.login === userAuth?.viewer?.login &&
+                    reviewer?.user?.node_id === userAuth?.viewer?.id,
+            )
+            ?.sort(
+                (a, b) =>
+                    new Date(a.submitted_at!).getTime() -
+                    new Date(b.submitted_at!).getTime(),
+            );
+
+        if (!myReviews?.length) {
+            return;
+        }
+
+        const lastMy = myReviews.pop();
+
+        if (lastMy?.state === 'APPROVED') {
+            return;
+        } else {
+            await this.approvePullRequest({ organizationAndTeamData, prNumber, repository });
+        }
+    }
+
     async approvePullRequest(params: {
         organizationAndTeamData: OrganizationAndTeamData;
         prNumber: number;
@@ -3432,26 +3503,6 @@ export class GithubService
 
             const octokit = await this.instanceOctokit(organizationAndTeamData);
 
-            const { data: reviews } = await octokit.rest.pulls.listReviews({
-                owner: githubAuthDetail.org,
-                repo: repository.name,
-                pull_number: prNumber,
-            });
-
-            if (reviews.length > 0) {
-                const lastReview = reviews[reviews.length - 1];
-
-                if (lastReview.state === PullRequestReviewState.APPROVED) {
-                    this.logger.log({
-                        message: `Pull request #${prNumber} has already been approved.`,
-                        context: GithubService.name,
-                        serviceName: 'GithubService approvePullRequest',
-                        metadata: params,
-                    });
-                    return;
-                }
-            }
-
             await octokit.rest.pulls.createReview({
                 owner: githubAuthDetail.org,
                 repo: repository.name,
@@ -3862,13 +3913,13 @@ export class GithubService
                         // So we need one of them to actually mark the thread as resolved and the other to match the id we saved in the database.
                         return firstComment
                             ? {
-                                  id: firstComment.id, // Used to actually resolve the thread
-                                  threadId: reviewThread.id,
-                                  isResolved: reviewThread.isResolved,
-                                  isOutdated: reviewThread.isOutdated,
-                                  fullDatabaseId: firstComment.fullDatabaseId, // The REST API id, used to match comments saved in the database.
-                                  body: firstComment.body,
-                              }
+                                id: firstComment.id, // Used to actually resolve the thread
+                                threadId: reviewThread.id,
+                                isResolved: reviewThread.isResolved,
+                                isOutdated: reviewThread.isOutdated,
+                                fullDatabaseId: firstComment.fullDatabaseId, // The REST API id, used to match comments saved in the database.
+                                body: firstComment.body,
+                            }
                             : null;
                     })
                     .filter((comment) => comment !== null);
@@ -3895,11 +3946,11 @@ export class GithubService
                     // So we need one of them to actually mark the thread as resolved and the other to match the id we saved in the database.
                     return firstComment
                         ? {
-                              id: firstComment.id, // Used to actually resolve the thread
-                              reviewId: review.id,
-                              fullDatabaseId: firstComment.fullDatabaseId, // The REST API id, used to match comments saved in the database.
-                              body: firstComment.body,
-                          }
+                            id: firstComment.id, // Used to actually resolve the thread
+                            reviewId: review.id,
+                            fullDatabaseId: firstComment.fullDatabaseId, // The REST API id, used to match comments saved in the database.
+                            body: firstComment.body,
+                        }
                         : null;
                 })
                 .filter((comment) => comment !== null);
diff --git a/src/core/infrastructure/adapters/services/gitlab.service.ts b/src/core/infrastructure/adapters/services/gitlab.service.ts
index 712ba0aae..372473bf5 100644
--- a/src/core/infrastructure/adapters/services/gitlab.service.ts
+++ b/src/core/infrastructure/adapters/services/gitlab.service.ts
@@ -1,5 +1,4 @@
 import { ICodeManagementService } from '@/core/domain/platformIntegrations/interfaces/code-management.interface';
-import { DeployFrequency } from '@/core/domain/platformIntegrations/types/codeManagement/deployFrequency.type';
 import {
     PullRequestCodeReviewTime,
     PullRequestReviewComment,
@@ -41,7 +40,6 @@ import { getChatGPT } from '@/shared/utils/langchainCommon/document';
 import { safelyParseMessageContent } from '@/shared/utils/safelyParseMessageContent';
 import { PinoLoggerService } from './logger/pino.service';
 import { PromptService } from './prompt.service';
-import { CommitLeadTimeForChange } from '@/core/domain/platformIntegrations/types/codeManagement/commitLeadTimeForChange.type';
 import * as moment from 'moment-timezone';
 import { Commit } from '@/config/types/general/commit.type';
 import { IntegrationConfigEntity } from '@/core/domain/integrationConfigs/entities/integration-config.entity';
@@ -69,25 +67,25 @@ import { CreateAuthIntegrationStatus } from '@/shared/domain/enums/create-auth-i
 import { ReviewComment } from '@/config/types/general/codeReview.type';
 import { getSeverityLevelShield } from '@/shared/utils/codeManagement/severityLevel';
 import { getCodeReviewBadge } from '@/shared/utils/codeManagement/codeReviewBadge';
+import { KODY_CODE_REVIEW_COMPLETED_MARKER } from '@/shared/utils/codeManagement/codeCommentMarkers';
 
 @Injectable()
 @IntegrationServiceDecorator(PlatformType.GITLAB, 'codeManagement')
 export class GitlabService
     implements
-        Omit<
-            ICodeManagementService,
-            | 'getOrganizations'
-            | 'getPullRequestsWithChangesRequested'
-            | 'getListOfValidReviews'
-            | 'getPullRequestReviewThreads'
-            | 'getRepositoryAllFiles'
-            | 'getAuthenticationOAuthToken'
-            | 'getPullRequestDetails'
-            | 'getCommitsByReleaseMode'
-            | 'getDataForCalculateDeployFrequency'
-            | 'requestChangesPullRequest'
-        >
-{
+    Omit<
+        ICodeManagementService,
+        | 'getOrganizations'
+        | 'getPullRequestsWithChangesRequested'
+        | 'getListOfValidReviews'
+        | 'getPullRequestReviewThreads'
+        | 'getRepositoryAllFiles'
+        | 'getAuthenticationOAuthToken'
+        | 'getPullRequestDetails'
+        | 'getCommitsByReleaseMode'
+        | 'getDataForCalculateDeployFrequency'
+        | 'requestChangesPullRequest'
+    > {
     constructor(
         @Inject(INTEGRATION_SERVICE_TOKEN)
         private readonly integrationService: IIntegrationService,
@@ -106,7 +104,7 @@ export class GitlabService
 
         private readonly promptService: PromptService,
         private readonly logger: PinoLoggerService,
-    ) {}
+    ) { }
 
     async getPullRequestByNumber(params: {
         organizationAndTeamData: OrganizationAndTeamData;
@@ -576,7 +574,7 @@ export class GitlabService
                                     avatar_url: project.namespace?.avatar_url,
                                     organizationName: project.namespace?.name,
                                     visibility: (project?.visibility ===
-                                    'public'
+                                        'public'
                                         ? 'public'
                                         : 'private') as 'public' | 'private',
                                     selected:
@@ -627,7 +625,7 @@ export class GitlabService
                                     avatar_url: project.namespace?.avatar_url,
                                     organizationName: project.namespace?.name,
                                     visibility: (project?.visibility ===
-                                    'public'
+                                        'public'
                                         ? 'public'
                                         : 'private') as 'public' | 'private',
                                     selected:
@@ -746,8 +744,8 @@ export class GitlabService
                         pr.state === GitlabPullRequestState.OPENED
                             ? PullRequestState.OPENED
                             : pr.state === GitlabPullRequestState.CLOSED
-                              ? PullRequestState.CLOSED
-                              : PullRequestState.ALL,
+                                ? PullRequestState.CLOSED
+                                : PullRequestState.ALL,
                     pull_number: pr.iid,
                     project_id: pr.project_id,
                     prURL: pr.web_url,
@@ -1495,7 +1493,7 @@ export class GitlabService
             actionStatement,
             this.formatSub(translations.talkToKody),
             this.formatSub(translations.feedback) +
-                '<!-- kody-codereview -->&#8203;\n&#8203;',
+            '<!-- kody-codereview -->&#8203;\n&#8203;',
         ]
             .join('\n')
             .trim();
@@ -2223,6 +2221,54 @@ export class GitlabService
         }
     }
 
+    async checkIfPullRequestShouldBeApproved(params: {
+        organizationAndTeamData: OrganizationAndTeamData;
+        prNumber: number;
+        repository: { id: string; name: string; };
+    }): Promise<any | null> {
+        try {
+            const { organizationAndTeamData, repository, prNumber } = params;
+
+            const gitlabAuthDetail = await this.getAuthDetails(
+                organizationAndTeamData,
+            );
+
+            const gitlabAPI = this.instanceGitlabApi(gitlabAuthDetail);
+
+            const approvalSettings = await gitlabAPI.MergeRequestApprovals.showConfiguration(repository.id, { mergerequestIId: prNumber });
+
+            const currentUser = await gitlabAPI.Users.showCurrentUser();
+
+            const approvedBy = approvalSettings?.approved_by;
+
+            const isApprovedByCurrentUser = approvedBy
+                ? approvedBy.some((approval) => (approval?.user?.id === currentUser.id))
+                : false;
+
+            if (isApprovedByCurrentUser) {
+                return null;
+            }
+
+            await this.approvePullRequest({ organizationAndTeamData, prNumber, repository });
+
+            this.logger.log({
+                message: `Approved pull request #${prNumber}`,
+                context: GitlabService.name,
+                serviceName: 'GitlabService approvePullRequest',
+                metadata: params,
+            });
+        } catch (error) {
+            this.logger.error({
+                message: `Error to approve pull request #${params.prNumber}`,
+                context: GitlabService.name,
+                serviceName: 'GitlabService checkIfPullRequestShouldBeApproved',
+                error: error,
+                metadata: params,
+            });
+            return null;
+        }
+    }
+
     async approvePullRequest(params: {
         organizationAndTeamData: OrganizationAndTeamData;
         repository: { name: string; id: string };
@@ -2475,8 +2521,8 @@ export class GitlabService
                         pr.state === GitlabPullRequestState.OPENED
                             ? PullRequestState.OPENED
                             : pr.state === GitlabPullRequestState.CLOSED
-                              ? PullRequestState.CLOSED
-                              : PullRequestState.ALL,
+                                ? PullRequestState.CLOSED
+                                : PullRequestState.ALL,
                     pull_number: pr.iid,
                     project_id: pr.project_id,
                     prURL: pr.web_url,
@@ -2532,7 +2578,7 @@ export class GitlabService
                     return (
                         firstDiscussionComment.resolvable &&
                         !firstDiscussionComment.body.includes(
-                            '## Code Review Completed! 🔥',
+                            KODY_CODE_REVIEW_COMPLETED_MARKER,
                         )
                     ); // Exclude comments with the specific string
                 })
@@ -2541,7 +2587,7 @@ export class GitlabService
                     const firstDiscussionComment = discussion.notes[0];
                     const isDiscussionResolved: boolean =
                         firstDiscussionComment.resolved &&
-                        firstDiscussionComment.resolved === true
+                            firstDiscussionComment.resolved === true
                             ? true
                             : false;
 
diff --git a/src/core/infrastructure/adapters/services/platformIntegration/codeManagement.service.ts b/src/core/infrastructure/adapters/services/platformIntegration/codeManagement.service.ts
index 9a197db97..1f9f0f27d 100644
--- a/src/core/infrastructure/adapters/services/platformIntegration/codeManagement.service.ts
+++ b/src/core/infrastructure/adapters/services/platformIntegration/codeManagement.service.ts
@@ -24,7 +24,6 @@ export class CodeManagementService implements ICodeManagementService {
         private platformIntegrationFactory: PlatformIntegrationFactory,
     ) { }
 
-
     async getTypeIntegration(
         organizationAndTeamData: OrganizationAndTeamData,
     ): Promise<PlatformType> {
@@ -941,4 +940,21 @@ export class CodeManagementService implements ICodeManagementService {
         return codeManagementService.getListOfValidReviews(params);
 
     }
+
+    async checkIfPullRequestShouldBeApproved(params: {
+        organizationAndTeamData: OrganizationAndTeamData;
+        prNumber: number;
+        repository: { id: string; name: string; };
+    }, type?: PlatformType): Promise<any | null> {
+        if (!type) {
+            type = await this.getTypeIntegration(
+                extractOrganizationAndTeamData(params),
+            );
+        }
+
+        const codeManagementService =
+            this.platformIntegrationFactory.getCodeManagementService(type);
+
+        return codeManagementService.checkIfPullRequestShouldBeApproved(params);
+    }
 }
diff --git a/src/shared/utils/codeManagement/codeCommentMarkers.ts b/src/shared/utils/codeManagement/codeCommentMarkers.ts
new file mode 100644
index 000000000..3d1990f20
--- /dev/null
+++ b/src/shared/utils/codeManagement/codeCommentMarkers.ts
@@ -0,0 +1,5 @@
+const KODY_CODE_REVIEW_COMPLETED_MARKER = '## Code Review Completed! 🔥';
+const KODY_CRITICAL_ISSUE_COMMENT_MARKER = '# Found critical issues please';
+const KODY_START_COMMAND_MARKER = '@kody start';
+
+export { KODY_CODE_REVIEW_COMPLETED_MARKER, KODY_CRITICAL_ISSUE_COMMENT_MARKER, KODY_START_COMMAND_MARKER }


Reviews:
user: login: kody-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T20:36:00Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T20:36:03Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T20:36:06Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T20:36:09Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T20:36:12Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T20:36:15Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T20:36:22Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T20:36:26Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T20:36:29Z
user: login: kody-ai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T20:54:06Z
user: login: kody-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T22:26:33Z

Review comments:
diff_hunk: @@ -110,4 +110,10 @@ export interface ICodeManagementService
         repository: Partial<Repository>,
     }): Promise<PullRequestsWithChangesRequested[] | null>
 
+    checkIfPullRequestShouldBeApproved(params: {
+        organizationAndTeamData: OrganizationAndTeamData,
+        prNumber: number,
+        repository: { id: string; name: string };
+    }): Promise<any | null>;
path: src/core/domain/platformIntegrations/interfaces/code-management.interface.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-312B4B?labelColor=C9BBF2) ![Maintainability](https://img.shields.io/badge/Maintainability-0091EA) ![medium](https://img.shields.io/badge/severity_level-medium-1A7BBE)
```typescript
checkIfPullRequestShouldBeApproved(params: {
        organizationAndTeamData: OrganizationAndTeamData,
        prNumber: number,
        repository: { id: string; name: string };
    }): Promise<boolean | null>;
```
The return type of 'checkIfPullRequestShouldBeApproved' is currently 'any | null'. Consider using a more specific type to improve type safety and maintainability.

<sub>Talk to Kody by mentioning @kody</sub>


<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub>

<!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-04-25T20:36:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -633,6 +633,22 @@ export class AzureReposRequestHelper {
         return data;
     }
 
+    async getListOfPullRequestReviewers(params: {
+        orgName: string;
+        token: string;
+        projectId: string;
+        repositoryId: string;
+        prId: number;
+    }): Promise<any> {
path: src/core/infrastructure/adapters/services/azureRepos/azure-repos-request-helper.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-312B4B?labelColor=C9BBF2) ![Maintainability](https://img.shields.io/badge/Maintainability-0091EA) ![medium](https://img.shields.io/badge/severity_level-medium-1A7BBE)
```typescript
    // Define this interface based on the actual API response structure
    interface AzureRepoReviewer {
      id: string;
      displayName: string;
      uniqueName: string;
      vote: number;
      // ... other relevant fields
    }

    // ... inside the class ...

    async getListOfPullRequestReviewers(params: {
        orgName: string;
        token: string;
        projectId: string;
        repositoryId: string;
        prId: number;
    }): Promise<AzureRepoReviewer[]> { // Use the specific type
        const instance = await this.azureRequest(params);
        const url = `/${params.projectId}/_apis/git/repositories/${params.repositoryId}/pullRequests/${params.prId}/reviewers/?api-version=7.1`;
        // Consider typing the axios response for better internal consistency
        const { data } = await instance.get<{ value: AzureRepoReviewer[] }>(url);
        return data?.value ?? [];
    }
```
The method `getListOfPullRequestReviewers` currently returns `Promise<any>`. Using `any` bypasses TypeScript's type checking, reducing code safety and maintainability. It's recommended to define a specific interface or type representing the structure of an Azure DevOps reviewer object (based on the actual API response) and use that as the return type (e.g., `Promise<AzureRepoReviewer[]>`). This improves type safety, code clarity, and developer experience.

<sub>Talk to Kody by mentioning @kody</sub>


<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub>

<!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-04-25T20:36:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1473,7 +1472,7 @@ export class BitbucketService
                             path: lineComment?.path,
                             to: this.sanitizeLine(
                                 params.lineComment.start_line ??
-                                    params.lineComment.line,
+                                params.lineComment.line,
                             ),
path: src/core/infrastructure/adapters/services/bitbucket/bitbucket.service.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-312B4B?labelColor=C9BBF2) ![Potential Issues](https://img.shields.io/badge/Potential_Issues-B71C1C) ![high](https://img.shields.io/badge/severity_level-high-6B6B92)
```typescript
to: this.sanitizeLine(
    params.lineComment.start_line ??
    params.lineComment.line ??
    1, // Default to line 1 if both are null/undefined
),
```
The nullish coalescing operator (`??`) might result in `null` or `undefined` being passed to `this.sanitizeLine` if both `params.lineComment.start_line` and `params.lineComment.line` are absent. Since `parseInt(null)` and `parseInt(undefined)` return `NaN`, this could lead to an invalid value being sent to the Bitbucket API for the comment line number, potentially causing the API request to fail. Consider adding a default fallback value (e.g., 1) or ensuring `sanitizeLine` handles `null`/`undefined` gracefully to prevent passing `NaN`.

<sub>Talk to Kody by mentioning @kody</sub>


<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub>

<!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-04-25T20:36:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2817,6 +2816,61 @@ export class BitbucketService
         }
     }
 
+    async checkIfPullRequestShouldBeApproved(params: {
+        organizationAndTeamData: OrganizationAndTeamData,
+        prNumber: number,
+        repository: { id: string; name: string };
+    }) {
+        try {
+            const { organizationAndTeamData, prNumber, repository } = params;
+
+            const bitbucketAuthDetails = await this.getAuthDetails(
+                organizationAndTeamData,
+            );
+
+            const workspace = await this.getWorkspaceFromRepository(
+                organizationAndTeamData,
+                repository.id,
+            );
+
+            if (!workspace) {
+                return null;
+            }
+
+            const bitbucketAPI =
+                this.instanceBitbucketApi(bitbucketAuthDetails);
path: src/core/infrastructure/adapters/services/bitbucket/bitbucket.service.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-312B4B?labelColor=C9BBF2) ![Potential Issues](https://img.shields.io/badge/Potential_Issues-B71C1C) ![high](https://img.shields.io/badge/severity_level-high-6B6B92)
```typescript
const bitbucketAuthDetails = await this.getAuthDetails(
    organizationAndTeamData,
);

// Add this check
if (!bitbucketAuthDetails) {
    this.logger.warn({ message: 'Bitbucket auth details not found', context: this.checkIfPullRequestShouldBeApproved.name, metadata: { organizationAndTeamData } });
    return null;
}

const workspace = await this.getWorkspaceFromRepository(
    organizationAndTeamData,
    repository.id,
);

if (!workspace) {
    return null;
}

const bitbucketAPI =
    this.instanceBitbucketApi(bitbucketAuthDetails);
```
The `getAuthDetails` method can return `null`. However, the returned value `bitbucketAuthDetails` is used later in `instanceBitbucketApi(bitbucketAuthDetails)` without checking if it's null. This will lead to a runtime error if authentication details are not found. Add a check for `bitbucketAuthDetails` after fetching it and return `null` early if it's missing.

<sub>Talk to Kody by mentioning @kody</sub>


<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub>

<!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-04-25T20:36:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2817,6 +2816,61 @@ export class BitbucketService
         }
     }
 
+    async checkIfPullRequestShouldBeApproved(params: {
+        organizationAndTeamData: OrganizationAndTeamData,
+        prNumber: number,
+        repository: { id: string; name: string };
+    }) {
+        try {
+            const { organizationAndTeamData, prNumber, repository } = params;
+
+            const bitbucketAuthDetails = await this.getAuthDetails(
+                organizationAndTeamData,
+            );
+
+            const workspace = await this.getWorkspaceFromRepository(
+                organizationAndTeamData,
+                repository.id,
+            );
+
+            if (!workspace) {
+                return null;
+            }
+
+            const bitbucketAPI =
+                this.instanceBitbucketApi(bitbucketAuthDetails);
+
+            const currentUser = (await bitbucketAPI.users.getAuthedUser({})).data;
+
+            const activities = await bitbucketAPI.pullrequests.listActivities({
+                repo_slug: `{${repository.id}}`,
+                workspace: `{${workspace}}`,
+                pull_request_id: prNumber,
+            }).then((res) => this.getPaginatedResults(bitbucketAPI, res));
+
+            const isApprovedByCurrentUser = activities
+                .find((activity: any) => (activity.approval && (activity.approval?.user?.uuid === currentUser?.uuid)));
path: src/core/infrastructure/adapters/services/bitbucket/bitbucket.service.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-312B4B?labelColor=C9BBF2) ![Refactoring](https://img.shields.io/badge/Refactoring-304FFE) ![low](https://img.shields.io/badge/severity_level-low-1A8EBC)
```typescript
.find((activity: Schema.PullrequestActivity) => activity.approval?.user?.uuid === currentUser?.uuid); // Also includes type suggestion
```
The condition to check if the current user approved the PR can be simplified by consistently using optional chaining. The current check `(activity.approval && (activity.approval?.user?.uuid === currentUser?.uuid))` is slightly redundant. Refactoring to `activity.approval?.user?.uuid === currentUser?.uuid` is more concise and idiomatic.

<sub>Talk to Kody by mentioning @kody</sub>


<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub>

<!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-04-25T20:36:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3190,6 +3237,9 @@ export class BitbucketService
                         ) &&
                         !comment?.content?.raw.includes(
                             '# Found critical issues please',
+                        ) &&
+                        !comment?.content?.raw.includes(
+                            '@kody start',
path: src/core/infrastructure/adapters/services/bitbucket/bitbucket.service.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-312B4B?labelColor=C9BBF2) ![Maintainability](https://img.shields.io/badge/Maintainability-0091EA) ![medium](https://img.shields.io/badge/severity_level-medium-1A7BBE)
```typescript
// At class level or in a constants file:
// const KODY_CRITICAL_ISSUE_COMMENT_MARKER = '# Found critical issues please';
// const KODY_START_COMMAND_MARKER = '@kody start';

// In the filter:
!comment?.content?.raw.includes(
    KODY_CRITICAL_ISSUE_COMMENT_MARKER,
) &&
!comment?.content?.raw.includes(
    KODY_START_COMMAND_MARKER,
)
```
The `filter` method uses hardcoded strings like `'# Found critical issues please'` and `'@kody start'` to exclude certain comments. This makes the code harder to maintain, as these strings might change or be used elsewhere. Define these strings as named constants (e.g., `KODY_CRITICAL_ISSUE_COMMENT_MARKER`, `KODY_START_COMMAND_MARKER`) at a suitable scope (like the top of the class or a dedicated constants file) and reference these constants in the filter logic.

<sub>Talk to Kody by mentioning @kody</sub>


<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub>

<!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-04-25T20:36:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3418,6 +3418,77 @@ export class GithubService
         }
     }
 
+    async checkIfPullRequestShouldBeApproved(params: {
+        organizationAndTeamData: OrganizationAndTeamData;
+        prNumber: number;
+        repository: { id: string; name: string; };
+    }): Promise<any | null> {
+        const { organizationAndTeamData, prNumber, repository } = params;
+
+        const githubAuth = await this.getGithubAuthDetails(
+            organizationAndTeamData,
+        );
+
+        const octokit = await this.instanceOctokit(
+            organizationAndTeamData,
+            githubAuth,
+        );
+
+        const graphQLWithAuth = await this.instanceGraphQL(
+            organizationAndTeamData,
+        );
+
+        const query = `
+        query {
+          viewer {
+            login
+            id
+            __typename
+          }
+        }
+      `;
+
+        const variables = {};
+        const userAuth: {
+            viewer: { login: string; id: string; __typename: string };
+        } = await graphQLWithAuth(query, variables);
+
+        const { data: allReviews } = await octokit.rest.pulls.listReviews({
+            owner: githubAuth.org,
+            repo: repository.name,
+            pull_number: prNumber,
+            per_page: 100,
+        });
+
+        if (!allReviews?.length && allReviews) {
path: src/core/infrastructure/adapters/services/github/github.service.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-312B4B?labelColor=C9BBF2) ![Refactoring](https://img.shields.io/badge/Refactoring-304FFE) ![low](https://img.shields.io/badge/severity_level-low-1A8EBC)
```typescript
if (!allReviews?.length) {
```
The condition `!allReviews?.length && allReviews` contains a redundant check. If `!allReviews?.length` is evaluated, `allReviews` itself (being an array, even if empty) will always be truthy in this context, making the `&& allReviews` part unnecessary. The check can be simplified for clarity.

<sub>Talk to Kody by mentioning @kody</sub>


<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub>

<!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-04-25T20:36:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -941,4 +940,21 @@ export class CodeManagementService implements ICodeManagementService {
         return codeManagementService.getListOfValidReviews(params);
 
     }
+
+    async checkIfPullRequestShouldBeApproved(params: {
+        organizationAndTeamData: OrganizationAndTeamData;
+        prNumber: number;
+        repository: { id: string; name: string; };
+    }, type?: PlatformType): Promise<any | null> {
+        if (!type) {
+            type = await this.getTypeIntegration(
+                extractOrganizationAndTeamData(params),
+            );
+        }
+
+        const codeManagementService =
+            this.platformIntegrationFactory.getCodeManagementService(type);
path: src/core/infrastructure/adapters/services/platformIntegration/codeManagement.service.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-312B4B?labelColor=C9BBF2) ![Refactoring](https://img.shields.io/badge/Refactoring-304FFE) ![medium](https://img.shields.io/badge/severity_level-medium-1A7BBE)
```typescript
        // Example helper method (add to the class)
        private async getPlatformService(params: any, type?: PlatformType): Promise<ICodeManagementService | null> {
            const resolvedType = type ?? await this.getTypeIntegration(extractOrganizationAndTeamData(params));
            if (!resolvedType) {
                // Consider logging or throwing an error if a type is expected but not found
                return null;
            }
            // Add handling for potential errors from getCodeManagementService if necessary
            return this.platformIntegrationFactory.getCodeManagementService(resolvedType);
        }

        // Usage in checkIfPullRequestShouldBeApproved
        async checkIfPullRequestShouldBeApproved(params: ..., type?: PlatformType): Promise<any | null> {
            const codeManagementService = await this.getPlatformService(params, type);
            if (!codeManagementService) {
                return null; // Or handle error appropriately based on expected behavior
            }
            return codeManagementService.checkIfPullRequestShouldBeApproved(params);
        }
```
The logic to determine the platform `type` (if not provided) and retrieve the corresponding `codeManagementService` using the factory (lines 949-956) is duplicated across numerous methods in this class. Extracting this common logic into a private helper method would significantly reduce code duplication, improve maintainability, and ensure consistency. The helper could accept `params` and optionally `type`, returning the specific service instance or null.

<sub>Talk to Kody by mentioning @kody</sub>


<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub>

<!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-04-25T20:36:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -941,4 +940,21 @@ export class CodeManagementService implements ICodeManagementService {
         return codeManagementService.getListOfValidReviews(params);
 
     }
+
+    async checkIfPullRequestShouldBeApproved(params: {
+        organizationAndTeamData: OrganizationAndTeamData;
+        prNumber: number;
+        repository: { id: string; name: string; };
+    }, type?: PlatformType): Promise<any | null> {
+        if (!type) {
+            type = await this.getTypeIntegration(
+                extractOrganizationAndTeamData(params),
+            );
+        }
path: src/core/infrastructure/adapters/services/platformIntegration/codeManagement.service.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-312B4B?labelColor=C9BBF2) ![Potential Issues](https://img.shields.io/badge/Potential_Issues-B71C1C) ![high](https://img.shields.io/badge/severity_level-high-6B6B92)
```typescript
        if (!type) {
            type = await this.getTypeIntegration(
                extractOrganizationAndTeamData(params),
            );
        }

        // Explicitly check if type was resolved and return null if not
        if (!type) {
             return null;
        }

        const codeManagementService =
            this.platformIntegrationFactory.getCodeManagementService(type);
```
If the platform `type` is not provided and cannot be determined by `getTypeIntegration` (lines 949-953), the method currently proceeds to call `getCodeManagementService` with a potentially null/undefined `type`, or implicitly returns `undefined` if the factory handles it by returning null/undefined itself. The declared return type is `Promise<any | null>`. To ensure the method adheres to its signature and handles the 'type not found' case explicitly, add a check after determining the type and return `null` if it's still falsy.

<sub>Talk to Kody by mentioning @kody</sub>


<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub>

<!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-04-25T20:36:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,3 +211,31 @@ export interface AzureRepoDiffChange {
     additions?: number;
     deletions?: number;
 }
+
+/**
+ * Representa um reviewer retornado pela API
+ * IdentityRefWithVote (nome do tipo retornado)
+ */
+export interface AzureRepoReviewerWithVote {
+    _links: any;
path: src/core/domain/azureRepos/entities/azureRepoExtras.type.ts
user: login: kody-ai[bot]
body: ![kody code-review](https://img.shields.io/badge/kody-code--review-312B4B?labelColor=C9BBF2) ![Maintainability](https://img.shields.io/badge/Maintainability-0091EA) ![medium](https://img.shields.io/badge/severity_level-medium-1A7BBE)
```typescript
_links: AzureRepoLinks;
votedFor: AzureRepoReviewerWithVote[];
```
The 'any' type is used for properties in TypeScript, which reduces type safety and maintainability.

This issue appears in multiple locations:
* src/core/domain/azureRepos/entities/azureRepoExtras.type.ts: Lines 220-239
* src/core/domain/azureRepos/entities/azureRepoExtras.type.ts: Lines 222-236
Replace 'any' type with specific interface types to improve type safety and maintainability.


<sub>Talk to Kody by mentioning @kody</sub>


<sub>Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.</sub>

<!-- kody-codereview -->&#8203;
&#8203;
created_at: 2025-04-25T22:26:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: kody-ai[bot]
created_at: 2025-04-25T20:33:49Z
body: ## Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.



<details>
<summary>Kody Guide: Usage and Configuration</summary>

<details>
<summary>Interacting with Kody</summary>

- **Request a Review:** Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

- **Provide Feedback:** Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

</details>

<details>
<summary>Current Kody Configuration</summary>

<details>
<summary>Review Options</summary>

The following review options are enabled or disabled:

| Options                        | Enabled |
|-------------------------------|---------|
| **Security** | ✅ |
| **Code Style** | ❌ |
| **Kody Rules** | ✅ |
| **Refactoring** | ✅ |
| **Error Handling** | ❌ |
| **Maintainability** | ✅ |
| **Potential Issues** | ✅ |
| **Documentation And Comments** | ❌ |
| **Performance And Optimization** | ✅ |
| **Breaking Changes** | ❌ |

</details>

**[Access your configuration settings here.](https://app.kodus.io/settings/code-review/global/general)**

</details>
</details>

<!-- kody-codereview -->
&#8203;
author_association: CONTRIBUTOR
user: login: kody-ai[bot]
created_at: 2025-04-25T20:53:33Z
body: # Kody Review Complete
**Great news!** 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

<details>
<summary>Kody Guide: Usage and Configuration</summary>

<details>
<summary>Interacting with Kody</summary>

- **Request a Review:** Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

- **Provide Feedback:** Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

</details>

<details>
<summary>Current Kody Configuration</summary>

<details>
<summary>Review Options</summary>

The following review options are enabled or disabled:

| Options                        | Enabled |
|-------------------------------|---------|
| **Security** | ✅ |
| **Code Style** | ❌ |
| **Kody Rules** | ✅ |
| **Refactoring** | ✅ |
| **Error Handling** | ❌ |
| **Maintainability** | ✅ |
| **Potential Issues** | ✅ |
| **Documentation And Comments** | ❌ |
| **Performance And Optimization** | ✅ |
| **Breaking Changes** | ❌ |

</details>

**[Access your configuration settings here.](https://app.kodus.io/settings/code-review/global/general)**

</details>
</details>

<!-- kody-codereview -->
&#8203;
author_association: CONTRIBUTOR
user: login: kody-ai[bot]
created_at: 2025-04-25T20:57:55Z
body: # Kody Review Complete
**Great news!** 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

<details>
<summary>Kody Guide: Usage and Configuration</summary>

<details>
<summary>Interacting with Kody</summary>

- **Request a Review:** Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

- **Provide Feedback:** Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

</details>

<details>
<summary>Current Kody Configuration</summary>

<details>
<summary>Review Options</summary>

The following review options are enabled or disabled:

| Options                        | Enabled |
|-------------------------------|---------|
| **Security** | ✅ |
| **Code Style** | ❌ |
| **Kody Rules** | ✅ |
| **Refactoring** | ✅ |
| **Error Handling** | ❌ |
| **Maintainability** | ✅ |
| **Potential Issues** | ✅ |
| **Documentation And Comments** | ❌ |
| **Performance And Optimization** | ✅ |
| **Breaking Changes** | ❌ |

</details>

**[Access your configuration settings here.](https://app.kodus.io/settings/code-review/global/general)**

</details>
</details>

<!-- kody-codereview -->
&#8203;
author_association: CONTRIBUTOR
user: login: kody-ai[bot]
created_at: 2025-04-25T22:25:16Z
body: ## Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.



<details>
<summary>Kody Guide: Usage and Configuration</summary>

<details>
<summary>Interacting with Kody</summary>

- **Request a Review:** Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

- **Provide Feedback:** Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

</details>

<details>
<summary>Current Kody Configuration</summary>

<details>
<summary>Review Options</summary>

The following review options are enabled or disabled:

| Options                        | Enabled |
|-------------------------------|---------|
| **Security** | ✅ |
| **Code Style** | ❌ |
| **Kody Rules** | ✅ |
| **Refactoring** | ✅ |
| **Error Handling** | ❌ |
| **Maintainability** | ✅ |
| **Potential Issues** | ✅ |
| **Documentation And Comments** | ❌ |
| **Performance And Optimization** | ✅ |
| **Breaking Changes** | ❌ |

</details>

**[Access your configuration settings here.](https://app.kodus.io/settings/code-review/global/general)**

</details>
</details>

<!-- kody-codereview -->
&#8203;
author_association: CONTRIBUTOR