Repository: erxes/erxes
Pull request #6000: feat: deal to msdynamic sync added
Author: login: Gerelsukh
Created: 2025-04-13T11:51:41Z | Merged: 2025-04-21T16:58:09Z


![Screenshot from 2025-04-15 20-39-03](https://github.com/user-attachments/assets/a341125d-82ca-4c60-91b6-3f7cbf5d6d98)
![Screenshot from 2025-04-15 21-07-18](https://github.com/user-attachments/assets/23099d6f-a92f-4ed6-880a-099413cecb76)



<!-- ELLIPSIS_HIDDEN -->


----

> [!IMPORTANT]
> Adds sales deal synchronization with MS Dynamics, new UI elements for configuration, and refines synchronization logic.
> 
>   - **Behavior**:
>     - Added `sales:deal` synchronization with MS Dynamics in `afterMutations.ts` and `syncDynamic.ts`.
>     - Introduced dynamic board and pipeline selection in `PerSettings.tsx`.
>     - Expanded sync history to include sales deals in `SyncHistoryList.tsx`.
>     - Added `sendSalesMessage` function in `messageBroker.ts`.
>   - **Improvements**:
>     - Enhanced order synchronization logic in `orderToDynamic` and `dealToDynamic` in `utils.ts`.
>     - Simplified customer synchronization logic in `utilsCustomer.ts`.
>     - Updated UI labels and constants in `constants.ts`.
>   - **Bug Fixes**:
>     - Fixed selector reference in order synchronization in `syncDynamic.ts`.
>     - Minor formatting fixes in `SyncHistoryList.tsx` and `PerSettings.tsx`.
>   - **Chores**:
>     - Code formatting improvements and minor refactoring for readability.
> 
> <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 fd275c04af6ca50342ad70f17b62be8abc707850. You can [customize](https://app.ellipsis.dev/erxes/settings/summaries) this summary. It will automatically update as commits are pushed.</sup>

----


<!-- ELLIPSIS_HIDDEN -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added support for synchronizing "sales:deal" updates with MS Dynamics, alongside existing "pos:order" synchronization.
  - Introduced new board and pipeline selection options with dynamic checkbox controls in settings.
  - Expanded sync history display to include both sales deals and POS orders.
  - Added sales message sending capability to the message broker.

- **Improvements**
  - Enhanced sales order synchronization with detailed handling of order lines, customer info, and discount recalculations.
  - Simplified customer data synchronization logic to improve reliability.
  - Updated UI labels and constants for better clarity and consistency.
  - Refined sync log content display for sales deals.

- **Bug Fixes**
  - Fixed selector reference in order synchronization.
  - Minor formatting and consistency fixes in UI components and API utilities.

- **Chores**
  - Code formatting improvements and minor refactoring for readability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/packages/plugin-msdynamic-api/src/afterMutations.ts b/packages/plugin-msdynamic-api/src/afterMutations.ts
index dda2590ce8e..f12375ee296 100644
--- a/packages/plugin-msdynamic-api/src/afterMutations.ts
+++ b/packages/plugin-msdynamic-api/src/afterMutations.ts
@@ -1,11 +1,12 @@
 import { generateModels } from './connectionResolver';
 import { customerToDynamic } from './utilsCustomer';
-import { dealToDynamic, getConfig } from './utils';
+import { dealToDynamic, getConfig, orderToDynamic } from './utils';
 
 const allowTypes = {
   'core:customer': ['create'],
   'core:company': ['create'],
   'pos:order': ['synced'],
+  'sales:deal': ['update'],
 };
 
 export const afterMutationHandlers = async (subdomain, params) => {
@@ -73,19 +74,52 @@ export const afterMutationHandlers = async (subdomain, params) => {
       return;
     }
 
-    if (type === 'pos:order') {
-      syncLog = await models.SyncLogs.syncLogsAdd(syncLogDoc);
+    if (type === 'sales:deal' && action === 'update') {
+      const deal = params.updatedDocument || params.object;
+      const oldDeal = params.object;
+      const destinationStageId = deal.stageId || "";
+
+      if (!(destinationStageId && destinationStageId !== oldDeal.stageId)) {
+        return;
+      }
+
+      const configsArray = Object.values(configs) as any[];
+
+      const foundConfig = configsArray.find(
+        (config) => config.useBoard && config.stageId === destinationStageId
+      );
+
+      if (foundConfig) {
+        syncLog = await models.SyncLogs.syncLogsAdd(syncLogDoc);
 
-      if (action === 'synced') {
         await dealToDynamic(
           subdomain,
+          models,
           syncLog,
-          params.updatedDocument || params.object,
+          deal,
+          foundConfig
+        );
+      }
+      return;
+    }
+
+    if (type === 'pos:order' && action === 'synced') {
+      syncLog = await models.SyncLogs.syncLogsAdd(syncLogDoc);
+
+      const updatedDoc = params.updatedDocument || params.object;
+      const brandId = updatedDoc?.scopeBrandIds?.[0];
+      const config = configs[brandId || 'noBrand'];
+
+      if (!config.useBoard) {
+        await orderToDynamic(
+          subdomain,
           models,
-          configs
+          syncLog,
+          updatedDoc,
+          config
         );
-        return;
       }
+      return;
     }
   } catch (e) {
     await models.SyncLogs.updateOne(
diff --git a/packages/plugin-msdynamic-api/src/graphql/resolvers/mutations/syncDynamic.ts b/packages/plugin-msdynamic-api/src/graphql/resolvers/mutations/syncDynamic.ts
index 59707835732..f6dbb11d0c6 100644
--- a/packages/plugin-msdynamic-api/src/graphql/resolvers/mutations/syncDynamic.ts
+++ b/packages/plugin-msdynamic-api/src/graphql/resolvers/mutations/syncDynamic.ts
@@ -8,10 +8,10 @@ import {
 import {
   consumeCategory,
   consumeInventory,
-  dealToDynamic,
   getConfig,
   getExchangeRates,
   getPrice,
+  orderToDynamic,
 } from '../../../utils';
 import { consumeCustomers } from '../../../utilsCustomer';
 import { putUpdateLog } from '../../../logUtils';
@@ -388,12 +388,14 @@ const msdynamicSyncMutations = {
 
     try {
       const configs = await getConfig(subdomain, 'DYNAMIC', {});
-      response = await dealToDynamic(
+      const config = configs[order.scopeBrandIds[0] || 'noBrand'];
+
+      response = await orderToDynamic(
         subdomain,
+        models,
         syncLog,
         order,
-        models,
-        configs
+        config
       );
     } catch (e) {
       await models.SyncLogs.updateOne(
diff --git a/packages/plugin-msdynamic-api/src/graphql/resolvers/syncLog.ts b/packages/plugin-msdynamic-api/src/graphql/resolvers/syncLog.ts
index 44dc5a42372..7aed70dc5d1 100644
--- a/packages/plugin-msdynamic-api/src/graphql/resolvers/syncLog.ts
+++ b/packages/plugin-msdynamic-api/src/graphql/resolvers/syncLog.ts
@@ -15,13 +15,19 @@ export default {
       subdomain,
       action: "users.findOne",
       data: { _id: syncLog.createdBy },
-      isRPC: true
+      isRPC: true,
     });
   },
 
   async content(syncLog: ISyncLogDocument, {}, {}: IContext) {
     const { contentType, contentId } = syncLog;
 
+    if (contentType === "sales:deal") {
+      return (
+        syncLog.consumeData?.object.name.split(":").pop().trim() || contentId
+      );
+    }
+
     if (contentType === "pos:order") {
       return (
         syncLog.consumeData?.number ||
@@ -48,5 +54,5 @@ export default {
     }
 
     return contentId;
-  }
+  },
 };
diff --git a/packages/plugin-msdynamic-api/src/messageBroker.ts b/packages/plugin-msdynamic-api/src/messageBroker.ts
index 7261c0c630f..f6d23a75f2d 100644
--- a/packages/plugin-msdynamic-api/src/messageBroker.ts
+++ b/packages/plugin-msdynamic-api/src/messageBroker.ts
@@ -30,6 +30,15 @@ export const sendPosMessage = async (
   });
 };
 
+export const sendSalesMessage = async (
+  args: MessageArgsOmitService
+): Promise<any> => {
+  return sendMessage({
+    serviceName: "sales",
+    ...args
+  });
+};
+
 export const sendCoreMessage = async (
   args: MessageArgsOmitService
 ): Promise<any> => {
diff --git a/packages/plugin-msdynamic-api/src/utils.ts b/packages/plugin-msdynamic-api/src/utils.ts
index fcc9e74c3f0..5604a9d3b24 100644
--- a/packages/plugin-msdynamic-api/src/utils.ts
+++ b/packages/plugin-msdynamic-api/src/utils.ts
@@ -1,6 +1,10 @@
 import fetch from 'node-fetch';
 import { IModels } from './connectionResolver';
-import { sendCoreMessage, sendPosMessage } from './messageBroker';
+import {
+  sendCoreMessage,
+  sendSalesMessage,
+  sendPosMessage,
+} from './messageBroker';
 import { ISyncLogDocument } from './models/definitions/dynamic';
 import { getMsdCustomerInfo } from './utilsCustomer';
 import { putCreateLog, putDeleteLog, putUpdateLog } from './logUtils';
@@ -215,14 +219,322 @@ export const consumeCategory = async (
 
 export const dealToDynamic = async (
   subdomain: string,
+  models: IModels,
   syncLog: ISyncLogDocument,
-  params: any,
+  deal: any,
+  config: any
+) => {
+  const brandId = config.branId;
+
+  let msdCustomer: any = {};
+
+  let orderMsdNo: string = '';
+  let orderItemsMsdNo: any = {};
+  const extraData = deal.extraData || {};
+  const syncErkhetInfo = extraData.msdynamic || {};
+  orderMsdNo = syncErkhetInfo.no;
+  orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+
+  try {
+    let customer;
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = '';
+    let lineUrlParam = '';
+    let postMethod = 'POST';
+    const postHeaders = {
+      'Content-Type': 'application/json',
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        'base64'
+      )}`,
+    };
+
+    const conformities = await sendCoreMessage({
+      subdomain,
+      action: 'conformities.findConformities',
+      data: {
+        mainType: 'deal',
+        mainTypeId: { $in: deal._id },
+      },
+      isRPC: true,
+      defaultValue: [],
+    });
+
+    if (conformities.length > 0) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        conformities[0].relTypeId,
+        conformities[0].relType,
+        brandId,
+        config
+      );
+
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
+
+    const user = await sendCoreMessage({
+      subdomain,
+      action: 'users.findOne',
+      data: {
+        _id: deal.assignedUserIds[0],
+      },
+      isRPC: true,
+    });
+
+    const rawDescription = deal.description.replace(/<\/?p>/g, '').trim();
+
+    const sellAddress = rawDescription.slice(0, 100);
+    const sellAddress2 = rawDescription.slice(100, 150);
+
+    const sendData: any = {
+      Sell_to_Customer_No: msdCustomer?.No
+        ? msdCustomer?.No
+        : config.defaultUserCode,
+      Sell_to_Phone_No: customer?.primaryPhone || '',
+      Sell_to_E_Mail: customer?.primaryEmail || '',
+      External_Document_No: deal.number || deal.name.split(':').pop().trim(),
+      Responsibility_Center: config.responsibilityCenter || '',
+      Sync_Type: config.syncType || '',
+      Mobile_Phone_No: customer?.primaryPhone || '',
+      VAT_Bus_Posting_Group: config.vatBusPostingGroup || '',
+      Payment_Terms_Code: config.paymentTermsCode || '',
+      Payment_Method_Code: config.paymentMethodCode || 'CASH',
+      Customer_Price_Group: config.customerPricingGroup || '',
+      Prices_Including_VAT: true,
+      BillType: config.billType || 'Receipt',
+      Location_Code: config.locationCode || '',
+      Deal_Type_Code: config.dealType || 'NORMAL',
+      Salesperson_Code: user?.employeeId ?? '',
+      Sell_to_Address: sellAddress,
+      Sell_to_Address_2: sellAddress2,
+      CustomerNo:
+        customer?.customFieldsDataByFieldCode?.vatCustomer?.value ||
+        customer?.customFieldsDataByFieldCode?.vatCompany?.value,
+    };
+
+    if (orderMsdNo) {
+      urlParam = `(Document_Type='Order', No='${orderMsdNo}')`;
+      lineUrlParam = `(Document_Type='Order',Document_No='${orderMsdNo}',Line_No=%Ln)`;
+      postMethod = 'PATCH';
+      postHeaders['If-Match'] = '*';
+      sendData.No = orderMsdNo;
+    }
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          sendData,
+          sendStr: JSON.stringify(sendData),
+        },
+      }
+    );
+
+    if (!deal.productsData.length) {
+      throw new Error('Has not items order');
+    }
+
+    const responseSale = await fetch(`${salesApi}${urlParam}`, {
+      method: postMethod,
+      headers: postHeaders,
+      body: JSON.stringify(sendData),
+    }).then((res) => res.json());
+    const lineNoById = {};
+
+    if (responseSale) {
+      const products = await sendCoreMessage({
+        subdomain,
+        action: 'products.find',
+        data: {
+          _id: { $in: deal.productsData.map((item) => item.productId) },
+        },
+        isRPC: true,
+      });
+
+      const productById = {};
+
+      for (const product of products) {
+        productById[product._id] = product;
+      }
+
+      let totalDiscount = 0;
+
+      for (const item of deal.productsData) {
+        let lineUrlP = '';
+        let linePostMethod = 'POST';
+        let linePostHeaders = {
+          'Content-Type': 'application/json',
+          Authorization: `Basic ${Buffer.from(
+            `${username}:${password}`
+          ).toString('base64')}`,
+        };
+
+        const lineNo = orderItemsMsdNo[item._id] || '';
+        const product = productById[item.productId];
+
+        if (!product) {
+          await models.SyncLogs.updateOne(
+            { _id: syncLog._id },
+            {
+              $set: {
+                error: `not found product ${product._id}`,
+              },
+            }
+          );
+          continue;
+        }
+
+        const sendSalesLine: any = {
+          Document_No: responseSale.No,
+          Type: 'Item',
+          No: productById[item.productId]
+            ? productById[item.productId].code
+            : '',
+          Quantity: item.quantity || 0,
+          Location_Code: config.locationCode,
+        };
+
+        if (lineNo) {
+          lineUrlP = lineUrlParam.replace('%Ln', lineNo);
+          linePostMethod = 'PATCH';
+          linePostHeaders['If-Match'] = '*';
+          sendSalesLine.Line_No = lineNo;
+        }
+
+        await models.SyncLogs.updateOne(
+          { _id: syncLog._id },
+          {
+            $push: {
+              sendSales: JSON.stringify(sendSalesLine),
+            },
+          }
+        );
+
+        const responseSaleLine = await fetch(`${salesLineApi}${lineUrlP}`, {
+          method: linePostMethod,
+          headers: linePostHeaders,
+          body: JSON.stringify(sendSalesLine),
+        }).then((res) => res.json());
+
+        if (responseSaleLine?.error?.message) {
+          const foundSyncLog = (await models.SyncLogs.findOne({
+            _id: syncLog._id,
+          })) || { error: '' };
+          await models.SyncLogs.updateOne(
+            { _id: syncLog._id },
+            {
+              $set: {
+                error: `${foundSyncLog.error ? foundSyncLog.error : ''} - ${
+                  responseSaleLine.error.message
+                }`,
+              },
+            }
+          );
+        }
+
+        await models.SyncLogs.updateOne(
+          { _id: syncLog._id },
+          {
+            $push: {
+              responseSales: JSON.stringify(responseSaleLine),
+            },
+          }
+        );
+        lineNoById[item._id] = lineNo || responseSaleLine.Line_No;
+        totalDiscount += item.discountAmount ?? 0;
+      }
+
+      const soapApiUrl = config.discountSoapApi; // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
+      if (soapApiUrl && totalDiscount) {
+        const soapHeaders = {
+          'Content-Type': 'text/xml',
+          SOAPAction:
+            'urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount',
+          Authorization: `Basic ${Buffer.from(
+            `${username}:${password}`
+          ).toString('base64')}`,
+        };
+        const soapDiscountSendData = `
+          <?xml version="1.0"?>
+          <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+                    xmlns:tns="urn:microsoft-dynamics-schemas/codeunit/PictureService">
+          <soapenv:Header/>
+          <soapenv:Body>
+            <tns:ReCalculateSalesOrderDiscount>
+              <tns:code>${responseSale.No}</tns:code>
+              <tns:invoiceDiscountAmount>${totalDiscount}</tns:invoiceDiscountAmount>
+            </tns:ReCalculateSalesOrderDiscount>
+          </soapenv:Body>
+          </soapenv:Envelope>
+        `;
+        await fetch(`${soapApiUrl}`, {
+          method: 'POST',
+          headers: soapHeaders,
+          body: soapDiscountSendData,
+        });
+      }
+    }
+
+    await sendSalesMessage({
+      subdomain,
+      action: 'deals.updateOne',
+      data: {
+        selector: { _id: deal._id },
+        modifier: {
+          $set: {
+            extraData: {
+              ...(extraData || {}),
+              msdynamic: {
+                no: orderMsdNo || responseSale.No,
+                lineNos: lineNoById,
+              },
+            },
+          },
+        },
+      },
+      isRPC: true,
+    });
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          responseData: responseSale,
+          responseStr: JSON.stringify(responseSale),
+        },
+      }
+    );
+
+    return responseSale;
+  } catch (e) {
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      { $set: { error: e.message } }
+    );
+    console.log(e, 'error');
+  }
+};
+
+export const orderToDynamic = async (
+  subdomain: string,
   models: IModels,
-  configs: any
+  syncLog: ISyncLogDocument,
+  order: any,
+  config: any
 ) => {
-  const order = params;
   const brandId = order.scopeBrandIds[0];
-  const config = configs[brandId || 'noBrand'];
 
   let msdCustomer: any = {};
 
@@ -293,10 +605,6 @@ export const dealToDynamic = async (
       BillType: config.billType || 'Receipt',
       Location_Code: config.locationCode || '',
       Deal_Type_Code: config.dealType || 'NORMAL',
-      Salesperson_Code:
-        config.title === 'Beverage'
-          ? msdCustomer?.Salesperson_Code || '3144'
-          : '',
       CustomerNo:
         customer?.customFieldsDataByFieldCode?.vatCustomer?.value ||
         customer?.customFieldsDataByFieldCode?.vatCompany?.value,
@@ -416,8 +724,9 @@ export const dealToDynamic = async (
             { _id: syncLog._id },
             {
               $set: {
-                error: `${foundSyncLog.error ? foundSyncLog.error : ''} - ${responseSaleLine.error.message
-                  }`,
+                error: `${foundSyncLog.error ? foundSyncLog.error : ''} - ${
+                  responseSaleLine.error.message
+                }`,
               },
             }
           );
@@ -435,16 +744,17 @@ export const dealToDynamic = async (
         totalDiscount += item.discountAmount ?? 0;
       }
 
-      const soapApiUrl = config.discountSoapApi // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
+      const soapApiUrl = config.discountSoapApi; // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
 
       if (soapApiUrl && totalDiscount) {
         const soapHeaders = {
-          "Content-Type": "text/xml",
-          SOAPAction: "urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount",
+          'Content-Type': 'text/xml',
+          SOAPAction:
+            'urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount',
           Authorization: `Basic ${Buffer.from(
             `${username}:${password}`
           ).toString('base64')}`,
-        }
+        };
 
         const soapDiscountSendData = `
           <?xml version="1.0"?>
@@ -472,7 +782,7 @@ export const dealToDynamic = async (
       subdomain,
       action: 'orders.updateOne',
       data: {
-        selector: { _id: params._id },
+        selector: { _id: order._id },
         modifier: {
           $set: {
             syncErkhetInfo: JSON.stringify({
@@ -640,7 +950,7 @@ export const getExchangeRates = async (config: ExchangeRateConfig) => {
       if (
         !latestByCurrency[currency] ||
         new Date(item.Starting_Date) >
-        new Date(latestByCurrency[currency].Starting_Date)
+          new Date(latestByCurrency[currency].Starting_Date)
       ) {
         latestByCurrency[currency] = item;
       }
diff --git a/packages/plugin-msdynamic-api/src/utilsCustomer.ts b/packages/plugin-msdynamic-api/src/utilsCustomer.ts
index 80775d31c20..6a8c524f8a1 100644
--- a/packages/plugin-msdynamic-api/src/utilsCustomer.ts
+++ b/packages/plugin-msdynamic-api/src/utilsCustomer.ts
@@ -119,14 +119,7 @@ const getSendDataCustomer = async (subdomain, customer, config) => {
   return sendData;
 };
 
-const checkSend = async (
-  subdomain,
-  models: IModels,
-  customer,
-  config,
-  filterStr,
-  syncLog
-) => {
+const checkSend = async (customer, config, filterStr) => {
   const { customerApi, username, password } = config;
 
   const responseChecker = await fetch(
@@ -174,44 +167,6 @@ const checkSend = async (
   if (!responseChecker.value?.length) {
     return customer;
   }
-
-  const sendData = await getSendDataCustomer(subdomain, customer, config);
-
-  await models.SyncLogs.updateOne(
-    { _id: syncLog._id },
-    {
-      $set: {
-        sendData,
-        sendStr: JSON.stringify(sendData),
-      },
-    }
-  );
-
-  let responseCustomerData;
-
-  try {
-    responseCustomerData = await fetch(customerApi, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-        Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
-          'base64'
-        )}`,
-      },
-      body: JSON.stringify(sendData),
-    });
-  } catch (e) {
-    await models.SyncLogs.updateOne(
-      { _id: syncLog._id },
-      {
-        $set: {
-          error: e.message,
-        },
-      }
-    );
-  }
-
-  return responseCustomerData;
 };
 
 export const getMsdCustomerInfo = async (
@@ -243,24 +198,10 @@ export const getMsdCustomerInfo = async (
     }
 
     filterStr = `No eq '${relation.no}'`;
-    msdCustomer = await checkSend(
-      subdomain,
-      models,
-      customer,
-      config,
-      filterStr,
-      syncLog
-    );
+    msdCustomer = await checkSend(customer, config, filterStr);
   } else {
     filterStr = `Phone_No eq '${customer.primaryPhone}'`;
-    msdCustomer = await checkSend(
-      subdomain,
-      models,
-      customer,
-      config,
-      filterStr,
-      syncLog
-    );
+    msdCustomer = await checkSend(customer, config, filterStr);
   }
 
   const brandIds = customer?.scopeBrandIds || [];
diff --git a/packages/plugin-msdynamic-ui/src/components/SyncHistoryList.tsx b/packages/plugin-msdynamic-ui/src/components/SyncHistoryList.tsx
index 4ab0097dcdf..6bc1af609db 100755
--- a/packages/plugin-msdynamic-ui/src/components/SyncHistoryList.tsx
+++ b/packages/plugin-msdynamic-ui/src/components/SyncHistoryList.tsx
@@ -34,7 +34,10 @@ const SyncHistoryList = ({
     const { No, Sell_to_Customer_No, Sell_to_Customer_Name, error } =
       item.responseData || {};
 
-    if (item?.contentType === 'pos:order') {
+    if (
+      item?.contentType === "pos:order" ||
+      item?.contentType === "sales:deal"
+    ) {
       const renderSales = () => {
         return (item?.responseSales || []).map((listItem, index: number) => {
           const jsonObject = JSON.parse(listItem);
@@ -51,10 +54,12 @@ const SyncHistoryList = ({
                 {__("Number")}: {item.consumeData?.number || ""}
               </FormGroup>
               <FormGroup>
-                {__("Created Date")}: {dayjs(item.consumeData?.createdAt).format("lll") || ""}
+                {__("Created Date")}:{" "}
+                {dayjs(item.consumeData?.createdAt).format("lll") || ""}
               </FormGroup>
               <FormGroup>
-                {__("Paid Date")}: {dayjs(item?.consumeData?.paidDate).format("lll") || ""}
+                {__("Paid Date")}:{" "}
+                {dayjs(item?.consumeData?.paidDate).format("lll") || ""}
               </FormGroup>
               <FormGroup>
                 {__("Sync Date")}: {dayjs(item?.createdAt).format("lll") || ""}
diff --git a/packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx b/packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx
index a0ef1b7f9d4..fff44c7b29c 100644
--- a/packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx
+++ b/packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx
@@ -5,15 +5,19 @@ import {
   FormControl,
   FormGroup,
   Icon,
-} from '@erxes/ui/src/components';
-import { FormColumn, FormWrapper } from '@erxes/ui/src/styles/main';
-
-import { IConfigsMap } from '../../types';
-import { KEY_LABELS } from '../../constants';
-import { MainStyleModalFooter as ModalFooter } from '@erxes/ui/src/styles/eindex';
-import React from 'react';
-import SelectBrand from '@erxes/ui-inbox/src/settings/integrations/containers/SelectBrand';
-import { __ } from '@erxes/ui/src/utils';
+} from "@erxes/ui/src/components";
+import { FormColumn, FormWrapper } from "@erxes/ui/src/styles/main";
+import client from "@erxes/ui/src/apolloClient";
+import { gql } from "@apollo/client";
+import { queries as boardQueries } from "@erxes/ui-sales/src/boards/graphql";
+
+import { IConfigsMap } from "../../types";
+import { KEY_LABELS } from "../../constants";
+import { MainStyleModalFooter as ModalFooter } from "@erxes/ui/src/styles/eindex";
+import React from "react";
+import SelectBrand from "@erxes/ui-inbox/src/settings/integrations/containers/SelectBrand";
+import { __ } from "@erxes/ui/src/utils";
+import BoardSelectContainer from "@erxes/ui-sales/src/boards/containers/BoardSelect";
 
 type Props = {
   configsMap: IConfigsMap;
@@ -26,6 +30,7 @@ type Props = {
 type State = {
   config: any;
   hasOpen: boolean;
+  pipeline?: any;
 };
 
 class PerSettings extends React.Component<Props, State> {
@@ -40,7 +45,7 @@ class PerSettings extends React.Component<Props, State> {
 
   onSave = (e) => {
     e.preventDefault();
-    const { configsMap, currentConfigKey = '' } = this.props;
+    const { configsMap, currentConfigKey = "" } = this.props;
     const { config } = this.state;
     const newKey = config.brandId;
 
@@ -70,6 +75,29 @@ class PerSettings extends React.Component<Props, State> {
     this.onChangeConfig(brandId, e.target.value);
   };
 
+  onChangeBoard = (boardId: string) => {
+    this.setState({ config: { ...this.state.config, boardId } });
+  };
+
+  onChangePipeline = (pipelineId: string) => {
+    this.setState({ config: { ...this.state.config, pipelineId } }, () => {
+      if (pipelineId) {
+        client
+          .query({
+            query: gql(boardQueries.pipelineDetail),
+            variables: { _id: pipelineId },
+          })
+          .then(({ data }) => {
+            this.setState({ pipeline: data?.salesPipelineDetail || {} });
+          });
+      }
+    });
+  };
+
+  onChangeStage = (stageId: string) => {
+    this.setState({ config: { ...this.state.config, stageId } });
+  };
+
   onChangeInput = (code: string, e) => {
     this.onChangeConfig(code, e.target.value);
   };
@@ -89,6 +117,25 @@ class PerSettings extends React.Component<Props, State> {
     );
   };
 
+  onChangeCheckbox = (code: string, e) => {
+    this.onChangeConfig(code, e.target.checked);
+  };
+
+  renderCheckbox = (key: string) => {
+    const { config } = this.state;
+
+    return (
+      <FormGroup>
+        <ControlLabel>{KEY_LABELS[key]}</ControlLabel>
+        <FormControl
+          checked={config[key]}
+          componentclass="checkbox"
+          onChange={this.onChangeCheckbox.bind(this, key)}
+        />
+      </FormGroup>
+    );
+  };
+
   render() {
     const { config } = this.state;
 
@@ -97,13 +144,13 @@ class PerSettings extends React.Component<Props, State> {
         title={__(config.title)}
         beforeTitle={<Icon icon="settings" />}
         transparent={true}
-        open={this.props.currentConfigKey === 'newDYNAMIC' ? true : false}
+        open={this.props.currentConfigKey === "newDYNAMIC" ? true : false}
       >
         <FormGroup>
-          <ControlLabel>{'Title'}</ControlLabel>
+          <ControlLabel>{"Title"}</ControlLabel>
           <FormControl
             defaultValue={config.title}
-            onChange={this.onChangeInput.bind(this, 'title')}
+            onChange={this.onChangeInput.bind(this, "title")}
             required={true}
             autoFocus={true}
           />
@@ -112,49 +159,64 @@ class PerSettings extends React.Component<Props, State> {
           <SelectBrand
             isRequired={true}
             defaultValue={config.brandId}
-            onChange={this.onChangeBrand.bind(this, 'brandId')}
+            onChange={this.onChangeBrand.bind(this, "brandId")}
           />
         </FormGroup>
 
         <FormWrapper>
           <FormColumn>
-            {this.renderInput('itemApi')}
-            {this.renderInput('itemCategoryApi')}
-            {this.renderInput('priceApi')}
-            {this.renderInput('pricePriority')}
-            {this.renderInput('username')}
-            {this.renderInput('password', {type: 'password'})}
+            {this.renderInput("itemApi")}
+            {this.renderInput("itemCategoryApi")}
+            {this.renderInput("priceApi")}
+            {this.renderInput("pricePriority")}
+            {this.renderInput("username")}
+            {this.renderInput("password", { type: "password" })}
           </FormColumn>
 
           <FormColumn>
-            {this.renderInput('customerApi')}
-            {this.renderInput('salesApi')}
-            {this.renderInput('salesLineApi')}
-            {this.renderInput('exchangeRateApi')}
-            {this.renderInput('discountSoapApi')}
+            {this.renderInput("customerApi")}
+            {this.renderInput("salesApi")}
+            {this.renderInput("salesLineApi")}
+            {this.renderInput("exchangeRateApi")}
+            {this.renderInput("discountSoapApi")}
+            {this.renderCheckbox("useBoard")}
+            {(this.state.config.useBoard && (
+              <FormGroup>
+                <BoardSelectContainer
+                  type="deal"
+                  autoSelectStage={false}
+                  boardId={config.boardId}
+                  pipelineId={config.pipelineId}
+                  stageId={config.stageId}
+                  onChangeBoard={this.onChangeBoard}
+                  onChangePipeline={this.onChangePipeline}
+                  onChangeStage={this.onChangeStage}
+                />
+              </FormGroup>
+            )) || <></>}
           </FormColumn>
         </FormWrapper>
 
         <CollapseContent title="General settings">
           <FormWrapper>
             <FormColumn>
-              {this.renderInput('genBusPostingGroup')}
-              {this.renderInput('vatBusPostingGroup')}
-              {this.renderInput('customerPostingGroup')}
-              {this.renderInput('customerPricingGroup')}
-              {this.renderInput('customerDiscGroup')}
-              {this.renderInput('syncType')}
-              {this.renderInput('defaultUserCode')}
+              {this.renderInput("genBusPostingGroup")}
+              {this.renderInput("vatBusPostingGroup")}
+              {this.renderInput("customerPostingGroup")}
+              {this.renderInput("customerPricingGroup")}
+              {this.renderInput("customerDiscGroup")}
+              {this.renderInput("syncType")}
+              {this.renderInput("defaultUserCode")}
             </FormColumn>
             <FormColumn>
-              {this.renderInput('locationCode')}
-              {this.renderInput('reminderCode')}
-              {this.renderInput('responsibilityCenter')}
-              {this.renderInput('billType')}
-              {this.renderInput('dealType')}
-              {this.renderInput('paymentTermsCode')}
-              {this.renderInput('paymentMethodCode')}
-              {this.renderInput('defaultCompanyCode')}
+              {this.renderInput("locationCode")}
+              {this.renderInput("reminderCode")}
+              {this.renderInput("responsibilityCenter")}
+              {this.renderInput("billType")}
+              {this.renderInput("dealType")}
+              {this.renderInput("paymentTermsCode")}
+              {this.renderInput("paymentMethodCode")}
+              {this.renderInput("defaultCompanyCode")}
             </FormColumn>
           </FormWrapper>
         </CollapseContent>
diff --git a/packages/plugin-msdynamic-ui/src/constants.ts b/packages/plugin-msdynamic-ui/src/constants.ts
index c97c711076c..e013a46b0f6 100644
--- a/packages/plugin-msdynamic-ui/src/constants.ts
+++ b/packages/plugin-msdynamic-ui/src/constants.ts
@@ -1,37 +1,38 @@
 export const menuDynamic = [
-  { title: 'Sync History', link: '/sync-msdynamic-history' },
-  { title: 'Check orders', link: '/check-sync-orders' },
-  { title: 'Check Categories', link: '/msdynamic-category' },
-  { title: 'Check Products', link: '/msdynamic-products' },
-  { title: 'Check Price', link: '/msdynamic-price' },
-  { title: 'Check Customers', link: '/msdynamic-customers' },
+  { title: "Sync History", link: "/sync-msdynamic-history" },
+  { title: "Check orders", link: "/check-sync-orders" },
+  { title: "Check Categories", link: "/msdynamic-category" },
+  { title: "Check Products", link: "/msdynamic-products" },
+  { title: "Check Price", link: "/msdynamic-price" },
+  { title: "Check Customers", link: "/msdynamic-customers" },
 ];
 
 export const KEY_LABELS = {
-  itemApi: 'item api',
-  itemCategoryApi: 'item category api',
-  priceApi: 'price api',
-  pricePriority: 'price priority',
-  customerApi: 'customer api',
-  salesApi: 'sales api',
-  salesLineApi: 'sales line api',
-  exchangeRateApi: 'exchange rate api',
-  discountSoapApi: 'Discount SOAP api',
-  username: 'username',
-  password: 'password',
-  genBusPostingGroup: 'Gen_Bus_Posting_Group',
-  vatBusPostingGroup: 'VAT_Bus_Posting_Group',
-  customerPostingGroup: 'Customer Posting Group',
-  customerPricingGroup: 'Customer Pricing Group',
-  customerDiscGroup: 'Customer Disc Group',
-  paymentTermsCode: 'Payment Terms Code',
-  paymentMethodCode: 'Payment Method Code',
-  reminderCode: 'Location Code for Remainder',
-  locationCode: 'Location Code for sale',
-  responsibilityCenter: 'Responsibility center',
-  billType: 'Bill Type',
-  dealType: 'Deal type code',
-  syncType: 'Sync type',
-  defaultUserCode: 'Default User Code',
-  defaultCompanyCode: 'Default Company Code',
+  itemApi: "item api",
+  itemCategoryApi: "item category api",
+  priceApi: "price api",
+  pricePriority: "price priority",
+  customerApi: "customer api",
+  salesApi: "sales api",
+  salesLineApi: "sales line api",
+  exchangeRateApi: "exchange rate api",
+  discountSoapApi: "Discount SOAP api",
+  username: "username",
+  password: "password",
+  genBusPostingGroup: "Gen_Bus_Posting_Group",
+  vatBusPostingGroup: "VAT_Bus_Posting_Group",
+  customerPostingGroup: "Customer Posting Group",
+  customerPricingGroup: "Customer Pricing Group",
+  customerDiscGroup: "Customer Disc Group",
+  paymentTermsCode: "Payment Terms Code",
+  paymentMethodCode: "Payment Method Code",
+  reminderCode: "Location Code for Remainder",
+  locationCode: "Location Code for sale",
+  responsibilityCenter: "Responsibility center",
+  billType: "Bill Type",
+  dealType: "Deal type code",
+  syncType: "Sync type",
+  defaultUserCode: "Default User Code",
+  defaultCompanyCode: "Default Company Code",
+  useBoard: "Use deal board",
 };
diff --git a/packages/plugin-pos-api/src/models/definitions/orders.ts b/packages/plugin-pos-api/src/models/definitions/orders.ts
index 69b287971ec..6cf10e4aecd 100644
--- a/packages/plugin-pos-api/src/models/definitions/orders.ts
+++ b/packages/plugin-pos-api/src/models/definitions/orders.ts
@@ -74,7 +74,7 @@ export interface IPosOrder {
     subscriptionId: string;
     status: string;
   };
-  extraInfo?: any
+  extraInfo?: any;
 }
 export interface IPosOrderDocument extends IPosOrder, Document {
   _id: string;
@@ -276,7 +276,11 @@ export const posOrderSchema = schemaHooksWrapper(
       label: 'Subscription Info'
     }),
     closeDate: field({ type: Date, optional: true, label: 'Close Date' }),
-    extraInfo: field({ type: Schema.Types.Mixed, optional: true, label: 'Extra Info' })
+    extraInfo: field({
+      type: Schema.Types.Mixed,
+      optional: true,
+      label: 'Extra Info'
+    })
   }),
   'erxes_posOrders'
 );
diff --git a/packages/plugin-pos-api/src/utils.ts b/packages/plugin-pos-api/src/utils.ts
index ebd6af8d7de..bc3b69f2c48 100644
--- a/packages/plugin-pos-api/src/utils.ts
+++ b/packages/plugin-pos-api/src/utils.ts
@@ -162,7 +162,7 @@ export const confirmLoyalties = async (subdomain: string, order: IPosOrder) => {
     }
   }
 
-  const confirmItems = (order.items || [])
+  const confirmItems = order.items || [];
 
   if (!confirmItems.length) {
     return;
@@ -850,7 +850,12 @@ export const syncOrderFromClient = async ({
         subdomain,
         action: 'erxes-posclient-to-pos-api',
         data: {
-          order: { ...newOrder, convertDealId, posToken, subToken: toPos.token }
+          order: {
+            ...newOrder,
+            convertDealId,
+            posToken,
+            subToken: toPos.token
+          }
         },
         pos: toPos
       });
diff --git a/packages/plugin-sales-api/src/models/definitions/deals.ts b/packages/plugin-sales-api/src/models/definitions/deals.ts
index c0c2bbf75e5..55cf6e49c34 100644
--- a/packages/plugin-sales-api/src/models/definitions/deals.ts
+++ b/packages/plugin-sales-api/src/models/definitions/deals.ts
@@ -83,4 +83,5 @@ export const dealSchema = new Schema({
 
   productsData: field({ type: [productDataSchema], label: 'Products' }),
   paymentsData: field({ type: Object, optional: true, label: 'Payments' }),
+  extraData: field({ type: Object, optional: true })
 });


Reviews:
user: login: sourcery-ai[bot]
body: Hey @Gerelsukh - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider adding a more descriptive title, such as 'feat: Integrate MS Dynamics sales order sync' for better clarity.
- It would be helpful to include a brief explanation of the purpose and functionality of the `useBoard` config option.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-13T11:52:39Z
user: login: ellipsis-dev[bot]
body: :x: Changes requested. Reviewed everything up to 6e5f4d3c9f4b5237a8e4408934974cbb0833927d in <ellipsistime>2 minutes and 31 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `2852` lines of code in `7` files
 - Skipped `0` files when reviewing.
 - Skipped posting `20` drafted comments based on config settings.


<details>
<summary>1. <code>packages/plugin-msdynamic-api/src/utils.ts:364</code></summary>

- **Draft comment:** 
In dealToDynamic, inside the loop for order items, using `product._id` for an error message when product is falsy will always be undefined. Consider logging the item's productId instead.
- **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-msdynamic-api/src/utils.ts:410</code></summary>

- **Draft comment:** 
When updating the syncLogs error field, consider concatenating errors in a more structured format to ease debugging.
- **Reason this comment was not posted:** 
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 20% vs. threshold = 50%
The comment makes a reasonable suggestion about improving error logging, but it's quite vague. It doesn't specify what structure to use or why the current approach is problematic. The current code's approach of concatenating with dashes is simple but functional. Without more specific guidance, this comment may not be actionable enough.
The comment could be valid - structured error logging is generally better than simple string concatenation. But am I being too harsh in expecting more specific guidance in the comment?
While structured logging is good, a vague suggestion without concrete recommendations isn't very helpful to the PR author. The comment needs to be more specific to be truly actionable.
The comment should be deleted because it makes a vague suggestion without providing specific, actionable guidance on how to improve the error logging structure.

</details>

<details>
<summary>3. <code>packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx:156</code></summary>

- **Draft comment:** 
Consider adding type annotations for event handlers (like onChange and onClick) to avoid implicit 'any' and improve maintainability.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>4. <code>packages/plugin-msdynamic-ui/src/constants.ts:10</code></summary>

- **Draft comment:** 
KEY_LABELS mapping appears consistent. Ensure that label keys match any UI translations requirements in future updates.
- **Reason this comment was not posted:** 
Confidence changes required: `20%` <= threshold `50%`
None

</details>

<details>
<summary>5. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:1</code></summary>

- **Draft comment:** 
Model definitions look consistent. Double-check enum values and labels to ensure they align with business requirements.
- **Reason this comment was not posted:** 
Confidence changes required: `10%` <= threshold `50%`
None

</details>

<details>
<summary>6. <code>packages/plugin-pos-api/src/utils.ts:65</code></summary>

- **Draft comment:** 
In getBranchesUtil, use of process.env.ALL_AUTO_INIT check is done by checking membership in an array; ensure environment variable types are as expected. Consider using a standardized boolean conversion.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>7. <code>packages/plugin-pos-api/src/utils.ts:244</code></summary>

- **Draft comment:** 
In updateCustomer, when constructing pushInfo.addresses, consider validating marker values explicitly instead of relying on OR conditions; this will prevent potential accidental falsy values.
- **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>8. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:291</code></summary>

- **Draft comment:** 
The common fields schema is spread into boardSchema and pipelineSchema; ensure that any future extensions maintain consistency with these common definitions.
- **Reason this comment was not posted:** 
Confidence changes required: `30%` <= threshold `50%`
None

</details>

<details>
<summary>9. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:389</code></summary>

- **Draft comment:** 
For pipelineSchema, the labels for 'excludeCategoryIds' and 'excludeProductIds' seem inconsistent ('product exclude categories' vs 'exclude products'). Consider standardizing the labels.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>10. <code>packages/plugin-msdynamic-api/src/afterMutations.ts:77</code></summary>

- **Draft comment:** 
The sync for 'sales:deal' is commented out; please add a comment explaining why or consider removing/deprecating this branch if it's no longer needed.
- **Reason this comment was not posted:** 
Comment did not seem useful. Confidence is useful = `0%` <= threshold `50%`
The comment is asking for an explanation or a decision on whether to remove or deprecate a branch of code. This is not a direct code suggestion or a request for a specific test, and it asks for confirmation of intent, which violates the rules.

</details>

<details>
<summary>11. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:4</code></summary>

- **Draft comment:** 
There is a possible typo in the import path ('../../contants' instead of '../../constants'). Verify the correct module name.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>12. <code>packages/plugin-pos-api/src/utils.ts:508</code></summary>

- **Draft comment:** 
In createDealPerOrder, the condition for mobileAmount incorrectly assigns cashAmount to paymentsData.bank. Likely should use newOrder.mobileAmount instead.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>13. <code>packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx:36</code></summary>

- **Draft comment:** 
Consider refactoring this class component to a functional component with hooks for modern React best practices.
- **Reason this comment was not posted:** 
Confidence changes required: `50%` <= threshold `50%`
None

</details>

<details>
<summary>14. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:267</code></summary>

- **Draft comment:** 
The labels for 'branchIds' and 'departmentIds' in commonItemFieldsSchema are set as 'Tags', which might be a copy‐paste error. Please review and update the labels appropriately.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>15. <code>packages/plugin-msdynamic-api/src/utils.ts:322</code></summary>

- **Draft comment:** 
Typographical issue: The error message 'Has not items order' is awkwardly phrased. Consider changing it to something like 'Order has no items' for clarity.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>16. <code>packages/plugin-msdynamic-ui/src/constants.ts:29</code></summary>

- **Draft comment:** 
Typographical error: The value for 'reminderCode' is set to "Location Code for Remainder". Consider changing 'Remainder' to 'Reminder' to match the key name and intended meaning.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>17. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:4</code></summary>

- **Draft comment:** 
Typo detected: 'contants' on line 4 should likely be 'constants'. Please correct the spelling to improve code clarity.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>18. <code>packages/plugin-pos-api/src/utils.ts:31</code></summary>

- **Draft comment:** 
Consider renaming the variable 'childs' (and possibly the related action strings 'categories.withChilds' and 'tagWithChilds') to 'children' for improved readability and standard English usage.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>19. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:66</code></summary>

- **Draft comment:** 
Typographical error: 'proccessId' should be corrected to 'processId'.
- **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>20. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:350</code></summary>

- **Draft comment:** 
Typographical error: In the label for 'excludeCheckUserIds', 'Users elligible to see all cards' should be corrected to 'Users eligible to see all cards'.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>


Workflow ID: <workflowid>`wflow_MkJGHvdZbzhJeQlO`</workflowid>

</details>


----
**Want Ellipsis to fix these issues?** Tag `@ellipsis-dev` in a comment. You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T11:54:22Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🔭 Outside diff range comments (4)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (4)</summary><blockquote>

`42-87`: _🛠️ Refactor suggestion_

**Use stronger typing for 'document'**  
The local variable `document` is typed as `any`. Creating a dedicated interface for product data can enhance type safety and maintainability.

---

`191-214`: _🛠️ Refactor suggestion_

**Consistent logging for category updates**  
For creating or updating categories, consider adding logs or using `putCreateLog`/`putUpdateLog` to maintain a uniform audit trail across products and categories.

---

`216-324`: _🛠️ Refactor suggestion_

**Refactor large try block**  
The long try/catch from lines 239 to 324 includes multiple external API calls. Extracting these into smaller helper functions could improve readability and handle errors more granularly.

---

`901-951`: _🛠️ Refactor suggestion_

**Enhance error handling**  
If the exchange rate API call fails or returns an unexpected format, providing fallback exchange rates or retry logic helps maintain system stability.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (14)</summary><blockquote>

<details>
<summary>packages/plugin-pos-api/src/utils.ts (3)</summary><blockquote>

`197-199`: **Simplify nested condition with optional chaining**

This nested condition could be simplified using optional chaining for better readability.


```diff
- if (
-   afterMutations["pos:order"] &&
-   afterMutations["pos:order"]["synced"] &&
-   afterMutations["pos:order"]["synced"].length
- ) {
+ if (afterMutations["pos:order"]?.["synced"]?.length) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 197-199: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`348-348`: **Simplify null check with optional chaining**

This null check could be simplified using optional chaining for better readability.


```diff
- if ((doneOrder.deliveryInfo || {}).dealId) {
+ if (doneOrder.deliveryInfo?.dealId) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 348-348: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`759-759`: **Simplify null check with optional chaining**

This null check could be simplified using optional chaining for better readability.


```diff
- if (response && response._id) {
+ if (response?._id) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 759-759: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</blockquote></details>
<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (4)</summary><blockquote>

`30-41`: **Improve null-safety**  
Consider using optional chaining for `doc?.No` and `doc?.code` if `doc` can be undefined. Likewise, `product?.scopeBrandIds` can help avoid referencing an undefined property.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 40-40: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`153-190`: **Handle possible null values in `doc`**  
Be cautious when accessing `doc.code` or `doc.Code`. Verify that `doc` is always defined and consider optional chaining.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 163-163: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`326-508`: **Enhance concurrency handling**  
Sequential fetch calls in the loop can cause performance bottlenecks. Consider using concurrency (e.g., Promise.all) to optimize multiple requests.

---

`626-799`: **Consider concurrency for item updates**  
Similar to `dealToDynamic`, the loop with multiple fetch requests could be parallelized. Also review error handling: partial failures may need more robust handling.

</blockquote></details>
<details>
<summary>packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx (5)</summary><blockquote>

`74-101`: **Consider error handling for pipeline queries**  
`onChangePipeline` invokes `client.query` without handling potential errors. Add a try/catch or `.catch` chain to prevent unhandled promise rejections.

---

`120-137`: **Validate checkbox usage**  
Ensure the `componentclass="checkbox"` attribute matches the UI library's expectations. Using a capital “C” in “componentClass” may be required if that’s the standard property.

---

`147-147`: **Remove redundant ternary**  
`open={this.props.currentConfigKey === "newDYNAMIC" ? true : false}` can be simplified to `open={this.props.currentConfigKey === "newDYNAMIC"}`.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 147-147: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
 Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with


(lint/complexity/noUselessTernary)

</details>

</details>

---

`183-197`: **Replace redundant fragment**  
Using `|| <></>` may be unnecessary if your UI library supports conditional rendering directly. Simplify by conditionally rendering `<FormGroup>` if `useBoard` is true.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 196-196: Avoid using unnecessary Fragment.

A fragment is redundant if it contains only one child, or if it is the child of a html element, and is not a keyed fragment.

(lint/complexity/noUselessFragments)

</details>

</details>

---

`203-219`: **Ensure naming consistency**  
Keys like `genBusPostingGroup` might benefit from a unified naming scheme (e.g. `generalBusinessPostingGroup`) if it aligns with your team's style.

</blockquote></details>
<details>
<summary>packages/plugin-msdynamic-api/src/afterMutations.ts (2)</summary><blockquote>

`26-44`: **Check usage of default config**  
You provide an empty object `{}` as the default value for `"DYNAMIC"`. Consider reinforcing it with more robust fallback logic if config is missing.

---

`120-124`: **Potential undefined syncLog**  
If an error is thrown before `syncLog` is assigned, `syncLog._id` may be undefined. Consider defensive checks or localizing the error handling.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f8f8a306aaf83d4d71035e5bf137a56acf411bab and 6e5f4d3c9f4b5237a8e4408934974cbb0833927d.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `packages/plugin-msdynamic-api/src/afterMutations.ts` (4 hunks)
* `packages/plugin-msdynamic-api/src/utils.ts` (29 hunks)
* `packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx` (7 hunks)
* `packages/plugin-msdynamic-ui/src/constants.ts` (1 hunks)
* `packages/plugin-pos-api/src/models/definitions/orders.ts` (3 hunks)
* `packages/plugin-pos-api/src/utils.ts` (38 hunks)
* `packages/plugin-sales-api/src/models/definitions/boards.ts` (4 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (5)</summary>

<details>
<summary>packages/plugin-sales-api/src/models/definitions/boards.ts (2)</summary><blockquote>

<details>
<summary>packages/plugin-tasks-api/src/models/definitions/constants.ts (7)</summary>

* `BOARD_TYPES` (64-68)
* `TIME_TRACK_TYPES` (56-62)
* `BOARD_STATUSES` (45-49)
* `BOARD_STATUSES_OPTIONS` (51-54)
* `VISIBLITIES` (1-5)
* `HACK_SCORING_TYPES` (7-12)
* `PROBABILITY` (14-43)

</details>
<details>
<summary>packages/api-utils/src/types.ts (1)</summary>

* `customFieldSchema` (103-123)

</details>

</blockquote></details>
<details>
<summary>packages/plugin-pos-api/src/models/definitions/orders.ts (1)</summary><blockquote>

<details>
<summary>packages/plugin-xyp-api/src/utils.ts (1)</summary>

* `schemaHooksWrapper` (34-36)

</details>

</blockquote></details>
<details>
<summary>packages/plugin-pos-api/src/utils.ts (5)</summary><blockquote>

<details>
<summary>packages/plugin-ebarimt-api/src/utils.ts (1)</summary>

* `getConfig` (13-20)

</details>
<details>
<summary>packages/plugin-loyalties-api/src/messageBroker.ts (1)</summary>

* `sendCoreMessage` (125-132)

</details>
<details>
<summary>packages/plugin-ebarimt-api/src/messageBroker.ts (2)</summary>

* `sendCoreMessage` (198-205)
* `sendSalesMessage` (189-196)

</details>
<details>
<summary>packages/plugin-loyalties-api/src/utils.ts (1)</summary>

* `getChildCategories` (10-21)

</details>
<details>
<summary>packages/core/src/messageBroker.ts (1)</summary>

* `sendSalesMessage` (1476-1483)

</details>

</blockquote></details>
<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (3)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/logUtils.ts (3)</summary>

* `putUpdateLog` (35-46)
* `putCreateLog` (22-33)
* `putDeleteLog` (48-59)

</details>
<details>
<summary>packages/core/src/connectionResolver.ts (1)</summary>

* `IModels` (155-196)

</details>
<details>
<summary>packages/plugin-msdynamic-api/src/utilsCustomer.ts (1)</summary>

* `getMsdCustomerInfo` (217-315)

</details>

</blockquote></details>
<details>
<summary>packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx (3)</summary><blockquote>

<details>
<summary>packages/api-utils/src/elasticsearch.ts (1)</summary>

* `client` (167-169)

</details>
<details>
<summary>packages/plugin-msdynamic-ui/src/constants.ts (1)</summary>

* `KEY_LABELS` (10-38)

</details>
<details>
<summary>packages/erxes-ui/src/styles/main.ts (1)</summary>

* `FormColumn` (805-805)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/plugin-pos-api/src/utils.ts</summary>

[error] 197-199: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

---

[error] 348-348: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

---

[error] 759-759: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>
<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts</summary>

[error] 40-40: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

---

[error] 163-163: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>
<details>
<summary>packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx</summary>

[error] 147-147: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
 Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with


(lint/complexity/noUselessTernary)

---

[error] 196-196: Avoid using unnecessary Fragment.

A fragment is redundant if it contains only one child, or if it is the child of a html element, and is not a keyed fragment.

(lint/complexity/noUselessFragments)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: runtest
* GitHub Check: ui
* GitHub Check: Analyze (go)
* GitHub Check: Analyze (python)
* GitHub Check: Analyze (javascript-typescript)

</details>

<details>
<summary>🔇 Additional comments (23)</summary><blockquote>

<details>
<summary>packages/plugin-sales-api/src/models/definitions/boards.ts (3)</summary>

`1-2`: **Consistent string delimiter update**

The change from single quotes to double quotes for string literals in imports and other parts of the code aligns with the standardization effort across the codebase.



Also applies to: 10-12

---

`147-147`: **Good addition of the duration field for attachments**

The duration field is a useful addition to the attachmentSchema, properly typed as a Number and marked as optional for backward compatibility.

---

`285-289`: **Good addition of the scopeBrandIds field**

This new field enables linking items with specific brands. It's properly typed as an array of strings and marked as optional for backward compatibility.

</details>
<details>
<summary>packages/plugin-msdynamic-ui/src/constants.ts (3)</summary>

`2-7`: **Consistent string delimiter update**

The change from single quotes to double quotes for string literals in the menuDynamic array aligns with the standardization effort across the codebase.

---

`11-36`: **Consistent string delimiter update**

The change from single quotes to double quotes for string literals in the KEY_LABELS object values aligns with the standardization effort across the codebase.

---

`37-37`: **Good addition of useBoard configuration option**

Adding the "useBoard" key to the KEY_LABELS object supports deal board functionality and maintains consistency with the existing pattern of keys and values in the object.

</details>
<details>
<summary>packages/plugin-pos-api/src/models/definitions/orders.ts (1)</summary>

`1-4`: **Consistent string delimiter update**

The change from single quotes to double quotes for string literals in imports aligns with the standardization effort across the codebase.

</details>
<details>
<summary>packages/plugin-pos-api/src/utils.ts (2)</summary>

`1-20`: **Consistent string delimiter update**

The change from single quotes to double quotes for string literals in imports and other parts of the code aligns with the standardization effort across the codebase.

---

`547-547`: **Properly utilizing the new scopeBrandIds field**

The scopeBrandIds field is correctly passed to the sales service when creating a deal. This confirms the field should be persisted in the database, reinforcing the need to add it to the posOrderSchema.

</details>
<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (5)</summary>

`1-6`: **Imports look good**  
These imports, including `"node-fetch"`, appear consistent with a server-side environment. No issues identified.

---

`14-21`: **Add fallback for missing config**  
When returning the config object, consider verifying that its contents are defined. A non-existent `config` may cause errors in subsequent calls.

---

`107-142`: **Ensure brand removal logic meets business requirements**  
Currently, if no other brand IDs remain, the product is fully deleted. Confirm if there’s any use case for archiving products rather than removing them entirely.

---

`800-846`: **Check for off-by-one or empty arrays**  
Ensure that `prods` is not empty before you reference `prods[0]`. Otherwise, an undefined access could occur.

---

`848-899`: **Review redundant date checks**  
Verify that the date filters (e.g., `p.Starting_Date`, `p.Ending_Date`) correctly handle boundary conditions. Potential off-by-one errors might exclude valid products.

</details>
<details>
<summary>packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx (3)</summary>

`8-20`: **Import organization**  
These imports look properly grouped and relevant to this section of the UI code.

---

`166-173`: **LGTM**  
Declaring inputs for itemApi, priceApi, and similar config fields seems straightforward. No immediate issues found.

---

`240-248`: **Finalize save logic**  
Consider adding error handling or success messages after the `save` callback. This helps users understand whether their changes were successfully applied.

</details>
<details>
<summary>packages/plugin-msdynamic-api/src/afterMutations.ts (6)</summary>

`1-3`: **Imports**  
These bring in the new `orderToDynamic` function and maintain consistency with existing modules.

---

`6-10`: **Added 'sales:deal' type**  
Extending `allowTypes` with `"sales:deal": ["update"]` lets you handle updates to deals. Confirm whether other actions (create, remove) might also require hooks.

---

`49-61`: **No issues found**  
Using `syncLogsAdd` for `"core:customer"` creation flows aligns with existing patterns in the codebase.

---

`63-75`: **Company creation**  
Similar approach to customers, which ensures consistent logic. No problems noted.

---

`77-100`: **Feature gating on stage**  
`dealToDynamic` is commented out but clamped by `config.stageId === updatedDoc?.stageId`. Confirm if you'll restore or remove this commented logic.

---

`101-118`: **Pos order synergy**  
`orderToDynamic` executes only if `action === "synced" && !config.useBoard`. This seems intentional, but ensure the business logic is thoroughly tested.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-13T11:57:02Z
user: login: ellipsis-dev[bot]
body: :x: Changes requested. Incremental review on e10ab58359ea5aaf7f6ddc333d77d516a3dc2f85 in <ellipsistime>2 minutes and 0 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `225` lines of code in `2` files
 - Skipped `0` files when reviewing.
 - Skipped posting `14` drafted comments based on config settings.


<details>
<summary>1. <code>packages/plugin-msdynamic-api/src/afterMutations.ts:77</code></summary>

- **Draft comment:** 
Good cleanup of commented legacy syncLog calls. Using 'updatedDoc' uniformly improves readability.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>packages/plugin-msdynamic-api/src/utils.ts:392</code></summary>

- **Draft comment:** 
When product is undefined, accessing product._id in the error message may throw an exception. Consider using item.productId instead.
- **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>3. <code>packages/plugin-msdynamic-api/src/utils.ts:345</code></summary>

- **Draft comment:** 
Switching from using 'order.items' to 'order.productsData' is likely intentional; ensure the data model is consistent across different order types (deal vs order).
- **Reason this comment was not posted:** 
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 0% vs. threshold = 50%
The comment is asking the author to verify their intention, which violates our rules. It's not pointing out a clear issue that needs to be fixed. The existence of both `items` and `productsData` in different functions suggests this is an intentional difference to handle different types of orders. Without more context about the broader system design, we can't say this is actually a problem.
Maybe there really is an inconsistency that should be fixed? The parallel functions using different data models could be a source of bugs.
Even if there might be an inconsistency, the comment doesn't provide clear evidence that this is a problem. It's just asking for verification, which our rules explicitly say not to do.
Delete the comment. It's asking for verification rather than pointing out a clear issue, and we don't have enough context to know if the different data models are actually problematic.

</details>

<details>
<summary>4. <code>packages/plugin-msdynamic-api/src/afterMutations.ts:83</code></summary>

- **Draft comment:** 
Removed commented-out syncLog and dealToDynamic calls. Confirm that creating syncLog only on stageId match is intended and that no essential logic was lost.
- **Reason this comment was not posted:** 
Comment looked like it was already resolved.

</details>

<details>
<summary>5. <code>packages/plugin-msdynamic-api/src/afterMutations.ts:100</code></summary>

- **Draft comment:** 
Using the 'updatedDoc' variable consistently in the pos:order branch improves readability compared to repeating the expression. This looks good.
- **Reason this comment was not posted:** 
Confidence changes required: `30%` <= threshold `50%`
None

</details>

<details>
<summary>6. <code>packages/plugin-msdynamic-api/src/utils.ts:345</code></summary>

- **Draft comment:** 
The dealToDynamic function now checks for items using 'order.productsData' instead of 'order.items'. Ensure that the upstream data structure for deal sync indeed uses 'productsData'.
- **Reason this comment was not posted:** 
Comment did not seem useful. Confidence is useful = `0%` <= threshold `50%`
The comment is asking the PR author to ensure that the upstream data structure uses `productsData`, which violates the rule against asking the author to ensure behavior is intended. It does not provide a specific code suggestion or ask for a test to be written.

</details>

<details>
<summary>7. <code>packages/plugin-msdynamic-api/src/utils.ts:307</code></summary>

- **Draft comment:** 
External_Document_No now appends a hardcoded suffix 'TESTBYERXES'. Consider making this value configurable if it’s intended for production.
- **Reason this comment was not posted:** 
Marked as duplicate.

</details>

<details>
<summary>8. <code>packages/plugin-msdynamic-api/src/utils.ts:392</code></summary>

- **Draft comment:** 
When a product is not found, the error message references 'product._id', but 'product' will be undefined. Use the item's productId instead to avoid undefined references.
- **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>9. <code>packages/plugin-msdynamic-api/src/utils.ts:670</code></summary>

- **Draft comment:** 
Similarly in orderToDynamic, when a product is missing, referencing 'product._id' can lead to undefined values. Consider logging the missing product using item.productId instead.
- **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>10. <code>packages/plugin-msdynamic-api/src/utils.ts:349</code></summary>

- **Draft comment:** 
Consider checking the fetch response status (e.g., response.ok) before parsing the response as JSON to properly handle HTTP errors.
- **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>11. <code>packages/plugin-msdynamic-api/src/utils.ts:399</code></summary>

- **Draft comment:** 
The code now uses 'item.quantity' instead of 'item.count' for order items. Ensure that this change aligns with the upstream data structure and that all order items reliably provide a 'quantity'.
- **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>12. <code>packages/plugin-msdynamic-api/src/utils.ts:425</code></summary>

- **Draft comment:** 
Multiple calls to models.SyncLogs.updateOne inside loops could impact performance. Consider batching updates if many items are processed.
- **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>13. <code>packages/plugin-msdynamic-api/src/utils.ts:345</code></summary>

- **Draft comment:** 
Typo: The error message 'Has not items order' is grammatically awkward. Consider rephrasing it to something like 'No items in order' for better clarity.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>14. <code>packages/plugin-msdynamic-api/src/utils.ts:392</code></summary>

- **Draft comment:** 
Typo: In the error update message, `not found product ${product._id}` could cause issues if 'product' is undefined. Consider using `item.productId` or rephrasing the message (e.g., 'Product not found: ' + item.productId) to avoid referencing an undefined property.
- **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>


Workflow ID: <workflowid>`wflow_NPuqU6V20XNdXGYf`</workflowid>

</details>


----
**Want Ellipsis to fix these issues?** Tag `@ellipsis-dev` in a comment. You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T05:50:50Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🔭 Outside diff range comments (3)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (3)</summary><blockquote>

`216-248`: _🛠️ Refactor suggestion_

**Function dealToDynamic should include validation for required parameters.**

The function accepts parameters without validating them before usage. While config validation happens, there's no validation for the order properties or other parameters.


```diff
export const dealToDynamic = async (
  subdomain: string,
  syncLog: ISyncLogDocument,
  params: any,
  models: IModels,
  config: any
) => {
  const order = params;
+ if (!order || !order.scopeBrandIds || !order.scopeBrandIds.length) {
+   throw new Error("Invalid deal data: Missing required properties");
+ }
  const brandId = order.scopeBrandIds[0];

  let msdCustomer: any = {};

  let orderMsdNo: string = "";
  let orderItemsMsdNo: any = {};
  try {
    const syncErkhetInfo = JSON.parse(order.syncErkhetInfo);
    orderMsdNo = syncErkhetInfo.no;
    orderItemsMsdNo = syncErkhetInfo.lineNos || {};
  } catch {
    orderMsdNo = order.syncErkhetInfo;
  }

  try {
    let customer;
    if (
      !config?.customerApi ||
      !config?.salesApi ||
      !config?.salesLineApi ||
      !config?.username ||
      !config?.password
    ) {
      throw new Error(`MS Dynamic config not found..., ${brandId}`);
    }
```

---

`528-573`: _🛠️ Refactor suggestion_

**Function orderToDynamic should follow the same validation pattern as dealToDynamic.**

For consistency, the orderToDynamic function should validate input parameters in the same way as dealToDynamic.


```diff
export const orderToDynamic = async (
  subdomain: string,
  syncLog: ISyncLogDocument,
  params: any,
  models: IModels,
  config: any
) => {
  const order = params;
+ if (!order || !order.scopeBrandIds || !order.scopeBrandIds.length) {
+   throw new Error("Invalid order data: Missing required properties");
+ }
  const brandId = order.scopeBrandIds[0];

  let msdCustomer: any = {};

  let orderMsdNo: string = "";
  let orderItemsMsdNo: any = {};
  try {
    const syncErkhetInfo = JSON.parse(order.syncErkhetInfo);
    orderMsdNo = syncErkhetInfo.no;
    orderItemsMsdNo = syncErkhetInfo.lineNos || {};
  } catch {
    orderMsdNo = order.syncErkhetInfo;
  }

  try {
    let customer;

    if (
      !config?.customerApi ||
      !config?.salesApi ||
      !config?.salesLineApi ||
      !config?.username ||
      !config?.password
    ) {
      throw new Error(`MS Dynamic config not found..., ${brandId}`);
    }
```

---

`671-677`: _⚠️ Potential issue_

**Fix undefined product reference in error log.**

Similar to the issue in dealToDynamic, the error message references product._id when product doesn't exist.


```diff
        if (!product) {
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
-                error: `not found product ${product._id}`,
+                error: `not found product for ID: ${item.productId}`,
              },
            }
          );
```

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (3)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (3)</summary><blockquote>

`415-456`: **Simplify ternary expression for error handling.**

The ternary operator can be simplified using the logical OR operator for better readability.



```diff
          const foundSyncLog = (await models.SyncLogs.findOne({
            _id: syncLog._id,
          })) || { error: "" };
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
-                error: `${foundSyncLog.error ? foundSyncLog.error : ""} - ${
+                error: `${foundSyncLog.error || ""} - ${
                  responseSaleLine.error.message
                }`,
              },
            }
          );
```

---

`631-631`: **Fix grammatically incorrect error message.**

The error message "Has not items order" is grammatically incorrect and should be reworded for clarity.



```diff
    if (!order.items.length) {
-      throw new Error("Has not items order");
+      throw new Error("Order has no items");
    }
```

---

`726-728`: **Simplify ternary expression for error handling.**

The ternary operator can be simplified using the logical OR operator for better readability.



```diff
                error: `${foundSyncLog.error ? foundSyncLog.error : ""} - ${
                  responseSaleLine.error.message
                }`,
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (2)</summary><blockquote>

`326-347`: **Improve error message for missing order items.**

The error message "Has not items order" is grammatically incorrect and could be clearer.


```diff
    if (orderMsdNo) {
      urlParam = `(Document_Type='Order', No='${orderMsdNo}')`;
      lineUrlParam = `(Document_Type='Order',Document_No='${orderMsdNo}',Line_No=%Ln)`;
      postMethod = "PATCH";
      postHeaders["If-Match"] = "*";
      sendData.No = orderMsdNo;
    }

    await models.SyncLogs.updateOne(
      { _id: syncLog._id },
      {
        $set: {
          sendData,
          sendStr: JSON.stringify(sendData),
        },
      }
    );

    if (!order.productsData.length) {
-      throw new Error("Has not items order");
+      throw new Error("Order has no items");
    }
```

---

`40-40`: **Consider using optional chaining for safer property access.**

The code can benefit from using optional chaining for safer property access in several places.


```diff
- const brandIds = (product || {}).scopeBrandIds || [];
+ const brandIds = product?.scopeBrandIds || [];
```

```diff
- const brandIds = (productCategory || {}).scopeBrandIds || [];
+ const brandIds = productCategory?.scopeBrandIds || [];
```

```diff
- Salesperson_Code: user && user.employeeId,
+ Salesperson_Code: user?.employeeId,
```


Also applies to: 163-163, 319-319

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 40-40: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6e5f4d3c9f4b5237a8e4408934974cbb0833927d and e10ab58359ea5aaf7f6ddc333d77d516a3dc2f85.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/plugin-msdynamic-api/src/afterMutations.ts` (4 hunks)
* `packages/plugin-msdynamic-api/src/utils.ts` (29 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/plugin-msdynamic-api/src/afterMutations.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (3)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/logUtils.ts (3)</summary>

* `putUpdateLog` (35-46)
* `putCreateLog` (22-33)
* `putDeleteLog` (48-59)

</details>
<details>
<summary>packages/plugin-msdynamic-api/src/utilsCustomer.ts (1)</summary>

* `getMsdCustomerInfo` (217-315)

</details>
<details>
<summary>packages/core/src/connectionResolver.ts (1)</summary>

* `IModels` (155-196)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts</summary>

[error] 40-40: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

---

[error] 163-163: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

---

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: Analyze (go)

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (12)</summary>

`1-3`: **LGTM: String literal formatting changes for consistency.**

These changes standardize the codebase by replacing single quotes with double quotes for string literals, which aligns with the PR objectives of improving consistency.



Also applies to: 17-17

---

`30-31`: **LGTM: Consistent string formatting in consumeInventory function.**

These changes maintain the functionality while updating string literals to use double quotes consistently across the codebase.



Also applies to: 42-43, 50-50, 55-57, 60-61, 63-63, 77-78, 81-81, 107-107, 141-141

---

`153-154`: **LGTM: Consistent string formatting in consumeCategory function.**

These changes maintain the functionality while updating string literals to use double quotes consistently across the codebase.



Also applies to: 165-166, 172-172, 175-175, 181-182, 194-195, 201-202, 206-206, 209-210

---

`249-259`: **LGTM: Variable declarations for API requests.**

This section correctly initializes variables and headers needed for API requests to Microsoft Dynamics.

---

`348-370`: **LGTM: API call and product data retrieval.**

This section correctly handles the API call to create/update a sales order and retrieves product information for line items.

---

`371-386`: **LGTM: Line item preparation.**

The code properly initializes variables for tracking discounts and prepares for processing line items.

---

`398-414`: **LGTM: Sales line preparation for API request.**

This section correctly prepares the data for the sales line API request, handling both new lines and updates to existing lines.

---

`457-487`: **LGTM: Discount handling via SOAP API.**

This section correctly handles applying discounts through the SOAP API when applicable.

---

`488-526`: **LGTM: Order update and response handling.**

The code properly updates the order record with synchronization information and handles the response data.

---

`824-825`: **LGTM: Date comparison improvements.**

The code now correctly handles date comparisons in the filtering logic.

---

`875-876`: **LGTM: Date comparison improvements.**

The code now correctly handles date comparisons in the filtering logic.



Also applies to: 883-884

---

`921-922`: **LGTM: Error message and formatting improvements.**

These changes improve the clarity of error messages and standardize string formatting.



Also applies to: 932-933, 942-943, 952-953, 967-968

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-14T05:52:31Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>packages/plugin-pos-api/src/utils.ts (1)</summary><blockquote>

`200-230`: _🛠️ Refactor suggestion_

**Enable integration with MS Dynamics deal synchronization.**

The `otherPlugins` function handles afterMutations for `pos:order` events, but it could be enhanced to specifically support the MS Dynamics integration for deals mentioned in the PR objectives.

Consider modifying the function to properly integrate with MS Dynamics deal synchronization by ensuring that when orders are converted to deals, the corresponding deals are also synced to MS Dynamics according to the new board/pipeline/stage configuration.

This would provide the complete integration path from POS orders to MS Dynamics via the deal creation process.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 205-207: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (8)</summary><blockquote>

<details>
<summary>packages/plugin-inbox-ui/src/inbox/components/conversationDetail/workarea/Post.tsx (2)</summary><blockquote>

`58-58`: **Consider exposing `MAX_LENGTH` as a configurable prop.**

While `MAX_LENGTH = 50` fulfills a straightforward truncation requirement, consider making it a prop or extracting it into a configuration. This approach allows reusability and easier maintenance if future changes require different truncation limits.

---

`67-69`: **Robust truncation logic.**

The truncation logic is clear and straightforward. However, consider providing unit tests or snapshot tests to verify different edge cases (e.g., content length exactly equal to the limit, empty content, etc.).

</blockquote></details>
<details>
<summary>packages/plugin-facebook-api/src/store.ts (2)</summary><blockquote>

`279-303`: **Consider updating more fields if the post changes.**

Currently, if the fetched post has changed attributes (e.g., new attachments, updated timestamps) apart from `content`, these are not updated in the existing record. Evaluating whether to sync additional fields would ensure data consistency.

---

`483-518`: **Helpful abstraction for fetching post details.**

This new function encapsulates the retrieval of Facebook post data effectively. The error logging and rethrow approach is appropriate. Validate whether any additional fields (e.g., attachments, media links) need to be synced, similar to other post creation flows, to maintain consistency.

</blockquote></details>
<details>
<summary>packages/plugin-pos-api/src/utils.ts (4)</summary><blockquote>

`205-207`: **Consider using optional chaining for nested object access.**

The nested conditional checks can be simplified with optional chaining for better readability.

```diff
-  if (
-    afterMutations["pos:order"] &&
-    afterMutations["pos:order"]["synced"] &&
-    afterMutations["pos:order"]["synced"].length
-  ) {
+  if (afterMutations["pos:order"]?.["synced"]?.length) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 205-207: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`356-366`: **Consider using optional chaining for property access.**

The conditional check for the dealId property can be simplified with optional chaining.

```diff
-  if ((doneOrder.deliveryInfo || {}).dealId) {
+  if (doneOrder.deliveryInfo?.dealId) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 356-356: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`767-774`: **Consider using optional chaining for conditional response handling.**

The nested conditional checks for response properties can be simplified with optional chaining.

```diff
-      if (response && response._id) {
+      if (response?._id) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 767-767: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`490-596`: **Consider updating deal creation to support MS Dynamics synchronization.**

The `createDealPerOrder` function creates deals but doesn't appear to have integration with the new MS Dynamics functionality mentioned in the PR objectives. Consider adding a hook or notification to trigger MS Dynamics synchronization when deals are created.

```diff
  await sendSalesMessage({
    subdomain,
    action: "salesPipelinesChanged",
    data: {
      pipelineId: currentCardsConfig.pipelineId,
      action: "itemAdd",
      data: {
        item: cardDeal,
        destinationStageId: currentCardsConfig.stageId,
      },
    },
  });

+ // Trigger MS Dynamics sync for the newly created deal
+ await otherPlugins(subdomain, { _id: cardDeal._id, type: "deal" }, null, newOrder.userId);
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 508-508: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e10ab58359ea5aaf7f6ddc333d77d516a3dc2f85 and 382c46ff54ccafda922a254ee9d7bb7db2ef6d74.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `packages/plugin-facebook-api/src/store.ts` (2 hunks)
* `packages/plugin-inbox-ui/src/inbox/components/conversationDetail/workarea/Post.tsx` (1 hunks)
* `packages/plugin-pos-api/src/models/definitions/orders.ts` (3 hunks)
* `packages/plugin-pos-api/src/utils.ts` (39 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/plugin-pos-api/src/models/definitions/orders.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>packages/plugin-pos-api/src/utils.ts (6)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (1)</summary>

* `getConfig` (14-21)

</details>
<details>
<summary>packages/plugin-ebarimt-api/src/utils.ts (1)</summary>

* `getConfig` (13-20)

</details>
<details>
<summary>packages/plugin-loyalties-api/src/messageBroker.ts (1)</summary>

* `sendCoreMessage` (129-136)

</details>
<details>
<summary>packages/plugin-ebarimt-api/src/messageBroker.ts (2)</summary>

* `sendCoreMessage` (198-205)
* `sendSalesMessage` (189-196)

</details>
<details>
<summary>packages/plugin-loyalties-api/src/utils.ts (1)</summary>

* `getChildCategories` (11-22)

</details>
<details>
<summary>packages/core/src/messageBroker.ts (1)</summary>

* `sendSalesMessage` (1476-1483)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/plugin-pos-api/src/utils.ts</summary>

[error] 205-207: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

---

[error] 356-356: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

---

[error] 767-767: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>packages/plugin-inbox-ui/src/inbox/components/conversationDetail/workarea/Post.tsx (4)</summary>

`52-52`: **Good handling of an optional property.**

Changing `content: string` to `content?: string` correctly accounts for cases where `content` may be missing. This eliminates potential undefined errors and ensures safer property access.

---

`64-64`: **Good approach using defaults in destructuring.**

Using `content = ''` in the destructuring statement safely handles `undefined` values. This prevents runtime errors when rendering and ensures a consistent type for `content`.

---

`73-77`: **Conditional rendering for permalink URL is correct.**

By guarding the link with `{permalink_url && (...)}`, the component avoids rendering a defective `<a>` tag. This approach strengthens the component’s resilience against missing or invalid URLs.

---

`79-88`: **Final content display logic is well-structured.**

The code gracefully handles optional `content` by conditionally presenting the `<NameDisplay>` only when there’s meaningful text. The “See more/Less” toggle is also clear and intuitive.

</details>
<details>
<summary>packages/plugin-facebook-api/src/store.ts (1)</summary>

`307-307`: **No changes needed.**

This line appears to be a spacing or formatting change with no functional impact.

</details>
<details>
<summary>packages/plugin-pos-api/src/utils.ts (3)</summary>

`1-20`: **Consistent string formatting applied.**

The changes to import statements and module exports align with the project's string formatting standardization from single quotes to double quotes.

---

`21-28`: **Consistent string formatting in utility functions.**

The string literal style has been standardized from single quotes to double quotes in the utility functions, improving consistency across the codebase.



Also applies to: 30-41, 43-54

---

`747-765`: <details>
<summary>✅ Verification successful</summary>

**Verify that Microsoft Dynamics sync is integrated with the POS order sync logic.**

This PR adds deal synchronization to MS Dynamics, but there doesn't appear to be explicit integration in the `syncOrderFromClient` function which handles order synchronization across systems.

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for MS Dynamics integration points in afterMutation handlers
# This will help verify if pos:order events are properly handled by MS Dynamics sync

echo "Searching for MS Dynamics integration with POS orders..."
rg -A 5 -B 5 "pos:order" packages/plugin-msdynamic-api/src/
echo "---"
echo "Checking for deal sync handlers in MS Dynamics..."
rg -A 10 "sales:deal" packages/plugin-msdynamic-api/src/
```

Length of output: 6054

---



</details>

**Microsoft Dynamics Sync Integration Verified**

After reviewing the integration points, I confirmed that Microsoft Dynamics sync for POS orders is correctly hooked up via the after-mutation handlers (see, for example, the `"pos:order"` branch in `packages/plugin-msdynamic-api/src/afterMutations.ts`). Although the `syncOrderFromClient` function in `packages/plugin-pos-api/src/utils.ts` does not explicitly call MS Dynamics functions, the POS order events are propagated correctly through the system, triggering the expected Dynamics integrations.

- **Key Integration Points:**
  - `packages/plugin-msdynamic-api/src/afterMutations.ts` — Contains a handler for `"pos:order"` events.
  - `packages/plugin-msdynamic-api/src/graphql/resolvers/mutations/syncDynamic.ts` — Shows the creation and handling of synchronization logs for POS orders.

No further changes are required regarding this integration.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-14T06:20:25Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on 43406eb41f8a4e52672dc0bdd3a19d8fcd95460e in <ellipsistime>1 minute and 46 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `597` lines of code in `2` files
 - Skipped `0` files when reviewing.
 - Skipped posting `11` drafted comments based on config settings.


<details>
<summary>1. <code>packages/plugin-msdynamic-api/src/utils.ts:307</code></summary>

- **Draft comment:** 
Removed test suffix from External_Document_No. Confirm that this change is intentional.
- **Reason this comment was not posted:** 
Comment looked like it was already resolved.

</details>

<details>
<summary>2. <code>packages/plugin-msdynamic-api/src/utilsCustomer.ts:529</code></summary>

- **Draft comment:** 
Calls to companyRequest and customerRequest in consumeCustomers are not awaited. Consider adding await to ensure asynchronous operations complete.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>3. <code>packages/plugin-msdynamic-api/src/utilsCustomer.ts:543</code></summary>

- **Draft comment:** 
Comparison of Partner_Type to a single space (' ') may be error prone. Verify that ' ' is the intended value to check for.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>4. <code>packages/plugin-msdynamic-api/src/utils.ts:307</code></summary>

- **Draft comment:** 
Before splitting order.name on ':', ensure it actually contains a colon to avoid unexpected results.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>5. <code>packages/plugin-msdynamic-api/src/utils.ts:387</code></summary>

- **Draft comment:** 
Accessing product._id when product is undefined may throw an error; consider using item.productId in the error message.
- **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>6. <code>packages/plugin-msdynamic-api/src/utils.ts:345</code></summary>

- **Draft comment:** 
The error message 'Has not items order' is unclear; consider rewording it to something like 'Order contains no items'.
- **Reason this comment was not posted:** 
Confidence changes required: `33%` <= threshold `50%`
None

</details>

<details>
<summary>7. <code>packages/plugin-msdynamic-api/src/utilsCustomer.ts:531</code></summary>

- **Draft comment:** 
Asynchronous calls to companyRequest and customerRequest are not awaited in consumeCustomers; consider adding 'await' to ensure proper execution order.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>8. <code>packages/plugin-msdynamic-api/src/utilsCustomer.ts:543</code></summary>

- **Draft comment:** 
Comparing doc?.Partner_Type to a single space (' ') may be brittle; consider using explicit checks for empty strings or undefined values.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>9. <code>packages/plugin-msdynamic-api/src/utilsCustomer.ts:289</code></summary>

- **Draft comment:** 
The logic in companyRequest and customerRequest is very similar; consider refactoring common functionalities to reduce duplication.
- **Reason this comment was not posted:** 
Confidence changes required: `33%` <= threshold `50%`
None

</details>

<details>
<summary>10. <code>packages/plugin-msdynamic-api/src/utils.ts:345</code></summary>

- **Draft comment:** 
Consider rephrasing the error message 'Has not items order' to something clearer like 'No items found in order'. This will help anyone reading the error know what went wrong.
- **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>11. <code>packages/plugin-msdynamic-api/src/utils.ts:630</code></summary>

- **Draft comment:** 
Consider rephrasing the error message 'Has not items order' to something clearer like 'No items found in order'. This would improve clarity for developers and users inspecting logs.
- **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>


Workflow ID: <workflowid>`wflow_JRkAuvjkTQEocTVD`</workflowid>

</details>


----
You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:53:00Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (7)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (7)</summary><blockquote>

`345-347`: **Improve error message wording.**

The error message is grammatically incorrect and could be clearer.



```diff
-      throw new Error("Has not items order");
+      throw new Error("Order has no items");
```

---

`439-441`: **Simplify ternary expression.**

The ternary can be simplified using the logical OR operator.



```diff
-                error: `${foundSyncLog.error ? foundSyncLog.error : ""} - ${
+                error: `${foundSyncLog.error || ""} - ${
                   responseSaleLine.error.message
                 }`,
```

---

`630-632`: **Improve error message wording in orderToDynamic.**

The error message is grammatically incorrect and could be clearer.



```diff
-      throw new Error("Has not items order");
+      throw new Error("Order has no items");
```

---

`671-682`: _⚠️ Potential issue_

**Fix undefined product reference in error log.**

When logging an error for a missing product, the code references `product._id` which is undefined since the condition is `!product`.



```diff
        if (!product) {
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
-                error: `not found product ${product._id}`,
+                error: `not found product for ID: ${item.productId}`,
              },
            }
          );
          continue;
        }
```

---

`726-728`: **Simplify ternary expression.**

The ternary can be simplified using the logical OR operator.



```diff
-                error: `${foundSyncLog.error ? foundSyncLog.error : ""} - ${
+                error: `${foundSyncLog.error || ""} - ${
                   responseSaleLine.error.message
                 }`,
```

---

`281-285`: _⚠️ Potential issue_

**Add null check for msdCustomerInfo.relation.**

The code may attempt to access properties of potentially undefined objects.



Apply this fix to prevent potential null/undefined errors:

```diff
-      if (msdCustomerInfo) {
-        msdCustomer = msdCustomerInfo.relation?.response;
-        customer = msdCustomerInfo.customer;
-      }
+      if (msdCustomerInfo && msdCustomerInfo.relation) {
+        msdCustomer = msdCustomerInfo.relation.response;
+        customer = msdCustomerInfo.customer;
+      }
```

---

`387-397`: _⚠️ Potential issue_

**Fix undefined product reference in error log.**

When logging an error for a missing product, the code references `product._id` which is undefined since the condition is `!product`.



```diff
        if (!product) {
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
-                error: `not found product ${product._id}`,
+                error: `not found product for ID: ${item.productId}`,
              },
            }
          );
          continue;
        }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (3)</summary><blockquote>

`319-319`: **Use optional chaining for cleaner code.**

The current code could be simplified using optional chaining.


```diff
-      Salesperson_Code: user && user.employeeId,
+      Salesperson_Code: user?.employeeId,
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`459-461`: **Remove commented code.**

There is a commented URL in the code that should be removed or properly documented if it's meant as an example.


```diff
-      const soapApiUrl = config.discountSoapApi; // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
+      const soapApiUrl = config.discountSoapApi;
```

---

`746-747`: **Remove commented code.**

There is a commented URL in the code that should be removed or properly documented if it's meant as an example.


```diff
-      const soapApiUrl = config.discountSoapApi; // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
+      const soapApiUrl = config.discountSoapApi;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 382c46ff54ccafda922a254ee9d7bb7db2ef6d74 and 43406eb41f8a4e52672dc0bdd3a19d8fcd95460e.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/plugin-msdynamic-api/src/utils.ts` (29 hunks)
* `packages/plugin-msdynamic-api/src/utilsCustomer.ts` (20 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* packages/plugin-msdynamic-api/src/utilsCustomer.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts</summary>

[error] 40-40: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

---

[error] 163-163: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

---

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: ui
* GitHub Check: ui
* GitHub Check: runtest
* GitHub Check: Analyze (python)
* GitHub Check: Analyze (go)
* GitHub Check: Analyze (javascript-typescript)

</details>

<details>
<summary>🔇 Additional comments (9)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (9)</summary>

`17-17`: **String literal standardization.**

The action name has been standardized to use double quotes, which is consistent with the rest of the codebase changes.

---

`42-42`: **Improved conditional checks for product modifications.**

The conditional logic for handling product updates, creations, and deletions has been refined with clear conditions and proper brand ID handling.



Also applies to: 50-53

---

`230-236`: **Enhanced parsing of sync information.**

The code now properly handles the extraction of MS Dynamics order number and line numbers from JSON data, with a fallback to using the raw string when parsing fails.

---

`241-248`: **Improved configuration validation.**

The code now validates all required MS Dynamic configuration parameters before proceeding with the sync, preventing potential runtime errors.

---

`528-537`: **New orderToDynamic function mirrors dealToDynamic.**

This new function is appropriately structured to handle POS orders similar to how deals are handled. The core functionality for synchronizing to MS Dynamics is reused, with specific adaptations for the order data structure.

---

`921-922`: **Improve error message for missing configuration.**

The error message has been updated to be more descriptive and consistent with other error messages.

---

`931-933`: **Standardized HTTP headers.**

The headers for API requests have been standardized to use double quotes for consistency.

---

`942-943`: **Improved error message for API response validation.**

The error message now clearly indicates what went wrong with the exchange rate API response.

---

`967-968`: **Enhanced error logging.**

Error logging has been improved to use console.error for better visibility of exchange rate fetch failures.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-15T12:53:47Z
user: login: ellipsis-dev[bot]
body: :x: Changes requested. Incremental review on d5811e52bb3da208232c997c07300318300b6233 in <ellipsistime>1 minute and 45 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `63` lines of code in `2` files
 - Skipped `0` files when reviewing.
 - Skipped posting `5` drafted comments based on config settings.


<details>
<summary>1. <code>packages/plugin-msdynamic-api/src/graphql/resolvers/syncLog.ts:25</code></summary>

- **Draft comment:** 
Guard against undefined 'object' or 'name' before splitting.
- **Reason this comment was not posted:** 
Marked as duplicate.

</details>

<details>
<summary>2. <code>packages/plugin-msdynamic-ui/src/components/SyncHistoryList.tsx:37</code></summary>

- **Draft comment:** 
Ensure added 'sales:deal' case aligns with UI expectations and formatting.
- **Reason this comment was not posted:** 
Confidence changes required: `33%` <= threshold `50%`
None

</details>

<details>
<summary>3. <code>packages/plugin-msdynamic-ui/src/components/SyncHistoryList.tsx:37</code></summary>

- **Draft comment:** 
Verify that UI rendering for 'sales:deal' aligns with expected data structure, as it now shares logic with 'pos:order'.
- **Reason this comment was not posted:** 
Confidence changes required: `33%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>packages/plugin-msdynamic-ui/src/components/SyncHistoryList.tsx:73</code></summary>

- **Draft comment:** 
Typographical error: Consider changing {__("Customer no")} to {__("Customer No")} for consistency in capitalization.
- **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-msdynamic-ui/src/components/SyncHistoryList.tsx:94</code></summary>

- **Draft comment:** 
Typographical error: The id "hurData" in the <tbody> element appears potentially mistyped. Consider using a clearer id such as "salesData" (if that is the intended purpose) to avoid confusion.
- **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>


Workflow ID: <workflowid>`wflow_2nwm1t9rmLftLgPn`</workflowid>

</details>


----
**Want Ellipsis to fix these issues?** Tag `@ellipsis-dev` in a comment. You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T01:27:43Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 43406eb41f8a4e52672dc0bdd3a19d8fcd95460e and de9eef41552e381e218e1948b6e3f7a7a10d0de0.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/plugin-msdynamic-api/src/graphql/resolvers/syncLog.ts` (2 hunks)
* `packages/plugin-msdynamic-ui/src/components/SyncHistoryList.tsx` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/plugin-msdynamic-api/src/graphql/resolvers/syncLog.ts</summary>

[error] 22-22: Unexpected empty object pattern.



(lint/correctness/noEmptyPattern)

---

[error] 22-22: Unexpected empty object pattern.



(lint/correctness/noEmptyPattern)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Analyze (javascript-typescript)

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-ui/src/components/SyncHistoryList.tsx (2)</summary>

`37-40`: **LGTM: Deal type support added to sync history**

The condition has been expanded to include `"sales:deal"` alongside `"pos:order"`, allowing the same UI rendering logic to be applied to both types. This change is aligned with the backend enhancements for Microsoft Dynamics synchronization.

---

`57-58`: **Spacing improved for better readability**

The addition of spaces before template literals improves code readability and maintains consistent formatting.



Also applies to: 61-62

</details>
<details>
<summary>packages/plugin-msdynamic-api/src/graphql/resolvers/syncLog.ts (2)</summary>

`18-19`: **Trailing comma added**

Added a trailing comma after `isRPC: true`, which improves code consistency and makes future additions to this object cleaner.

---

`25-29`: **LGTM: Support for sales:deal content type added**

Added logic to extract and display the deal name from `syncLog.consumeData?.object.name`, with appropriate fallback to `contentId`. This change complements the UI changes in SyncHistoryList.tsx to fully support deal synchronization with MS Dynamics.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-16T01:30:19Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on ec0541d0497c16894d28d4a6728b3025f497a65c in <ellipsistime>2 minutes and 13 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `743` lines of code in `1` files
 - Skipped `0` files when reviewing.
 - Skipped posting `12` drafted comments based on config settings.


<details>
<summary>1. <code>packages/plugin-msdynamic-api/src/utils.ts:392</code></summary>

- **Draft comment:** 
Possible null-reference: when a product is not found, the error message uses 'product._id', which may be undefined. Consider using 'item.productId' or a safe fallback.
- **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-msdynamic-api/src/utils.ts:672</code></summary>

- **Draft comment:** 
Similar potential null-reference: in orderToDynamic, logging 'not found product ${product._id}' may cause an error when product is undefined. Use a safe property (e.g. item.productId) instead.
- **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>3. <code>packages/plugin-msdynamic-api/src/utils.ts:345</code></summary>

- **Draft comment:** 
Grammar suggestion: Consider changing error text 'Has not items order' to 'No items in order' for clarity.
- **Reason this comment was not posted:** 
Confidence changes required: `50%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>packages/plugin-msdynamic-api/src/utils.ts:519</code></summary>

- **Draft comment:** 
Consider using a structured logging solution instead of console.log for error output in production code.
- **Reason this comment was not posted:** 
Confidence changes required: `50%` <= threshold `50%`
None

</details>

<details>
<summary>5. <code>packages/plugin-msdynamic-api/src/utils.ts:240</code></summary>

- **Draft comment:** 
Consider refactoring duplicate code between dealToDynamic and orderToDynamic to improve maintainability.
- **Reason this comment was not posted:** 
Confidence changes required: `33%` <= threshold `50%`
None

</details>

<details>
<summary>6. <code>packages/plugin-msdynamic-api/src/utils.ts:392</code></summary>

- **Draft comment:** 
Avoid accessing '_id' on an undefined 'product'. Use a safe fallback (e.g. item.productId) in the error message.
- **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>7. <code>packages/plugin-msdynamic-api/src/utils.ts:672</code></summary>

- **Draft comment:** 
Avoid accessing '_id' on an undefined 'product'. Use a safe fallback (e.g. item.productId) in the error log.
- **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>8. <code>packages/plugin-msdynamic-api/src/utils.ts:370</code></summary>

- **Draft comment:** 
Consider refactoring the nearly identical logic for processing order items in dealToDynamic and orderToDynamic into a shared helper function to reduce duplication.
- **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>9. <code>packages/plugin-msdynamic-api/src/utils.ts:349</code></summary>

- **Draft comment:** 
Validate fetch responses (e.g. check response.ok) before calling res.json() to handle HTTP errors gracefully.
- **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>10. <code>packages/plugin-msdynamic-api/src/utils.ts:216</code></summary>

- **Draft comment:** 
Avoid using 'any' for function parameters (such as config). Consider defining proper TypeScript interfaces to improve type safety.
- **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>11. <code>packages/plugin-msdynamic-api/src/utils.ts:346</code></summary>

- **Draft comment:** 
Consider rephrasing the error message 'Has not items order' to something more natural like 'No items in order'.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>12. <code>packages/plugin-msdynamic-api/src/utils.ts:392</code></summary>

- **Draft comment:** 
Consider rewording the error message 'not found product ${product._id}' to a clearer version such as 'Product not found'.
- **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>


Workflow ID: <workflowid>`wflow_PpedFAXO8o5emLSt`</workflowid>

</details>


----
You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T01:50:42Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (1)</summary><blockquote>

`671-682`: _⚠️ Potential issue_

**Fix undefined product reference in error log (orderToDynamic)**

Same issue as in the `dealToDynamic` function - referencing `product._id` when product is undefined.


```diff
        if (!product) {
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
-                error: `not found product ${product._id}`,
+                error: `not found product for ID: ${item.productId}`,
              },
            }
          );

          continue;
        }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (8)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (8)</summary><blockquote>

`302-304`: **Simplify ternary expression**

This ternary can be simplified using the logical OR operator.



```diff
-      Sell_to_Customer_No: msdCustomer?.No
-        ? msdCustomer?.No
-        : config.defaultUserCode,
+      Sell_to_Customer_No: msdCustomer?.No || config.defaultUserCode,
```

---

`345-347`: **Improve error message clarity**

The error message could be clearer.



```diff
-      throw new Error('Has not items order');
+      throw new Error('Order has no items');
```

---

`439-442`: **Simplify ternary expression in error handling**

This ternary can be simplified using the logical OR operator.



```diff
-                error: `${foundSyncLog.error ? foundSyncLog.error : ''} - ${
+                error: `${foundSyncLog.error || ''} - ${
                  responseSaleLine.error.message
                }`,
```

---

`726-728`: **Simplify ternary expression in orderToDynamic error handling**

Similar to the previous function, this ternary can be simplified.



```diff
-                error: `${foundSyncLog.error ? foundSyncLog.error : ''} - ${
+                error: `${foundSyncLog.error || ''} - ${
                  responseSaleLine.error.message
                }`,
```

---

`216-526`: _🛠️ Refactor suggestion_

**Validate input parameters**

The `dealToDynamic` function should validate its input parameters before using them.



Check that `params`, including `scopeBrandIds` and other required properties, are defined before using them. Consider adding validation at the beginning of the function:

```typescript
  if (!params || !params.scopeBrandIds || !params.scopeBrandIds.length) {
    throw new Error('Invalid order parameters: missing required fields');
  }
  const order = params;
  const brandId = order.scopeBrandIds[0];
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`260-285`: _🛠️ Refactor suggestion_

**Ensure customer data exists before accessing properties**

The conformity lookup and customer information retrieval looks good, but there's a missing validation step before accessing properties.



```diff
    if (conformities.length > 0) {
      const msdCustomerInfo = await getMsdCustomerInfo(
        subdomain,
        models,
        conformities[0].relTypeId,
        conformities[0].relType,
        brandId,
        config
      );

-      if (msdCustomerInfo) {
+      if (msdCustomerInfo && msdCustomerInfo.relation) {
        msdCustomer = msdCustomerInfo.relation?.response;
        customer = msdCustomerInfo.customer;
      }
    }
```

---

`287-294`: _🛠️ Refactor suggestion_

**Add error handling for missing user information**

The function retrieves user data but doesn't handle the case where user might not be found.



```diff
    const user = await sendCoreMessage({
      subdomain,
      action: 'users.findOne',
      data: {
        _id: order.assignedUserIds[0],
      },
      isRPC: true,
+     defaultValue: null,
    });

+   if (!user && order.assignedUserIds && order.assignedUserIds.length > 0) {
+     console.warn(`User not found for ID: ${order.assignedUserIds[0]}`);
+   }
```

---

`386-397`: _⚠️ Potential issue_

**Fix undefined product reference in error log**

When logging an error for a missing product, the code references `product._id` which is undefined since the condition is `!product`.



```diff
        if (!product) {
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
-                error: `not found product ${product._id}`,
+                error: `not found product for ID: ${item.productId}`,
              },
            }
          );
          continue;
        }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (1)</summary><blockquote>

`319-319`: **Use optional chaining**

Replace the current conditional check with optional chaining for better readability.


```diff
-      Salesperson_Code: user && user.employeeId,
+      Salesperson_Code: user?.employeeId,
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between de9eef41552e381e218e1948b6e3f7a7a10d0de0 and ec0541d0497c16894d28d4a6728b3025f497a65c.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/plugin-msdynamic-api/src/utils.ts` (4 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: runtest
* GitHub Check: ui
* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: Analyze (python)
* GitHub Check: Analyze (go)

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (6)</summary>

`223-236`: **Robust JSON parsing in the new dealToDynamic function**

Good approach using try/catch for parsing the JSON string, with a fallback to using the string directly if parsing fails. This provides graceful handling of potentially malformed data.

---

`241-248`: **Thorough config validation**

Excellent defensive programming by checking all required configuration parameters before proceeding.

---

`307-307`: **Improved External_Document_No formatting**

Good cleanup of the External_Document_No assignment by removing the hardcoded "TESTBYERXES" suffix.

---

`528-534`: **Consistent function implementation for orderToDynamic**

Good implementation of the `orderToDynamic` function following a similar pattern to `dealToDynamic`, making the codebase more maintainable.

---

`746-756`: **Consistent SOAP API header formatting**

Good consistent formatting of Content-Type and SOAPAction headers across functions.

---

`952-953`: **Proper indentation in date comparison**

Fixed indentation in the date comparison expression improves code readability.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-16T01:50:55Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on 343718a722f2491f00a4aab52d563ccdf2d1f57d in <ellipsistime>1 minute and 33 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `606` lines of code in `2` files
 - Skipped `0` files when reviewing.
 - Skipped posting `8` drafted comments based on config settings.


<details>
<summary>1. <code>packages/plugin-msdynamic-api/src/afterMutations.ts:105</code></summary>

- **Draft comment:** 
Ensure syncLog is defined before using it in the catch block. If an error occurs before syncLog is assigned, models.SyncLogs.updateOne may throw.
- **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-msdynamic-api/src/utilsCustomer.ts:529</code></summary>

- **Draft comment:** 
Consider awaiting calls to companyRequest and customerRequest in consumeCustomers to ensure proper async flow, unless parallel execution is desired.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>3. <code>packages/plugin-msdynamic-api/src/utilsCustomer.ts:543</code></summary>

- **Draft comment:** 
Checking Partner_Type against a single space (' ') may be error-prone. Consider trimming or using a more robust check.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>4. <code>packages/plugin-msdynamic-api/src/afterMutations.ts:106</code></summary>

- **Draft comment:** 
In the catch block, ensure syncLog is defined before accessing syncLog._id to avoid runtime errors.
- **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-msdynamic-api/src/utilsCustomer.ts:167</code></summary>

- **Draft comment:** 
checkSend returns the customer object when no MSD record is found, but later code expects a property 'No'. Return a consistent object structure to avoid undefined property errors.
- **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>6. <code>packages/plugin-msdynamic-api/src/utilsCustomer.ts:532</code></summary>

- **Draft comment:** 
The async functions companyRequest and customerRequest are invoked in consumeCustomers without awaiting their promises. Consider awaiting them to ensure proper error handling and execution order.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>7. <code>packages/plugin-msdynamic-api/src/utilsCustomer.ts:568</code></summary>

- **Draft comment:** 
The customers.removeCustomers call is missing the 'isRPC: true' flag that is present in companies.removeCompanies. For consistency and potential RPC requirements, add it.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>8. <code>packages/plugin-msdynamic-api/src/utilsCustomer.ts:84</code></summary>

- **Draft comment:** 
Consider adding error handling for the external fetch call in getSendDataCustomer to manage potential failures from the service.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>


Workflow ID: <workflowid>`wflow_McarHxFIZmkuY0Dn`</workflowid>

</details>


----
You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T01:52:22Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on 8237c59e29fc1b065b26712200990c004df18c73 in <ellipsistime>1 minute and 15 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `1127` lines of code in `3` files
 - Skipped `0` files when reviewing.
 - Skipped posting `9` drafted comments based on config settings.


<details>
<summary>1. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:1</code></summary>

- **Draft comment:** 
All changes in this file are consistent style updates (switching double quotes to single quotes). No functional or logical issues found.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>packages/plugin-pos-api/src/utils.ts:1</code></summary>

- **Draft comment:** 
The modifications are style updates (single quotes in string literals) and minor formatting improvements. Code logic remains unchanged.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>3. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:1</code></summary>

- **Draft comment:** 
Formatting updates for consistency (e.g. single quotes for string literals) are applied throughout. No functional or logical issues found.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>packages/plugin-pos-api/src/utils.ts:516</code></summary>

- **Draft comment:** 
Bug: In createDealPerOrder, the branch for newOrder.mobileAmount incorrectly assigns newOrder.cashAmount to paymentsData.bank. It should use newOrder.mobileAmount.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>5. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:4</code></summary>

- **Draft comment:** 
Typo detected in the import statement at line 4. The directory 'contants' is likely a typo and should be 'constants' to match conventional naming.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>6. <code>packages/plugin-pos-api/src/utils.ts:31</code></summary>

- **Draft comment:** 
The variable 'childs' might be better named 'children' to follow standard English pluralization. Consider renaming it for clarity.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>7. <code>packages/plugin-pos-api/src/utils.ts:44</code></summary>

- **Draft comment:** 
The variable 'childs' in the getChildTags function should be renamed to 'children' for consistency and clarity.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>8. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:66</code></summary>

- **Draft comment:** 
Typo found: Change 'proccessId' to 'processId'.
- **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>9. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:350</code></summary>

- **Draft comment:** 
Typo found: Change 'Users elligible to see all cards' to 'Users eligible to see all cards'.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>


Workflow ID: <workflowid>`wflow_uM7JN23AZmlyNTdM`</workflowid>

</details>


----
You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T07:46:07Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on 66648c6838118bc40e0f5e351c0ba4bd7ad1c1b8 in <ellipsistime>1 minute and 29 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `1142` lines of code in `3` files
 - Skipped `0` files when reviewing.
 - Skipped posting `13` drafted comments based on config settings.


<details>
<summary>1. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:117</code></summary>

- **Draft comment:** 
Stylistic change: Removed trailing comma from the field options. Verify that this aligns with your project's style guidelines.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:129</code></summary>

- **Draft comment:** 
Consistency: Trailing comma removed from bonusCount field configuration. This is purely a formatting update.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>3. <code>packages/plugin-pos-api/src/utils.ts:13</code></summary>

- **Draft comment:** 
Formatting update: Trailing comma removed in object literals passed to sendCoreMessage. Confirm this matches your code style.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>packages/plugin-pos-api/src/utils.ts:61</code></summary>

- **Draft comment:** 
Minor formatting change observed: trailing commas removed from query objects. This update appears to be purely stylistic.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>5. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:146</code></summary>

- **Draft comment:** 
Stylistic update: Removed trailing comma in the attachmentSchema definition. Ensure that this change is consistent with project formatting standards.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>6. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:265</code></summary>

- **Draft comment:** 
Formatting consistency: Trailing commas have been removed in the commonItemFieldsSchema. These changes are purely stylistic.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>7. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:340</code></summary>

- **Draft comment:** 
BUG: The coordinates array in the customFieldsData object is missing the longitude value. It should include both [marker.longitude || marker.lng, marker.latitude || marker.lat] to comply with GeoJSON.
- **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>8. <code>packages/plugin-pos-api/src/utils.ts:261</code></summary>

- **Draft comment:** 
BUG: In updateCustomer, the coordinates array now only includes marker.latitude. The longitude (marker.longitude || marker.lng) is omitted, breaking the expected [longitude, latitude] order.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>9. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:144</code></summary>

- **Draft comment:** 
STYLE: The changes in this file are purely formatting (trailing comma removals) and appear consistent with the team's style guidelines.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>10. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:4</code></summary>

- **Draft comment:** 
The import path '../../contants' appears to have a typographical error. It likely should be '../../constants' if that is the intended folder name.
- **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>11. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:129</code></summary>

- **Draft comment:** 
The label for the 'isTake' field ('order eat but some take') is unclear and may be a typographical error. Consider revising it to better describe its purpose (e.g., 'Dine-in order or take-away').
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>12. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:134</code></summary>

- **Draft comment:** 
The label for the 'isInner' field ('inner or skip ebarimt') is somewhat unclear and may benefit from clarification. If 'ebarimt' is a domain-specific term, consider adding context or adjusting the wording to improve clarity.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>13. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:352</code></summary>

- **Draft comment:** 
Typographical error: The label for excludeCheckUserIds is 'Users elligible to see all cards'. Consider changing it to 'Users eligible to see all cards' for better clarity.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>


Workflow ID: <workflowid>`wflow_iTrFsQjPbUtY4ZUs`</workflowid>

</details>


----
You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T07:52:09Z
user: login: ellipsis-dev[bot]
body: :x: Changes requested. Incremental review on d365d3b002b077f31c303af092b498a606e37052 in <ellipsistime>1 minute and 34 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `47` lines of code in `2` files
 - Skipped `0` files when reviewing.
 - Skipped posting `4` drafted comments based on config settings.


<details>
<summary>1. <code>packages/plugin-msdynamic-api/src/afterMutations.ts:80</code></summary>

- **Draft comment:** 
Redundant check: 'if (foundConfig.stageId === updatedDoc?.stageId)' is unnecessary as 'find' already filters by stageId.
- **Reason this comment was not posted:** 
Marked as duplicate.

</details>

<details>
<summary>2. <code>packages/plugin-msdynamic-api/src/utils.ts:224</code></summary>

- **Draft comment:** 
Potential typo: using 'config.branId' may be a mistake; verify intended property name.
- **Reason this comment was not posted:** 
Marked as duplicate.

</details>

<details>
<summary>3. <code>packages/plugin-msdynamic-api/src/afterMutations.ts:80</code></summary>

- **Draft comment:** 
New logic now selects config using stageId instead of scopeBrandIds. Confirm this change is intended.
- **Reason this comment was not posted:** 
Comment looked like it was already resolved.

</details>

<details>
<summary>4. <code>packages/plugin-msdynamic-api/src/utils.ts:224</code></summary>

- **Draft comment:** 
Typo suspected: 'config.branId' likely should be 'config.brandId' to be consistent with other usages.
- **Reason this comment was not posted:** 
Marked as duplicate.

</details>


Workflow ID: <workflowid>`wflow_8Xf0r6VNZaNY8cK8`</workflowid>

</details>


----
**Want Ellipsis to fix these issues?** Tag `@ellipsis-dev` in a comment. You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T04:47:50Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (3)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (3)</summary><blockquote>

`345-347`: **Improve error message grammar**

The error message's phrasing is unclear and could be improved for better readability.

```diff
-      throw new Error('Has not items order');
+      throw new Error('Order has no items');
```

---

`387-397`: **Fix undefined product reference in error log**

When logging an error for a missing product, the code references `product._id` which is undefined because the condition is `!product`.

```diff
        if (!product) {
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
-                error: `not found product ${product._id}`,
+                error: `not found product for ID: ${item.productId}`,
              },
            }
          );
          continue;
        }
```

---

`281-285`: _🛠️ Refactor suggestion_

**Add null check before accessing customer properties**

Ensure that `msdCustomerInfo.relation` exists before accessing its properties to prevent potential runtime errors.

```diff
-      if (msdCustomerInfo) {
+      if (msdCustomerInfo && msdCustomerInfo.relation) {
         msdCustomer = msdCustomerInfo.relation?.response;
         customer = msdCustomerInfo.customer;
       }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (6)</summary><blockquote>

`319-319`: **Use optional chaining syntax for safer property access**

Use optional chaining to simplify the code and make it more robust when user object might be undefined.

```diff
-  Salesperson_Code: user && user.employeeId,
+  Salesperson_Code: user?.employeeId,
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`439-442`: **Simplify ternary statement in error handling**

The condition can be simplified to use the logical OR operator for cleaner code.

```diff
-                error: `${foundSyncLog.error ? foundSyncLog.error : ''} - ${
+                error: `${foundSyncLog.error || ''} - ${
                  responseSaleLine.error.message
                }`,
```

---

`287-295`: **Add error handling for missing user information**

The function retrieves user data but doesn't handle the case where the user might not be found. Consider adding default value and error handling.

```diff
    const user = await sendCoreMessage({
      subdomain,
      action: 'users.findOne',
      data: {
        _id: order.assignedUserIds[0],
      },
      isRPC: true,
+     defaultValue: null,
    });

+   if (!user && order.assignedUserIds && order.assignedUserIds.length > 0) {
+     console.warn(`User not found for ID: ${order.assignedUserIds[0]}`);
+   }
```

---

`216-526`: **Consider optimizing API calls with parallel execution**

The function makes multiple sequential API calls that could potentially be optimized using Promise.all for parallel execution, especially when processing order items.


For example, the item processing loop could be refactored to use Promise.all:

```javascript
const itemPromises = order.productsData.map(async (item) => {
  // Processing logic for each item
  // ...
  return { lineNo: responseSaleLine.Line_No, discount: item.discountAmount ?? 0 };
});

const itemResults = await Promise.all(itemPromises);
const lineNoById = {};
let totalDiscount = 0;

for (let i = 0; i < order.productsData.length; i++) {
  const item = order.productsData[i];
  const result = itemResults[i];
  if (result) {
    lineNoById[item._id] = result.lineNo;
    totalDiscount += result.discount;
  }
}
```

This could improve performance, especially when dealing with many items.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`238-525`: **Add inline comments for complex logic sections**

The function is quite complex with many steps. Consider adding inline comments to explain the purpose of different sections of the code, which would make it easier to understand and maintain.


For example:
```javascript
// Parse sync info to get order numbers
try {
  const syncErkhetInfo = JSON.parse(order.syncErkhetInfo);
  // ...
}

// Validate required configuration parameters
if (!config?.customerApi || /* ... */) {
  // ...
}

// Retrieve customer information from conformities
const conformities = await sendCoreMessage({
  // ...
});

// Process and send order header to MS Dynamics
const responseSale = await fetch(/* ... */);

// Process each order item and send to MS Dynamics
for (const item of order.productsData) {
  // ...
}

// Apply discount calculations if needed
if (soapApiUrl && totalDiscount) {
  // ...
}

// Update order sync status
await sendPosMessage({
  // ...
});
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`301-325`: **Consider refactoring the sendData construction for better maintainability**

The `sendData` object is quite large and could be structured better by grouping related fields together or using spread operators with smaller, focused objects.


For example:
```javascript
const customerData = {
  Sell_to_Customer_No: msdCustomer?.No || config.defaultUserCode,
  Sell_to_Phone_No: customer?.primaryPhone || '',
  Sell_to_E_Mail: customer?.primaryEmail || '',
  Mobile_Phone_No: customer?.primaryPhone || '',
  CustomerNo:
    customer?.customFieldsDataByFieldCode?.vatCustomer?.value ||
    customer?.customFieldsDataByFieldCode?.vatCompany?.value,
};

const addressData = {
  Sell_to_Address: sellAddress,
  Sell_to_Address_2: sellAddress2,
};

const orderData = {
  External_Document_No: order.name.split(':').pop().trim(),
  Responsibility_Center: config.responsibilityCenter || '',
  Sync_Type: config.syncType || '',
  // ...other order fields
};

const sendData = {
  ...customerData,
  ...addressData,
  ...orderData,
  Salesperson_Code: user?.employeeId,
};
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 66648c6838118bc40e0f5e351c0ba4bd7ad1c1b8 and d365d3b002b077f31c303af092b498a606e37052.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/plugin-msdynamic-api/src/afterMutations.ts` (2 hunks)
* `packages/plugin-msdynamic-api/src/utils.ts` (4 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/plugin-msdynamic-api/src/afterMutations.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: runtest
* GitHub Check: ui
* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: Analyze (go)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-17T04:49:29Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on 565ad09721cf0a9264a94ab869aaeb5db1d3a4f6 in <ellipsistime>1 minute and 20 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `1180` lines of code in `4` files
 - Skipped `0` files when reviewing.
 - Skipped posting `11` drafted comments based on config settings.


<details>
<summary>1. <code>packages/plugin-msdynamic-api/src/afterMutations.ts:83</code></summary>

- **Draft comment:** 
Removed check for 'useBoard' in update action block. Confirm this change in behavior (now calling dealToDynamic for all foundConfig) is intended.
- **Reason this comment was not posted:** 
Comment looked like it was already resolved.

</details>

<details>
<summary>2. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:60</code></summary>

- **Draft comment:** 
Removed optional field 'scopeBrandIds' from IPosOrder. Verify that all dependent code is updated accordingly.
- **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>3. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:115</code></summary>

- **Draft comment:** 
Trailing commas added in schema field definitions for consistency. No logic change.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>packages/plugin-pos-api/src/utils.ts:22</code></summary>

- **Draft comment:** 
Multiple trailing comma and formatting updates; ensure these changes do not affect message parameters.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>5. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:144</code></summary>

- **Draft comment:** 
Trailing comma additions in schema definitions for consistency; no functional changes.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>6. <code>packages/plugin-msdynamic-api/src/afterMutations.ts:83</code></summary>

- **Draft comment:** 
Removed the 'useBoard' check and extra stageId matching in the 'sales:deal' update block. Confirm that syncing should now always occur when a config is found.
- **Reason this comment was not posted:** 
Comment looked like it was already resolved.

</details>

<details>
<summary>7. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:74</code></summary>

- **Draft comment:** 
The 'scopeBrandIds' property was removed from the IPosOrder interface. Please verify that this removal is intentional and that any code referencing this field (e.g. in syncOrderFromClient) is updated accordingly.
- **Reason this comment was not posted:** 
Comment did not seem useful. Confidence is useful = `50%` <= threshold `50%`
The comment is asking the PR author to verify the intention behind the removal of a property from an interface. This violates the rule against asking the author to confirm their intention. However, it also suggests ensuring that any code referencing the removed field is updated, which is a specific and actionable suggestion. The comment is partially useful but needs to be rephrased to focus on the actionable part.

</details>

<details>
<summary>8. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:147</code></summary>

- **Draft comment:** 
Styling improvements (trailing commas and formatting) have been applied consistently. No functional issues noted.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>9. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:4</code></summary>

- **Draft comment:** 
Typo detected: The import path '../../contants' appears to be misspelled. It should likely be '../../constants'.
- **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>10. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:66</code></summary>

- **Draft comment:** 
Typo: The property name 'proccessId' seems to be misspelled. It should likely be 'processId'.
- **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>11. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:347</code></summary>

- **Draft comment:** 
Typo: In the label for 'excludeCheckUserIds', the word 'elligible' is misspelled. It should be 'eligible'.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>


Workflow ID: <workflowid>`wflow_w6ZZbOjBFNWoEu1X`</workflowid>

</details>


----
You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T05:21:56Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on 894f86c221d37205147e8360433b29a368db8986 in <ellipsistime>1 minute and 33 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `641` lines of code in `1` files
 - Skipped `0` files when reviewing.
 - Skipped posting `7` drafted comments based on config settings.


<details>
<summary>1. <code>packages/plugin-pos-api/src/utils.ts:26</code></summary>

- **Draft comment:** 
Styling: Removal of trailing commas and minor formatting updates appear consistent. Ensure your linter/style config is updated accordingly.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>packages/plugin-pos-api/src/utils.ts:153</code></summary>

- **Draft comment:** 
Styling: Consistent removal of extra commas in nested objects (e.g., in confirmLoyalties) improves readability.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>3. <code>packages/plugin-pos-api/src/utils.ts:339</code></summary>

- **Draft comment:** 
Styling: Ensure coordinate arrays retain the correct order (longitude/latitude) after formatting changes.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>packages/plugin-pos-api/src/utils.ts:611</code></summary>

- **Draft comment:** 
Styling: Standardized removal of trailing commas in function calls (e.g., in createDealPerOrder) is clear. No issues detected.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>5. <code>packages/plugin-pos-api/src/utils.ts:748</code></summary>

- **Draft comment:** 
Styling: Updated function arguments in syncErkhetRemainder and syncInventoriesRem for consistency. Verify that these formatting changes don’t affect runtime behavior (they appear safe).
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>6. <code>packages/plugin-pos-api/src/utils.ts:8</code></summary>

- **Draft comment:** 
Stylistic update: Multiple trailing commas have been removed from object literals and arrays. Ensure that this formatting change aligns with the project’s style guidelines.
- **Reason this comment was not posted:** 
Confidence changes required: `33%` <= threshold `50%`
None

</details>

<details>
<summary>7. <code>packages/plugin-pos-api/src/utils.ts:510</code></summary>

- **Draft comment:** 
Possible bug: In the createDealPerOrder function, when newOrder.mobileAmount is truthy, paymentsData.bank.amount is set to newOrder.cashAmount. Likely it should use newOrder.mobileAmount instead.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>


Workflow ID: <workflowid>`wflow_AmGmAqKp5YwYFJuW`</workflowid>

</details>


----
You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T05:29:33Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on 4091894a864d2110d742d538bd32ca299bbc92b6 in <ellipsistime>1 minute and 36 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `308` lines of code in `1` files
 - Skipped `0` files when reviewing.
 - Skipped posting `5` drafted comments based on config settings.


<details>
<summary>1. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:7</code></summary>

- **Draft comment:** 
Style-only changes: Removing trailing commas throughout. Confirm consistency with project style guidelines.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:7</code></summary>

- **Draft comment:** 
Stylistic update: trailing commas have been removed from object literals (e.g. the import for TIME_TRACK_TYPES and schema property definitions). This change is consistent throughout the file—confirm it aligns with the project’s style guide/lint rules.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>3. <code>packages/plugin-sales-api/src/models/definitions/boards.ts:66</code></summary>

- **Draft comment:** 
Typo: 'proccessId' should be corrected to 'processId' for clarity.
- **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-sales-api/src/models/definitions/boards.ts:23</code></summary>

- **Draft comment:** 
Typo in comment: Consider clarifying or correcting 'remove 2row' (e.g., 'remove two rows').
- **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-sales-api/src/models/definitions/boards.ts:347</code></summary>

- **Draft comment:** 
Typo: 'Users elligible to see all cards' should be corrected to 'Users eligible to see all cards'.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>


Workflow ID: <workflowid>`wflow_dxiCdKGAsJrM6EJB`</workflowid>

</details>


----
You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T05:41:20Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on d959785d3710cd609aebc880432cf25ea703bb4a in <ellipsistime>2 minutes and 16 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `186` lines of code in `1` files
 - Skipped `0` files when reviewing.
 - Skipped posting `15` drafted comments based on config settings.


<details>
<summary>1. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:116</code></summary>

- **Draft comment:** 
Minor style change: removed trailing comma in discountAmount field. Ensure this aligns with the project's style guidelines.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:120</code></summary>

- **Draft comment:** 
Minor style change: removed trailing comma in discountPercent field. Confirm consistency with styling standards.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>3. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:132</code></summary>

- **Draft comment:** 
Removed trailing comma from isTake field. This is a minor formatting change; ensure consistency across similar definitions.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:137</code></summary>

- **Draft comment:** 
Removed trailing comma from isInner field. Maintain consistent formatting as per project style.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>5. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:244</code></summary>

- **Draft comment:** 
Trailing comma removed in syncErkhetInfo field. This is a style adjustment; verify consistency with other fields.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>6. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:250</code></summary>

- **Draft comment:** 
Removed trailing comma in deliveryInfo field definition. Confirm alignment with the chosen code style.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>7. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:254</code></summary>

- **Draft comment:** 
Removed trailing comma from description field. Minor formatting update; ensure uniformity across the schema.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>8. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:259</code></summary>

- **Draft comment:** 
Removed trailing comma in isPre field. This change appears stylistic; ensure it matches project conventions.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>9. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:266</code></summary>

- **Draft comment:** 
Removed trailing comma from convertDealId field definition. This is a minor style update.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>10. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:272</code></summary>

- **Draft comment:** 
Removed trailing comma in returnInfo field definition. Formatting change; ensure consistent style.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>11. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:277</code></summary>

- **Draft comment:** 
Removed trailing comma in subscriptionInfo field definition. This is a minor style improvement.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>12. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:283</code></summary>

- **Draft comment:** 
Removed trailing comma in extraInfo field definition. Ensure overall consistency with other schema definitions.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>13. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:114</code></summary>

- **Draft comment:** 
Cosmetic update: trailing commas removed from object literals in schema definitions for consistency. Confirm that this style aligns with your project’s guidelines.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>14. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:4</code></summary>

- **Draft comment:** 
Typo found: The import statement on line 4 uses '../../contants'. It looks like it should be '../../constants'.
- **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>15. <code>packages/plugin-pos-api/src/models/definitions/orders.ts:128</code></summary>

- **Draft comment:** 
Possible typo in label for 'isTake': 'order eat but some take' seems unclear. Please verify if this was intentional or if it needs rephrasing.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>


Workflow ID: <workflowid>`wflow_RzTGyqEDYae2EKyP`</workflowid>

</details>


----
You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T05:43:34Z
user: login: ellipsis-dev[bot]
body: > [!CAUTION]
> **Changes requested  :x:**
> <details>
> <summary>Reviewed 62fc24ba8f3490a9fdaf2cf0822898c0dbe357a7 in <ellipsistime>1 minute and 38 seconds</ellipsistime>. Click for details.</summary> 
> 
> - Reviewed `241` lines of code in `4` files
> - Skipped `0` files when reviewing.
> - Skipped posting `12` 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-msdynamic-api/src/utils.ts:224</code></summary>
> 
> - **Draft comment:** 
> Possible typo: using 'config.branId' instead of 'config.brandId'? Verify the config property name.
> - **Reason this comment was not posted:**
> Comment was on unchanged code.
> 
> 
> </details>
> 
> <details>
> <summary>2. <code>packages/plugin-msdynamic-api/src/utils.ts:386</code></summary>
> 
> - **Draft comment:** 
> Avoid accessing 'product._id' when 'product' is falsy. Consider logging 'item.productId' instead.
> - **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>3. <code>packages/plugin-sales-api/src/models/definitions/deals.ts:86</code></summary>
> 
> - **Draft comment:** 
> New 'extraData' field added. Consider adding explicit validation or schema definitions if specific data structure is expected.
> - **Reason this comment was not posted:**
> Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 20% vs. threshold = 50% The comment suggests adding validation for extraData, which is reasonable since other similar fields (productsData, paymentsData) have specific schemas/interfaces. However, the field is intentionally typed as 'any' in the interface, suggesting it's meant to be flexible. The name "extraData" also implies it's meant to be a catch-all field. Adding validation might defeat its purpose. I might be overthinking this - the field name and type hints that it's intentionally flexible. Also, suggesting validation without knowing the intended use case could lead to unnecessary restrictions. While schema validation can improve code quality, in this case the field appears intentionally flexible, and adding validation without clear requirements could be counterproductive. Delete the comment since the flexible nature of extraData appears intentional, and suggesting validation without knowing the intended use case isn't helpful.
> 
> 
> </details>
> 
> <details>
> <summary>4. <code>packages/plugin-msdynamic-api/src/afterMutations.ts:80</code></summary>
> 
> - **Draft comment:** 
> Consider adding a debug log when the destination stage ID is unchanged (i.e. when destinationStageId === oldDeal.stageId) to aid in tracing why deal sync is skipped.
> - **Reason this comment was not posted:**
> Confidence changes required: `50%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>5. <code>packages/plugin-msdynamic-api/src/afterMutations.ts:88</code></summary>
> 
> - **Draft comment:** 
> Consider logging a warning if no configuration is found for the updated stage (destinationStageId) to improve debuggability.
> - **Reason this comment was not posted:**
> Confidence changes required: `30%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>6. <code>packages/plugin-msdynamic-api/src/utils.ts:386</code></summary>
> 
> - **Draft comment:** 
> Avoid using 'product._id' when 'product' is undefined; use 'item.productId' instead to prevent runtime errors.
> - **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>7. <code>packages/plugin-msdynamic-api/src/utils.ts:627</code></summary>
> 
> - **Draft comment:** 
> Ensure that 'order.items' is defined to prevent runtime errors; consider using optional chaining (e.g., order.items?.length).
> - **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>8. <code>packages/plugin-msdynamic-api/src/utils.ts:668</code></summary>
> 
> - **Draft comment:** 
> Avoid referencing 'product._id' when 'product' is undefined; use 'item.productId' to prevent errors.
> - **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>9. <code>packages/plugin-msdynamic-api/src/utils.ts:224</code></summary>
> 
> - **Draft comment:** 
> Typo: The property 'config.branId' appears to be misspelled. It likely should be 'config.brandId' for consistency with similar usage in the code.
> - **Reason this comment was not posted:**
> Comment was on unchanged code.
> 
> 
> </details>
> 
> <details>
> <summary>10. <code>packages/plugin-msdynamic-api/src/utils.ts:342</code></summary>
> 
> - **Draft comment:** 
> Typo/grammar: The error message 'Has not items order' is awkward. Consider rephrasing it to something like 'No items in order' for clarity.
> - **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>11. <code>packages/plugin-msdynamic-api/src/utils.ts:388</code></summary>
> 
> - **Draft comment:** 
> Typo: The log message 'not found product ${product._id}' is awkward and may be misleading. Consider changing it to 'Product not found: ${product._id}'.
> - **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>12. <code>packages/plugin-sales-api/src/models/definitions/deals.ts:64</code></summary>
> 
> - **Draft comment:** 
> Typographical error: In the definition of 'vatPercent' (line 64), the label is set to 'Tax percent'. It should likely be changed to 'Vat percent' for clarity.
> - **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>
> 
> 
> Workflow ID: <workflowid>`wflow_VlbgARgxZScAdVYm`</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-04-21T08:53:07Z
user: login: ellipsis-dev[bot]
body: > [!CAUTION]
> **Changes requested  :x:**
> <details>
> <summary>Reviewed 786cba008e1a2793c45674a191a200fc3b909a7d in <ellipsistime>2 minutes and 0 seconds</ellipsistime>. Click for details.</summary> 
> 
> - Reviewed `96` lines of code in `1` files
> - Skipped `0` files when reviewing.
> - Skipped posting `10` 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-msdynamic-api/src/utils.ts:260</code></summary>
> 
> - **Draft comment:** 
> Using '$in: deal._id' may cause issues if deal._id is not an array. Consider wrapping it in an array (e.g. $in: [deal._id]).
> - **Reason this comment was not posted:**
> Marked as duplicate.
> 
> 
> </details>
> 
> <details>
> <summary>2. <code>packages/plugin-msdynamic-api/src/utils.ts:292</code></summary>
> 
> - **Draft comment:** 
> Replacing all 'order' references with 'deal' is good for consistency. Ensure that all related logic remains valid with this name change.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>3. <code>packages/plugin-msdynamic-api/src/utils.ts:221</code></summary>
> 
> - **Draft comment:** 
> Good cleanup: redundant alias assignment 'const order = deal' was removed. Directly using 'deal' improves clarity.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>4. <code>packages/plugin-msdynamic-api/src/utils.ts:293</code></summary>
> 
> - **Draft comment:** 
> Ensure that 'deal.description' exists and is a string before calling .replace(), to avoid runtime errors.
> - **Reason this comment was not posted:**
> Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 20% vs. threshold = 50% The code change is about renaming 'order' to 'deal' and adding some null checks for extraData. The comment points out a potential issue with deal.description that could cause runtime errors. However, this seems like a speculative issue - we don't have evidence that deal.description could be undefined or non-string. The function appears to be an internal API that likely has type guarantees from its callers. The comment raises a valid defensive programming point. Runtime type checking can prevent errors in edge cases. However, I may be overconfident in assuming the types are guaranteed by the calling code. While defensive programming is good practice, we don't have clear evidence that this is actually an issue that needs fixing. The comment is speculative and asks for verification rather than pointing out a definite problem. The comment should be deleted because it is speculative and asks for verification without strong evidence of an actual issue. It violates the rule against making speculative comments or asking for verification.
> 
> 
> </details>
> 
> <details>
> <summary>5. <code>packages/plugin-msdynamic-api/src/utils.ts:302</code></summary>
> 
> - **Draft comment:** 
> Verify that 'deal.number' is defined before using it as 'External_Document_No'. A fallback or validation might prevent unexpected undefined values.
> - **Reason this comment was not posted:**
> Confidence changes required: `33%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>6. <code>packages/plugin-msdynamic-api/src/utils.ts:339</code></summary>
> 
> - **Draft comment:** 
> The error message 'Has not items order' could be rephrased for clarity, e.g. 'No items found in deal'.
> - **Reason this comment was not posted:**
> Confidence changes required: `33%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>7. <code>packages/plugin-msdynamic-api/src/utils.ts:493</code></summary>
> 
> - **Draft comment:** 
> When spreading 'extraData' in the update (using '...extraData || {}'), the expression could be clearer if written as '{ ...(extraData || {}) }'.
> - **Reason this comment was not posted:**
> Confidence changes required: `33%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>8. <code>packages/plugin-msdynamic-api/src/utils.ts:388</code></summary>
> 
> - **Draft comment:** 
> Avoid using 'product._id' in the error message when 'product' may be undefined. Consider logging 'item.productId' instead.
> - **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>9. <code>packages/plugin-msdynamic-api/src/utils.ts:223</code></summary>
> 
> - **Draft comment:** 
> Typo alert: The property 'branId' is used here (config.branId). It seems like it should be 'brandId' to match the intended meaning.
> - **Reason this comment was not posted:**
> Comment was on unchanged code.
> 
> 
> </details>
> 
> <details>
> <summary>10. <code>packages/plugin-msdynamic-api/src/utils.ts:341</code></summary>
> 
> - **Draft comment:** 
> The error message 'Has not items order' may be unclear or grammatically incorrect. Consider changing it to something like 'No items in order' for clarity.
> - **Reason this comment was not posted:**
> Comment was on unchanged code.
> 
> 
> </details>
> 
> 
> Workflow ID: <workflowid>`wflow_dmdGsRio9kZnJsSo`</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-04-21T09:08:14Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (8)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (8)</summary><blockquote>

`341-343`: **Improve error message clarity.**

The error message is unclear and contains grammatical issues.



```diff
-      throw new Error('Has not items order');
+      throw new Error('Order has no items');
```

---

`667-678`: _⚠️ Potential issue_

**Fix undefined product reference in error log.**

The error message references `product._id` when product is undefined.



```diff
        if (!product) {
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
-                error: `not found product ${product._id}`,
+                error: `not found product for ID: ${item.productId}`,
              },
            }
          );
          continue;
        }
```

---

`722-724`: **Simplify ternary expression.**

The ternary expression can be simplified using the logical OR operator.



```diff
-                error: `${foundSyncLog.error ? foundSyncLog.error : ''} - ${
+                error: `${foundSyncLog.error || ""} - ${
```

---

`223-223`: _⚠️ Potential issue_

**Fix the typo in property name.**

There's a typo in the variable name: `branId` should be `brandId`.



```diff
-  const brandId = config.branId;
+  const brandId = config.brandId;
```

---

`277-280`: _🛠️ Refactor suggestion_

**Add validation for msdCustomerInfo.relation.**

The code should verify that `msdCustomerInfo.relation` exists before accessing it.



```diff
-      if (msdCustomerInfo) {
+      if (msdCustomerInfo && msdCustomerInfo.relation) {
         msdCustomer = msdCustomerInfo.relation?.response;
         customer = msdCustomerInfo.customer;
       }
```

---

`383-392`: _⚠️ Potential issue_

**Fix undefined product reference in error log.**

When logging an error for a missing product, the code references `product._id` which is undefined since the condition is `!product`.



```diff
        if (!product) {
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
-                error: `not found product ${product._id}`,
+                error: `not found product for ID: ${item.productId}`,
              },
            }
          );
          continue;
        }
```

---

`287-287`: _🛠️ Refactor suggestion_

**Use optional chaining when accessing array elements.**

The code should use optional chaining to safely access `deal.assignedUserIds[0]`, which might be undefined if the array is empty.


```diff
-        _id: deal.assignedUserIds[0],
+        _id: deal.assignedUserIds?.[0],
```

---

`261-261`: _🛠️ Refactor suggestion_

**Ensure mainTypeId is properly formatted for $in operator.**

The MongoDB $in operator expects an array. If `deal._id` is a single value, this might cause issues.


```diff
-        mainTypeId: { $in: deal._id },
+        mainTypeId: { $in: Array.isArray(deal._id) ? deal._id : [deal._id] },
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (2)</summary><blockquote>

`315-315`: **Use optional chaining for cleaner code.**

Replace the conditional check with optional chaining for better readability.


```diff
-      Salesperson_Code: user && user.employeeId,
+      Salesperson_Code: user?.employeeId,
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 315-315: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`626-628`: **Use more descriptive error messages.**

The error message could be more descriptive to help with debugging.


```diff
-      throw new Error('Has not items order');
+      throw new Error('Order has no items');
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d959785d3710cd609aebc880432cf25ea703bb4a and 786cba008e1a2793c45674a191a200fc3b909a7d.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `packages/plugin-msdynamic-api/src/afterMutations.ts` (2 hunks)
* `packages/plugin-msdynamic-api/src/messageBroker.ts` (1 hunks)
* `packages/plugin-msdynamic-api/src/utils.ts` (4 hunks)
* `packages/plugin-sales-api/src/models/definitions/deals.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/plugin-msdynamic-api/src/afterMutations.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>packages/plugin-msdynamic-api/src/messageBroker.ts (1)</summary><blockquote>

<details>
<summary>packages/api-utils/src/core.ts (2)</summary>

* `MessageArgsOmitService` (254-258)
* `sendMessage` (260-291)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts</summary>

[error] 315-315: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>packages/plugin-sales-api/src/models/definitions/deals.ts (1)</summary>

`86-86`: **Schema extension looks good.**

The addition of the optional `extraData` field to the dealSchema enables storing additional metadata required for MS Dynamics synchronization.

</details>
<details>
<summary>packages/plugin-msdynamic-api/src/messageBroker.ts (1)</summary>

`33-40`: **Well-structured message broker function.**

The new `sendSalesMessage` function follows the established pattern of other messaging functions in the file, making it consistent and easy to understand.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-21T09:21:41Z
user: login: ellipsis-dev[bot]
body: > [!IMPORTANT]
> **Looks good to me!  :+1:**
> <details>
> <summary>Reviewed 5b5f6a25cd60eaec75f7546eb8686eee549af86f in <ellipsistime>1 minute and 45 seconds</ellipsistime>. Click for details.</summary> 
> 
> - Reviewed `106` lines of code in `2` files
> - Skipped `0` files when reviewing.
> - Skipped posting `14` 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-msdynamic-api/src/graphql/resolvers/mutations/syncDynamic.ts:390</code></summary>
> 
> - **Draft comment:** 
> Replacing dealToDynamic with orderToDynamic and obtaining config from order.scopeBrandIds: ensure that the update reflects the intended mapping (i.e. that order and deal objects are compatible for dynamic sync).
> - **Reason this comment was not posted:**
> Comment did not seem useful. Confidence is useful = `0%` <= threshold `50%` The comment is asking the author to ensure that the update reflects the intended mapping between order and deal objects. This is a request for confirmation of intention, which is not allowed according to the rules. Therefore, this comment should be removed.
> 
> 
> </details>
> 
> <details>
> <summary>2. <code>packages/plugin-msdynamic-api/src/utils.ts:307</code></summary>
> 
> - **Draft comment:** 
> Changed External_Document_No from deal.number to deal.name. Confirm that this is intentional and consistent with data expectations.
> - **Reason this comment was not posted:**
> Comment looked like it was already resolved.
> 
> 
> </details>
> 
> <details>
> <summary>3. <code>packages/plugin-msdynamic-api/src/utils.ts:495</code></summary>
> 
> - **Draft comment:** 
> Updated error message interpolation for responseSaleLine error – check that the new formatting does not obscure critical error info.
> - **Reason this comment was not posted:**
> Confidence changes required: `50%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>4. <code>packages/plugin-msdynamic-api/src/utils.ts:497</code></summary>
> 
> - **Draft comment:** 
> Spread operator for extraData updated to { ...(extraData || {}) } - ensure that this correctly merges extraData into msdynamic property.
> - **Reason this comment was not posted:**
> Confidence changes required: `50%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>5. <code>packages/plugin-msdynamic-api/src/graphql/resolvers/mutations/syncDynamic.ts:4</code></summary>
> 
> - **Draft comment:** 
> Replaced import of dealToDynamic with orderToDynamic. Confirm that orderToDynamic fully handles the intended order sync logic.
> - **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>6. <code>packages/plugin-msdynamic-api/src/graphql/resolvers/mutations/syncDynamic.ts:388</code></summary>
> 
> - **Draft comment:** 
> Switched from dealToDynamic to orderToDynamic with reordered parameters. Verify that extracting the config via order.scopeBrandIds[0] reliably returns a valid config; consider handling missing config cases.
> - **Reason this comment was not posted:**
> Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 20% vs. threshold = 50% The function rename and parameter reordering is just an implementation detail that was intentionally changed. The suggestion about null checking scopeBrandIds is defensive programming, but the current code already handles the undefined case with || 'noBrand'. Without seeing the schema/type of order object or how it's used elsewhere, we can't be certain if null checking is actually needed. I might be underestimating the risk of order.scopeBrandIds being null vs undefined. The current code would throw if scopeBrandIds was null. While true, we don't have enough context about the order object's schema or validation to know if null is a possible value. The comment is speculative without this evidence. The comment should be deleted as it combines implementation observations with speculative suggestions without clear evidence of an actual issue.
> 
> 
> </details>
> 
> <details>
> <summary>7. <code>packages/plugin-msdynamic-api/src/utils.ts:300</code></summary>
> 
> - **Draft comment:** 
> Changed External_Document_No source from deal.number to deal.name. Ensure that deal.name is the intended field for the external document identifier.
> - **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>8. <code>packages/plugin-msdynamic-api/src/utils.ts:432</code></summary>
> 
> - **Draft comment:** 
> Improved error message formatting in the SyncLogs update. The revised template literal is clearer—verify that the added spacing meets your logging format requirements.
> - **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>9. <code>packages/plugin-msdynamic-api/src/utils.ts:490</code></summary>
> 
> - **Draft comment:** 
> Refactored the spread operator for extraData to use ...(extraData || {}). This explicit grouping clarifies intent for object merging.
> - **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>10. <code>packages/plugin-msdynamic-api/src/utils.ts:719</code></summary>
> 
> - **Draft comment:** 
> Consistent error formatting update in orderToDynamic’s error logging. This change improves readability; verify that it doesn’t alter intended error content.
> - **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>11. <code>packages/plugin-msdynamic-api/src/utils.ts:944</code></summary>
> 
> - **Draft comment:** 
> Minor indentation adjustment in the new Date() comparison. This looks cosmetic; ensure it doesn't affect the logic.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>12. <code>packages/plugin-msdynamic-api/src/utils.ts:227</code></summary>
> 
> - **Draft comment:** 
> Typo: it looks like 'config.branId' should be 'config.brandId' to match the intended naming.
> - **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>13. <code>packages/plugin-msdynamic-api/src/utils.ts:345</code></summary>
> 
> - **Draft comment:** 
> The error message 'Has not items order' is awkward. Consider rephrasing it to something clearer like 'No items in order' or 'Order contains no items'.
> - **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>14. <code>packages/plugin-msdynamic-api/src/utils.ts:392</code></summary>
> 
> - **Draft comment:** 
> The error message 'not found product ${product._id}' is awkwardly phrased. It would be clearer to use 'Product not found: ${product._id}' or a similar format.
> - **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>
> 
> 
> Workflow ID: <workflowid>`wflow_rOSNaHMSLUJMvf4w`</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-04-21T11:30:19Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (10)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (10)</summary><blockquote>

`345-347`: **Improve error message readability.**

The current error message is not grammatically correct. Consider rephrasing for clarity.

```diff
-      throw new Error('Has not items order');
+      throw new Error('Order has no items');
```

---

`631-633`: **Improve error message readability in orderToDynamic function.**

The same grammatical error appears in the orderToDynamic function and should be corrected.

```diff
-      throw new Error('Has not items order');
+      throw new Error('Order has no items');
```

---

`672-683`: _⚠️ Potential issue_

**Fix incorrect reference in error message in orderToDynamic function.**

The same error with referencing a null product's ID appears in the orderToDynamic function.

```diff
        if (!product) {
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
-                error: `not found product ${product._id}`,
+                error: `not found product for ID: ${item.productId}`,
              },
            }
          );

          continue;
        }
```

---

`727-729`: **Simplify ternary expression.**

The ternary expression can be simplified using logical OR for cleaner, more readable code.

```diff
-                error: `${foundSyncLog.error ? foundSyncLog.error : ''} - ${
+                error: `${foundSyncLog.error || ''} - ${
                  responseSaleLine.error.message
                }`,
```

---

`227-227`: _⚠️ Potential issue_

**Fix typo in config property name.**

There's a typo in accessing the `branId` property which should be `brandId` to match the property name used throughout the codebase.

```diff
-  const brandId = config.branId;
+  const brandId = config.brandId;
```

---

`234-236`: _🛠️ Refactor suggestion_

**Use optional chaining for safer property access.**

When accessing nested properties, use optional chaining to prevent potential errors when parent objects might be undefined.

```diff
  const extraData = deal.extraData || {};
-  const syncErkhetInfo = extraData.msdynamic || {};
+  const syncErkhetInfo = extraData?.msdynamic || {};
  orderMsdNo = syncErkhetInfo.no;
  orderItemsMsdNo = syncErkhetInfo.lineNos || {};
```

---

`265-265`: _🛠️ Refactor suggestion_

**Ensure proper $in operator usage with MongoDB.**

The `$in` operator expects an array. If `deal._id` is a single value (string), wrapping it in an array will ensure the query works correctly.

```diff
-        mainTypeId: { $in: deal._id },
+        mainTypeId: { $in: Array.isArray(deal._id) ? deal._id : [deal._id] },
```

---

`291-291`: _🛠️ Refactor suggestion_

**Use optional chaining for array access.**

Safely access potential undefined arrays using optional chaining to prevent errors if the array is empty or undefined.

```diff
-        _id: deal.assignedUserIds[0],
+        _id: deal.assignedUserIds?.[0],
```

---

`387-397`: _⚠️ Potential issue_

**Fix incorrect reference in error message.**

When logging an error for a missing product, the code incorrectly references `product._id` which is undefined since the condition is `!product`.

```diff
        if (!product) {
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
-                error: `not found product ${product._id}`,
+                error: `not found product for ID: ${item.productId}`,
              },
            }
          );
          continue;
        }
```

---

`498-503`: _🛠️ Refactor suggestion_

**Fix spread operator with nullable object.**

The spread operator should have parentheses around the OR expression to handle undefined values properly.

```diff
            extraData: {
-              ...extraData || {},
+              ...(extraData || {}),
              msdynamic: {
                no: orderMsdNo || responseSale.No,
                lineNos: lineNoById,
              },
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (2)</summary><blockquote>

`319-319`: **Use optional chaining for better code readability.**

Replace the boolean check with optional chaining for a more concise and modern syntax.

```diff
-      Salesperson_Code: user && user.employeeId,
+      Salesperson_Code: user?.employeeId,
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

---

`747-747`: **Remove hardcoded URL from comment.**

The commented URL may contain sensitive information and should be removed from the code.

```diff
-      const soapApiUrl = config.discountSoapApi; // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
+      const soapApiUrl = config.discountSoapApi;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 786cba008e1a2793c45674a191a200fc3b909a7d and 5b5f6a25cd60eaec75f7546eb8686eee549af86f.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/plugin-msdynamic-api/src/graphql/resolvers/mutations/syncDynamic.ts` (2 hunks)
* `packages/plugin-msdynamic-api/src/utils.ts` (6 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: runtest
* GitHub Check: ui
* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: Analyze (go)

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/graphql/resolvers/mutations/syncDynamic.ts (2)</summary>

`14-14`: **Import addition aligns with implementation changes.**

The addition of `orderToDynamic` import makes sense with the implementation changes below, as the function is now used in the `toSendMsdOrders` mutation.

---

`391-399`: **Good refactoring to use specific config and new orderToDynamic function.**

The change from using a generic function to the specialized `orderToDynamic` function with a specific config improves code clarity and maintainability. This aligns with the PR objective of adding MS Dynamics synchronization for sales deals.

</details>
<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (3)</summary>

`3-7`: **Import addition supports new sales message functionality.**

The addition of `sendSalesMessage` import aligns with the PR objective to add synchronization functionality for sales deals with Microsoft Dynamics.

---

`785-786`: **Selector update improves code clarity.**

Changing from using a generic parameter to the specific `order._id` improves readability and maintainability.

---

`530-817`: **Comprehensive implementation of orderToDynamic function.**

The orderToDynamic function provides robust MS Dynamics synchronization for orders, with appropriate error handling, customer info retrieval, and sync log updates. The function aligns with the PR's objectives and includes sufficient error handling throughout.

Some additional validation could be added for order.items, order.scopeBrandIds, and customer data before use, but overall the implementation is solid and complete.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-21T11:32:51Z
user: login: ellipsis-dev[bot]
body: > [!CAUTION]
> **Changes requested  :x:**
> <details>
> <summary>Reviewed afc4915c3ab980881d896580aae2c41d6c41aba8 in <ellipsistime>1 minute and 24 seconds</ellipsistime>. Click for details.</summary> 
> 
> - Reviewed `13` lines of code in `1` files
> - Skipped `0` files when reviewing.
> - Skipped posting `4` 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-msdynamic-api/src/utils.ts:307</code></summary>
> 
> - **Draft comment:** 
> Using deal.name.split(':').pop().trim() assumes deal.name is a string with a colon. Consider adding a null/format check to avoid errors for unexpected formats.
> - **Reason this comment was not posted:**
> Marked as duplicate.
> 
> 
> </details>
> 
> <details>
> <summary>2. <code>packages/plugin-msdynamic-api/src/utils.ts:227</code></summary>
> 
> - **Draft comment:** 
> Typo detected: 'config.branId' is likely a mistake and should be changed to 'config.brandId' to maintain consistency.
> - **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>3. <code>packages/plugin-msdynamic-api/src/utils.ts:346</code></summary>
> 
> - **Draft comment:** 
> Consider rewording the error message 'Has not items order' to a clearer message like 'No items in order' for improved readability and clarity.
> - **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-msdynamic-api/src/utils.ts:632</code></summary>
> 
> - **Draft comment:** 
> The error message 'Has not items order' is awkward. Consider revising it to 'No items in order' to improve clarity.
> - **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>
> 
> 
> Workflow ID: <workflowid>`wflow_0x8N457yBEHrHtJF`</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-04-21T11:41:16Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (10)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (10)</summary><blockquote>

`302-304`: **Simplify conditional logic.**

The ternary expression can be simplified using the logical OR operator for better readability.


```diff
-      Sell_to_Customer_No: msdCustomer?.No
-        ? msdCustomer?.No
-        : config.defaultUserCode,
+      Sell_to_Customer_No: msdCustomer?.No || config.defaultUserCode,
```

---

`345-347`: **Improve error message clarity.**

The current error message is grammatically incorrect and unclear.


```diff
    if (!deal.productsData.length) {
-      throw new Error('Has not items order');
+      throw new Error('Order has no items');
    }
```

---

`227-227`: _⚠️ Potential issue_

**Fix the typo in config property name.**

There's a typo in the variable reference: `branId` should be `brandId` to match the property name used elsewhere in the codebase.


```diff
-  const brandId = config.branId;
+  const brandId = config.brandId;
```

---

`234-236`: _🛠️ Refactor suggestion_

**Ensure safe access to extraData properties.**

Use optional chaining to safely access `extraData.msdynamic` properties, preventing potential runtime errors.


```diff
-  const syncErkhetInfo = extraData.msdynamic || {};
+  const syncErkhetInfo = extraData?.msdynamic || {};
   orderMsdNo = syncErkhetInfo.no;
   orderItemsMsdNo = syncErkhetInfo.lineNos || {};
```

---

`265-265`: _⚠️ Potential issue_

**Fix MongoDB $in operator usage.**

The `$in` operator requires an array, but `deal._id` is being passed directly. This could cause unexpected behavior in the query execution.


```diff
-        mainTypeId: { $in: deal._id },
+        mainTypeId: { $in: Array.isArray(deal._id) ? deal._id : [deal._id] },
```

---

`281-285`: _🛠️ Refactor suggestion_

**Add null check before accessing response properties.**

Ensure that `msdCustomerInfo.relation` exists before accessing its properties to prevent potential runtime errors.


```diff
-      if (msdCustomerInfo) {
+      if (msdCustomerInfo && msdCustomerInfo.relation) {
         msdCustomer = msdCustomerInfo.relation?.response;
         customer = msdCustomerInfo.customer;
       }
```

---

`291-294`: _⚠️ Potential issue_

**Add null check for assignedUserIds array.**

The code assumes that `deal.assignedUserIds` is a non-empty array, which could lead to runtime errors if it's empty or undefined.


```diff
      data: {
-        _id: deal.assignedUserIds[0],
+        _id: deal.assignedUserIds?.[0],
      },
```

---

`307-307`: _🛠️ Refactor suggestion_

**Validate deal.name before using string operations.**

The code assumes `deal.name` is a string and tries to split it, which could cause runtime errors if it's undefined or not a string.


```diff
-      External_Document_No: deal.number || deal.name.split(':').pop().trim(),
+      External_Document_No: deal.number || (deal.name && typeof deal.name === 'string' ? deal.name.split(':').pop().trim() : ''),
```

---

`387-397`: _⚠️ Potential issue_

**Fix undefined product reference in error log.**

The code tries to log `product._id` after confirming that product doesn't exist, which would cause a runtime error.


```diff
        if (!product) {
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
-                error: `not found product ${product._id}`,
+                error: `not found product for ID: ${item.productId}`,
              },
            }
          );
          continue;
        }
```

---

`498-498`: _🛠️ Refactor suggestion_

**Fix spread operator with potentially undefined object.**

The spread operator needs parentheses around the OR expression to handle undefined values properly.


```diff
-              ...extraData || {},
+              ...(extraData || {}),
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (1)</summary><blockquote>

`319-319`: **Use optional chaining for user.employeeId access.**

This would prevent potential undefined errors if user is null.

```diff
-      Salesperson_Code: user && user.employeeId,
+      Salesperson_Code: user?.employeeId,
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5b5f6a25cd60eaec75f7546eb8686eee549af86f and ab6389c335f321bc22923531e6aed2baf66aebab.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/plugin-msdynamic-api/src/utils.ts` (6 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: ui
* GitHub Check: Analyze (javascript-typescript)

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>packages/plugin-msdynamic-api/src/utils.ts (5)</summary>

`3-7`: **LGTM: Import structure enhanced for new functionality.**

The import structure has been updated to include `sendSalesMessage`, which is used in the new MS Dynamics integration for deals.

---

`727-729`: **LGTM: Improved error message formatting.**

The error message formatting has been made consistent with the pattern used in the `dealToDynamic` function.

---

`747-748`: **LGTM: Consistent formatting in SOAP request configuration.**

The formatting for the SOAP request headers and configuration is now consistent with the patterns used elsewhere in the file.



Also applies to: 751-757

---

`785-786`: **LGTM: Fixed selector reference for order updates.**

The selector now correctly references `order._id` instead of what was likely `params._id` before, ensuring order updates target the correct document.

---

`953-954`: **LGTM: Fixed indentation in date comparison.**

The indentation in the date comparison expression is now correctly aligned.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-21T11:44:34Z
user: login: ellipsis-dev[bot]
body: > [!IMPORTANT]
> **Looks good to me!  :+1:**
> <details>
> <summary>Reviewed fd275c04af6ca50342ad70f17b62be8abc707850 in <ellipsistime>45 seconds</ellipsistime>. Click for details.</summary> 
> 
> - Reviewed `13` lines of code in `1` files
> - Skipped `0` files when reviewing.
> - Skipped posting `4` 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-msdynamic-api/src/utils.ts:316</code></summary>
> 
> - **Draft comment:** 
> Using 'user?.employeeId ?? '' ensures a valid string when user is null. Confirm that an empty string is acceptable for Salesperson_Code. Otherwise, it's a good improvement over 'user && user.employeeId'.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>2. <code>packages/plugin-msdynamic-api/src/utils.ts:319</code></summary>
> 
> - **Draft comment:** 
> Using optional chaining with nullish coalescing ensures Salesperson_Code is always a string. Confirm that '' is acceptable as the fallback.
> - **Reason this comment was not posted:**
> Confidence changes required: `0%` <= threshold `50%` None
> 
> 
> </details>
> 
> <details>
> <summary>3. <code>packages/plugin-msdynamic-api/src/utils.ts:227</code></summary>
> 
> - **Draft comment:** 
> Typo detected: 'config.branId' on line 227 likely should be 'config.brandId' for consistency with similar usages elsewhere in the file.
> - **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-msdynamic-api/src/utils.ts:346</code></summary>
> 
> - **Draft comment:** 
> Consider updating the error message 'Has not items order' (found on lines 346 and 632) to a more grammatically correct phrasing, for example 'No items in order', to improve clarity.
> - **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>
> 
> 
> Workflow ID: <workflowid>`wflow_kRxCrAbyhWpxlhMl`</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-04-21T16:58:31Z

Review comments:
diff_hunk: @@ -1,11 +1,12 @@
-import { generateModels } from './connectionResolver';
-import { customerToDynamic } from './utilsCustomer';
-import { dealToDynamic, getConfig } from './utils';
+import { generateModels } from "./connectionResolver";
+import { customerToDynamic } from "./utilsCustomer";
+import { dealToDynamic, getConfig, orderToDynamic } from "./utils";
 
 const allowTypes = {
-  'core:customer': ['create'],
-  'core:company': ['create'],
-  'pos:order': ['synced'],
+  "core:customer": ["create"],
+  "core:company": ["create"],
+  "pos:order": ["synced"],
+  "sales:deal": ["update"],
 };
 
 export const afterMutationHandlers = async (subdomain, params) => {
path: packages/plugin-msdynamic-api/src/afterMutations.ts
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider refactoring the afterMutationHandlers function to use a mapping to dispatch logic to separate handler functions based on the mutation type, instead of using a deeply nested if/else chain. 

Consider extracting the per-type logic into separate handler functions and using a mapping to dispatch. This avoids deeply nested if/else chains. For example:

```js
const mutationHandlers = {
  "core:customer": async (subdomain, params, models, configs, syncLogDoc) => {
    const syncLog = await models.SyncLogs.syncLogsAdd(syncLogDoc);
    await customerToDynamic(
      subdomain,
      syncLog,
      params.updatedDocument || params.object,
      "customer",
      models,
      configs
    );
  },
  "core:company": async (subdomain, params, models, configs, syncLogDoc) => {
    const syncLog = await models.SyncLogs.syncLogsAdd(syncLogDoc);
    await customerToDynamic(
      subdomain,
      syncLog,
      params.updatedDocument || params.object,
      "company",
      models,
      configs
    );
  },
  "sales:deal": async (subdomain, params, models, configs) => {
    const updatedDoc = params.updatedDocument || params.object;
    const brandId = updatedDoc?.scopeBrandIds?.[0];
    const config = configs[brandId || "noBrand"];
    if (params.action === "update" && config?.useBoard) {
      if (config.stageId === updatedDoc?.stageId) {
        // optionally add dynamic handling if needed
      }
    }
  },
  "pos:order": async (subdomain, params, models, configs, syncLogDoc) => {
    const syncLog = await models.SyncLogs.syncLogsAdd(syncLogDoc);
    const updatedDoc = params.updatedDocument || params.object;
    const brandId = updatedDoc?.scopeBrandIds?.[0];
    const config = configs[brandId || "noBrand"];
    if (params.action === "synced" && !config.useBoard) {
      await orderToDynamic(
        subdomain,
        syncLog,
        params.updatedDocument || params.object,
        models,
        config
      );
    }
  },
};

export const afterMutationHandlers = async (subdomain, params) => {
  const { type, action, user } = params;
  if (!Object.keys(allowTypes).includes(type) || !allowTypes[type].includes(action)) {
    return;
  }
  let configs;
  try {
    configs = await getConfig(subdomain, "DYNAMIC", {});
    if (!configs || !Object.keys(configs).length) return;
  } catch (e) {
    return;
  }
  const models = await generateModels(subdomain);
  const syncLogDoc = {
    type: "",
    contentType: type,
    contentId: params.object?._id,
    createdAt: new Date(),
    createdBy: user?._id,
    consumeData: params,
    consumeStr: JSON.stringify(params),
  };

  try {
    const handler = mutationHandlers[type];
    if (handler) {
      await handler(subdomain, params, models, configs, syncLogDoc);
    }
  } catch (e) {
    if (models?.SyncLogs && models.SyncLogs.updateOne && syncLogDoc?._id) {
      await models.SyncLogs.updateOne(
        { _id: syncLogDoc._id },
        { $set: { error: e.message } }
      );
    }
  }
};
```

**Actionable Steps:**

1. **Extract Handlers:** Move the logic for each mutation type to its own function.
2. **Dispatch via Mapping:** Replace nested conditions with a mapping that selects the handler based on type.
3. **Keep Common Operations Out:** Move shared operations (like generating syncLogDoc) to a common section outside the individual handlers.

These changes maintain functionality while reducing nested conditionals and enhancing readability.
created_at: 2025-04-13T11:52:39Z
author_association: NONE
diff_hunk: @@ -410,14 +409,15 @@ export const dealToDynamic = async (
         if (responseSaleLine?.error?.message) {
           const foundSyncLog = (await models.SyncLogs.findOne({
             _id: syncLog._id,
-          })) || { error: '' };
+          })) || { error: "" };
 
           await models.SyncLogs.updateOne(
             { _id: syncLog._id },
             {
               $set: {
-                error: `${foundSyncLog.error ? foundSyncLog.error : ''} - ${responseSaleLine.error.message
-                  }`,
+                error: `${foundSyncLog.error ? foundSyncLog.error : ""} - ${
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Avoid unneeded ternary statements ([`simplify-ternary`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/simplify-ternary))

```suggestion
                error: `${foundSyncLog.error || ""} - ${
```

<br/><details><summary>Explanation</summary>It is possible to simplify certain ternary statements into either use of an `||` or `!`.
This makes the code easier to read, since there is no conditional logic.
</details>
created_at: 2025-04-13T11:52:39Z
author_association: NONE
diff_hunk: @@ -435,17 +435,308 @@ export const dealToDynamic = async (
         totalDiscount += item.discountAmount ?? 0;
       }
 
-      const soapApiUrl = config.discountSoapApi // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
+      const soapApiUrl = config.discountSoapApi; // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
 
       if (soapApiUrl && totalDiscount) {
         const soapHeaders = {
           "Content-Type": "text/xml",
-          SOAPAction: "urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount",
+          SOAPAction:
+            "urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount",
           Authorization: `Basic ${Buffer.from(
             `${username}:${password}`
-          ).toString('base64')}`,
+          ).toString("base64")}`,
+        };
+
+        const soapDiscountSendData = `
+          <?xml version="1.0"?>
+          <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+                    xmlns:tns="urn:microsoft-dynamics-schemas/codeunit/PictureService">
+          <soapenv:Header/>
+          <soapenv:Body>
+            <tns:ReCalculateSalesOrderDiscount>
+              <tns:code>${responseSale.No}</tns:code>
+              <tns:invoiceDiscountAmount>${totalDiscount}</tns:invoiceDiscountAmount>
+            </tns:ReCalculateSalesOrderDiscount>
+          </soapenv:Body>
+          </soapenv:Envelope>
+        `;
+
+        await fetch(`${soapApiUrl}`, {
+          method: "POST",
+          headers: soapHeaders,
+          body: soapDiscountSendData,
+        });
+      }
+    }
+
+    await sendPosMessage({
+      subdomain,
+      action: "orders.updateOne",
+      data: {
+        selector: { _id: params._id },
+        modifier: {
+          $set: {
+            syncErkhetInfo: JSON.stringify({
+              no: orderMsdNo || responseSale.No,
+              lineNos: lineNoById,
+            }),
+            syncedErkhet: true,
+          },
+        },
+      },
+      isRPC: true,
+    });
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          responseData: responseSale,
+          responseStr: JSON.stringify(responseSale),
+        },
+      }
+    );
+
+    return responseSale;
+  } catch (e) {
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      { $set: { error: e.message } }
+    );
+    console.log(e, "error");
+  }
+};
+
+export const orderToDynamic = async (
+  subdomain: string,
+  syncLog: ISyncLogDocument,
+  params: any,
+  models: IModels,
+  config: any
+) => {
+  const order = params;
+  const brandId = order.scopeBrandIds[0];
+
+  let msdCustomer: any = {};
+
+  let orderMsdNo: string = "";
+  let orderItemsMsdNo: any = {};
+  try {
+    const syncErkhetInfo = JSON.parse(order.syncErkhetInfo);
+    orderMsdNo = syncErkhetInfo.no;
+    orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+  } catch {
+    orderMsdNo = order.syncErkhetInfo;
+  }
+
+  try {
+    let customer;
+
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = "";
+    let lineUrlParam = "";
+    let postMethod = "POST";
+    const postHeaders = {
+      "Content-Type": "application/json",
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        "base64"
+      )}`,
+    };
+
+    if (order?.customerId) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        order.customerId,
+        order.customerType,
+        brandId,
+        config
+      );
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
+
+    const sendData: any = {
+      Sell_to_Customer_No: msdCustomer?.No
+        ? msdCustomer?.No
+        : config.defaultUserCode,
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Avoid unneeded ternary statements ([`simplify-ternary`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/simplify-ternary))

```suggestion
      Sell_to_Customer_No: msdCustomer?.No || config.defaultUserCode,

```

<br/><details><summary>Explanation</summary>It is possible to simplify certain ternary statements into either use of an `||` or `!`.
This makes the code easier to read, since there is no conditional logic.
</details>
created_at: 2025-04-13T11:52:39Z
author_association: NONE
diff_hunk: @@ -435,17 +435,308 @@ export const dealToDynamic = async (
         totalDiscount += item.discountAmount ?? 0;
       }
 
-      const soapApiUrl = config.discountSoapApi // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
+      const soapApiUrl = config.discountSoapApi; // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
 
       if (soapApiUrl && totalDiscount) {
         const soapHeaders = {
           "Content-Type": "text/xml",
-          SOAPAction: "urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount",
+          SOAPAction:
+            "urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount",
           Authorization: `Basic ${Buffer.from(
             `${username}:${password}`
-          ).toString('base64')}`,
+          ).toString("base64")}`,
+        };
+
+        const soapDiscountSendData = `
+          <?xml version="1.0"?>
+          <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+                    xmlns:tns="urn:microsoft-dynamics-schemas/codeunit/PictureService">
+          <soapenv:Header/>
+          <soapenv:Body>
+            <tns:ReCalculateSalesOrderDiscount>
+              <tns:code>${responseSale.No}</tns:code>
+              <tns:invoiceDiscountAmount>${totalDiscount}</tns:invoiceDiscountAmount>
+            </tns:ReCalculateSalesOrderDiscount>
+          </soapenv:Body>
+          </soapenv:Envelope>
+        `;
+
+        await fetch(`${soapApiUrl}`, {
+          method: "POST",
+          headers: soapHeaders,
+          body: soapDiscountSendData,
+        });
+      }
+    }
+
+    await sendPosMessage({
+      subdomain,
+      action: "orders.updateOne",
+      data: {
+        selector: { _id: params._id },
+        modifier: {
+          $set: {
+            syncErkhetInfo: JSON.stringify({
+              no: orderMsdNo || responseSale.No,
+              lineNos: lineNoById,
+            }),
+            syncedErkhet: true,
+          },
+        },
+      },
+      isRPC: true,
+    });
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          responseData: responseSale,
+          responseStr: JSON.stringify(responseSale),
+        },
+      }
+    );
+
+    return responseSale;
+  } catch (e) {
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      { $set: { error: e.message } }
+    );
+    console.log(e, "error");
+  }
+};
+
+export const orderToDynamic = async (
+  subdomain: string,
+  syncLog: ISyncLogDocument,
+  params: any,
+  models: IModels,
+  config: any
+) => {
+  const order = params;
+  const brandId = order.scopeBrandIds[0];
+
+  let msdCustomer: any = {};
+
+  let orderMsdNo: string = "";
+  let orderItemsMsdNo: any = {};
+  try {
+    const syncErkhetInfo = JSON.parse(order.syncErkhetInfo);
+    orderMsdNo = syncErkhetInfo.no;
+    orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+  } catch {
+    orderMsdNo = order.syncErkhetInfo;
+  }
+
+  try {
+    let customer;
+
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = "";
+    let lineUrlParam = "";
+    let postMethod = "POST";
+    const postHeaders = {
+      "Content-Type": "application/json",
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        "base64"
+      )}`,
+    };
+
+    if (order?.customerId) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        order.customerId,
+        order.customerType,
+        brandId,
+        config
+      );
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
+
+    const sendData: any = {
+      Sell_to_Customer_No: msdCustomer?.No
+        ? msdCustomer?.No
+        : config.defaultUserCode,
+      Sell_to_Phone_No: customer?.primaryPhone || "",
+      Sell_to_E_Mail: customer?.primaryEmail || "",
+      External_Document_No: order.number,
+      Responsibility_Center: config.responsibilityCenter || "",
+      Sync_Type: config.syncType || "",
+      Mobile_Phone_No: customer?.primaryPhone || "",
+      VAT_Bus_Posting_Group: config.vatBusPostingGroup || "",
+      Payment_Terms_Code: config.paymentTermsCode || "",
+      Payment_Method_Code: config.paymentMethodCode || "CASH",
+      Customer_Price_Group: config.customerPricingGroup || "",
+      Prices_Including_VAT: true,
+      BillType: config.billType || "Receipt",
+      Location_Code: config.locationCode || "",
+      Deal_Type_Code: config.dealType || "NORMAL",
+      CustomerNo:
+        customer?.customFieldsDataByFieldCode?.vatCustomer?.value ||
+        customer?.customFieldsDataByFieldCode?.vatCompany?.value,
+    };
+
+    if (orderMsdNo) {
+      urlParam = `(Document_Type='Order', No='${orderMsdNo}')`;
+      lineUrlParam = `(Document_Type='Order',Document_No='${orderMsdNo}',Line_No=%Ln)`;
+      postMethod = "PATCH";
+      postHeaders["If-Match"] = "*";
+      sendData.No = orderMsdNo;
+    }
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          sendData,
+          sendStr: JSON.stringify(sendData),
+        },
+      }
+    );
+
+    if (!order.items.length) {
+      throw new Error("Has not items order");
+    }
+
+    const responseSale = await fetch(`${salesApi}${urlParam}`, {
+      method: postMethod,
+      headers: postHeaders,
+      body: JSON.stringify(sendData),
+    }).then((res) => res.json());
+
+    const lineNoById = {};
+
+    if (responseSale) {
+      const products = await sendCoreMessage({
+        subdomain,
+        action: "products.find",
+        data: { _id: { $in: order.items.map((item) => item.productId) } },
+        isRPC: true,
+      });
+
+      const productById = {};
+
+      for (const product of products) {
+        productById[product._id] = product;
+      }
+
+      let totalDiscount = 0;
+
+      for (const item of order.items) {
+        let lineUrlP = "";
+        let linePostMethod = "POST";
+        let linePostHeaders = {
+          "Content-Type": "application/json",
+          Authorization: `Basic ${Buffer.from(
+            `${username}:${password}`
+          ).toString("base64")}`,
+        };
+
+        const lineNo = orderItemsMsdNo[item._id] || "";
+        const product = productById[item.productId];
+
+        if (!product) {
+          await models.SyncLogs.updateOne(
+            { _id: syncLog._id },
+            {
+              $set: {
+                error: `not found product ${product._id}`,
+              },
+            }
+          );
+
+          continue;
         }
 
+        const sendSalesLine: any = {
+          Document_No: responseSale.No,
+          Type: "Item",
+          No: productById[item.productId]
+            ? productById[item.productId].code
+            : "",
+          Quantity: item.count || 0,
+          // Unit_Price: item.unitPrice || 0,
+          Location_Code: config.locationCode,
+        };
+
+        if (lineNo) {
+          lineUrlP = lineUrlParam.replace("%Ln", lineNo);
+          linePostMethod = "PATCH";
+          linePostHeaders["If-Match"] = "*";
+          sendSalesLine.Line_No = lineNo;
+        }
+
+        await models.SyncLogs.updateOne(
+          { _id: syncLog._id },
+          {
+            $push: {
+              sendSales: JSON.stringify(sendSalesLine),
+            },
+          }
+        );
+
+        const responseSaleLine = await fetch(`${salesLineApi}${lineUrlP}`, {
+          method: linePostMethod,
+          headers: linePostHeaders,
+          body: JSON.stringify(sendSalesLine),
+        }).then((res) => res.json());
+
+        if (responseSaleLine?.error?.message) {
+          const foundSyncLog = (await models.SyncLogs.findOne({
+            _id: syncLog._id,
+          })) || { error: "" };
+
+          await models.SyncLogs.updateOne(
+            { _id: syncLog._id },
+            {
+              $set: {
+                error: `${foundSyncLog.error ? foundSyncLog.error : ""} - ${
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Avoid unneeded ternary statements ([`simplify-ternary`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/simplify-ternary))

```suggestion
                error: `${foundSyncLog.error || ""} - ${
```

<br/><details><summary>Explanation</summary>It is possible to simplify certain ternary statements into either use of an `||` or `!`.
This makes the code easier to read, since there is no conditional logic.
</details>
created_at: 2025-04-13T11:52:39Z
author_association: NONE
diff_hunk: @@ -45,44 +46,72 @@ export const afterMutationHandlers = async (subdomain, params) => {
   let syncLog;
 
   try {
-    if (type === 'core:customer' && action === 'create') {
+    if (type === "core:customer" && action === "create") {
       syncLog = await models.SyncLogs.syncLogsAdd(syncLogDoc);
 
       await customerToDynamic(
         subdomain,
         syncLog,
         params.updatedDocument || params.object,
-        'customer',
+        "customer",
         models,
         configs
       );
       return;
     }
 
-    if (type === 'core:company' && action === 'create') {
+    if (type === "core:company" && action === "create") {
       syncLog = await models.SyncLogs.syncLogsAdd(syncLogDoc);
 
       await customerToDynamic(
         subdomain,
         syncLog,
         params.updatedDocument || params.object,
-        'company',
+        "company",
         models,
         configs
       );
       return;
     }
 
-    if (type === 'pos:order') {
+    if (type === "sales:deal") {
+      // syncLog = await models.SyncLogs.syncLogsAdd(syncLogDoc);
+
+      const updatedDoc = params.updatedDocument || params.object;
+      const brandId = updatedDoc?.scopeBrandIds?.[0];
+      const config = configs[brandId || "noBrand"];
+
+      if (action === "update" && config?.useBoard) {
+        const updatedDoc = params.updatedDocument || params.object;
+
+        if (config.stageId === updatedDoc?.stageId) {
+          // await dealToDynamic(
path: packages/plugin-msdynamic-api/src/afterMutations.ts
user: login: ellipsis-dev[bot]
body: The commented out `dealToDynamic` call in the `'sales:deal'` block makes the logic unclear. Either remove the commented code or explain why it’s kept for future reference.
```suggestion

```
created_at: 2025-04-13T11:54:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -435,17 +435,308 @@ export const dealToDynamic = async (
         totalDiscount += item.discountAmount ?? 0;
       }
 
-      const soapApiUrl = config.discountSoapApi // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
+      const soapApiUrl = config.discountSoapApi; // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
 
       if (soapApiUrl && totalDiscount) {
         const soapHeaders = {
           "Content-Type": "text/xml",
-          SOAPAction: "urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount",
+          SOAPAction:
+            "urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount",
           Authorization: `Basic ${Buffer.from(
             `${username}:${password}`
-          ).toString('base64')}`,
+          ).toString("base64")}`,
+        };
+
+        const soapDiscountSendData = `
+          <?xml version="1.0"?>
+          <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+                    xmlns:tns="urn:microsoft-dynamics-schemas/codeunit/PictureService">
+          <soapenv:Header/>
+          <soapenv:Body>
+            <tns:ReCalculateSalesOrderDiscount>
+              <tns:code>${responseSale.No}</tns:code>
+              <tns:invoiceDiscountAmount>${totalDiscount}</tns:invoiceDiscountAmount>
+            </tns:ReCalculateSalesOrderDiscount>
+          </soapenv:Body>
+          </soapenv:Envelope>
+        `;
+
+        await fetch(`${soapApiUrl}`, {
+          method: "POST",
+          headers: soapHeaders,
+          body: soapDiscountSendData,
+        });
+      }
+    }
+
+    await sendPosMessage({
+      subdomain,
+      action: "orders.updateOne",
+      data: {
+        selector: { _id: params._id },
+        modifier: {
+          $set: {
+            syncErkhetInfo: JSON.stringify({
+              no: orderMsdNo || responseSale.No,
+              lineNos: lineNoById,
+            }),
+            syncedErkhet: true,
+          },
+        },
+      },
+      isRPC: true,
+    });
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          responseData: responseSale,
+          responseStr: JSON.stringify(responseSale),
+        },
+      }
+    );
+
+    return responseSale;
+  } catch (e) {
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      { $set: { error: e.message } }
+    );
+    console.log(e, "error");
+  }
+};
+
+export const orderToDynamic = async (
+  subdomain: string,
+  syncLog: ISyncLogDocument,
+  params: any,
+  models: IModels,
+  config: any
+) => {
+  const order = params;
+  const brandId = order.scopeBrandIds[0];
+
+  let msdCustomer: any = {};
+
+  let orderMsdNo: string = "";
+  let orderItemsMsdNo: any = {};
+  try {
+    const syncErkhetInfo = JSON.parse(order.syncErkhetInfo);
+    orderMsdNo = syncErkhetInfo.no;
+    orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+  } catch {
+    orderMsdNo = order.syncErkhetInfo;
+  }
+
+  try {
+    let customer;
+
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = "";
+    let lineUrlParam = "";
+    let postMethod = "POST";
+    const postHeaders = {
+      "Content-Type": "application/json",
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        "base64"
+      )}`,
+    };
+
+    if (order?.customerId) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        order.customerId,
+        order.customerType,
+        brandId,
+        config
+      );
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
+
+    const sendData: any = {
+      Sell_to_Customer_No: msdCustomer?.No
+        ? msdCustomer?.No
+        : config.defaultUserCode,
+      Sell_to_Phone_No: customer?.primaryPhone || "",
+      Sell_to_E_Mail: customer?.primaryEmail || "",
+      External_Document_No: order.number,
+      Responsibility_Center: config.responsibilityCenter || "",
+      Sync_Type: config.syncType || "",
+      Mobile_Phone_No: customer?.primaryPhone || "",
+      VAT_Bus_Posting_Group: config.vatBusPostingGroup || "",
+      Payment_Terms_Code: config.paymentTermsCode || "",
+      Payment_Method_Code: config.paymentMethodCode || "CASH",
+      Customer_Price_Group: config.customerPricingGroup || "",
+      Prices_Including_VAT: true,
+      BillType: config.billType || "Receipt",
+      Location_Code: config.locationCode || "",
+      Deal_Type_Code: config.dealType || "NORMAL",
+      CustomerNo:
+        customer?.customFieldsDataByFieldCode?.vatCustomer?.value ||
+        customer?.customFieldsDataByFieldCode?.vatCompany?.value,
+    };
+
+    if (orderMsdNo) {
+      urlParam = `(Document_Type='Order', No='${orderMsdNo}')`;
+      lineUrlParam = `(Document_Type='Order',Document_No='${orderMsdNo}',Line_No=%Ln)`;
+      postMethod = "PATCH";
+      postHeaders["If-Match"] = "*";
+      sendData.No = orderMsdNo;
+    }
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          sendData,
+          sendStr: JSON.stringify(sendData),
+        },
+      }
+    );
+
+    if (!order.items.length) {
+      throw new Error("Has not items order");
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: ellipsis-dev[bot]
body: Typographical issue: The error message `'Has not items order'` appears again and could be reworded to `'Order has no items'` to be clearer.
```suggestion
      throw new Error("Order has no items");
```
created_at: 2025-04-13T11:54:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,6 +74,7 @@ export interface IPosOrder {
     subscriptionId: string;
     status: string;
   };
+  scopeBrandIds?: string[];
path: packages/plugin-pos-api/src/models/definitions/orders.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Missing schema definition for scopeBrandIds**

While the scopeBrandIds field is properly added to the IPosOrder interface, it's not defined in the posOrderSchema. This could cause data to be received but not persisted in the database.


The field is used in packages/plugin-pos-api/src/utils.ts (line 777) to set data on orders, and it's also passed to the sales service in createDealPerOrder (line 547). Add the field to posOrderSchema to ensure it's properly persisted:

```diff
  }),
+ scopeBrandIds: field({
+   type: [String],
+   optional: true,
+   label: "Related brands"
+ }),
  closeDate: field({ type: Date, optional: true, label: "Close Date" }),
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  }),
  scopeBrandIds: field({
    type: [String],
    optional: true,
    label: "Related brands"
  }),
  closeDate: field({ type: Date, optional: true, label: "Close Date" }),
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-13T11:57:01Z
author_association: NONE
diff_hunk: @@ -435,17 +435,308 @@ export const dealToDynamic = async (
         totalDiscount += item.discountAmount ?? 0;
       }
 
-      const soapApiUrl = config.discountSoapApi // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
+      const soapApiUrl = config.discountSoapApi; // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
 
       if (soapApiUrl && totalDiscount) {
         const soapHeaders = {
           "Content-Type": "text/xml",
-          SOAPAction: "urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount",
+          SOAPAction:
+            "urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount",
           Authorization: `Basic ${Buffer.from(
             `${username}:${password}`
-          ).toString('base64')}`,
+          ).toString("base64")}`,
+        };
+
+        const soapDiscountSendData = `
+          <?xml version="1.0"?>
+          <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+                    xmlns:tns="urn:microsoft-dynamics-schemas/codeunit/PictureService">
+          <soapenv:Header/>
+          <soapenv:Body>
+            <tns:ReCalculateSalesOrderDiscount>
+              <tns:code>${responseSale.No}</tns:code>
+              <tns:invoiceDiscountAmount>${totalDiscount}</tns:invoiceDiscountAmount>
+            </tns:ReCalculateSalesOrderDiscount>
+          </soapenv:Body>
+          </soapenv:Envelope>
+        `;
+
+        await fetch(`${soapApiUrl}`, {
+          method: "POST",
+          headers: soapHeaders,
+          body: soapDiscountSendData,
+        });
+      }
+    }
+
+    await sendPosMessage({
+      subdomain,
+      action: "orders.updateOne",
+      data: {
+        selector: { _id: params._id },
+        modifier: {
+          $set: {
+            syncErkhetInfo: JSON.stringify({
+              no: orderMsdNo || responseSale.No,
+              lineNos: lineNoById,
+            }),
+            syncedErkhet: true,
+          },
+        },
+      },
+      isRPC: true,
+    });
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          responseData: responseSale,
+          responseStr: JSON.stringify(responseSale),
+        },
+      }
+    );
+
+    return responseSale;
+  } catch (e) {
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      { $set: { error: e.message } }
+    );
+    console.log(e, "error");
+  }
+};
+
+export const orderToDynamic = async (
+  subdomain: string,
+  syncLog: ISyncLogDocument,
+  params: any,
+  models: IModels,
+  config: any
+) => {
+  const order = params;
+  const brandId = order.scopeBrandIds[0];
+
+  let msdCustomer: any = {};
+
+  let orderMsdNo: string = "";
+  let orderItemsMsdNo: any = {};
+  try {
+    const syncErkhetInfo = JSON.parse(order.syncErkhetInfo);
+    orderMsdNo = syncErkhetInfo.no;
+    orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+  } catch {
+    orderMsdNo = order.syncErkhetInfo;
+  }
+
+  try {
+    let customer;
+
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = "";
+    let lineUrlParam = "";
+    let postMethod = "POST";
+    const postHeaders = {
+      "Content-Type": "application/json",
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        "base64"
+      )}`,
+    };
+
+    if (order?.customerId) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        order.customerId,
+        order.customerType,
+        brandId,
+        config
+      );
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
+
+    const sendData: any = {
+      Sell_to_Customer_No: msdCustomer?.No
+        ? msdCustomer?.No
+        : config.defaultUserCode,
+      Sell_to_Phone_No: customer?.primaryPhone || "",
+      Sell_to_E_Mail: customer?.primaryEmail || "",
+      External_Document_No: order.number,
+      Responsibility_Center: config.responsibilityCenter || "",
+      Sync_Type: config.syncType || "",
+      Mobile_Phone_No: customer?.primaryPhone || "",
+      VAT_Bus_Posting_Group: config.vatBusPostingGroup || "",
+      Payment_Terms_Code: config.paymentTermsCode || "",
+      Payment_Method_Code: config.paymentMethodCode || "CASH",
+      Customer_Price_Group: config.customerPricingGroup || "",
+      Prices_Including_VAT: true,
+      BillType: config.billType || "Receipt",
+      Location_Code: config.locationCode || "",
+      Deal_Type_Code: config.dealType || "NORMAL",
+      CustomerNo:
+        customer?.customFieldsDataByFieldCode?.vatCustomer?.value ||
+        customer?.customFieldsDataByFieldCode?.vatCompany?.value,
+    };
+
+    if (orderMsdNo) {
+      urlParam = `(Document_Type='Order', No='${orderMsdNo}')`;
+      lineUrlParam = `(Document_Type='Order',Document_No='${orderMsdNo}',Line_No=%Ln)`;
+      postMethod = "PATCH";
+      postHeaders["If-Match"] = "*";
+      sendData.No = orderMsdNo;
+    }
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          sendData,
+          sendStr: JSON.stringify(sendData),
+        },
+      }
+    );
+
+    if (!order.items.length) {
+      throw new Error("Has not items order");
+    }
+
+    const responseSale = await fetch(`${salesApi}${urlParam}`, {
+      method: postMethod,
+      headers: postHeaders,
+      body: JSON.stringify(sendData),
+    }).then((res) => res.json());
+
+    const lineNoById = {};
+
+    if (responseSale) {
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Validate input parameters**  
Check that `params`, including `scopeBrandIds` and `syncErkhetInfo`, are defined before using them. Otherwise, consider safe access patterns to avoid runtime errors.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-13T11:57:02Z
author_association: NONE
diff_hunk: @@ -26,6 +30,7 @@ type Props = {
 type State = {
   config: any;
   hasOpen: boolean;
+  pipeline?: any;
 };
path: packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add explicit type for 'pipeline'**  
Using `any` for `pipeline` impairs clarity. Consider introducing an interface modeling pipeline data to leverage type checking.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-13T11:57:02Z
author_association: NONE
diff_hunk: @@ -218,15 +218,326 @@ export const dealToDynamic = async (
   syncLog: ISyncLogDocument,
   params: any,
   models: IModels,
-  configs: any
+  config: any
 ) => {
   const order = params;
   const brandId = order.scopeBrandIds[0];
-  const config = configs[brandId || 'noBrand'];
 
   let msdCustomer: any = {};
 
-  let orderMsdNo: string = '';
+  let orderMsdNo: string = "";
+  let orderItemsMsdNo: any = {};
+  try {
+    const syncErkhetInfo = JSON.parse(order.syncErkhetInfo);
+    orderMsdNo = syncErkhetInfo.no;
+    orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+  } catch {
+    orderMsdNo = order.syncErkhetInfo;
+  }
+
+  try {
+    let customer;
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = "";
+    let lineUrlParam = "";
+    let postMethod = "POST";
+    const postHeaders = {
+      "Content-Type": "application/json",
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        "base64"
+      )}`,
+    };
+
+    const conformities = await sendCoreMessage({
+      subdomain,
+      action: "conformities.findConformities",
+      data: {
+        mainType: "deal",
+        mainTypeId: { $in: order._id },
+      },
+      isRPC: true,
+      defaultValue: [],
+    });
+
+    if (conformities.length > 0) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        conformities[0].relTypeId,
+        conformities[0].relType,
+        brandId,
+        config
+      );
+
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
+
+    const user = await sendCoreMessage({
+      subdomain,
+      action: "users.findOne",
+      data: {
+        _id: order.assignedUserIds[0],
+      },
+      isRPC: true,
+    });
+
+    const rawDescription = order.description.replace(/<\/?p>/g, "").trim();
+
+    const sellAddress = rawDescription.slice(0, 100);
+    const sellAddress2 = rawDescription.slice(100, 150);
+
+    const sendData: any = {
+      Sell_to_Customer_No: msdCustomer?.No
+        ? msdCustomer?.No
+        : config.defaultUserCode,
+      Sell_to_Phone_No: customer?.primaryPhone || "",
+      Sell_to_E_Mail: customer?.primaryEmail || "",
+      External_Document_No: `${order.name.split(":").pop().trim()}TESTBYERXES`,
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: ellipsis-dev[bot]
body: Hardcoded 'TESTBYERXES' appended to `External_Document_No` looks like a temporary placeholder. Remove or parameterize before production.
```suggestion
      External_Document_No: `${order.name.split(":").pop().trim()}`,
```
created_at: 2025-04-14T05:50:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -218,15 +218,326 @@ export const dealToDynamic = async (
   syncLog: ISyncLogDocument,
   params: any,
   models: IModels,
-  configs: any
+  config: any
 ) => {
   const order = params;
   const brandId = order.scopeBrandIds[0];
-  const config = configs[brandId || 'noBrand'];
 
   let msdCustomer: any = {};
 
-  let orderMsdNo: string = '';
+  let orderMsdNo: string = "";
+  let orderItemsMsdNo: any = {};
+  try {
+    const syncErkhetInfo = JSON.parse(order.syncErkhetInfo);
+    orderMsdNo = syncErkhetInfo.no;
+    orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+  } catch {
+    orderMsdNo = order.syncErkhetInfo;
+  }
+
+  try {
+    let customer;
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = "";
+    let lineUrlParam = "";
+    let postMethod = "POST";
+    const postHeaders = {
+      "Content-Type": "application/json",
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        "base64"
+      )}`,
+    };
+
+    const conformities = await sendCoreMessage({
+      subdomain,
+      action: "conformities.findConformities",
+      data: {
+        mainType: "deal",
+        mainTypeId: { $in: order._id },
+      },
+      isRPC: true,
+      defaultValue: [],
+    });
+
+    if (conformities.length > 0) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        conformities[0].relTypeId,
+        conformities[0].relType,
+        brandId,
+        config
+      );
+
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
+
+    const user = await sendCoreMessage({
+      subdomain,
+      action: "users.findOne",
+      data: {
+        _id: order.assignedUserIds[0],
+      },
+      isRPC: true,
+    });
+
+    const rawDescription = order.description.replace(/<\/?p>/g, "").trim();
+
+    const sellAddress = rawDescription.slice(0, 100);
+    const sellAddress2 = rawDescription.slice(100, 150);
+
+    const sendData: any = {
+      Sell_to_Customer_No: msdCustomer?.No
+        ? msdCustomer?.No
+        : config.defaultUserCode,
+      Sell_to_Phone_No: customer?.primaryPhone || "",
+      Sell_to_E_Mail: customer?.primaryEmail || "",
+      External_Document_No: `${order.name.split(":").pop().trim()}TESTBYERXES`,
+      Responsibility_Center: config.responsibilityCenter || "",
+      Sync_Type: config.syncType || "",
+      Mobile_Phone_No: customer?.primaryPhone || "",
+      VAT_Bus_Posting_Group: config.vatBusPostingGroup || "",
+      Payment_Terms_Code: config.paymentTermsCode || "",
+      Payment_Method_Code: config.paymentMethodCode || "CASH",
+      Customer_Price_Group: config.customerPricingGroup || "",
+      Prices_Including_VAT: true,
+      BillType: config.billType || "Receipt",
+      Location_Code: config.locationCode || "",
+      Deal_Type_Code: config.dealType || "NORMAL",
+      Salesperson_Code: user && user.employeeId,
+      Sell_to_Address: sellAddress,
+      Sell_to_Address_2: sellAddress2,
+      CustomerNo:
+        customer?.customFieldsDataByFieldCode?.vatCustomer?.value ||
+        customer?.customFieldsDataByFieldCode?.vatCompany?.value,
+    };
+
+    if (orderMsdNo) {
+      urlParam = `(Document_Type='Order', No='${orderMsdNo}')`;
+      lineUrlParam = `(Document_Type='Order',Document_No='${orderMsdNo}',Line_No=%Ln)`;
+      postMethod = "PATCH";
+      postHeaders["If-Match"] = "*";
+      sendData.No = orderMsdNo;
+    }
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          sendData,
+          sendStr: JSON.stringify(sendData),
+        },
+      }
+    );
+
+    if (!order.productsData.length) {
+      throw new Error("Has not items order");
+    }
+
+    const responseSale = await fetch(`${salesApi}${urlParam}`, {
+      method: postMethod,
+      headers: postHeaders,
+      body: JSON.stringify(sendData),
+    }).then((res) => res.json());
+    const lineNoById = {};
+
+    if (responseSale) {
+      const products = await sendCoreMessage({
+        subdomain,
+        action: "products.find",
+        data: {
+          _id: { $in: order.productsData.map((item) => item.productId) },
+        },
+        isRPC: true,
+      });
+
+      const productById = {};
+
+      for (const product of products) {
+        productById[product._id] = product;
+      }
+
+      let totalDiscount = 0;
+
+      for (const item of order.productsData) {
+        let lineUrlP = "";
+        let linePostMethod = "POST";
+        let linePostHeaders = {
+          "Content-Type": "application/json",
+          Authorization: `Basic ${Buffer.from(
+            `${username}:${password}`
+          ).toString("base64")}`,
+        };
+
+        const lineNo = orderItemsMsdNo[item._id] || "";
+        const product = productById[item.productId];
+
+        if (!product) {
+          await models.SyncLogs.updateOne(
+            { _id: syncLog._id },
+            {
+              $set: {
+                error: `not found product ${product._id}`,
+              },
+            }
+          );
+          continue;
+        }
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix undefined product reference in error log.**

When logging an error for a missing product, the code references `product._id` which is undefined since the condition is `!product`.


```diff
        if (!product) {
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
-                error: `not found product ${product._id}`,
+                error: `not found product for ID: ${item.productId}`,
              },
            }
          );
          continue;
        }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        if (!product) {
          await models.SyncLogs.updateOne(
            { _id: syncLog._id },
            {
              $set: {
                error: `not found product for ID: ${item.productId}`,
              },
            }
          );
          continue;
        }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit ec0541d
created_at: 2025-04-14T05:52:30Z
author_association: NONE
diff_hunk: @@ -218,15 +218,326 @@ export const dealToDynamic = async (
   syncLog: ISyncLogDocument,
   params: any,
   models: IModels,
-  configs: any
+  config: any
 ) => {
   const order = params;
   const brandId = order.scopeBrandIds[0];
-  const config = configs[brandId || 'noBrand'];
 
   let msdCustomer: any = {};
 
-  let orderMsdNo: string = '';
+  let orderMsdNo: string = "";
+  let orderItemsMsdNo: any = {};
+  try {
+    const syncErkhetInfo = JSON.parse(order.syncErkhetInfo);
+    orderMsdNo = syncErkhetInfo.no;
+    orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+  } catch {
+    orderMsdNo = order.syncErkhetInfo;
+  }
+
+  try {
+    let customer;
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = "";
+    let lineUrlParam = "";
+    let postMethod = "POST";
+    const postHeaders = {
+      "Content-Type": "application/json",
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        "base64"
+      )}`,
+    };
+
+    const conformities = await sendCoreMessage({
+      subdomain,
+      action: "conformities.findConformities",
+      data: {
+        mainType: "deal",
+        mainTypeId: { $in: order._id },
+      },
+      isRPC: true,
+      defaultValue: [],
+    });
+
+    if (conformities.length > 0) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        conformities[0].relTypeId,
+        conformities[0].relType,
+        brandId,
+        config
+      );
+
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Ensure customer data exists before accessing properties.**

The code retrieves conformities and customer info but doesn't fully validate that the data exists before accessing properties.


```diff
    const conformities = await sendCoreMessage({
      subdomain,
      action: "conformities.findConformities",
      data: {
        mainType: "deal",
        mainTypeId: { $in: order._id },
      },
      isRPC: true,
      defaultValue: [],
    });

    if (conformities.length > 0) {
      const msdCustomerInfo = await getMsdCustomerInfo(
        subdomain,
        models,
        conformities[0].relTypeId,
        conformities[0].relType,
        brandId,
        config
      );

-      if (msdCustomerInfo) {
+      if (msdCustomerInfo && msdCustomerInfo.relation) {
        msdCustomer = msdCustomerInfo.relation?.response;
        customer = msdCustomerInfo.customer;
      }
    }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    const conformities = await sendCoreMessage({
      subdomain,
      action: "conformities.findConformities",
      data: {
        mainType: "deal",
        mainTypeId: { $in: order._id },
      },
      isRPC: true,
      defaultValue: [],
    });

    if (conformities.length > 0) {
      const msdCustomerInfo = await getMsdCustomerInfo(
        subdomain,
        models,
        conformities[0].relTypeId,
        conformities[0].relType,
        brandId,
        config
      );

      if (msdCustomerInfo && msdCustomerInfo.relation) {
        msdCustomer = msdCustomerInfo.relation?.response;
        customer = msdCustomerInfo.customer;
      }
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit ec0541d
created_at: 2025-04-14T05:52:30Z
author_association: NONE
diff_hunk: @@ -218,15 +218,326 @@ export const dealToDynamic = async (
   syncLog: ISyncLogDocument,
   params: any,
   models: IModels,
-  configs: any
+  config: any
 ) => {
   const order = params;
   const brandId = order.scopeBrandIds[0];
-  const config = configs[brandId || 'noBrand'];
 
   let msdCustomer: any = {};
 
-  let orderMsdNo: string = '';
+  let orderMsdNo: string = "";
+  let orderItemsMsdNo: any = {};
+  try {
+    const syncErkhetInfo = JSON.parse(order.syncErkhetInfo);
+    orderMsdNo = syncErkhetInfo.no;
+    orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+  } catch {
+    orderMsdNo = order.syncErkhetInfo;
+  }
+
+  try {
+    let customer;
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = "";
+    let lineUrlParam = "";
+    let postMethod = "POST";
+    const postHeaders = {
+      "Content-Type": "application/json",
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        "base64"
+      )}`,
+    };
+
+    const conformities = await sendCoreMessage({
+      subdomain,
+      action: "conformities.findConformities",
+      data: {
+        mainType: "deal",
+        mainTypeId: { $in: order._id },
+      },
+      isRPC: true,
+      defaultValue: [],
+    });
+
+    if (conformities.length > 0) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        conformities[0].relTypeId,
+        conformities[0].relType,
+        brandId,
+        config
+      );
+
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
+
+    const user = await sendCoreMessage({
+      subdomain,
+      action: "users.findOne",
+      data: {
+        _id: order.assignedUserIds[0],
+      },
+      isRPC: true,
+    });
+
+    const rawDescription = order.description.replace(/<\/?p>/g, "").trim();
+
+    const sellAddress = rawDescription.slice(0, 100);
+    const sellAddress2 = rawDescription.slice(100, 150);
+
+    const sendData: any = {
+      Sell_to_Customer_No: msdCustomer?.No
+        ? msdCustomer?.No
+        : config.defaultUserCode,
+      Sell_to_Phone_No: customer?.primaryPhone || "",
+      Sell_to_E_Mail: customer?.primaryEmail || "",
+      External_Document_No: `${order.name.split(":").pop().trim()}TESTBYERXES`,
+      Responsibility_Center: config.responsibilityCenter || "",
+      Sync_Type: config.syncType || "",
+      Mobile_Phone_No: customer?.primaryPhone || "",
+      VAT_Bus_Posting_Group: config.vatBusPostingGroup || "",
+      Payment_Terms_Code: config.paymentTermsCode || "",
+      Payment_Method_Code: config.paymentMethodCode || "CASH",
+      Customer_Price_Group: config.customerPricingGroup || "",
+      Prices_Including_VAT: true,
+      BillType: config.billType || "Receipt",
+      Location_Code: config.locationCode || "",
+      Deal_Type_Code: config.dealType || "NORMAL",
+      Salesperson_Code: user && user.employeeId,
+      Sell_to_Address: sellAddress,
+      Sell_to_Address_2: sellAddress2,
+      CustomerNo:
+        customer?.customFieldsDataByFieldCode?.vatCustomer?.value ||
+        customer?.customFieldsDataByFieldCode?.vatCompany?.value,
+    };
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for missing user information.**

The function retrieves user data but doesn't handle the case where user might not be found.


```diff
    const user = await sendCoreMessage({
      subdomain,
      action: "users.findOne",
      data: {
        _id: order.assignedUserIds[0],
      },
      isRPC: true,
+     defaultValue: null,
    });

+   if (!user && order.assignedUserIds && order.assignedUserIds.length > 0) {
+     console.warn(`User not found for ID: ${order.assignedUserIds[0]}`);
+   }

    const rawDescription = order.description.replace(/<\/?p>/g, "").trim();

    const sellAddress = rawDescription.slice(0, 100);
    const sellAddress2 = rawDescription.slice(100, 150);

    const sendData: any = {
      Sell_to_Customer_No: msdCustomer?.No
        ? msdCustomer?.No
        : config.defaultUserCode,
      Sell_to_Phone_No: customer?.primaryPhone || "",
      Sell_to_E_Mail: customer?.primaryEmail || "",
      External_Document_No: `${order.name.split(":").pop().trim()}TESTBYERXES`,
      Responsibility_Center: config.responsibilityCenter || "",
      Sync_Type: config.syncType || "",
      Mobile_Phone_No: customer?.primaryPhone || "",
      VAT_Bus_Posting_Group: config.vatBusPostingGroup || "",
      Payment_Terms_Code: config.paymentTermsCode || "",
      Payment_Method_Code: config.paymentMethodCode || "CASH",
      Customer_Price_Group: config.customerPricingGroup || "",
      Prices_Including_VAT: true,
      BillType: config.billType || "Receipt",
      Location_Code: config.locationCode || "",
      Deal_Type_Code: config.dealType || "NORMAL",
      Salesperson_Code: user && user.employeeId,
      Sell_to_Address: sellAddress,
      Sell_to_Address_2: sellAddress2,
      CustomerNo:
        customer?.customFieldsDataByFieldCode?.vatCustomer?.value ||
        customer?.customFieldsDataByFieldCode?.vatCompany?.value,
    };
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    const user = await sendCoreMessage({
      subdomain,
      action: "users.findOne",
      data: {
        _id: order.assignedUserIds[0],
      },
      isRPC: true,
      defaultValue: null,
    });

    if (!user && order.assignedUserIds && order.assignedUserIds.length > 0) {
      console.warn(`User not found for ID: ${order.assignedUserIds[0]}`);
    }

    const rawDescription = order.description.replace(/<\/?p>/g, "").trim();

    const sellAddress = rawDescription.slice(0, 100);
    const sellAddress2 = rawDescription.slice(100, 150);

    const sendData: any = {
      Sell_to_Customer_No: msdCustomer?.No
        ? msdCustomer?.No
        : config.defaultUserCode,
      Sell_to_Phone_No: customer?.primaryPhone || "",
      Sell_to_E_Mail: customer?.primaryEmail || "",
      External_Document_No: `${order.name.split(":").pop().trim()}TESTBYERXES`,
      Responsibility_Center: config.responsibilityCenter || "",
      Sync_Type: config.syncType || "",
      Mobile_Phone_No: customer?.primaryPhone || "",
      VAT_Bus_Posting_Group: config.vatBusPostingGroup || "",
      Payment_Terms_Code: config.paymentTermsCode || "",
      Payment_Method_Code: config.paymentMethodCode || "CASH",
      Customer_Price_Group: config.customerPricingGroup || "",
      Prices_Including_VAT: true,
      BillType: config.billType || "Receipt",
      Location_Code: config.locationCode || "",
      Deal_Type_Code: config.dealType || "NORMAL",
      Salesperson_Code: user && user.employeeId,
      Sell_to_Address: sellAddress,
      Sell_to_Address_2: sellAddress2,
      CustomerNo:
        customer?.customFieldsDataByFieldCode?.vatCustomer?.value ||
        customer?.customFieldsDataByFieldCode?.vatCompany?.value,
    };
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-14T05:52:30Z
author_association: NONE
diff_hunk: @@ -15,13 +15,19 @@ export default {
       subdomain,
       action: "users.findOne",
       data: { _id: syncLog.createdBy },
-      isRPC: true
+      isRPC: true,
     });
   },
 
   async content(syncLog: ISyncLogDocument, {}, {}: IContext) {
     const { contentType, contentId } = syncLog;
 
+    if (contentType === "sales:deal") {
+      return (
+        syncLog.consumeData?.object.name.split(":").pop().trim() || contentId
path: packages/plugin-msdynamic-api/src/graphql/resolvers/syncLog.ts
user: login: ellipsis-dev[bot]
body: Use optional chaining for `object` (e.g. `consumeData?.object?.name`) to avoid runtime errors when `object` is undefined.
```suggestion
        syncLog.consumeData?.object?.name?.split(":").pop().trim() || contentId
```
created_at: 2025-04-16T01:27:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,13 +15,19 @@ export default {
       subdomain,
       action: "users.findOne",
       data: { _id: syncLog.createdBy },
-      isRPC: true
+      isRPC: true,
     });
   },
 
   async content(syncLog: ISyncLogDocument, {}, {}: IContext) {
path: packages/plugin-msdynamic-api/src/graphql/resolvers/syncLog.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix the empty object pattern in resolver parameter**

The static analysis tool correctly identified an issue with empty object patterns in this resolver.


Consider refactoring this line to properly handle the GraphQL resolver parameters:

```diff
- async content(syncLog: ISyncLogDocument, {}, {}: IContext) {
+ async content(syncLog: ISyncLogDocument, _args: {}, { }: IContext) {
```

This pattern appears in other resolvers too, so consider standardizing the approach across all resolvers.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  async content(syncLog: ISyncLogDocument, _args: {}, { }: IContext) {
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 22-22: Unexpected empty object pattern.



(lint/correctness/noEmptyPattern)

---

[error] 22-22: Unexpected empty object pattern.



(lint/correctness/noEmptyPattern)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-16T01:30:18Z
author_association: NONE
diff_hunk: @@ -73,17 +74,41 @@ export const afterMutationHandlers = async (subdomain, params) => {
       return;
     }
 
+    if (type === 'sales:deal') {
+      const updatedDoc = params.updatedDocument || params.object;
+
+      const array = Object.values(configs) as any[];
+
+      const foundConfig = array.find(
+        (config) => config.stageId === updatedDoc?.stageId
+      );
+
+      if (action === 'update' && foundConfig?.useBoard) {
+        if (foundConfig.stageId === updatedDoc?.stageId) {
path: packages/plugin-msdynamic-api/src/afterMutations.ts
user: login: ellipsis-dev[bot]
body: Redundant check: The inner `if (foundConfig.stageId === updatedDoc?.stageId)` is unnecessary since `foundConfig` is already chosen by `stageId`.
```suggestion

```
created_at: 2025-04-17T04:47:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -218,11 +218,322 @@ export const dealToDynamic = async (
   syncLog: ISyncLogDocument,
   params: any,
   models: IModels,
-  configs: any
+  config: any
+) => {
+  const order = params;
+  const brandId = config.branId;
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: ellipsis-dev[bot]
body: Typo: In the `dealToDynamic` function, the property `branId` on the config object is likely a typo and should be corrected to `brandId` for consistency with its usage elsewhere in the file.
```suggestion
  const brandId = config.brandId;
```
created_at: 2025-04-17T04:47:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -218,11 +218,322 @@ export const dealToDynamic = async (
   syncLog: ISyncLogDocument,
   params: any,
   models: IModels,
-  configs: any
+  config: any
+) => {
+  const order = params;
+  const brandId = config.branId;
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix the typo in `branId`**

There's a typo in the variable name `branId` which should be `brandId`. This could lead to undefined values being used in the rest of the function.

```diff
-  const brandId = config.branId;
+  const brandId = config.brandId;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const brandId = config.brandId;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-17T04:49:28Z
author_association: NONE
diff_hunk: @@ -215,14 +215,322 @@ export const consumeCategory = async (
 
 export const dealToDynamic = async (
   subdomain: string,
+  models: IModels,
   syncLog: ISyncLogDocument,
-  params: any,
+  deal: any,
+  config: any
+) => {
+  const order = deal;
+  const brandId = config.branId;
+
+  let msdCustomer: any = {};
+
+  let orderMsdNo: string = '';
+  let orderItemsMsdNo: any = {};
+  const extraData = order.extraData || {};
+  const syncErkhetInfo = extraData.msdynamic;
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: ellipsis-dev[bot]
body: Verify that `extraData.msdynamic` exists before accessing its properties to avoid runtime errors (e.g., `syncErkhetInfo` might be undefined).
```suggestion
  const syncErkhetInfo = extraData.msdynamic || {};
```

created_at: 2025-04-21T08:53:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -215,14 +215,321 @@ export const consumeCategory = async (
 
 export const dealToDynamic = async (
   subdomain: string,
+  models: IModels,
   syncLog: ISyncLogDocument,
-  params: any,
+  deal: any,
+  config: any
+) => {
+  const brandId = config.branId;
+
+  let msdCustomer: any = {};
+
+  let orderMsdNo: string = '';
+  let orderItemsMsdNo: any = {};
+  const extraData = deal.extraData || {};
+  const syncErkhetInfo = extraData.msdynamic || {};
+  orderMsdNo = syncErkhetInfo.no;
+  orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+
+  try {
+    let customer;
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = '';
+    let lineUrlParam = '';
+    let postMethod = 'POST';
+    const postHeaders = {
+      'Content-Type': 'application/json',
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        'base64'
+      )}`,
+    };
+
+    const conformities = await sendCoreMessage({
+      subdomain,
+      action: 'conformities.findConformities',
+      data: {
+        mainType: 'deal',
+        mainTypeId: { $in: deal._id },
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: ellipsis-dev[bot]
body: Ensure that `deal._id` is an array when using the `$in` operator. If it’s a single value, consider using `[$in: [deal._id]]` to meet MongoDB's expectations.
```suggestion
        mainTypeId: { $in: Array.isArray(deal._id) ? deal._id : [deal._id] },
```

created_at: 2025-04-21T09:08:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -215,14 +215,321 @@
 
 export const dealToDynamic = async (
   subdomain: string,
+  models: IModels,
   syncLog: ISyncLogDocument,
-  params: any,
+  deal: any,
+  config: any
+) => {
+  const brandId = config.branId;
+
+  let msdCustomer: any = {};
+
+  let orderMsdNo: string = '';
+  let orderItemsMsdNo: any = {};
+  const extraData = deal.extraData || {};
+  const syncErkhetInfo = extraData.msdynamic || {};
+  orderMsdNo = syncErkhetInfo.no;
+  orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+
+  try {
+    let customer;
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = '';
+    let lineUrlParam = '';
+    let postMethod = 'POST';
+    const postHeaders = {
+      'Content-Type': 'application/json',
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        'base64'
+      )}`,
+    };
+
+    const conformities = await sendCoreMessage({
+      subdomain,
+      action: 'conformities.findConformities',
+      data: {
+        mainType: 'deal',
+        mainTypeId: { $in: deal._id },
+      },
+      isRPC: true,
+      defaultValue: [],
+    });
+
+    if (conformities.length > 0) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        conformities[0].relTypeId,
+        conformities[0].relType,
+        brandId,
+        config
+      );
+
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
+
+    const user = await sendCoreMessage({
+      subdomain,
+      action: 'users.findOne',
+      data: {
+        _id: deal.assignedUserIds[0],
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: ellipsis-dev[bot]
body: Accessing `deal.assignedUserIds[0]` may throw an error if the array is empty or undefined. Validate this field before use.
```suggestion
        _id: deal.assignedUserIds?.[0],
```

created_at: 2025-04-21T09:08:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -215,14 +215,321 @@ export const consumeCategory = async (
 
 export const dealToDynamic = async (
   subdomain: string,
+  models: IModels,
   syncLog: ISyncLogDocument,
-  params: any,
+  deal: any,
+  config: any
+) => {
+  const brandId = config.branId;
+
+  let msdCustomer: any = {};
+
+  let orderMsdNo: string = '';
+  let orderItemsMsdNo: any = {};
+  const extraData = deal.extraData || {};
+  const syncErkhetInfo = extraData.msdynamic || {};
+  orderMsdNo = syncErkhetInfo.no;
+  orderItemsMsdNo = syncErkhetInfo.lineNos || {};
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Safely access extraData.msdynamic properties.**

The code should verify `extraData.msdynamic` exists before accessing its properties.


```diff
-  const syncErkhetInfo = extraData.msdynamic || {};
+  const syncErkhetInfo = extraData?.msdynamic || {};
   orderMsdNo = syncErkhetInfo.no;
   orderItemsMsdNo = syncErkhetInfo.lineNos || {};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const syncErkhetInfo = extraData?.msdynamic || {};
  orderMsdNo = syncErkhetInfo.no;
  orderItemsMsdNo = syncErkhetInfo.lineNos || {};
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-21T09:21:40Z
author_association: NONE
diff_hunk: @@ -215,14 +215,321 @@
 
 export const dealToDynamic = async (
   subdomain: string,
+  models: IModels,
   syncLog: ISyncLogDocument,
-  params: any,
+  deal: any,
+  config: any
+) => {
+  const brandId = config.branId;
+
+  let msdCustomer: any = {};
+
+  let orderMsdNo: string = '';
+  let orderItemsMsdNo: any = {};
+  const extraData = deal.extraData || {};
+  const syncErkhetInfo = extraData.msdynamic || {};
+  orderMsdNo = syncErkhetInfo.no;
+  orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+
+  try {
+    let customer;
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = '';
+    let lineUrlParam = '';
+    let postMethod = 'POST';
+    const postHeaders = {
+      'Content-Type': 'application/json',
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        'base64'
+      )}`,
+    };
+
+    const conformities = await sendCoreMessage({
+      subdomain,
+      action: 'conformities.findConformities',
+      data: {
+        mainType: 'deal',
+        mainTypeId: { $in: deal._id },
+      },
+      isRPC: true,
+      defaultValue: [],
+    });
+
+    if (conformities.length > 0) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        conformities[0].relTypeId,
+        conformities[0].relType,
+        brandId,
+        config
+      );
+
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
+
+    const user = await sendCoreMessage({
+      subdomain,
+      action: 'users.findOne',
+      data: {
+        _id: deal.assignedUserIds[0],
+      },
+      isRPC: true,
+    });
+
+    const rawDescription = deal.description.replace(/<\/?p>/g, '').trim();
+
+    const sellAddress = rawDescription.slice(0, 100);
+    const sellAddress2 = rawDescription.slice(100, 150);
+
+    const sendData: any = {
+      Sell_to_Customer_No: msdCustomer?.No
+        ? msdCustomer?.No
+        : config.defaultUserCode,
+      Sell_to_Phone_No: customer?.primaryPhone || '',
+      Sell_to_E_Mail: customer?.primaryEmail || '',
+      External_Document_No: deal.number,
+      Responsibility_Center: config.responsibilityCenter || '',
+      Sync_Type: config.syncType || '',
+      Mobile_Phone_No: customer?.primaryPhone || '',
+      VAT_Bus_Posting_Group: config.vatBusPostingGroup || '',
+      Payment_Terms_Code: config.paymentTermsCode || '',
+      Payment_Method_Code: config.paymentMethodCode || 'CASH',
+      Customer_Price_Group: config.customerPricingGroup || '',
+      Prices_Including_VAT: true,
+      BillType: config.billType || 'Receipt',
+      Location_Code: config.locationCode || '',
+      Deal_Type_Code: config.dealType || 'NORMAL',
+      Salesperson_Code: user && user.employeeId,
+      Sell_to_Address: sellAddress,
+      Sell_to_Address_2: sellAddress2,
+      CustomerNo:
+        customer?.customFieldsDataByFieldCode?.vatCustomer?.value ||
+        customer?.customFieldsDataByFieldCode?.vatCompany?.value,
+    };
+
+    if (orderMsdNo) {
+      urlParam = `(Document_Type='Order', No='${orderMsdNo}')`;
+      lineUrlParam = `(Document_Type='Order',Document_No='${orderMsdNo}',Line_No=%Ln)`;
+      postMethod = 'PATCH';
+      postHeaders['If-Match'] = '*';
+      sendData.No = orderMsdNo;
+    }
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          sendData,
+          sendStr: JSON.stringify(sendData),
+        },
+      }
+    );
+
+    if (!deal.productsData.length) {
+      throw new Error('Has not items order');
+    }
+
+    const responseSale = await fetch(`${salesApi}${urlParam}`, {
+      method: postMethod,
+      headers: postHeaders,
+      body: JSON.stringify(sendData),
+    }).then((res) => res.json());
+    const lineNoById = {};
+
+    if (responseSale) {
+      const products = await sendCoreMessage({
+        subdomain,
+        action: 'products.find',
+        data: {
+          _id: { $in: deal.productsData.map((item) => item.productId) },
+        },
+        isRPC: true,
+      });
+
+      const productById = {};
+
+      for (const product of products) {
+        productById[product._id] = product;
+      }
+
+      let totalDiscount = 0;
+
+      for (const item of deal.productsData) {
+        let lineUrlP = '';
+        let linePostMethod = 'POST';
+        let linePostHeaders = {
+          'Content-Type': 'application/json',
+          Authorization: `Basic ${Buffer.from(
+            `${username}:${password}`
+          ).toString('base64')}`,
+        };
+
+        const lineNo = orderItemsMsdNo[item._id] || '';
+        const product = productById[item.productId];
+
+        if (!product) {
+          await models.SyncLogs.updateOne(
+            { _id: syncLog._id },
+            {
+              $set: {
+                error: `not found product ${product._id}`,
+              },
+            }
+          );
+          continue;
+        }
+
+        const sendSalesLine: any = {
+          Document_No: responseSale.No,
+          Type: 'Item',
+          No: productById[item.productId]
+            ? productById[item.productId].code
+            : '',
+          Quantity: item.quantity || 0,
+          Location_Code: config.locationCode,
+        };
+
+        if (lineNo) {
+          lineUrlP = lineUrlParam.replace('%Ln', lineNo);
+          linePostMethod = 'PATCH';
+          linePostHeaders['If-Match'] = '*';
+          sendSalesLine.Line_No = lineNo;
+        }
+
+        await models.SyncLogs.updateOne(
+          { _id: syncLog._id },
+          {
+            $push: {
+              sendSales: JSON.stringify(sendSalesLine),
+            },
+          }
+        );
+
+        const responseSaleLine = await fetch(`${salesLineApi}${lineUrlP}`, {
+          method: linePostMethod,
+          headers: linePostHeaders,
+          body: JSON.stringify(sendSalesLine),
+        }).then((res) => res.json());
+
+        if (responseSaleLine?.error?.message) {
+          const foundSyncLog = (await models.SyncLogs.findOne({
+            _id: syncLog._id,
+          })) || { error: '' };
+          await models.SyncLogs.updateOne(
+            { _id: syncLog._id },
+            {
+              $set: {
+                error: `${foundSyncLog.error ? foundSyncLog.error : ''} - ${responseSaleLine.error.message
+                  }`,
+              },
+            }
+          );
+        }
+
+        await models.SyncLogs.updateOne(
+          { _id: syncLog._id },
+          {
+            $push: {
+              responseSales: JSON.stringify(responseSaleLine),
+            },
+          }
+        );
+        lineNoById[item._id] = lineNo || responseSaleLine.Line_No;
+        totalDiscount += item.discountAmount ?? 0;
+      }
+
+      const soapApiUrl = config.discountSoapApi; // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
+      if (soapApiUrl && totalDiscount) {
+        const soapHeaders = {
+          'Content-Type': 'text/xml',
+          SOAPAction:
+            'urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount',
+          Authorization: `Basic ${Buffer.from(
+            `${username}:${password}`
+          ).toString('base64')}`,
+        };
+        const soapDiscountSendData = `
+          <?xml version="1.0"?>
+          <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+                    xmlns:tns="urn:microsoft-dynamics-schemas/codeunit/PictureService">
+          <soapenv:Header/>
+          <soapenv:Body>
+            <tns:ReCalculateSalesOrderDiscount>
+              <tns:code>${responseSale.No}</tns:code>
+              <tns:invoiceDiscountAmount>${totalDiscount}</tns:invoiceDiscountAmount>
+            </tns:ReCalculateSalesOrderDiscount>
+          </soapenv:Body>
+          </soapenv:Envelope>
+        `;
+        await fetch(`${soapApiUrl}`, {
+          method: 'POST',
+          headers: soapHeaders,
+          body: soapDiscountSendData,
+        });
+      }
+    }
+
+    await sendSalesMessage({
+      subdomain,
+      action: 'deals.updateOne',
+      data: {
+        selector: { _id: deal._id },
+        modifier: {
+          $set: {
+            extraData: {
+              ...extraData || {},
+              msdynamic: {
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Fix spread operator with nullable object.**

The spread operator should have parentheses around the OR expression to handle undefined values properly.


```diff
-              ...extraData || {},
+              ...(extraData || {}),
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
              ...(extraData || {}),
              msdynamic: {
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 5b5f6a2
created_at: 2025-04-21T09:21:41Z
author_association: NONE
diff_hunk: @@ -215,14 +219,322 @@ export const consumeCategory = async (
 
 export const dealToDynamic = async (
   subdomain: string,
+  models: IModels,
   syncLog: ISyncLogDocument,
-  params: any,
+  deal: any,
+  config: any
+) => {
+  const brandId = config.branId;
+
+  let msdCustomer: any = {};
+
+  let orderMsdNo: string = '';
+  let orderItemsMsdNo: any = {};
+  const extraData = deal.extraData || {};
+  const syncErkhetInfo = extraData.msdynamic || {};
+  orderMsdNo = syncErkhetInfo.no;
+  orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+
+  try {
+    let customer;
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = '';
+    let lineUrlParam = '';
+    let postMethod = 'POST';
+    const postHeaders = {
+      'Content-Type': 'application/json',
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        'base64'
+      )}`,
+    };
+
+    const conformities = await sendCoreMessage({
+      subdomain,
+      action: 'conformities.findConformities',
+      data: {
+        mainType: 'deal',
+        mainTypeId: { $in: deal._id },
+      },
+      isRPC: true,
+      defaultValue: [],
+    });
+
+    if (conformities.length > 0) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        conformities[0].relTypeId,
+        conformities[0].relType,
+        brandId,
+        config
+      );
+
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
+
+    const user = await sendCoreMessage({
+      subdomain,
+      action: 'users.findOne',
+      data: {
+        _id: deal.assignedUserIds[0],
+      },
+      isRPC: true,
+    });
+
+    const rawDescription = deal.description.replace(/<\/?p>/g, '').trim();
+
+    const sellAddress = rawDescription.slice(0, 100);
+    const sellAddress2 = rawDescription.slice(100, 150);
+
+    const sendData: any = {
+      Sell_to_Customer_No: msdCustomer?.No
+        ? msdCustomer?.No
+        : config.defaultUserCode,
+      Sell_to_Phone_No: customer?.primaryPhone || '',
+      Sell_to_E_Mail: customer?.primaryEmail || '',
+      External_Document_No: deal.name,
+      Responsibility_Center: config.responsibilityCenter || '',
+      Sync_Type: config.syncType || '',
+      Mobile_Phone_No: customer?.primaryPhone || '',
+      VAT_Bus_Posting_Group: config.vatBusPostingGroup || '',
+      Payment_Terms_Code: config.paymentTermsCode || '',
+      Payment_Method_Code: config.paymentMethodCode || 'CASH',
+      Customer_Price_Group: config.customerPricingGroup || '',
+      Prices_Including_VAT: true,
+      BillType: config.billType || 'Receipt',
+      Location_Code: config.locationCode || '',
+      Deal_Type_Code: config.dealType || 'NORMAL',
+      Salesperson_Code: user && user.employeeId,
+      Sell_to_Address: sellAddress,
+      Sell_to_Address_2: sellAddress2,
+      CustomerNo:
+        customer?.customFieldsDataByFieldCode?.vatCustomer?.value ||
+        customer?.customFieldsDataByFieldCode?.vatCompany?.value,
+    };
+
+    if (orderMsdNo) {
+      urlParam = `(Document_Type='Order', No='${orderMsdNo}')`;
+      lineUrlParam = `(Document_Type='Order',Document_No='${orderMsdNo}',Line_No=%Ln)`;
+      postMethod = 'PATCH';
+      postHeaders['If-Match'] = '*';
+      sendData.No = orderMsdNo;
+    }
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          sendData,
+          sendStr: JSON.stringify(sendData),
+        },
+      }
+    );
+
+    if (!deal.productsData.length) {
+      throw new Error('Has not items order');
+    }
+
+    const responseSale = await fetch(`${salesApi}${urlParam}`, {
+      method: postMethod,
+      headers: postHeaders,
+      body: JSON.stringify(sendData),
+    }).then((res) => res.json());
+    const lineNoById = {};
+
+    if (responseSale) {
+      const products = await sendCoreMessage({
+        subdomain,
+        action: 'products.find',
+        data: {
+          _id: { $in: deal.productsData.map((item) => item.productId) },
+        },
+        isRPC: true,
+      });
+
+      const productById = {};
+
+      for (const product of products) {
+        productById[product._id] = product;
+      }
+
+      let totalDiscount = 0;
+
+      for (const item of deal.productsData) {
+        let lineUrlP = '';
+        let linePostMethod = 'POST';
+        let linePostHeaders = {
+          'Content-Type': 'application/json',
+          Authorization: `Basic ${Buffer.from(
+            `${username}:${password}`
+          ).toString('base64')}`,
+        };
+
+        const lineNo = orderItemsMsdNo[item._id] || '';
+        const product = productById[item.productId];
+
+        if (!product) {
+          await models.SyncLogs.updateOne(
+            { _id: syncLog._id },
+            {
+              $set: {
+                error: `not found product ${product._id}`,
+              },
+            }
+          );
+          continue;
+        }
+
+        const sendSalesLine: any = {
+          Document_No: responseSale.No,
+          Type: 'Item',
+          No: productById[item.productId]
+            ? productById[item.productId].code
+            : '',
+          Quantity: item.quantity || 0,
+          Location_Code: config.locationCode,
+        };
+
+        if (lineNo) {
+          lineUrlP = lineUrlParam.replace('%Ln', lineNo);
+          linePostMethod = 'PATCH';
+          linePostHeaders['If-Match'] = '*';
+          sendSalesLine.Line_No = lineNo;
+        }
+
+        await models.SyncLogs.updateOne(
+          { _id: syncLog._id },
+          {
+            $push: {
+              sendSales: JSON.stringify(sendSalesLine),
+            },
+          }
+        );
+
+        const responseSaleLine = await fetch(`${salesLineApi}${lineUrlP}`, {
+          method: linePostMethod,
+          headers: linePostHeaders,
+          body: JSON.stringify(sendSalesLine),
+        }).then((res) => res.json());
+
+        if (responseSaleLine?.error?.message) {
+          const foundSyncLog = (await models.SyncLogs.findOne({
+            _id: syncLog._id,
+          })) || { error: '' };
+          await models.SyncLogs.updateOne(
+            { _id: syncLog._id },
+            {
+              $set: {
+                error: `${foundSyncLog.error ? foundSyncLog.error : ''} - ${
+                  responseSaleLine.error.message
+                }`,
+              },
+            }
+          );
+        }
+
+        await models.SyncLogs.updateOne(
+          { _id: syncLog._id },
+          {
+            $push: {
+              responseSales: JSON.stringify(responseSaleLine),
+            },
+          }
+        );
+        lineNoById[item._id] = lineNo || responseSaleLine.Line_No;
+        totalDiscount += item.discountAmount ?? 0;
+      }
+
+      const soapApiUrl = config.discountSoapApi; // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
+      if (soapApiUrl && totalDiscount) {
+        const soapHeaders = {
+          'Content-Type': 'text/xml',
+          SOAPAction:
+            'urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount',
+          Authorization: `Basic ${Buffer.from(
+            `${username}:${password}`
+          ).toString('base64')}`,
+        };
+        const soapDiscountSendData = `
+          <?xml version="1.0"?>
+          <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+                    xmlns:tns="urn:microsoft-dynamics-schemas/codeunit/PictureService">
+          <soapenv:Header/>
+          <soapenv:Body>
+            <tns:ReCalculateSalesOrderDiscount>
+              <tns:code>${responseSale.No}</tns:code>
+              <tns:invoiceDiscountAmount>${totalDiscount}</tns:invoiceDiscountAmount>
+            </tns:ReCalculateSalesOrderDiscount>
+          </soapenv:Body>
+          </soapenv:Envelope>
+        `;
+        await fetch(`${soapApiUrl}`, {
+          method: 'POST',
+          headers: soapHeaders,
+          body: soapDiscountSendData,
+        });
+      }
+    }
+
+    await sendSalesMessage({
+      subdomain,
+      action: 'deals.updateOne',
+      data: {
+        selector: { _id: deal._id },
+        modifier: {
+          $set: {
+            extraData: {
+              ...(extraData || {}),
+              msdynamic: {
+                no: orderMsdNo || responseSale.No,
+                lineNos: lineNoById,
+              },
+            },
+          },
+        },
+      },
+      isRPC: true,
+    });
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          responseData: responseSale,
+          responseStr: JSON.stringify(responseSale),
+        },
+      }
+    );
+
+    return responseSale;
+  } catch (e) {
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      { $set: { error: e.message } }
+    );
+    console.log(e, 'error');
+  }
+};
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Review of dealToDynamic function implementation.**

The dealToDynamic function has been implemented to handle MS Dynamics synchronization for deals, including API calls, customer info retrieval, and error handling. The function aligns with the PR objective of adding sales deal synchronization.

A few other issues to address:
1. The External_Document_No field is using the raw deal.name value without any special processing. Consider if this needs formatting similarly to the orderToDynamic function.
2. Consider adding validation for deal.assignedUserIds and deal.productsData before using them.

Overall, the implementation looks comprehensive and handles the main synchronization tasks effectively.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check consistency in External_Document_No field formatting between deal and order sync

echo "Checking External_Document_No field formatting in dealToDynamic and orderToDynamic functions:"
echo "---"
echo "dealToDynamic uses: External_Document_No: deal.name"
echo "orderToDynamic uses: External_Document_No: order.number"
echo "---"
echo "These should likely be formatted similarly for consistency."
```

Length of output: 589

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate orderToDynamic implementation and inspect External_Document_No usage
rg -n "export const orderToDynamic" packages/plugin-msdynamic-api/src/utils.ts
echo
echo "Snippet around External_Document_No in orderToDynamic:"
rg -C3 "External_Document_No" -n packages/plugin-msdynamic-api/src/utils.ts
```

Length of output: 1059

---



</details>

**dealToDynamic: External_Document_No consistency & input validation**

• External_Document_No is set to `deal.name` (raw), which mirrors `orderToDynamic`’s use of raw `order.number`. If you need to enforce specific formatting (e.g. trimming, sanitization, length limits), extract a shared helper and apply it in both functions.  
• Add upfront validation for critical inputs to prevent runtime errors:  
  – Ensure `deal.assignedUserIds` is a non‑empty array before accessing `[0]`.  
  – Ensure `deal.productsData` is an array with items having `productId` and `quantity`.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-21T11:32:50Z
author_association: NONE
diff_hunk: @@ -215,14 +219,322 @@ export const consumeCategory = async (
 
 export const dealToDynamic = async (
   subdomain: string,
+  models: IModels,
   syncLog: ISyncLogDocument,
-  params: any,
+  deal: any,
+  config: any
+) => {
+  const brandId = config.branId;
+
+  let msdCustomer: any = {};
+
+  let orderMsdNo: string = '';
+  let orderItemsMsdNo: any = {};
+  const extraData = deal.extraData || {};
+  const syncErkhetInfo = extraData.msdynamic || {};
+  orderMsdNo = syncErkhetInfo.no;
+  orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+
+  try {
+    let customer;
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = '';
+    let lineUrlParam = '';
+    let postMethod = 'POST';
+    const postHeaders = {
+      'Content-Type': 'application/json',
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        'base64'
+      )}`,
+    };
+
+    const conformities = await sendCoreMessage({
+      subdomain,
+      action: 'conformities.findConformities',
+      data: {
+        mainType: 'deal',
+        mainTypeId: { $in: deal._id },
+      },
+      isRPC: true,
+      defaultValue: [],
+    });
+
+    if (conformities.length > 0) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        conformities[0].relTypeId,
+        conformities[0].relType,
+        brandId,
+        config
+      );
+
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
+
+    const user = await sendCoreMessage({
+      subdomain,
+      action: 'users.findOne',
+      data: {
+        _id: deal.assignedUserIds[0],
+      },
+      isRPC: true,
+    });
+
+    const rawDescription = deal.description.replace(/<\/?p>/g, '').trim();
+
+    const sellAddress = rawDescription.slice(0, 100);
+    const sellAddress2 = rawDescription.slice(100, 150);
+
+    const sendData: any = {
+      Sell_to_Customer_No: msdCustomer?.No
+        ? msdCustomer?.No
+        : config.defaultUserCode,
+      Sell_to_Phone_No: customer?.primaryPhone || '',
+      Sell_to_E_Mail: customer?.primaryEmail || '',
+      External_Document_No: deal.number || deal.name.split(':').pop().trim(),
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: ellipsis-dev[bot]
body: Potential runtime error: if `deal.name` is undefined or not a string, calling `split` on it may throw. Consider adding a guard (e.g. `deal.name && typeof deal.name === 'string'`) before using `split`, or defaulting to a safe value.
```suggestion
      External_Document_No: deal.number || (deal.name && typeof deal.name === 'string' ? deal.name.split(':').pop().trim() : ''),
```

created_at: 2025-04-21T11:41:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -215,14 +219,322 @@ export const consumeCategory = async (
 
 export const dealToDynamic = async (
   subdomain: string,
+  models: IModels,
   syncLog: ISyncLogDocument,
-  params: any,
+  deal: any,
+  config: any
+) => {
+  const brandId = config.branId;
+
+  let msdCustomer: any = {};
+
+  let orderMsdNo: string = '';
+  let orderItemsMsdNo: any = {};
+  const extraData = deal.extraData || {};
+  const syncErkhetInfo = extraData.msdynamic || {};
+  orderMsdNo = syncErkhetInfo.no;
+  orderItemsMsdNo = syncErkhetInfo.lineNos || {};
+
+  try {
+    let customer;
+    if (
+      !config?.customerApi ||
+      !config?.salesApi ||
+      !config?.salesLineApi ||
+      !config?.username ||
+      !config?.password
+    ) {
+      throw new Error(`MS Dynamic config not found..., ${brandId}`);
+    }
+    const { salesApi, salesLineApi, username, password } = config;
+    let urlParam = '';
+    let lineUrlParam = '';
+    let postMethod = 'POST';
+    const postHeaders = {
+      'Content-Type': 'application/json',
+      Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
+        'base64'
+      )}`,
+    };
+
+    const conformities = await sendCoreMessage({
+      subdomain,
+      action: 'conformities.findConformities',
+      data: {
+        mainType: 'deal',
+        mainTypeId: { $in: deal._id },
+      },
+      isRPC: true,
+      defaultValue: [],
+    });
+
+    if (conformities.length > 0) {
+      const msdCustomerInfo = await getMsdCustomerInfo(
+        subdomain,
+        models,
+        conformities[0].relTypeId,
+        conformities[0].relType,
+        brandId,
+        config
+      );
+
+      if (msdCustomerInfo) {
+        msdCustomer = msdCustomerInfo.relation?.response;
+        customer = msdCustomerInfo.customer;
+      }
+    }
+
+    const user = await sendCoreMessage({
+      subdomain,
+      action: 'users.findOne',
+      data: {
+        _id: deal.assignedUserIds[0],
+      },
+      isRPC: true,
+    });
+
+    const rawDescription = deal.description.replace(/<\/?p>/g, '').trim();
+
+    const sellAddress = rawDescription.slice(0, 100);
+    const sellAddress2 = rawDescription.slice(100, 150);
+
+    const sendData: any = {
+      Sell_to_Customer_No: msdCustomer?.No
+        ? msdCustomer?.No
+        : config.defaultUserCode,
+      Sell_to_Phone_No: customer?.primaryPhone || '',
+      Sell_to_E_Mail: customer?.primaryEmail || '',
+      External_Document_No: deal.number || deal.name.split(':').pop().trim(),
+      Responsibility_Center: config.responsibilityCenter || '',
+      Sync_Type: config.syncType || '',
+      Mobile_Phone_No: customer?.primaryPhone || '',
+      VAT_Bus_Posting_Group: config.vatBusPostingGroup || '',
+      Payment_Terms_Code: config.paymentTermsCode || '',
+      Payment_Method_Code: config.paymentMethodCode || 'CASH',
+      Customer_Price_Group: config.customerPricingGroup || '',
+      Prices_Including_VAT: true,
+      BillType: config.billType || 'Receipt',
+      Location_Code: config.locationCode || '',
+      Deal_Type_Code: config.dealType || 'NORMAL',
+      Salesperson_Code: user && user.employeeId,
+      Sell_to_Address: sellAddress,
+      Sell_to_Address_2: sellAddress2,
+      CustomerNo:
+        customer?.customFieldsDataByFieldCode?.vatCustomer?.value ||
+        customer?.customFieldsDataByFieldCode?.vatCompany?.value,
+    };
+
+    if (orderMsdNo) {
+      urlParam = `(Document_Type='Order', No='${orderMsdNo}')`;
+      lineUrlParam = `(Document_Type='Order',Document_No='${orderMsdNo}',Line_No=%Ln)`;
+      postMethod = 'PATCH';
+      postHeaders['If-Match'] = '*';
+      sendData.No = orderMsdNo;
+    }
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          sendData,
+          sendStr: JSON.stringify(sendData),
+        },
+      }
+    );
+
+    if (!deal.productsData.length) {
+      throw new Error('Has not items order');
+    }
+
+    const responseSale = await fetch(`${salesApi}${urlParam}`, {
+      method: postMethod,
+      headers: postHeaders,
+      body: JSON.stringify(sendData),
+    }).then((res) => res.json());
+    const lineNoById = {};
+
+    if (responseSale) {
+      const products = await sendCoreMessage({
+        subdomain,
+        action: 'products.find',
+        data: {
+          _id: { $in: deal.productsData.map((item) => item.productId) },
+        },
+        isRPC: true,
+      });
+
+      const productById = {};
+
+      for (const product of products) {
+        productById[product._id] = product;
+      }
+
+      let totalDiscount = 0;
+
+      for (const item of deal.productsData) {
+        let lineUrlP = '';
+        let linePostMethod = 'POST';
+        let linePostHeaders = {
+          'Content-Type': 'application/json',
+          Authorization: `Basic ${Buffer.from(
+            `${username}:${password}`
+          ).toString('base64')}`,
+        };
+
+        const lineNo = orderItemsMsdNo[item._id] || '';
+        const product = productById[item.productId];
+
+        if (!product) {
+          await models.SyncLogs.updateOne(
+            { _id: syncLog._id },
+            {
+              $set: {
+                error: `not found product ${product._id}`,
+              },
+            }
+          );
+          continue;
+        }
+
+        const sendSalesLine: any = {
+          Document_No: responseSale.No,
+          Type: 'Item',
+          No: productById[item.productId]
+            ? productById[item.productId].code
+            : '',
+          Quantity: item.quantity || 0,
+          Location_Code: config.locationCode,
+        };
+
+        if (lineNo) {
+          lineUrlP = lineUrlParam.replace('%Ln', lineNo);
+          linePostMethod = 'PATCH';
+          linePostHeaders['If-Match'] = '*';
+          sendSalesLine.Line_No = lineNo;
+        }
+
+        await models.SyncLogs.updateOne(
+          { _id: syncLog._id },
+          {
+            $push: {
+              sendSales: JSON.stringify(sendSalesLine),
+            },
+          }
+        );
+
+        const responseSaleLine = await fetch(`${salesLineApi}${lineUrlP}`, {
+          method: linePostMethod,
+          headers: linePostHeaders,
+          body: JSON.stringify(sendSalesLine),
+        }).then((res) => res.json());
+
+        if (responseSaleLine?.error?.message) {
+          const foundSyncLog = (await models.SyncLogs.findOne({
+            _id: syncLog._id,
+          })) || { error: '' };
+          await models.SyncLogs.updateOne(
+            { _id: syncLog._id },
+            {
+              $set: {
+                error: `${foundSyncLog.error ? foundSyncLog.error : ''} - ${
+                  responseSaleLine.error.message
+                }`,
+              },
+            }
+          );
+        }
+
+        await models.SyncLogs.updateOne(
+          { _id: syncLog._id },
+          {
+            $push: {
+              responseSales: JSON.stringify(responseSaleLine),
+            },
+          }
+        );
+        lineNoById[item._id] = lineNo || responseSaleLine.Line_No;
+        totalDiscount += item.discountAmount ?? 0;
+      }
+
+      const soapApiUrl = config.discountSoapApi; // "https://bc.erpmsm.mn:7047/MSM-Data/WS/Betastar%20LLC/Codeunit/PictureService";
+      if (soapApiUrl && totalDiscount) {
+        const soapHeaders = {
+          'Content-Type': 'text/xml',
+          SOAPAction:
+            'urn:microsoft-dynamics-schemas/codeunit/PictureService:ReCalculateSalesOrderDiscount',
+          Authorization: `Basic ${Buffer.from(
+            `${username}:${password}`
+          ).toString('base64')}`,
+        };
+        const soapDiscountSendData = `
+          <?xml version="1.0"?>
+          <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+                    xmlns:tns="urn:microsoft-dynamics-schemas/codeunit/PictureService">
+          <soapenv:Header/>
+          <soapenv:Body>
+            <tns:ReCalculateSalesOrderDiscount>
+              <tns:code>${responseSale.No}</tns:code>
+              <tns:invoiceDiscountAmount>${totalDiscount}</tns:invoiceDiscountAmount>
+            </tns:ReCalculateSalesOrderDiscount>
+          </soapenv:Body>
+          </soapenv:Envelope>
+        `;
+        await fetch(`${soapApiUrl}`, {
+          method: 'POST',
+          headers: soapHeaders,
+          body: soapDiscountSendData,
+        });
+      }
+    }
+
+    await sendSalesMessage({
+      subdomain,
+      action: 'deals.updateOne',
+      data: {
+        selector: { _id: deal._id },
+        modifier: {
+          $set: {
+            extraData: {
+              ...(extraData || {}),
+              msdynamic: {
+                no: orderMsdNo || responseSale.No,
+                lineNos: lineNoById,
+              },
+            },
+          },
+        },
+      },
+      isRPC: true,
+    });
+
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      {
+        $set: {
+          responseData: responseSale,
+          responseStr: JSON.stringify(responseSale),
+        },
+      }
+    );
+
+    return responseSale;
+  } catch (e) {
+    await models.SyncLogs.updateOne(
+      { _id: syncLog._id },
+      { $set: { error: e.message } }
+    );
+    console.log(e, 'error');
+  }
+};
path: packages/plugin-msdynamic-api/src/utils.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify deal synchronization behavior for edge cases.**

The `dealToDynamic` function needs validation for several potential edge cases:

1. Empty or undefined `deal.productsData`
2. Missing customer information
3. Network failures when calling MS Dynamics APIs
4. Handling of very large product collections

Consider adding more robust validation and error handling in these scenarios.

---

🏁 Script executed:

```shell
#!/bin/bash
# Look for existing error handling patterns in the codebase
echo "Checking for error handling patterns in MS Dynamics integration:"
rg -n "try\s*{.*}\s*catch\s*\(e\)" --multiline packages/plugin-msdynamic-api/src/

echo "\nChecking for validation patterns:"
rg -n "if\s*\(!.*\)" --multiline packages/plugin-msdynamic-api/src/

echo "\nChecking for dealToDynamic usage contexts:"
rg -A 5 -B 5 "dealToDynamic" packages/plugin-msdynamic-api/src/
```

Length of output: 7195

---



</details>

**Enhance dealToDynamic error handling and validation**

Several edge cases remain unguarded or silently swallowed:

• Validate productsData before use  
  - Replace  
    ```ts
    if (!deal.productsData.length) {
      throw new Error('Has not items order');
    }
    ```  
    with  
    ```ts
    if (!Array.isArray(deal.productsData) || deal.productsData.length === 0) {
      throw new Error('No products to synchronize');
    }
    ```  

• Fail fast on missing customer info  
  - After fetching customer, guard against undefined:  
    ```ts
    if (!customer || !msdCustomer.No) {
      throw new Error('Customer data incomplete for synchronization');
    }
    ```  

• Handle HTTP errors explicitly  
  - Check `response.ok` (or status) before calling `.json()`, and throw meaningful errors per call.  

• Network retries & per‐call catches  
  - Wrap each `fetch`/`sendCoreMessage` in its own try/catch (or add retry logic) so one failure doesn’t silently abort the entire sync.  

• Large collections & rate limiting  
  - Consider batching or limiting concurrency when iterating over `deal.productsData` to prevent timeouts or throttling.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 319-319: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-21T11:44:33Z
author_association: NONE

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-04-13T11:51:46Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide by Sourcery

This pull request introduces functionality to synchronize POS orders and deals to MS Dynamics. It adds a new `orderToDynamic` function for POS order synchronization and modifies the `dealToDynamic` function to handle deal updates based on a new `useBoard` configuration. Additionally, a checkbox is added to the MS Dynamics settings to enable or disable the deal board feature.

#### Sequence diagram for POS order synchronization to MS Dynamics

```mermaid
sequenceDiagram
  participant POS
  participant messageBroker
  participant msdynamic-api
  participant MS Dynamics

  POS->>messageBroker: orders.synced event
  messageBroker->>msdynamic-api: afterMutationHandlers(pos:order, synced)
  msdynamic-api->>msdynamic-api: orderToDynamic(order, models, config)
  msdynamic-api->>MS Dynamics: Fetch customer info
  MS Dynamics-->>msdynamic-api: Customer info
  msdynamic-api->>MS Dynamics: Create/Update Sales Order
  MS Dynamics-->>msdynamic-api: Sales Order No
  msdynamic-api->>messageBroker: orders.updateOne (syncErkhetInfo, syncedErkhet)
  messageBroker-->>POS: Order updated
```

#### Sequence diagram for Deal update synchronization to MS Dynamics

```mermaid
sequenceDiagram
  participant Sales
  participant messageBroker
  participant msdynamic-api
  participant MS Dynamics

  Sales->>messageBroker: deals.update event
  messageBroker->>msdynamic-api: afterMutationHandlers(sales:deal, update)
  alt config.useBoard && config.stageId === updatedDoc?.stageId
    msdynamic-api->>msdynamic-api: dealToDynamic(deal, models, config)
    msdynamic-api->>MS Dynamics: Fetch customer info
    MS Dynamics-->>msdynamic-api: Customer info
    msdynamic-api->>MS Dynamics: Create/Update Sales Order
    MS Dynamics-->>msdynamic-api: Sales Order No
  end
  msdynamic-api-->>Sales: Deal updated
```

#### Updated class diagram for MS Dynamics Configuration

```mermaid
classDiagram
  class MS Dynamics Configuration {
    String itemApi
    String itemCategoryApi
    String priceApi
    String pricePriority
    String customerApi
    String salesApi
    String salesLineApi
    String exchangeRateApi
    String discountSoapApi
    String username
    String password
    String genBusPostingGroup
    String vatBusPostingGroup
    String customerPostingGroup
    String customerPricingGroup
    String customerDiscGroup
    String paymentTermsCode
    String paymentMethodCode
    String reminderCode
    String locationCode
    String responsibilityCenter
    String billType
    String dealType
    String syncType
    String defaultUserCode
    String defaultCompanyCode
    Boolean useBoard
    String boardId
    String pipelineId
    String stageId
  }
  note for MS Dynamics Configuration "Added useBoard, boardId, pipelineId, and stageId attributes"
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Added `orderToDynamic` function to sync POS orders to MS Dynamics and modified `dealToDynamic` to handle deal updates based on board usage configuration. | <ul><li>Created `orderToDynamic` function to handle POS order synchronization.</li><li>Modified `dealToDynamic` to check for `useBoard` config and handle deal updates accordingly.</li><li>Updated afterMutationHandlers to use `orderToDynamic` for POS orders when `useBoard` is disabled.</li><li>Added sales:deal to allowTypes to trigger afterMutationHandlers.</li></ul> | `packages/plugin-msdynamic-api/src/utils.ts`<br/>`packages/plugin-msdynamic-api/src/afterMutations.ts` |
| Added a checkbox to the MS Dynamics settings to enable or disable the deal board feature. | <ul><li>Added `useBoard` config to MS Dynamic settings.</li><li>Added checkbox to enable or disable deal board feature.</li></ul> | `packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx`<br/>`packages/plugin-msdynamic-ui/src/constants.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!
- **Generate a plan of action for an issue:** Comment `@sourcery-ai plan` on
  an issue to generate a plan of action for it.

#### 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-04-13T11:51:48Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: failure by coderabbit.ai -->

> [!CAUTION]
> ## Review failed
> 
> The pull request is closed.

<!-- end of auto-generated comment: failure by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes introduce enhanced synchronization between order and deal data with MS Dynamics, expand support for new data types and actions, and improve UI components for configuration and history display. Key updates include the addition of the `orderToDynamic` function for syncing POS orders, extension of mutation handlers to support `"sales:deal"` updates, and improved logic for handling sync logs and configurations. UI components now offer more interactive settings for boards and pipelines, and the sync history list recognizes both order and deal types. Several files receive formatting and minor logic adjustments for consistency and clarity.

## Changes

| File(s)                                                                                      | Change Summary |
|----------------------------------------------------------------------------------------------|---------------|
| `packages/plugin-msdynamic-api/src/afterMutations.ts`                                        | Added support for `"sales:deal"` with `"update"` action in mutation handlers; updated logic for `"pos:order"` syncs; imported and used new `orderToDynamic` function; extended `allowTypes`. |
| `packages/plugin-msdynamic-api/src/utils.ts`                                                 | Rewrote `dealToDynamic` for improved MS Dynamics sync; added `orderToDynamic` for POS orders; updated HTTP request logic; improved error handling and logging; formatting fixes. |
| `packages/plugin-msdynamic-api/src/utilsCustomer.ts`                                         | Simplified `checkSend` to only perform GET and PATCH operations; removed customer creation logic; updated calls to match new signature. |
| `packages/plugin-msdynamic-api/src/graphql/resolvers/syncLog.ts`                             | Added handling for `"sales:deal"` in `content` resolver; minor formatting change in `createdUser` resolver. |
| `packages/plugin-msdynamic-ui/src/components/settings/PerSettings.tsx`                       | Enhanced state management for pipeline and board selection; added methods for handling board, pipeline, stage, and checkbox changes; improved rendering logic. |
| `packages/plugin-msdynamic-ui/src/components/SyncHistoryList.tsx`                            | Expanded row rendering to include `"sales:deal"` alongside `"pos:order"`; minor formatting adjustments in date display. |
| `packages/plugin-msdynamic-ui/src/constants.ts`                                              | Changed string literals to double quotes; added `useBoard` key to `KEY_LABELS`. |
| `packages/plugin-pos-api/src/models/definitions/orders.ts`                                   | Added missing semicolon to `extraInfo`; reformatted schema definition for clarity. |
| `packages/plugin-pos-api/src/utils.ts`                                                       | Simplified array fallback in `confirmLoyalties`; reformatted object spread in `syncOrderFromClient` for readability. |
| `packages/plugin-msdynamic-api/src/messageBroker.ts`                                         | Added new `sendSalesMessage` function to send messages to the sales service. |
| `packages/plugin-sales-api/src/models/definitions/deals.ts`                                  | Added optional `extraData` field to `dealSchema` for additional data storage. |
| `packages/plugin-msdynamic-api/src/graphql/resolvers/mutations/syncDynamic.ts`               | Replaced `dealToDynamic` with `orderToDynamic` call; extracted single config from configs for order sync; adjusted argument order for `orderToDynamic`. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant afterMutations
    participant dealToDynamic
    participant orderToDynamic
    participant MS_Dynamics

    Client->>afterMutations: Triggers "sales:deal" update
    afterMutations->>dealToDynamic: Call with deal data
    dealToDynamic->>MS_Dynamics: Sync deal (POST/PATCH)
    MS_Dynamics-->>dealToDynamic: Response
    dealToDynamic-->>afterMutations: Update sync status

    Client->>afterMutations: Triggers "pos:order" synced
    afterMutations->>orderToDynamic: Call with order data
    orderToDynamic->>MS_Dynamics: Sync order (POST/PATCH)
    MS_Dynamics-->>orderToDynamic: Response
    orderToDynamic-->>afterMutations: Update sync status
```

## Possibly related PRs

- erxes/erxes#6000: Modifies the same files (`afterMutations.ts` and `utils.ts`) with overlapping synchronization logic for `"sales:deal"` updates and the introduction of `orderToDynamic`.
- erxes/erxes#5925: Adds handling for deal and order updates syncing with MS Dynamics, including defining `orderToDynamic`, closely related to the current PR’s sync logic.

## Poem

> In the warren of code, new tunnels appear,  
> Deals and orders now sync without fear.  
> Boards and pipelines, settings anew,  
> With checkboxes hopping into the view.  
> MS Dynamics listens to every request,  
> As rabbits ensure your data’s addressed.  
> 🐇✨ Hoppily syncing, we’re always our best!

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNwSPbABsvkCiQBHbGlcSHFcLzpIACIAMxJqLiU0XwJIZkRaWQw0ZngGe2yCtFolWmjIAHc0ZAcBPNwaejkw2A9sREpIAHFKEi8HAGtYdAx6VHglDHFY+CiayAxHAS6ANgAGTY0YNus7BkxIFfQGBmlO+mpWj1j8H3xK+AwiSABVGwAZLgBtWEbuRAcAD0QKI6lg2AEGiYzCBlAAHtI4RREYggdxvF4gRtNgBdAAUf1wAOBoPBkOhLGRqOpSIxPmxm3WAEoNG4EMhbChphRFNgzrUirBeRh4AAvajwfBYfCxewpaSQZIDKrgyAAWXyvMQstCABFsrl8ogADTchhebC0J4vARoBiDMj0Lz4MEMM2vACSkDIsEwZzY01No3otGoaEgfrGXhtKGYvHwUkDuEQ2wA0iR5Cs/RIpVRfAwo6RkE8LVaPCVrc9ChhCyLxZLpZBbnwAAaIBWA5WtyDYbhhmglrCttCxGgUdV4RsYVMpnuYejtooGnJ5BgaOfbdw1uvShviJscgguBTx6XsSCtgDKRQAEkhj7IPg+N4h4T3UH2B1E0qXLUp5UiZBlVTHYPE6RobWQL1uUvKxKCvEhIOeWc3w/ZBfX9KJHlwEYBHwZxLjGHh4G4fonnA/pRAPGVuBo5B4mobAKFjLJV3yRg2gdfD4QUHk7lTIx1UUfpe37ahFT/cs4wTKR6CjWgY2rWV+AoJQ+CUxUF0YDoCDYPgnhbZhp1g1t8DUyhoHwFcjQYediNbZUrJstdWy3XYGF0lgugHCNECFesJRoyAXTdFBangeMY1mKInkvPB4AGABhLz9NfNzIAAQUWEhKmbbBa2C9snSvTt1XONBSHQ9BSli4c2EQDtSAAIV5R0KHSsJ8Gbe1EvUCT7CdWMOyA9IKuLNkDBgr8JKHCQ7ikdJFHgWJZFjVt4IoRDkKIVD3xDS8mBnXBMBTTrf1rf8PHIPKvBUfpg0DbAUBoDIzW0o7ZiIZiTKO3BeQGdzwKKELXT4mhpiVJBuDu+RUH8WZyHGYd/NrD5XXS7ZNQwcyjmwF5ZlRGraH8RrBsiMRcYRvpa0kmULL4VHd1FIKpSwbTItkm5zOMnaDqOxAHzIBg4awGCYW4c8gwOzKrG9BKY3EaRtiSkTmx56hxGrTneSTdhkG0vIcb4BH7WPWNMNpvwEjDNRFfke1tWQXCPCYJQ7U6Sbty5KSlGdypusQBh/DIRBYHwFMUB8XSqC1l4XbE796BgxjcGY5XIAAeSkfMvDNXDP3EmgfXhCHaGd3Z/MQV7AEwCIcaCIWO2dVXCNS1fAdTHSAXONI55DIFQlJeEbFWVHdhT3VnpTNHXE1jT74G+pum1iSJ4XgO31Fkd7iMthhhoCyeTKPcz5H++1BhtSb9GMcAoCdfg5TQPBCFIchY6iGFky4Xh+GEajJCKhaG7ZQqh1BaB0DfEwUA4ATANuzF+xAyDKCaKeb+fg0B5QcE4E8wCRJUDAZobQugwCGFvqYAw3AL6VTpJaMEGAwAZDYrZMAaBuDwCBIgCgDAgSjnHJOE69FXwcAMNEMRBgLBZU9Eg9+El6DYOMieFShZMDFnZK7IsWlSiRgXEPdWbZogjy7AkLw0Qew4RGK2aIM0aBmJOMFOKI4xyUAEdOW8ujKCIAygAdTaOzJUJjE4DXwKcZiwZ1CnioaTa4OU8rthOqQT0tBzFqgTnceg54DoJjOHQZA0ovBwzlAnauNCdHIBUc8Og2xPShBdlgTozhCyKgTgvJeJlBAiDEAxXGmSLHxU6M1AiakewDwEJEIi9BeaFljAnG6l4SmJOSdUuUtwCq0BnqEYOCRBzoB3KDF4H0UgqkciY5yhpXItxGAnMe2kE6rOIq0n6NF85+OtmnCgM4fTOAKZNAAYrjKxktATmXUnYvpVimZ0DsWbNmLzXZqxxnlLZs1dlM32Wafw/05hSAru0IuURaAhMcBeDmkdeCRWcFmKgxFPR6h3vQeIuAmm4oUBQUmksxjz2lF9J5bNlkxMecvDAAByZArYOgkEGYRHsq9KrhR6gMEgGzGBHJZTdApMlzKoLMgzM57E7L5UKs3PpCcbEEqJcmfm3LF68untcLAmLmKfISBQH56jLwgsstZc5+QZUFTEM3CYZ4KCoNiLyZgl4NBAgVl4+lMSRz3EqNAWQZEvF/06aEVAJBS5Oh/N1X2HgIWdg4MqaIPxrH4uiLiNy5hLCZS8OOaczturXNEHdIVeS5TZsliGqIuMMRjI4uwdQcxEBGCgAANWcPAQeFZaq0C4EWoCJaTFlsgN8Ct34q0fmHEch4ybU07svFQh0NC0Qw3xk8RhmQfUMFYewzh3DeHOInFOIRc5RHiIMBAMARgT2DDPeiOhV6mG3vvRwrhPCY3CM/dECRdbpFvxQVEBRlLH6cVUdId1nMtXoeKmMUqQFyqNRoeYhYlY80xIak1DwAg2pdBwyG0C24TkpD1bZP1RqmzVGQKbSm/h6BpHtGcOiJcYb5Aiaxrw9lFytJ7FE3ISFPEHQlZeZgIkBgytxr5D2Hh8BkQ7dU0I2bz5dI1Febut68kMwOppRYyxlNhpYJeZUGgTNUD1OGDQoH9WtjNBIFIkwUX+CCPAAT5nLP6vKdal4Cm2DjmDNpTFLESA4r4kZLe/NUoMYwLcSAuYIw2CsElFVPhQI1L4tXCg/JI4RiUCdRKKHOyqXUjwNAsgXQlHNP+ee2W+B/TNtyW4ZpPV8HiyUcMcaJUGVy/gM0R1rQ0SOfIDosYrCZWgElW8qlrCZyvNASAt5oDQCsGNXCihgIkDIpy5SWAE68GkMLPTT8sDZofLGUb9nmArA6mBQ1AamyhV44EYI1clSTcGmMFlRiWtdDSAndUFme4MGQLLT0cbsnnEVAkQssODKvXsJFRK3z5B9PW5t7buMrB7YOyF0HkcWyAUVJpcroQgc+jZeZeB9B2XSk6N0vgOORiaX5XVpATACqhCvJnWWWU5ZvE+PKwV8xiIEBOr4a0Qd8CS4wuvauyrOhQ92dL2XdOQhdWtgcLwnk7rFxh59zXEugzbEyi+ngvIBSC2eMqs1LLlSisvO5tAnmTreZvb5y5ePbMUS+z95ABXRiQCKyVq3vhtK+5iWi0KkeE685nHpwXnOOpGGynM2I/qiqjfYxcnjxNsJqkFnkdt+yOICDwO7kJWP4EeoZj2Dp1FI86vUhodQJAMg7urjbPDrmEy0Bq4gEPaAMoVaifznvw+mYAFEKDDCQp6WbWm+CI8i7ZazrXtJ2aWN9zxGKkIpbS55au3kZt5d0xk4co3oR9aSX5yr/158HSG7lxM5n5dAX4x6+QW7H7I6o5yxmgKSjSUC8gUDBjs55785Tb4osqjYB5oolJpzc57KGTdQJ7J5YxPC4xGSayxiEx0xlgAQCxCyQyqYqRe5ECRCQBBARx0yHbHanZtAlDKbaQm6nYAAa6oHwca/g6mAWvgyi0oi2bMKQLeBQjO14nYBmOoGAAA+qrEoPOI1IvBgJanFPhK3OXlxjOEDEEsXI4lXt6hHrQQbOTNRFTCQPEP4FbE5hGq2HFqmNoZMD2GkEPpQBoAEckrBAnMnqVr4DYjMrsJ/iXukBQTNlMIIs3ITFUORtwOJnVJeKQLgJvvCBUqQDYLNEEd1EwGygPtcgNBLNOloWJqTILNKJNJIvWo2vRBbq2haM4E2uht2lqn2nwAOjGAUMOkrGOt+pAL8hXs3ILEQDkO8niknIuk5A4bZPiHUISsZE8FwFVjaGaEzOjEQFwJ6DeGjK6HqBauwGaH4VwJgNvEtEoAMGccJC8cGK0oCKMLIMyD2IAEmELmpyGxa4WxkIOx2gGA+xWK3uzxD0bxGmwYxxroZxFxDAJx1xnkyYZoyoDxGATxrSeJvxR6vh1CxYQGl6DCPmLCbCEGT60GH6UAAAcrlP9sFBRguuvl6sjmCQIBCXsfYDCUQGaOph8QiR8UcUUCcaiVKVcTcdMCNgzESfNjFkSX8aZP+oBhevQteswmuOBo+lBuIIDB+mInBt+hQpqeSdqSBuHiwtgHSTwhLFLCmJwkhHHGiJtNtB6a+PCCImafBlIjIshvIo4IovIMopolMduCUXTP9HyGcLEmyUofbAdKKStGfJXIIh4MZDkKQJahYnFAnBtAhO6VBD2M6eQNMC7kmfpktr4PgR4AmAZrgPIL4aROROQNVByd0VQA6JnlRGIFEOwmRHZvVtoIDH9vFuHMAWZBgElJolKsMmaHOQuZhlYB2ZpL/tpKuZoleAkiQPOP4NyPGXPmcIJt1AgSsSivDrsPhIRHcZuRRHGgspRBTPRLfogGRAGlID8n9rueuU+V2WdjOd1uWM4UEJQGfN1IyrjiOZ2R4OOYlMgKttWJlJLPcJAElDGLcTYfEYWvBVuYKQNCarsLBW0KGF5pACXqUCOtKMtu9EmdOYoB6vOZoguaIIMDxN2fOhbleZxJxTxMRcXLGYlsRBGFUXnlWC8K2J4epBxdxPgPtMxeMPXLyGeZRnJV0E0opUTHqfkMtkcDUH2ndh5DFrahgNYVpSxNWKFBxIzsWUubQIhO+arNMJCZQN2QMN1ByWaJUAgLjgtnRTkD4PIJrjDO1iypWReG/vwKZYWhKk5RWeZUKsJSQNsEJCkforzHHBhpUgbMecZABLhLyPjOHO3nco1jPJdFaPhb2J0OhoSpCBwVwTsozgcShFajOEwaEGoDdi8CpCpUOJAAAFJXiiHbDZzKA+Bwp5XFg+gYBRiJktIsAcrsAB5PCNo/mZbaSNnpCYA0KWr+C26Ub3lqRZJAVvkAJ86TS1pZQNooJszNoxJKC9EdoDHwg9qoL9rNVDrTAjpYbTH7kDTNmUCtl16cntmjkUQAD8RJAA3Eer0WTCWVtGWShCSVabQpSbqWBg6YaUCNFUGG6TtJ6aWaTb6a2LBuOpaWSdjTqdSfqfjZBoTXzidM7imP6eIu0Yhsgh/KGTghGXKKJe6qJZEbsK2ALOzWdHOM2I1u7hSixBqk8PNF4ItAnOmbMAcMFKwUKSFKPvmAxOGoTs8C1dgNwc9U1WMh4K1TwbgAHGJkMfQFLadN8a2E9MjjJpeGmJvgAJraEfCZTNSb4fBXgZTbjsApbDXFke23pHlUBPH+X5AjADZPB5L/xmbgrhCRBe2thKSDDya8gtmjrwFoCLQrBkA2GUaqZW1m0W3ciT5dasE2h12DjbBXhE7toFLvQ+CCk2UvCyGg7i2Fo+3+2B3B2h194Z2hA5i0YkCV2xkMrG1sGt3NLdS1023m1t1ZS0X1nd27JzKOiyBgCD1NnaAUArmJVDKcnRCvANVjxnXlC/5+gyDz3sy8W3kj1+0B1B0h1h3prURmjdoLixjqAMRzH0X2xewJE5xHJ901bpxdUQyhAqQrXHSu3Wy7FYD+qaKXCCwATeHXANVUF8wZ63K7CVjBXoazKslH2mSj0/0T1h23WBkdGPV87dG7CvXtr9EqSDG9oZIjG/XjH/WTHjqQBMk+UPXvWf0BJvW8NdqfVO07ajF/XhCjpVB9D7VKBsgBkWm/qUJ03nrAYMJAoGks2ikPRAhKCIzBVoijaoRc1wY83Bn832BhloaRmYbRkeRRnEy7J5CGHVidBrh3BNiyOthB7763B950Qpkd4tmizxqehWAdyZy94nmUCxD2jpWQD77xpArpPqRXhNLGQ9g2NPDBWzWROlxUDRP4AypzBeDO0pUmTwzuEayNBRCEN+Qt0kBgB2b95iAW4RjMDeDiADMx5DOhCkXgSKYJNg0aP4ititlkRcAlNtDGQaAHrKyaiIjJIrl1kplcD/6Hkrl3QrBeBcDCpFHnx5OzbCqtishgTHnODXSVF+OyN2XuhhAprSCKn2ClN+T/5iCIMrCyAKGtCoAkO5XaQYiFTYAmQlBCC6TJhtF1rSP9GyPcN9FdF8NKMCMqPCMLXqOA2SNzWKiVBaNFUeDYttq4tPUfVfXDGeCDoiOkuCR6M/p/pGMUk6lmO0kE0MmAjU0uNIZuOoZKLC1Rmi1+ON7iarQxI1ALHGEXioMS0LwUDMA1Jj5poBYsSzrD2HTWpavoyyApCTGcYA5YAtDSFzzVgFTkCe5oZRLsBtBr7ODa6q5kVHJ2j9ndrNGwpzXrT4if6j4ZCQAAA+kb66uI6pwRYbr0yA0bsby+8V8yRQRTlAvy4a2Fcw0wVrzymeILyx1DEto2U9GabWjUlG+GtAqTQcOF0wxGNG1UCMnToaxtvTpt/Tgz099gD2XWQm6Q4z8Akz5AADwzis01ke3aYxESdmdGCQgwhBTwaR04ZobetSuwoNIaGj5DJAYW+WKQQ90hkJvYtYuDQMrzx5OMFLz1Cc3zQDReOi0YhxO2f0AMzYLoeUcUh7LsfAFh1r6L91nRjLdL8jeLijzLgjrLYxJLANUxzJHz3jnDiF9L71+LMHRLbLCHkxmjhVgh4tqA1osQsQujX63Lhjp61pJjuN+q5j9JxpiAKUT+aUnNorCGrjci7jgt6GItBgLGOlgwiEYwhbgaEUUUK0cwzQ8gdruY1YcWSmKB8y4JLAkJv+7tiJ25DkyJRAy+kc3ziw3ULolSfAmsY+dEz1yKxcEYcyj+ekXQ/gVR9ACeEY1O+21skFYOuMZqey7OK8E56c1Sx0Nsyq+S8gBmRkzh3Qm+tOIO5uaQwn+iJc721YDnz+4OJ0+1TKCA1YlV0jcaK06sayKp8YbzyAdDMUzTyA+I3A4c5AQDuxeciwimzIFur1kwHg/lSEbQfAaQ5OW2VdMSb21csYznIK/Kd7UyeXLwE3510LXr6yL0byTq2BLE9CyhGX+kk7Mz4I2uoQFn8YuVNnfKWF5kUl88qqFuktXEInToJJBR6omQrHjnFA9TR515qCaQM3MScyKrTE/gt+MhdVQe6AFA30yYoE5LEcfX97Fun7dw37DwO2SBuMCBsYbzJ56l/IVSRgd1bDMjLaXDGHCjjthLP1uHExo64jsxlhhOixgPX3dAi6UtAlDoonERhD1FkAl4vJ/JUJfd77ljrxeT7xD082fWKpuWi8Zosw0j+5F9O4Jx6pPPFuavrY+I23lA0vX0cviU44ivqvBgvPjiWNxjONjN+QjHRpyFr3z+6U1N+jPLNH9Ntp+ld6grLNjcbCsAAQWIpMC0ninCspRAMGAZYrfNPHkrQt97SR9nChwVyhdG/oIwtePZRZGr0oyDPYgfatcOl5HiAqxlVQrykt2f7AOzIyQQKQYqhixapaabUL5Sxls1efOcq3HyLKd0YOnQRAlq6rhaenlIM4xKi+MNGg0zGgq4h5Qv1YfCXQX5isuVeCpnB0WKrAsYSdg4J6SqO22TZWRl/ZwRf07AP+KARSfooQp98qOMh3AWxO1tVl5ug/qXY3tlro9lPSAHxr0wlffzPYU+sDiwYoVL2mGWgC7l3opl96EYc+H1HS4sBjIWRA2LxQX5tgkAyeE5tVln6Z9C0NnOgHfU8rWwdQ+fCgAHmoylIqEHWAiBAPx6sNMWXRCDjwyg7k9vqQjKnqIxp7TFyWYtWRgD2WKdpWBLLVRuy0Q4EccyghawprQMpdEseMYBoJRgTibVKAIVFQmWzwEV8C2B0SWv4DkSECKABhEGEBy6KFl02NjZ+A2lYG7cKO5pKjubz5bu88ajpVmmeCrKuk0S94J/C4GfDVxfSTjVhrzVkSoIY+/HGVoJzMqcpgownOquXz/7TAq+RrWSg8Dco59ky3GBYMAzGCUYC0RwWHpeGiBAoOAo2aFA5Hr7LpG+x7S0HTEboZI5QEXfIYUOKHh0OQRwXkIIU+S55c0/dNQWkH4ow4QIhOJvCTgtwec8coETKFAMgb70jYlBDtvPD8ZUtCOAEXAaNXGr8A8AGIUIA5V2DRAkoug1BCHhIAVBtI0QKwNoHoCHCKg1XYAi0DTqLNqwc7CTKEC/I5NaurYAAN4VBogAAXyeZHAOmx5WosXCAFANR+3QxgpPkhgvD942sdNtZT7QbC8A2wGHj/yM64xEevgVeCjzFqLCPAm1HHueRYbtEGB4HYnuh0g6MssOyjSnvB2p6A1kO8PXEdo1pZki5GzAykYzCMJM9BB/DNgXBzUZiDHEHgh8KfB8GaAUw74GwTTQMb2CbSVJO0vqS95PoKBLUejB1A44R8uO4raPh4ylZx8DApeVkjUEPg4wOgaQlGCVDKjjRZ+6fedEZhOAiZas7MCHsSkhioM/ml4FtjQkygQ9EAmcBoIhAoC5gzgXtR1F312QJhAmuTbcMYMDR/8PkhlVPGKiAJeiqokeCMLQUXCdAgx+QEgEyUUw9gIIN3ModIBKHyJB2wBToT50v5NldYXXS4C6Kh7uRUAlDYKAph8APQ3+uVWMRwxjCOh5kToBtqmNn47kgCqsfwMOKCJX9Ickya0c9S/KiBpOBQbMcGOVh0DiRYHDhkwIZYcMqRFPdgbSM4H0iZiEDd+mUEXS8i/8M4wjNIGHH4hnAu0LgMOJ9G7R/R6gQMauOZBcArA4aJACQGACPE9AmNXlnKPo40kH0Fja0a1HwDtRHeXLWmq7wt46kjENvIECLzRAVNRQ9EaxiYkcaccgy2o4IbqNj4CdDReUI5pAzlr9B6AM/RcEHkXx94ikHo1sJnGnpkYUBZQNDkCRSAbMx8S+DUNKCID4AO44EIFs2IYhNNVKxnNnPdHGQHRUAFEkKkA0+ogN8ulcIFiM0TSClzImPSYaoOcBqBz4J4SAikEEn4MPAo3XKr4QJEpgGJ2gqgVDzsnXDBI+PDceww6GsicWmHaDtSIPECixG0xCYZxLs6slFJyha4YHlqbB5wwXAViRmgqIjcCW2qZULxLKYakQJdHFCUqJ4ToScJtjbCSBExhO87BGUy3gqOt7ZTQQVAerv7yBDt9g+YzdInzhD61hkc4fbmlqKj5ES+OXjfKthmDQoMax5o7iV4Gry+pkBmDRMCrh5xXY7o55dMVgEYyDSuSFAMaXZBC7mT40BATns91oBZsUCPYRqRu2L691gi6xT2hNP8ARV5pLQCSnAwTa6oQSvqSAYoSmFPEVqAERFCXFMy1YTa7BeFDLzaQ61+2hDe7P4FzDa5EAGqcip/BiwoU2CMSWYCgWMyRAB+coEIh1C1xkRWoC4JJGmlxgZjfWF8SAHQ1bDCocYOMsYI83EnfS+y2qOTPKioQ1s8GieB8a6NqTdQMZa0ifDQCbpygmZCMhOMOmPKxCvo+M6etYU+wqQ2ZUPathcBu72ELppHDwkbWcxgzUsUoM0anhu7nS46fcJaApxkroSEpxSUQJCxlkXggUVDOFsq3WgMy06vM2oWpm07aCvi84bdhZLGAgdCeWLTyaTxYG8jhBxLOkUhwkYod8q4g5kVxP4HpweRSUoORwI5ZSC30zcFZvgB2mZB9paaM9mnQvaL1YIMcoHnFWVqRxlBCY3wJzFRnsATIteaQXj3glkJoEC1J2c/AIDcdUEX8dgFwCoBYJiJ+skBAQkMkQISEhgJuV/HUDhFEA2hcGXMCpa0BtCJSENCPPIRQBVgJAAAKyxAAALLQAADMDAAAJzbyBA68gAEy7yAA7GgAAAcJALeVvPWDXyD5u8reQfIvlbyGAqgR+bvN3kHzT5F8+gFAjvg+gAAjOsBUDrzr5u89eQfISDry0Ao4C+bEFWClAGAP83ebQAvkAL15IC1YGgAwUMBT5sQa+evOXlNzd5180+QwC3mrAyO68j+QcFiBhg/5p8tAKfPoXz0D5mC1QAAoECnz3CKC9+WQuAVbyX56wNeQIC3kgLiFaALeRvNPmrAL5p82gAwHWB8k95aAEBVwuvmxAGAtAA+fQo2AeAgFEAAJLAvcJSL15Z8kgBQpAUkBT5IC2+Voq3nXyBAa83ebECvlXywFtAdYEoHWDCLTFogdYPQpAW+LX5F8hgLgqfk7zT5182gFvLQCKK4lAgXeesHYXby35SS9eQUBMVQAX5l8xxWgCUVELT5r86RZsFkUIKT5yi9easH3l6LYgyi6RevIAWBKoAlCy+QwBgX2Kj5USgQOIvXl8LFFIC0+afM2AHyD56wVResB3mxBHFDAC+bvPaWQBVgaylxQwFWBQLr5ICxxQIGoXrASA6wWIOvJsU4LVFdoLeXySvm0AQF/S6+Ssr3mrB15e8gZekoGVYLYgu8u5Wkt3mjh1gf8yRQfOvlJLxFNii+SEtPkrK6l8C3xW/IcUMBYg4Cv+asESUHzElAga+asAPkIKSAJ82gAIDCV/Kt5yClZTEuIWrBCFDiveUopCUgKt5F8kgFAo2CiLd5fCnFbvK2UEqn5WKx5QYrfkkKaVYCvRRsBxV4qGA186+bMt3lUKmlpy4pesDZWSKIweSyABfKxWfy0AmwJxWwovnPzqFdS9+ZooPkgK2FmwXRWyqmU4q2lqqoZZvLwXsKRVhytAKIAvmby3VNCvFViqxXz0N5r80cFirlCqqVA9Sp+WgugVfKHF+ysZX/OgXfKSAeCugKfLtDILE1doAQMvIMBjzEBE8yYFPJnm5Q6A2hB+PoCAA=== -->

<!-- internal state end -->
<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡💬 Agentic Chat (Pro Plan, General Availability)</summary>
> 
> - We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ab6389c335f321bc22923531e6aed2baf66aebab and fd275c04af6ca50342ad70f17b62be8abc707850.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/plugin-msdynamic-api/src/utils.ts` (6 hunks)

</details>

</details>
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- 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=6000):

- 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.`

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 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-04-21T16:59:29Z
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=6000) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [27.7% Duplication on New Code](https://sonarcloud.io/component_measures?id=erxes_erxes&pullRequest=6000&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [C Reliability Rating on New Code](https://sonarcloud.io/dashboard?id=erxes_erxes&pullRequest=6000) (required ≥ A)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=erxes_erxes&pullRequest=6000)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarQube for IDE](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


author_association: NONE