Repository: erxes/erxes
Pull request #6020: fix(tms): tourgroup
Author: login: batorshih
Created: 2025-05-08T02:40:28Z | Merged: 2025-05-08T03:00:32Z

[ISSUE](https://github.com/erxes/erxes/issues/ISSUE)

### Context

Your context here.  Additionally, any screenshots.  Delete this line.


// Delete the below section once completed
### PR Checklist
- [ ] Description is clearly stated under Context section
- [ ] Screenshots and the additional verifications are attached

## Summary by Sourcery

Add support for grouping tours by group code in the tour management system

New Features:
- Introduce a new GraphQL query 'bmToursGroup' to fetch tours with group code support
- Add 'groupCode' field to the Tour schema to enable tour grouping

Enhancements:
- Modify tour query to support filtering and aggregating tours by group code
- Enhance tour listing to include grouped tours
<!-- ELLIPSIS_HIDDEN -->

----

> [!IMPORTANT]
> Adds `bmToursGroup` query to group tours by `groupCode` and updates schema and models accordingly.
> 
>   - **Behavior**:
>     - Adds `bmToursGroup` query in `tour.ts` to group tours by `groupCode`.
>     - Returns grouped and ungrouped tours in a combined list.
>   - **Schema**:
>     - Adds `groupCode` field to `Tour` type in `tour.ts`.
>     - Adds `bmToursGroup` query to `queries` in `tour.ts`.
>   - **Models**:
>     - Adds `groupCode` to `ITour` and `tourSchema` in `tour.ts`.
>   - **Misc**:
>     - Converts single quotes to double quotes in `tour.ts` for consistency.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=erxes%2Ferxes&utm_source=github&utm_medium=referral)<sup> for 39b06f4aa19f6d9a8ebdde65ba3bffea906ba234. You can [customize](https://app.ellipsis.dev/erxes/settings/summaries) this summary. It will automatically update as commits are pushed.</sup>

<!-- ELLIPSIS_HIDDEN -->


Diff:
diff --git a/packages/plugin-bm-api/src/graphql/resolvers/queries/tour.ts b/packages/plugin-bm-api/src/graphql/resolvers/queries/tour.ts
index c83e64ade41..14156296297 100644
--- a/packages/plugin-bm-api/src/graphql/resolvers/queries/tour.ts
+++ b/packages/plugin-bm-api/src/graphql/resolvers/queries/tour.ts
@@ -17,7 +17,7 @@ const tourQueries = {
       startDate2,
       endDate2,
       sortField = "createdAt",
-      sortDirection = -1,
+      sortDirection = -1
     },
     { models }: IContext
   ) {
@@ -79,12 +79,109 @@ const tourQueries = {
 
     return {
       list,
-      total,
+      total
+    };
+  },
+  async bmToursGroup(
+    _root,
+    {
+      categories,
+      page = 1,
+      perPage = 10,
+      status,
+      innerDate,
+      branchId,
+      tags,
+      startDate1,
+      endDate1,
+      startDate2,
+      endDate2,
+      sortField = "createdAt",
+      sortDirection = -1
+    },
+    { models }: IContext
+  ) {
+    const selector: any = {};
+
+    const skip = Math.max(0, page - 1) * perPage;
+    if (categories) {
+      selector.categories = { $in: categories };
+    }
+    if (status) {
+      selector.status = status;
+    }
+    if (branchId) {
+      selector.status = branchId;
+    }
+    if (tags) {
+      selector.tags = { $in: tags };
+    }
+    if (innerDate) {
+      const dateToCheck = innerDate;
+      selector.startDate = { $lte: dateToCheck };
+      selector.endDate = { $gte: dateToCheck };
+
+      // selector.$expr = {
+      //   $lte: [
+      //     dateToCheck,
+      //     {
+      //       $add: [
+      //         '$startDate',
+      //         { $multiply: ['$duration', 24 * 60 * 60 * 1000] },
+      //       ],
+      //     },
+      //   ],
+      // };
+    }
+
+    if (startDate2) {
+      if (!selector.startDate) selector.startDate = {};
+      selector.startDate["$lte"] = startDate2;
+    }
+    if (startDate1) {
+      if (!selector.startDate) selector.startDate = {};
+      selector.startDate["$gte"] = startDate1;
+    }
+
+    if (endDate2) {
+      if (!selector.endDate) selector.endDate = {};
+      selector.endDate["$lte"] = endDate2;
+    }
+    if (endDate1) {
+      if (!selector.endDate) selector.endDate = {};
+      selector.endDate["$gte"] = endDate1;
+    }
+    const list = await models.Tours.find({
+      ...selector,
+      groupCode: { $in: [null, ""] }
+    })
+      .limit(perPage)
+      .skip(skip)
+      .sort({ [sortField]: sortDirection === -1 ? sortDirection : 1 });
+    const total = await models.Tours.find(selector).countDocuments();
+
+    const group = await models.Tours.aggregate([
+      {
+        $match: {
+          ...selector,
+          groupCode: { $nin: [null, ""] } // Exclude null and empty strings
+        }
+      },
+      {
+        $group: {
+          _id: "$groupCode", // group by category
+          items: { $push: "$$ROOT" } // push full documents into an array
+        }
+      }
+    ]);
+    return {
+      list: [...group, ...list],
+      total
     };
   },
   async bmTourDetail(_root, { _id }, { models }: IContext) {
     return await models.Tours.findById(_id);
-  },
+  }
 };
 
 export default tourQueries;
diff --git a/packages/plugin-bm-api/src/graphql/schema/tour.ts b/packages/plugin-bm-api/src/graphql/schema/tour.ts
index 091b3888f86..941b05ba119 100644
--- a/packages/plugin-bm-api/src/graphql/schema/tour.ts
+++ b/packages/plugin-bm-api/src/graphql/schema/tour.ts
@@ -1,4 +1,4 @@
-import gql from 'graphql-tag';
+import gql from "graphql-tag";
 
 export const types = () => `
   extend type User @key(fields: "_id") {
@@ -16,6 +16,7 @@ export const types = () => `
     branchId: String
     name: String
     refNumber: String
+    groupCode: String
     content: String
     duration: Int
     location: [BMSLocation]
@@ -83,11 +84,14 @@ export const queries = `
   bmTours(branchId:String, sortField:String, sortDirection:Int, page:Int, perPage:Int, status: String, innerDate: Date,branchId: String, tags: [String],startDate1:Date,startDate2:Date,endDate1:Date,endDate2:Date): ListTour
   bmTourDetail(_id:String!,branchId: String): Tour
   bmOrders( tourId:String, customerId:String ,branchId: String):ListBmsOrder
+  bmToursGroup(branchId:String, sortField:String, sortDirection:Int, page:Int, perPage:Int, status: String, innerDate: Date,branchId: String, tags: [String],startDate1:Date,startDate2:Date,endDate1:Date,endDate2:Date): JSON
+
 `;
 
 const params = `
   branchId: String,
   name: String,
+  groupCode: String,
   content: String,
   itineraryId:String,
   startDate: Date,
diff --git a/packages/plugin-bm-api/src/models/definitions/tour.ts b/packages/plugin-bm-api/src/models/definitions/tour.ts
index 7eaddebc76a..59ad04fac40 100644
--- a/packages/plugin-bm-api/src/models/definitions/tour.ts
+++ b/packages/plugin-bm-api/src/models/definitions/tour.ts
@@ -1,8 +1,8 @@
-import { ICustomField } from '@erxes/api-utils/src/definitions/common';
-import { Schema, Document } from 'mongoose';
-import { CAR_SELECT_OPTIONS } from './constants';
-import { field, schemaHooksWrapper } from './utils';
-import { ILocation, locationSchema } from './itinerary';
+import { ICustomField } from "@erxes/api-utils/src/definitions/common";
+import { Schema, Document } from "mongoose";
+import { CAR_SELECT_OPTIONS } from "./constants";
+import { field, schemaHooksWrapper } from "./utils";
+import { ILocation, locationSchema } from "./itinerary";
 
 export interface IGuideItem {
   guideId: string;
@@ -10,6 +10,7 @@ export interface IGuideItem {
 }
 export interface ITour {
   name: string;
+  groupCode: string;
   refNumber?: string;
   content: string;
   duration: string;
@@ -39,10 +40,10 @@ export interface ITourDocument extends ITour, Document {
 }
 
 const STATUS_TYPES = [
-  { label: 'running', value: 'running' },
-  { label: 'compeleted', value: 'compeleted' },
-  { label: 'scheduled', value: 'scheduled' },
-  { label: 'cancelled', value: 'cancelled' },
+  { label: "running", value: "running" },
+  { label: "compeleted", value: "compeleted" },
+  { label: "scheduled", value: "scheduled" },
+  { label: "cancelled", value: "cancelled" }
 ];
 
 const getEnum = (): string[] => {
@@ -52,49 +53,50 @@ const getEnum = (): string[] => {
 export const guideItemSchema = new Schema(
   {
     guideId: field({ type: String, optional: true }),
-    type: field({ type: String, optional: true }),
+    type: field({ type: String, optional: true })
   },
   { _id: false }
 );
 export const tourSchema = schemaHooksWrapper(
   new Schema({
     _id: field({ pkey: true }),
-    createdAt: field({ type: Date, label: 'Created at' }),
-    modifiedAt: field({ type: Date, label: 'Modified at' }),
-    refNumber: field({ type: String, optional: true, label: 'refnumber' }),
-    name: field({ type: String, optional: true, label: 'name' }),
-    content: field({ type: String, optional: true, label: 'content' }),
-    duration: field({ type: Number, optional: true, label: 'number' }),
+    createdAt: field({ type: Date, label: "Created at" }),
+    modifiedAt: field({ type: Date, label: "Modified at" }),
+    refNumber: field({ type: String, optional: true, label: "refnumber" }),
+    name: field({ type: String, optional: true, label: "name" }),
+    groupCode: field({ type: String, optional: true, label: "groupCode" }),
+    content: field({ type: String, optional: true, label: "content" }),
+    duration: field({ type: Number, optional: true, label: "number" }),
     location: field({
       type: [locationSchema],
       optional: true,
-      label: 'location',
+      label: "location"
     }),
-    itineraryId: field({ type: String, optional: true, label: 'initeraryId' }),
-    startDate: field({ type: Date, optional: true, label: 'date' }),
-    endDate: field({ type: Date, optional: true, label: 'date' }),
-    groupSize: field({ type: Number, optional: true, label: 'group size' }),
-    guides: field({ type: [guideItemSchema], optional: true, label: 'guides' }),
+    itineraryId: field({ type: String, optional: true, label: "initeraryId" }),
+    startDate: field({ type: Date, optional: true, label: "date" }),
+    endDate: field({ type: Date, optional: true, label: "date" }),
+    groupSize: field({ type: Number, optional: true, label: "group size" }),
+    guides: field({ type: [guideItemSchema], optional: true, label: "guides" }),
     status: field({
       type: String,
-      default: '',
+      default: "",
       optional: true,
-      label: 'status',
-      esType: 'keyword',
+      label: "status",
+      esType: "keyword"
     }),
-    cost: field({ type: Number, optional: true, label: 'cost' }),
-    tags: field({ type: [String], optional: true, label: 'tags' }),
-    viewCount: field({ type: Number, optional: true, label: 'viewCount' }),
-    branchId: field({ type: String, optional: true, label: 'branchId' }),
+    cost: field({ type: Number, optional: true, label: "cost" }),
+    tags: field({ type: [String], optional: true, label: "tags" }),
+    viewCount: field({ type: Number, optional: true, label: "viewCount" }),
+    branchId: field({ type: String, optional: true, label: "branchId" }),
 
-    info1: field({ type: String, optional: true, label: 'info' }),
-    info2: field({ type: String, optional: true, label: 'info' }),
-    info3: field({ type: String, optional: true, label: 'info' }),
-    info4: field({ type: String, optional: true, label: 'info' }),
-    extra: field({ type: Object, optional: true, label: 'info' }),
+    info1: field({ type: String, optional: true, label: "info" }),
+    info2: field({ type: String, optional: true, label: "info" }),
+    info3: field({ type: String, optional: true, label: "info" }),
+    info4: field({ type: String, optional: true, label: "info" }),
+    extra: field({ type: Object, optional: true, label: "info" }),
 
-    images: field({ type: [String], optional: true, label: 'images' }),
-    imageThumbnail: field({ type: String, optional: true, label: 'images' }),
+    images: field({ type: [String], optional: true, label: "images" }),
+    imageThumbnail: field({ type: String, optional: true, label: "images" })
   }),
-  'erxes_tours'
+  "erxes_tours"
 );


Reviews:
user: login: ellipsis-dev[bot]
body: > [!CAUTION]
> **Changes requested  :x:**
> <details>
> <summary>Reviewed everything up to 39b06f4aa19f6d9a8ebdde65ba3bffea906ba234 in <ellipsistime>3 minutes and 24 seconds</ellipsistime>. Click for details.</summary> 
> 
> - Reviewed `278` lines of code in `3` files
> - Skipped `0` files when reviewing.
> - Skipped posting `7` draft comments. View those below.
> - Modify your [settings](https://app.ellipsis.dev/erxes/settings/code-review) and [rules](https://app.ellipsis.dev/erxes/code-review/rules) to customize what types of comments Ellipsis leaves. And don't forget to react with :+1: or :-1: to teach Ellipsis.
> 
> <details>
> <summary>1. <code>packages/plugin-bm-api/src/graphql/resolvers/queries/tour.ts:33</code></summary>
> 
> - **Draft comment:** 
> Potential bug: The branchId condition assigns its value to selector.status. Likely should be selector.branchId. This issue appears in both bmTours and bmToursGroup resolvers.
> - **Reason this comment was not posted:**
> Comment was not on a location in the diff, so it can't be submitted as a review comment.
> 
> 
> </details>
> 
> <details>
> <summary>2. <code>packages/plugin-bm-api/src/models/definitions/tour.ts:67</code></summary>
> 
> - **Draft comment:** 
> Ensure the new groupCode field is indexed if you plan to query on it frequently. Currently, it's added without an index which might impact performance in large collections.
> - **Reason this comment was not posted:**
> Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% This seems like a speculative performance optimization without clear evidence. We don't know if groupCode will be frequently queried. The fact that other similar fields don't have indexes suggests this isn't a pattern in the codebase. The comment starts with "Ensure..." which is a red flag per our rules. The suggestion could be valuable if there's heavy querying on this field. Database indexing decisions are important for performance. However, we don't have evidence that this field needs an index, and the comment is speculative ("if you plan to"). Per our rules, we should avoid speculative suggestions. Delete this comment as it makes a speculative suggestion about indexing without clear evidence of necessity, and it starts with "Ensure..." which violates our commenting rules.
> 
> 
> </details>
> 
> <details>
> <summary>3. <code>packages/plugin-bm-api/src/graphql/resolvers/queries/tour.ts:1</code></summary>
> 
> - **Draft comment:** 
> Remove the unused import 'skip' from 'node:test'. It may shadow the locally defined 'skip' variable and is unnecessary.
> - **Reason this comment was not posted:**
> Comment was not on a location in the diff, so it can't be submitted as a review comment.
> 
> 
> </details>
> 
> <details>
> <summary>4. <code>packages/plugin-bm-api/src/graphql/resolvers/queries/tour.ts:33</code></summary>
> 
> - **Draft comment:** 
> Incorrect assignment: using branchId to set selector.status. Likely intended to assign to selector.branchId.
> - **Reason this comment was not posted:**
> Comment was not on a location in the diff, so it can't be submitted as a review comment.
> 
> 
> </details>
> 
> <details>
> <summary>5. <code>packages/plugin-bm-api/src/graphql/resolvers/queries/tour.ts:177</code></summary>
> 
> - **Draft comment:** 
> Combined result from aggregation and find: merging group results with individual tours without unified sorting may lead to inconsistency. Consider ordering or structuring the response uniformly.
> - **Reason this comment was not posted:**
> Marked as duplicate.
> 
> 
> </details>
> 
> <details>
> <summary>6. <code>packages/plugin-bm-api/src/models/definitions/tour.ts:16</code></summary>
> 
> - **Draft comment:** 
> Type mismatch: The ITour interface defines 'duration' as a string, but the Mongoose schema sets it as a Number. Align the types for consistency.
> - **Reason this comment was not posted:**
> Comment was on unchanged code.
> 
> 
> </details>
> 
> <details>
> <summary>7. <code>packages/plugin-bm-api/src/models/definitions/tour.ts:75</code></summary>
> 
> - **Draft comment:** 
> Typographical error: 'initeraryId' should likely be corrected to 'itineraryId'.
> - **Reason this comment was not posted:**
> Comment was on unchanged code.
> 
> 
> </details>
> 
> 
> Workflow ID: <workflowid>`wflow_zrQRzRMmIrLQ9qsR`</workflowid>
> </details>
> 
> <sup>You can customize </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=erxes%2Ferxes&utm_source=github&utm_medium=referral)<sup> by changing your [verbosity settings](https://app.ellipsis.dev/erxes/settings/code-review), reacting with :+1: or :-1:, [replying to comments](https://docs.ellipsis.dev/features/code-review#teaching-ellipsis), or adding [code review rules](https://app.ellipsis.dev/erxes/code-review/rules).</sup>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T02:43:57Z

Review comments:
diff_hunk: @@ -79,12 +79,109 @@ const tourQueries = {
 
     return {
       list,
-      total,
+      total
+    };
+  },
+  async bmToursGroup(
+    _root,
+    {
+      categories,
+      page = 1,
+      perPage = 10,
+      status,
+      innerDate,
+      branchId,
+      tags,
+      startDate1,
+      endDate1,
+      startDate2,
+      endDate2,
+      sortField = "createdAt",
+      sortDirection = -1
+    },
+    { models }: IContext
+  ) {
+    const selector: any = {};
+
+    const skip = Math.max(0, page - 1) * perPage;
+    if (categories) {
+      selector.categories = { $in: categories };
+    }
+    if (status) {
+      selector.status = status;
+    }
+    if (branchId) {
+      selector.status = branchId;
+    }
+    if (tags) {
+      selector.tags = { $in: tags };
+    }
+    if (innerDate) {
+      const dateToCheck = innerDate;
+      selector.startDate = { $lte: dateToCheck };
+      selector.endDate = { $gte: dateToCheck };
+
+      // selector.$expr = {
+      //   $lte: [
+      //     dateToCheck,
+      //     {
+      //       $add: [
+      //         '$startDate',
+      //         { $multiply: ['$duration', 24 * 60 * 60 * 1000] },
+      //       ],
+      //     },
+      //   ],
+      // };
+    }
+
+    if (startDate2) {
+      if (!selector.startDate) selector.startDate = {};
+      selector.startDate["$lte"] = startDate2;
+    }
+    if (startDate1) {
+      if (!selector.startDate) selector.startDate = {};
+      selector.startDate["$gte"] = startDate1;
+    }
+
+    if (endDate2) {
+      if (!selector.endDate) selector.endDate = {};
+      selector.endDate["$lte"] = endDate2;
+    }
+    if (endDate1) {
+      if (!selector.endDate) selector.endDate = {};
+      selector.endDate["$gte"] = endDate1;
+    }
+    const list = await models.Tours.find({
+      ...selector,
+      groupCode: { $in: [null, ""] }
+    })
+      .limit(perPage)
+      .skip(skip)
+      .sort({ [sortField]: sortDirection === -1 ? sortDirection : 1 });
+    const total = await models.Tours.find(selector).countDocuments();
+
+    const group = await models.Tours.aggregate([
+      {
+        $match: {
+          ...selector,
+          groupCode: { $nin: [null, ""] } // Exclude null and empty strings
+        }
+      },
+      {
+        $group: {
+          _id: "$groupCode", // group by category
+          items: { $push: "$$ROOT" } // push full documents into an array
+        }
+      }
+    ]);
+    return {
+      list: [...group, ...list],
path: packages/plugin-bm-api/src/graphql/resolvers/queries/tour.ts
user: login: ellipsis-dev[bot]
body: Merging group aggregate and list arrays may lead to unpredictable ordering. Consider sorting or separating grouped and ungrouped results for clarity.

created_at: 2025-05-08T02:43:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,12 +79,109 @@
 
     return {
       list,
-      total,
+      total
+    };
+  },
+  async bmToursGroup(
+    _root,
+    {
+      categories,
+      page = 1,
+      perPage = 10,
+      status,
+      innerDate,
+      branchId,
+      tags,
+      startDate1,
+      endDate1,
+      startDate2,
+      endDate2,
+      sortField = "createdAt",
+      sortDirection = -1
+    },
+    { models }: IContext
+  ) {
+    const selector: any = {};
+
+    const skip = Math.max(0, page - 1) * perPage;
+    if (categories) {
+      selector.categories = { $in: categories };
+    }
+    if (status) {
+      selector.status = status;
+    }
+    if (branchId) {
+      selector.status = branchId;
path: packages/plugin-bm-api/src/graphql/resolvers/queries/tour.ts
user: login: ellipsis-dev[bot]
body: Incorrect assignment: `branchId` is being assigned to `selector.status` instead of `selector.branchId`.
```suggestion
      selector.branchId = branchId;
```

created_at: 2025-05-08T02:43:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,11 +84,14 @@ export const queries = `
   bmTours(branchId:String, sortField:String, sortDirection:Int, page:Int, perPage:Int, status: String, innerDate: Date,branchId: String, tags: [String],startDate1:Date,startDate2:Date,endDate1:Date,endDate2:Date): ListTour
   bmTourDetail(_id:String!,branchId: String): Tour
   bmOrders( tourId:String, customerId:String ,branchId: String):ListBmsOrder
+  bmToursGroup(branchId:String, sortField:String, sortDirection:Int, page:Int, perPage:Int, status: String, innerDate: Date,branchId: String, tags: [String],startDate1:Date,startDate2:Date,endDate1:Date,endDate2:Date): JSON
path: packages/plugin-bm-api/src/graphql/schema/tour.ts
user: login: ellipsis-dev[bot]
body: The `bmToursGroup` query returns a JSON type. Consider defining a dedicated GraphQL type for stronger type safety and clearer schema documentation.

created_at: 2025-05-08T02:43:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -39,10 +40,10 @@ export interface ITourDocument extends ITour, Document {
 }
 
 const STATUS_TYPES = [
-  { label: 'running', value: 'running' },
-  { label: 'compeleted', value: 'compeleted' },
-  { label: 'scheduled', value: 'scheduled' },
-  { label: 'cancelled', value: 'cancelled' },
+  { label: "running", value: "running" },
+  { label: "compeleted", value: "compeleted" },
path: packages/plugin-bm-api/src/models/definitions/tour.ts
user: login: ellipsis-dev[bot]
body: Typographical error: 'compeleted' should be corrected to 'completed'.
```suggestion
  { label: "completed", value: "completed" },
```

created_at: 2025-05-08T02:43:56Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-08T02:40:32Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide

This pull request introduces a new GraphQL query `bmToursGroup` to fetch tours, with a special handling for grouping tours that share a `groupCode`. The `groupCode` field has been added to the Tour data model and GraphQL schema. The new query filters and sorts tours, then combines individually listed tours (those without a `groupCode` or with a null/empty `groupCode`) with tours grouped by their `groupCode` using a MongoDB aggregation pipeline.

#### ER Diagram: Update to Tour Entity

```mermaid
erDiagram
  Tour {
    String _id PK
    Date createdAt
    Date modifiedAt
    String name
    String groupCode "New field"
    String refNumber
    String content
    Number duration
    Date startDate
    Date endDate
    String status
    Number cost
    String_array tags
    String branchId
    %% Other existing attributes
  }
```

#### Class Diagram: Update to TourQueries Resolvers

```mermaid
classDiagram
  class TourQueries {
    +async bmTours(...)
    +async bmTourDetail(...)
    +async bmToursGroup(categories, page, perPage, status, innerDate, branchId, tags, startDate1, endDate1, startDate2, endDate2, sortField, sortDirection, context) BmToursGroupResponse
  }
  note for TourQueries "Added method: +async bmToursGroup(...)"

  class BmToursGroupResponse {
    +Object[] list
    +Number total
  }

  TourQueries ..> BmToursGroupResponse : returns
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Implemented a new GraphQL query `bmToursGroup` for fetching and grouping tours. | <ul><li>Added a new resolver function `bmToursGroup` that filters tours based on provided arguments.</li><li>The resolver separates tours into two categories: those with a `groupCode` and those without.</li><li>Tours with a `groupCode` are grouped using MongoDB's aggregation framework.</li><li>Tours without a `groupCode` (or with null/empty `groupCode`) are fetched directly.</li><li>Both lists (grouped and individual) are combined before being returned.</li><li>Added `bmToursGroup` to the GraphQL query schema, specifying its arguments and return type `JSON` (which seems to be a placeholder for a structured list).</li></ul> | `packages/plugin-bm-api/src/graphql/resolvers/queries/tour.ts`<br/>`packages/plugin-bm-api/src/graphql/schema/tour.ts` |
| Added `groupCode` field to the Tour data model and GraphQL schema. | <ul><li>Added `groupCode` (string, optional) to the `ITour` interface and `tourSchema` in the tour model definition.</li><li>Added `groupCode` (String) to the `Tour` GraphQL type definition.</li><li>Included `groupCode` in the GraphQL mutation parameters for creating/updating tours.</li></ul> | `packages/plugin-bm-api/src/models/definitions/tour.ts`<br/>`packages/plugin-bm-api/src/graphql/schema/tour.ts` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-08T02:40:35Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=erxes/erxes&utm_content=6020):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-05-08T02:42:50Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=erxes_erxes&pullRequest=6020) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [47.9% Duplication on New Code](https://sonarcloud.io/component_measures?id=erxes_erxes&pullRequest=6020&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=erxes_erxes&pullRequest=6020)


author_association: NONE