Repository: erxes/erxes
Pull request #5996: chore: cronjob, number to word
Author: login: batmnkh2344
Created: 2025-04-07T06:23:13Z | Merged: 2025-04-09T04:48:57Z

## Summary by Sourcery

Add a daily cronjob for fetching currency exchange rates and implement a number to word conversion utility

New Features:
- Implement a number to word conversion utility for Mongolian language
- Add a daily cronjob to fetch currency exchange rates from Mongolian Bank

Chores:
- Integrate cronjob and number to word utility into existing project infrastructure
<!-- ELLIPSIS_HIDDEN -->


----

> [!IMPORTANT]
> Add number to Mongolian word conversion utility and daily currency exchange rate cronjob.
> 
>   - **New Features**:
>     - Add `numberToWord` function in `numberUtils.ts` for converting numbers to Mongolian words.
>     - Implement daily cronjob in `exchangeRates.ts` to fetch currency exchange rates from Mongolian Bank.
>   - **Integration**:
>     - Export `numberToWord` in `index.ts`.
>     - Add cronjob import in `configs.ts` to integrate with existing infrastructure.
> 
> <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 531121632a53725ae236b2a8c610d1096bd04539. 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**
	- Introduced a new utility to convert numbers into their Mongolian word representations.
	- Launched an automated daily process to update exchange rates, ensuring that financial data remains current and accurate.
	- Added a new module for managing exchange rate updates, enhancing financial data handling.
	- Included a new import for cronjob functionality in the configuration settings.
	- Added a method for handling daily jobs related to exchange rates.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/packages/api-utils/src/index.ts b/packages/api-utils/src/index.ts
index b2767f5a06f..9cf9d39440a 100644
--- a/packages/api-utils/src/index.ts
+++ b/packages/api-utils/src/index.ts
@@ -23,6 +23,7 @@ import { putCreateLog, putDeleteLog, putUpdateLog } from './logUtils';
 import { sendToWebhook } from './requests';
 import { updateUserScore, getScoringConfig } from './scoring';
 import { generateFieldsFromSchema } from './fieldUtils';
+import { numberToWord } from './numberUtils'
 
 import {
   can,
@@ -110,4 +111,5 @@ export default {
   afterQueryWrapper,
   dateToShortStr,
   shortStrToDate,
+  numberToWord
 };
diff --git a/packages/api-utils/src/numberUtils.ts b/packages/api-utils/src/numberUtils.ts
new file mode 100644
index 00000000000..1c8068087e8
--- /dev/null
+++ b/packages/api-utils/src/numberUtils.ts
@@ -0,0 +1,106 @@
+const DIGITS = [
+  { word: "тэг" },
+  { word: "нэг", prefix: "нэгэн" },
+  { word: "хоёр" },
+  { word: "гурав", prefix: "гурван" },
+  { word: "дөрөв", prefix: "дөрвөн" },
+  { word: "тав", prefix: "таван" },
+  { word: "зургаа", prefix: "зургаан" },
+  { word: "долоо", prefix: "долоон" },
+  { word: "найм", prefix: "найман" },
+  { word: "ес", prefix: "есөн" },
+];
+
+const TENS = [
+  { word: "арав", prefix: "арван" },
+  { word: "хорь", prefix: "хорин" },
+  { word: "гуч", prefix: "гучин" },
+  { word: "дөч", prefix: "дөчин" },
+  { word: "тавь", prefix: "тавин" },
+  { word: "жар", prefix: "жаран" },
+  { word: "дал", prefix: "далан" },
+  { word: "ная", prefix: "наян" },
+  { word: "ер", prefix: "ерэн" },
+];
+
+const SCALES = [
+  { word: "зуу", prefix: "зуун" },
+  { word: "мянга" },
+  { word: "сая" },
+  { word: "тэр бум" },
+  { word: "их наяд" },
+];
+
+const runner = (num: number, usePrefix = false): string => {
+  if (num === 0) return DIGITS[0].word;
+
+  if (num < 10) {
+    // 1, 2, 3, 4, 5, 6, 7, 8, 9
+    return usePrefix
+      ? DIGITS[num].prefix ?? DIGITS[num].word
+      : DIGITS[num].word;
+  }
+
+  if (num < 100) {
+    // 10 - 99
+    const quotient = Math.floor(num / 10); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 10; // #1, #2, #3, #4, #5, #6, #7, #8, #9
+
+    if (remainder === 0) {
+      // 10, 20, 30, 40, 50, 60, 70, 80, 90
+      return usePrefix ? TENS[quotient - 1].prefix : TENS[quotient - 1].word;
+    }
+
+    return `${TENS[quotient - 1].prefix} ${usePrefix
+        ? DIGITS[remainder].prefix ?? DIGITS[remainder].word
+        : DIGITS[remainder].word
+      }`;
+  }
+
+  if (num < 1_000) {
+    // 100 - 999
+    const quotient = Math.floor(num / 100); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 100; // ##, ##, ##, ##, ##, ##, ##, ##, ##
+
+    if (remainder === 0) {
+      // 100, 200, 300, 400, 500, 600, 700, 800, 900
+      return `${quotient === 1
+          ? DIGITS[quotient].word
+          : DIGITS[quotient].prefix ?? DIGITS[quotient].word
+        } ${usePrefix ? SCALES[0].prefix ?? SCALES[0].word : SCALES[0].word}`;
+    }
+
+    return `${quotient === 1
+        ? DIGITS[quotient].word
+        : DIGITS[quotient].prefix ?? DIGITS[quotient].word
+      } ${SCALES[0].prefix} ${runner(remainder, usePrefix)}`;
+  }
+
+  for (let i = SCALES.length - 1; i >= 1; i--) {
+    const scaleValue = 10 ** (i * 3); // 1_000, 1_000_000, ...
+
+    if (num >= scaleValue) {
+      const quotient = Math.floor(num / scaleValue);
+      const remainder = num % scaleValue;
+
+      if (remainder === 0) {
+        return `${runner(quotient, quotient !== 1)} ${SCALES[i].word}`;
+      }
+
+      return `${runner(quotient, quotient !== 1)} ${SCALES[i].word
+        } ${runner(remainder)}`;
+    }
+  }
+
+  return "Number is too large";
+};
+
+const numberToWord = (num: number): string => {
+  if (num > 1_000_000_000_000_000) {
+    throw new Error("Number is too large");
+  }
+
+  return runner(num, false);
+};
+
+export { numberToWord };
diff --git a/packages/plugin-accountings-api/src/configs.ts b/packages/plugin-accountings-api/src/configs.ts
index 441113122d0..ddfca0271ef 100644
--- a/packages/plugin-accountings-api/src/configs.ts
+++ b/packages/plugin-accountings-api/src/configs.ts
@@ -8,6 +8,7 @@ import imports from './imports';
 import logs from './logUtils';
 import { setupMessageConsumers } from './messageBroker';
 import * as permissions from './permissions';
+import cronjobs from "./cronjobs/exchangeRates"
 
 export default {
   name: 'accountings',
@@ -36,6 +37,7 @@ export default {
     logs: { consumers: logs },
     imports,
     forms,
+    cronjobs,
     permissions,
   },
 
diff --git a/packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts b/packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts
new file mode 100644
index 00000000000..715bd4e529d
--- /dev/null
+++ b/packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts
@@ -0,0 +1,76 @@
+import { sendCoreMessage } from "../messageBroker";
+
+const mntRates = async (
+  subdomain: string, NOW: Date, rateCurrencies: string[]
+): Promise<void> => {
+  const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
+  try {
+    const PARAMS = {
+      startDate: NOW.toLocaleDateString("en-CA"),
+      endDate: NOW.toLocaleDateString("en-CA"),
+    };
+
+    const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+
+    const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+      method: "POST",
+    }).then((res) => res.json());
+
+    const { success, data = [] } = response || {};
+
+    if (!success || !data.length) {
+      return;
+    }
+
+    const exchangeRates: Record<string, string> = data[0] || {};
+
+    for (const [rateCurrency, rate] of Object.entries(exchangeRates)) {
+      if (!rateCurrency || !rate) {
+        continue;
+      }
+
+      if (rateCurrencies.includes(rateCurrency)) {
+        await sendCoreMessage({
+          subdomain,
+          action: "exchangeRates.create",
+          data: {
+            mainCurrency: 'MNT',
+            rateCurrency,
+            rate: Number(rate.replace(/,/g, "")),
+            date: NOW,
+          },
+        });
+      }
+    }
+  } catch (error) {
+    console.log("error", error.message);
+  }
+}
+
+export default {
+  handleDailyJob: async ({ subdomain }) => {
+    const NOW = new Date();
+    const MAIN_CURRENCY = "mainCurrency";
+    const DEAL_CURRENCY = "dealCurrency";
+
+    const configs = await sendCoreMessage({
+      subdomain,
+      action: "getConfigs",
+      data: {},
+      isRPC: true,
+      defaultValue: {},
+    });
+
+    if (
+      !configs[MAIN_CURRENCY] ||
+      !configs[DEAL_CURRENCY]?.length
+    ) {
+      return;
+    }
+
+    if (configs[MAIN_CURRENCY] === 'MNT') {
+      await mntRates(subdomain, NOW, configs[DEAL_CURRENCY])
+    }
+  },
+};


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

**Overall Comments**:
- Consider adding a try-catch block around the `sendCoreMessage` call in `packages/plugin-accountings-api/src/cronjobs/accountings.ts` to handle potential errors.
- It would be helpful to add some unit tests for the number to word conversion logic in `packages/api-utils/src/numberUtils.ts`.

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

- 🟡 **General issues**: 3 issues found
- 🟢 **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-07T06:24:20Z
user: login: ellipsis-dev[bot]
body: :x: Changes requested. Reviewed everything up to cdf1650918e8dea61d13510a1fd7a5e87117fa0c in <ellipsistime>2 minutes and 32 seconds</ellipsistime>

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

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


<details>
<summary>1. <code>packages/plugin-accountings-api/src/cronjobs/accountings.ts:31</code></summary>

- **Draft comment:** 
Double-check the use of fetch with POST and query parameters; ensure this aligns with the API specification.
- **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 double-check their use of fetch with POST and query parameters, which violates the rule against asking the author to confirm or double-check things. It doesn't provide a specific suggestion or point out a clear issue.

</details>

<details>
<summary>2. <code>packages/api-utils/src/numberUtils.ts:75</code></summary>

- **Draft comment:** 
In the hundreds case (non-zero remainder), SCALES[0].prefix is used unconditionally. Use the 'usePrefix' flag to choose between the prefix and the default word for consistency.
- **Reason this comment was not posted:** 
Marked as duplicate.

</details>

<details>
<summary>3. <code>packages/api-utils/src/numberUtils.ts:93</code></summary>

- **Draft comment:** 
When processing numbers at thousand scales, the recursive call for the remainder omits the 'usePrefix' flag. Consider passing it for consistent behavior.
- **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 usePrefix parameter affects how numbers are formatted in Mongolian. The inconsistency could lead to different formatting rules being applied at different scale levels. However, I need to consider if this is actually a problem. Looking at the SCALES array, most scale words (mянга, сая, etc) don't even have prefix forms, so maybe the usePrefix parameter isn't relevant for these larger numbers.
I might be overthinking this - perhaps the author intentionally omitted usePrefix here because it's not needed for large numbers in Mongolian. Without deep knowledge of Mongolian number formatting rules, I can't be certain this is a bug.
While there is an inconsistency, I don't have strong evidence that this is incorrect. The different handling of usePrefix at different scales could be intentional based on Mongolian language rules.
Without expertise in Mongolian number formatting or clear evidence this causes actual problems, we should not keep this comment. The inconsistency might be intentional.

</details>

<details>
<summary>4. <code>packages/plugin-accountings-api/src/cronjobs/accountings.ts:51</code></summary>

- **Draft comment:** 
The sendCoreMessage call inside the loop is not awaited. Consider awaiting it or handling the returned promise to catch potential errors.
- **Reason this comment was not posted:** 
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 30% vs. threshold = 50%
This is a tricky one. While it's true that the Promise isn't being awaited, this might be intentional for performance - allowing multiple messages to be sent in parallel. The function is already in a try-catch block that would catch any errors. Not every Promise needs to be awaited, especially when fire-and-forget behavior is acceptable.
I might be missing potential race conditions or error handling issues. The function returns before these messages complete, which could be problematic if the system expects them to be done.
However, this appears to be a daily job that updates exchange rates - it's likely acceptable for the messages to complete asynchronously since they're independent updates. The try-catch will still catch and log any errors.
While the observation is technically correct, this may be intentional design and there isn't strong evidence that it needs to be changed. The comment should be removed.

</details>

<details>
<summary>5. <code>packages/plugin-accountings-api/src/configs.ts:40</code></summary>

- **Draft comment:** 
The 'cronjobs' configuration has been added to meta. Ensure this addition is documented and its lifecycle managed appropriately.
- **Reason this comment was not posted:** 
Confidence changes required: `50%` <= threshold `50%`
None

</details>

<details>
<summary>6. <code>packages/api-utils/src/index.ts:26</code></summary>

- **Draft comment:** 
Typo: The import statement on line 26 is missing a semicolon at the end. Please add a semicolon to keep the style consistent.
- **Reason this comment was not posted:** 
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50%
While technically correct that it's inconsistent with other imports, this is an extremely minor style issue that would likely be caught by linting or formatting tools. It doesn't affect functionality at all. The rules say not to make obvious or unimportant comments. This seems like the kind of trivial thing that should be handled by automated tools rather than PR comments.
The comment is technically correct about the inconsistency. Perhaps maintaining consistent style is more important than I'm giving it credit for?
While consistency is good, this is exactly the kind of minor formatting issue that should be handled by automated tools like Prettier or ESLint, not manual review comments.
This comment should be deleted as it's too minor and would be better handled by automated formatting tools.

</details>

<details>
<summary>7. <code>packages/plugin-accountings-api/src/configs.ts:11</code></summary>

- **Draft comment:** 
The import on line 11 uses double quotes instead of the single quotes used in the other import statements. Please update it to use single quotes for consistency.
- **Reason this comment was not posted:** 
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50%
While the comment is technically correct about the inconsistency, our rules state that we should not comment on things that would be obviously caught by the build. Most projects use linters/formatters that would automatically catch and fix quote style inconsistencies. Even if they don't, this is an extremely minor style issue that doesn't affect functionality.
Maybe the project doesn't have proper linting set up, in which case maintaining style consistency through code review comments would be important.
Even without linting, quote style consistency is such a minor issue that it doesn't warrant a code review comment. The code works exactly the same either way.
This comment should be deleted as it addresses a trivial style issue that should be handled by automated tools rather than code review.

</details>


Workflow ID: <workflowid>`wflow_KfS3BtMJmRUoXyGV`</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-07T06:25:55Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

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

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

`3-68`: _🛠️ Refactor suggestion_

**Add documentation and improve error handling in the daily job handler.**

The function implementation is generally well-structured, but it lacks documentation and has several areas for improvement:

1. Add JSDoc to explain the purpose and expected input/output of the function
2. Move constants outside the function for clarity and reusability
3. Improve error handling with more specific error logging
4. Validate that `configs[MAIN_CURRENCY]` exists before using it
5. Use a more robust URL construction approach


```diff
 import { sendCoreMessage } from "../messageBroker";
 
+// Constants for configuration keys
+const MAIN_CURRENCY = "mainCurrency";
+const DEAL_CURRENCY = "dealCurrency";
+const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
 export default {
+  /**
+   * Handles the daily job for fetching and creating currency exchange rates
+   * 
+   * @param {Object} params - The parameters for the function
+   * @param {string} params.subdomain - The subdomain to use for the core message
+   * @returns {Promise<void>}
+   */
   handleDailyJob: async ({ subdomain }) => {
+    if (!subdomain) {
+      console.error("Subdomain is required for handleDailyJob");
+      return;
+    }
+
     const NOW = new Date();
-    const MAIN_CURRENCY = "mainCurrency";
-    const DEAL_CURRENCY = "dealCurrency";
-
-    const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";

     try {
       const configs = await sendCoreMessage({
         subdomain,
         action: "getConfigs",
         data: {},
         isRPC: true,
         defaultValue: {},
       });

       if (!configs[MAIN_CURRENCY] && !configs[DEAL_CURRENCY]) {
+        console.log(`Currency configuration missing for subdomain: ${subdomain}`);
         return;
       }

       const PARAMS = {
         startDate: NOW.toLocaleDateString("en-CA"),
         endDate: NOW.toLocaleDateString("en-CA"),
       };

       const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+      const requestUrl = `${BASE_URL}?${QUERY_STRING}`;

-      const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+      const response: any = await fetch(requestUrl, {
         method: "POST",
       }).then((res) => res.json());

       const { success, data = [] } = response || {};

       if (!success || !data.length) {
+        console.log(`No exchange rate data available for date: ${NOW.toLocaleDateString("en-CA")}`);
         return;
       }

       const exchangeRates: Record<string, string> = data[0] || {};

       const rateCurrencies = configs[DEAL_CURRENCY] || [];
+      const mainCurrency = configs[MAIN_CURRENCY];
+      
+      if (!mainCurrency) {
+        console.error(`Main currency not configured for subdomain: ${subdomain}`);
+        return;
+      }

       for (const [rateCurrency, rate] of Object.entries(exchangeRates)) {
         if (!rateCurrency || !rate) {
           continue;
         }

         if (rateCurrencies.includes(rateCurrency)) {
           sendCoreMessage({
             subdomain,
             action: "exchangeRates.create",
             data: {
-              mainCurrency: configs[MAIN_CURRENCY],
+              mainCurrency,
               rateCurrency,
               rate: Number(rate.replace(/,/g, "")),
               date: NOW,
             },
           });
         }
       }
     } catch (error) {
-      console.log("error", error.message);
+      console.error(`Error in handleDailyJob for subdomain ${subdomain}:`, error.message);
     }
   },
 };
```

</blockquote></details>

</blockquote></details>

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

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

`114-114`: **Consider adding a named export for consistency.**

While `toWord` is correctly added to the default export object, it's not included in the named exports section (lines 47-84) like most other utility functions in this file. For consistency, consider adding it there as well.


```diff
export { shortStrToDate };
+export { toWord };
export {
  can,
  IActionMap,
```

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

`11-11`: **Maintain consistent quotation style.**

The import statement uses double quotes which is inconsistent with the single quotes style used in most other imports in this file.


```diff
-import cronjobs from "./cronjobs/accountings"
+import cronjobs from './cronjobs/accountings';
```

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

`34-100`: **The `numberToWord` function handles conversion cases well, but has some improvement opportunities.**

The function correctly handles different number ranges with appropriate logic for each case. However, there are opportunities for improvement:

1. Consider extracting the handling for each number range into separate helper functions to improve readability.
2. Add more explanatory comments for complex calculations like `scaleValue = 10 ** (i * 3)`.


```diff
+// Convert number to word representation based on its range
 const numberToWord = (num: number, usePrefix = false): string => {
   if (num === 0) return DIGITS[0].word;
 
+  // Handle single digits (1-9)
   if (num < 10) {
-    // 1, 2, 3, 4, 5, 6, 7, 8, 9
     return usePrefix
       ? DIGITS[num].prefix || DIGITS[num].word
       : DIGITS[num].word;
   }
 
+  // Handle two-digit numbers (10-99)
   if (num < 100) {
-    // 10 - 99
     const quotient = Math.floor(num / 10); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
     const remainder = num % 10; // #1, #2, #3, #4, #5, #6, #7, #8, #9
 
     if (remainder === 0) {
-      // 10, 20, 30, 40, 50, 60, 70, 80, 90
       return usePrefix ? TENS[quotient - 1].prefix : TENS[quotient - 1].word;
     }
 
     return `${TENS[quotient - 1].prefix} ${
       usePrefix
         ? DIGITS[remainder].prefix || DIGITS[remainder].word
         : DIGITS[remainder].word
     }`;
   }
 
+  // Handle three-digit numbers (100-999)
   if (num < 1_000) {
-    // 100 - 999
     const quotient = Math.floor(num / 100); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
     const remainder = num % 100; // ##, ##, ##, ##, ##, ##, ##, ##, ##
 
     if (remainder === 0) {
-      // 100, 200, 300, 400, 500, 600, 700, 800, 900
       return `${
         quotient === 1
           ? DIGITS[quotient].word
           : DIGITS[quotient].prefix || DIGITS[quotient].word
       } ${usePrefix ? SCALES[0].prefix || SCALES[0].word : SCALES[0].word}`;
     }
 
     return `${
       quotient === 1
         ? DIGITS[quotient].word
         : DIGITS[quotient].prefix || DIGITS[quotient].word
     } ${SCALES[0].prefix} ${numberToWord(remainder, usePrefix)}`;
   }
 
+  // Handle larger numbers using scales (thousands, millions, etc.)
   for (let i = SCALES.length - 1; i >= 1; i--) {
+    // Calculate scale value (1000, 1000000, etc.) - 10^(i*3) gives us powers of 1000
     const scaleValue = 10 ** (i * 3); // 1_000, 1_000_000, ...
 
     if (num >= scaleValue) {
       const quotient = Math.floor(num / scaleValue);
       const remainder = num % scaleValue;
 
       if (remainder === 0) {
         return `${numberToWord(quotient, quotient !== 1)} ${SCALES[i].word}`;
       }
 
       return `${numberToWord(quotient, quotient !== 1)} ${
         SCALES[i].word
       } ${numberToWord(remainder)}`;
     }
   }
 
   return "Number is too large";
 };
```

---

`103-105`: **Consider adding a named constant for the maximum supported number.**

Using a magic number like `1_000_000_000_000_000` directly in the code makes it harder to understand the intention. Consider defining it as a named constant at the top of the file with a comment explaining why this limit was chosen.


```diff
+// Maximum supported number (1 quadrillion - 10^15)
+const MAX_SUPPORTED_NUMBER = 1_000_000_000_000_000;

 const toWord = (num: number): string => {
-  if (num > 1_000_000_000_000_000) {
+  if (num > MAX_SUPPORTED_NUMBER) {
     throw new Error("Number is too large");
   }
```

</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 f33ecb8cd8d2a8277a11522e33c86140ad4a22c1 and cdf1650918e8dea61d13510a1fd7a5e87117fa0c.

</details>

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

* `packages/api-utils/src/index.ts` (2 hunks)
* `packages/api-utils/src/numberUtils.ts` (1 hunks)
* `packages/plugin-accountings-api/src/configs.ts` (2 hunks)
* `packages/plugin-accountings-api/src/cronjobs/accountings.ts` (1 hunks)

</details>

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

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

</details>

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

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

`26-26`: **Consistent import added for the new utility function.**

The import for `toWord` follows the same pattern as other utility imports in this file.

</details>
<details>
<summary>packages/plugin-accountings-api/src/configs.ts (1)</summary>

`40-40`: **Cronjobs integration looks good.**

The addition of `cronjobs` to the `meta` object is properly structured and follows the existing pattern.

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

`1-33`: **Well-structured constants for number word mappings.**

The array definitions for `DIGITS`, `TENS`, and `SCALES` provide a clean mapping of numerical values to their Mongolian word representations. The `prefix` property is a good approach for handling grammatical variants.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-07T06:28:11Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on e1a537e1fb20e63cfa5e98400cf2955fefdc2250 in <ellipsistime>38 seconds</ellipsistime>

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

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


<details>
<summary>1. <code>packages/api-utils/src/numberUtils.ts:54</code></summary>

- **Draft comment:** 
Refactored multi-line template literal formatting. Verify that the adjusted whitespace in the returned string remains as expected.
- **Reason this comment was not posted:** 
Confidence changes required: `33%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>packages/api-utils/src/numberUtils.ts:67</code></summary>

- **Draft comment:** 
Template literal reformat in hundreds section. Confirm that the spacing between the numeric word and scale word remains intentional.
- **Reason this comment was not posted:** 
Confidence changes required: `33%` <= threshold `50%`
None

</details>

<details>
<summary>3. <code>packages/api-utils/src/numberUtils.ts:89</code></summary>

- **Draft comment:** 
Ensure that the changed formatting in nested template literals (in the scales loop) does not inadvertently introduce extra whitespace in the output.
- **Reason this comment was not posted:** 
Confidence changes required: `33%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>packages/api-utils/src/numberUtils.ts:54</code></summary>

- **Draft comment:** 
Minor formatting changes to the template literal. The reformatting does not affect logic, but ensure consistency in indentation across similar constructs.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>5. <code>packages/api-utils/src/numberUtils.ts:67</code></summary>

- **Draft comment:** 
Formatting adjustments in the return statement for numbers 100-999 maintain logic. Double-check that similar formatting is used consistently throughout.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>6. <code>packages/api-utils/src/numberUtils.ts:90</code></summary>

- **Draft comment:** 
Template literal in the scale loop was reformatted. The changes are purely stylistic and preserve functional behavior.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>


Workflow ID: <workflowid>`wflow_XJNuK9jXefcTWlMs`</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-09T03:43:10Z
user: login: ellipsis-dev[bot]
body: :x: Changes requested. Incremental review on 876262f463c25ece1c8655c2fd4317895b20f3d2 in <ellipsistime>1 minute and 51 seconds</ellipsistime>

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

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


<details>
<summary>1. <code>packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts:31</code></summary>

- **Draft comment:** 
Ensure that 'rateCurrencies' is always an array and properly validated before calling .includes().
- **Reason this comment was not posted:** 
Confidence changes required: `50%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts:14</code></summary>

- **Draft comment:** 
Using POST with URL query parameters may not be intended. Verify the API expects POST without a body.
- **Reason this comment was not posted:** 
Comment did not seem useful. Confidence is useful = `30%` <= threshold `50%`
The comment is asking the author to verify the API's expected behavior, which violates the rule against asking for confirmation of intention. However, it does point out a potential issue with using POST and query parameters, which could be useful if rephrased to suggest a specific change or ask for clarification on the implementation.

</details>

<details>
<summary>3. <code>packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts:44</code></summary>

- **Draft comment:** 
Use console.error (or a logging utility) instead of console.log in the catch block for better error reporting.
- **Reason this comment was not posted:** 
Marked as duplicate.

</details>

<details>
<summary>4. <code>packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts:3</code></summary>

- **Draft comment:** 
Add explicit TypeScript type annotations for function parameters (subdomain, NOW, rateCurrencies) to improve type safety.
- **Reason this comment was not posted:** 
Marked as duplicate.

</details>

<details>
<summary>5. <code>packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts:51</code></summary>

- **Draft comment:** 
Consider renaming the variable 'NOW' to a lower-case 'now' since it isn't a compile-time constant.
- **Reason this comment was not posted:** 
Confidence changes required: `30%` <= threshold `50%`
None

</details>


Workflow ID: <workflowid>`wflow_dYhRiYAkJFtGY7zd`</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-09T04:01:49Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

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

<details>
<summary>packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts (2)</summary><blockquote>

`36-38`: **Hardcoded currency value**

The 'MNT' currency code is hardcoded in the data object, which could make the function less reusable if other main currencies need to be supported in the future.


```diff
     data: {
-      mainCurrency: 'MNT',
+      mainCurrency: configs[MAIN_CURRENCY] || 'MNT', // Use from config or fallback to 'MNT'
       rateCurrency,
```

---

`18-22`: **Add more robust response validation**

The current validation checks for `success` and `data.length`, but it would be beneficial to add more specific validation for the expected data structure.


```diff
     const { success, data = [] } = response || {};

-    if (!success || !data.length) {
+    if (!success || !data.length || !Array.isArray(data)) {
+      console.log("Invalid response format or empty data from exchange rate API");
       return;
     }
```

</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 e1a537e1fb20e63cfa5e98400cf2955fefdc2250 and 876262f463c25ece1c8655c2fd4317895b20f3d2.

</details>

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

* `packages/plugin-accountings-api/src/configs.ts` (2 hunks)
* `packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts` (1 hunks)

</details>

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

* packages/plugin-accountings-api/src/configs.ts

</details>

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

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

</details>

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

<details>
<summary>packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts (1)</summary>

`1-1`: **Consider adding unit tests**

This cronjob handles critical financial data (exchange rates), but there are no tests included. Consider adding unit tests to verify the functionality works as expected, especially for error handling and data processing logic.


Do you have a testing strategy for this new functionality? I'd be happy to help draft some unit tests if needed.

</details>

</blockquote></details>

</details>

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

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

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


<details>
<summary>1. <code>packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts:3</code></summary>

- **Draft comment:** 
Good addition of explicit type annotations for parameters and return type. This improves code clarity and type safety.
- **Reason this comment was not posted:** 
Confidence changes required: `10%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts:34</code></summary>

- **Draft comment:** 
Using 'await' with sendCoreMessage ensures proper asynchronous handling.
- **Reason this comment was not posted:** 
Confidence changes required: `10%` <= threshold `50%`
None

</details>

<details>
<summary>3. <code>packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts:3</code></summary>

- **Draft comment:** 
Good job adding explicit types for parameters and the return value in mntRates.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts:34</code></summary>

- **Draft comment:** 
Awaiting sendCoreMessage in the loop ensures sequential processing. If independent, consider parallelizing to improve performance.
- **Reason this comment was not posted:** 
Confidence changes required: `33%` <= threshold `50%`
None

</details>


Workflow ID: <workflowid>`wflow_DFaG59HWPUUMsBUl`</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-09T04:10:46Z
user: login: ellipsis-dev[bot]
body: :x: Changes requested. Incremental review on 3dfaec045ef727e07d4aac323c5d620c4b8e98d4 in <ellipsistime>2 minutes and 42 seconds</ellipsistime>

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

 - Looked at `72` 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/api-utils/src/index.ts:26</code></summary>

- **Draft comment:** 
Renamed import from 'toWord' to 'numberToWord' is correct. Ensure that the default export on line 114 updates the reference accordingly.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>packages/api-utils/src/numberUtils.ts:34</code></summary>

- **Draft comment:** 
The helper function is renamed to 'runner'. Consider adding a brief comment explaining its purpose as a recursive helper for clarity.
- **Reason this comment was not posted:** 
Confidence changes required: `33%` <= threshold `50%`
None

</details>

<details>
<summary>3. <code>packages/api-utils/src/numberUtils.ts:54</code></summary>

- **Draft comment:** 
In tens conversion, 'TENS[quotient - 1].prefix' is used unconditionally. Confirm that using the prefix form regardless of usePrefix is the intended behavior.
- **Reason this comment was not posted:** 
Confidence changes required: `33%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>packages/api-utils/src/index.ts:26</code></summary>

- **Draft comment:** 
Updated import to use 'numberToWord'; ensure all references match the new name.
- **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 all references match the new name, which is a form of asking for confirmation or double-checking. This violates the rule against asking the author to confirm or ensure things. The comment does not provide a specific code suggestion or point out a specific issue.

</details>

<details>
<summary>5. <code>packages/api-utils/src/numberUtils.ts:50</code></summary>

- **Draft comment:** 
Inconsistent use of the 'usePrefix' flag in the tens block: for multiples of 10 it conditionally uses word/prefix, but for non-multiples it always uses the prefix.
- **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/api-utils/src/numberUtils.ts:67</code></summary>

- **Draft comment:** 
In the hundreds block, the scale word is conditionally chosen when remainder is zero but always uses SCALES[0].prefix when remainder exists. Ensure consistent handling of 'usePrefix'.
- **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/api-utils/src/index.ts:26</code></summary>

- **Draft comment:** 
Typographical issue: The import statement on line 26 is missing a semicolon at the end. Please add a semicolon to maintain consistency with the other import statements.
- **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 about code style/formatting. This kind of issue would typically be caught by linters or formatters. The change in the diff is actually about renaming 'toWord' to 'numberToWord', not about semicolons. The semicolon issue is unrelated to the actual changes being made.
The semicolon inconsistency could indicate a larger pattern of code style issues that should be addressed. Maybe the author deliberately chose to omit semicolons as part of a new style guide.
Code style issues like missing semicolons should be handled by automated tools, not manual review comments. This is exactly the kind of thing that should be caught by the build process if it matters.
Delete this comment. It's about code formatting that should be handled by automated tools, and it's not related to the actual changes being made in the PR.

</details>

<details>
<summary>8. <code>packages/api-utils/src/index.ts:45</code></summary>

- **Draft comment:** 
Typographical issue: The module import on line 45 uses './quiriesWrappers'. This name appears to be misspelled and might be intended to be './queryWrappers' (or './queriesWrappers'). Please verify the correct module name and update 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>


Workflow ID: <workflowid>`wflow_99hODORvGHenBkHm`</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-09T04:16:25Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on 531121632a53725ae236b2a8c610d1096bd04539 in <ellipsistime>32 seconds</ellipsistime>

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

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


<details>
<summary>1. <code>packages/api-utils/src/numberUtils.ts:40</code></summary>

- **Draft comment:** 
Good use of nullish coalescing to check for defined prefix; ensures empty strings are preserved.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>packages/api-utils/src/numberUtils.ts:55</code></summary>

- **Draft comment:** 
Consistently applies '??' for prefix selection in two-digit numbers.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>3. <code>packages/api-utils/src/numberUtils.ts:69</code></summary>

- **Draft comment:** 
Uniformly using '??' ensures digit prefix fallback works as intended for hundreds.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>packages/api-utils/src/numberUtils.ts:70</code></summary>

- **Draft comment:** 
Using '??' for choosing scale prefix maintains consistency with digit handling.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>5. <code>packages/api-utils/src/numberUtils.ts:75</code></summary>

- **Draft comment:** 
Ensures consistent fallback logic in complex number formatting using '??'.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>6. <code>packages/api-utils/src/numberUtils.ts:40</code></summary>

- **Draft comment:** 
Good use of '??' to fallback only if prefix is null/undefined. Ensure that empty string values are correctly handled if intended.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>7. <code>packages/api-utils/src/numberUtils.ts:55</code></summary>

- **Draft comment:** 
Consistent replacement with '??' ensures fallback only for nullish prefixes. Verify behavior if an empty string is a valid prefix.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>8. <code>packages/api-utils/src/numberUtils.ts:69</code></summary>

- **Draft comment:** 
Using '??' here properly handles missing prefix values in hundreds. Confirm that this behavior aligns with all linguistic expectations.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>


Workflow ID: <workflowid>`wflow_xusOjqtb6LN7A2pC`</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-09T04:48:22Z

Review comments:
diff_hunk: @@ -0,0 +1,110 @@
+const DIGITS = [
+  { word: "тэг" },
+  { word: "нэг", prefix: "нэгэн" },
+  { word: "хоёр" },
+  { word: "гурав", prefix: "гурван" },
+  { word: "дөрөв", prefix: "дөрвөн" },
+  { word: "тав", prefix: "таван" },
+  { word: "зургаа", prefix: "зургаан" },
+  { word: "долоо", prefix: "долоон" },
+  { word: "найм", prefix: "найман" },
+  { word: "ес", prefix: "есөн" },
+];
+
+const TENS = [
+  { word: "арав", prefix: "арван" },
+  { word: "хорь", prefix: "хорин" },
+  { word: "гуч", prefix: "гучин" },
+  { word: "дөч", prefix: "дөчин" },
+  { word: "тавь", prefix: "тавин" },
+  { word: "жар", prefix: "жаран" },
+  { word: "дал", prefix: "далан" },
+  { word: "ная", prefix: "наян" },
+  { word: "ер", prefix: "ерэн" },
+];
+
+const SCALES = [
+  { word: "зуу", prefix: "зуун" },
+  { word: "мянга" },
+  { word: "сая" },
+  { word: "тэр бум" },
+  { word: "их наяд" },
+];
+
+const numberToWord = (num: number, usePrefix = false): string => {
+  if (num === 0) return DIGITS[0].word;
+
+  if (num < 10) {
+    // 1, 2, 3, 4, 5, 6, 7, 8, 9
+    return usePrefix
+      ? DIGITS[num].prefix || DIGITS[num].word
+      : DIGITS[num].word;
+  }
+
+  if (num < 100) {
+    // 10 - 99
+    const quotient = Math.floor(num / 10); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 10; // #1, #2, #3, #4, #5, #6, #7, #8, #9
+
+    if (remainder === 0) {
+      // 10, 20, 30, 40, 50, 60, 70, 80, 90
+      return usePrefix ? TENS[quotient - 1].prefix : TENS[quotient - 1].word;
+    }
+
+    return `${TENS[quotient - 1].prefix} ${
+      usePrefix
+        ? DIGITS[remainder].prefix || DIGITS[remainder].word
+        : DIGITS[remainder].word
+    }`;
+  }
+
+  if (num < 1_000) {
+    // 100 - 999
+    const quotient = Math.floor(num / 100); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 100; // ##, ##, ##, ##, ##, ##, ##, ##, ##
+
+    if (remainder === 0) {
+      // 100, 200, 300, 400, 500, 600, 700, 800, 900
+      return `${
+        quotient === 1
+          ? DIGITS[quotient].word
+          : DIGITS[quotient].prefix || DIGITS[quotient].word
+      } ${usePrefix ? SCALES[0].prefix || SCALES[0].word : SCALES[0].word}`;
+    }
+
+    return `${
+      quotient === 1
+        ? DIGITS[quotient].word
+        : DIGITS[quotient].prefix || DIGITS[quotient].word
+    } ${SCALES[0].prefix} ${numberToWord(remainder, usePrefix)}`;
path: packages/api-utils/src/numberUtils.ts
user: login: sourcery-ai[bot]
body: **suggestion:** Inconsistent treatment of scale labels in the 100-999 range.

When handling numbers between 100 and 999 with a non-zero remainder, the code always uses SCALES[0].prefix instead of conditionally selecting between the prefix and the word based on the usePrefix flag. For consistency with the tens block, consider applying the usePrefix check.

```suggestion
    return `${
      quotient === 1
        ? DIGITS[quotient].word
        : DIGITS[quotient].prefix || DIGITS[quotient].word
    } ${usePrefix ? SCALES[0].prefix || SCALES[0].word : SCALES[0].word} ${numberToWord(remainder, usePrefix)}`;
```
created_at: 2025-04-07T06:24:20Z
author_association: NONE
diff_hunk: @@ -0,0 +1,110 @@
+const DIGITS = [
+  { word: "тэг" },
+  { word: "нэг", prefix: "нэгэн" },
+  { word: "хоёр" },
+  { word: "гурав", prefix: "гурван" },
+  { word: "дөрөв", prefix: "дөрвөн" },
+  { word: "тав", prefix: "таван" },
+  { word: "зургаа", prefix: "зургаан" },
+  { word: "долоо", prefix: "долоон" },
+  { word: "найм", prefix: "найман" },
+  { word: "ес", prefix: "есөн" },
+];
+
+const TENS = [
+  { word: "арав", prefix: "арван" },
+  { word: "хорь", prefix: "хорин" },
+  { word: "гуч", prefix: "гучин" },
+  { word: "дөч", prefix: "дөчин" },
+  { word: "тавь", prefix: "тавин" },
+  { word: "жар", prefix: "жаран" },
+  { word: "дал", prefix: "далан" },
+  { word: "ная", prefix: "наян" },
+  { word: "ер", prefix: "ерэн" },
+];
+
+const SCALES = [
+  { word: "зуу", prefix: "зуун" },
+  { word: "мянга" },
+  { word: "сая" },
+  { word: "тэр бум" },
+  { word: "их наяд" },
+];
+
+const numberToWord = (num: number, usePrefix = false): string => {
+  if (num === 0) return DIGITS[0].word;
+
+  if (num < 10) {
+    // 1, 2, 3, 4, 5, 6, 7, 8, 9
+    return usePrefix
+      ? DIGITS[num].prefix || DIGITS[num].word
+      : DIGITS[num].word;
+  }
+
+  if (num < 100) {
+    // 10 - 99
+    const quotient = Math.floor(num / 10); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 10; // #1, #2, #3, #4, #5, #6, #7, #8, #9
+
+    if (remainder === 0) {
+      // 10, 20, 30, 40, 50, 60, 70, 80, 90
+      return usePrefix ? TENS[quotient - 1].prefix : TENS[quotient - 1].word;
+    }
+
+    return `${TENS[quotient - 1].prefix} ${
+      usePrefix
+        ? DIGITS[remainder].prefix || DIGITS[remainder].word
+        : DIGITS[remainder].word
+    }`;
+  }
+
+  if (num < 1_000) {
+    // 100 - 999
+    const quotient = Math.floor(num / 100); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 100; // ##, ##, ##, ##, ##, ##, ##, ##, ##
+
+    if (remainder === 0) {
+      // 100, 200, 300, 400, 500, 600, 700, 800, 900
+      return `${
+        quotient === 1
+          ? DIGITS[quotient].word
+          : DIGITS[quotient].prefix || DIGITS[quotient].word
+      } ${usePrefix ? SCALES[0].prefix || SCALES[0].word : SCALES[0].word}`;
+    }
+
+    return `${
+      quotient === 1
+        ? DIGITS[quotient].word
+        : DIGITS[quotient].prefix || DIGITS[quotient].word
+    } ${SCALES[0].prefix} ${numberToWord(remainder, usePrefix)}`;
+  }
+
+  for (let i = SCALES.length - 1; i >= 1; i--) {
+    const scaleValue = 10 ** (i * 3); // 1_000, 1_000_000, ...
+
+    if (num >= scaleValue) {
+      const quotient = Math.floor(num / scaleValue);
+      const remainder = num % scaleValue;
+
+      if (remainder === 0) {
+        return `${numberToWord(quotient, quotient !== 1)} ${SCALES[i].word}`;
+      }
+
+      return `${numberToWord(quotient, quotient !== 1)} ${
+        SCALES[i].word
+      } ${numberToWord(remainder)}`;
+    }
+  }
+
+  return "Number is too large";
path: packages/api-utils/src/numberUtils.ts
user: login: sourcery-ai[bot]
body: **suggestion:** Inconsistent error handling for large numbers.

The toWord function throws an error for numbers that are too large, while the numberToWord function simply returns a string. It may be clearer to consistently throw an error (or handle the error uniformly) in both cases.

```suggestion
  throw new Error("Number is too large");
```
created_at: 2025-04-07T06:24:20Z
author_association: NONE
diff_hunk: @@ -0,0 +1,67 @@
+import { sendCoreMessage } from "../messageBroker";
+
+export default {
+  handleDailyJob: async ({ subdomain }) => {
+    const NOW = new Date();
+    const MAIN_CURRENCY = "mainCurrency";
+    const DEAL_CURRENCY = "dealCurrency";
+
+    const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
+    try {
+      const configs = await sendCoreMessage({
+        subdomain,
+        action: "getConfigs",
+        data: {},
+        isRPC: true,
+        defaultValue: {},
+      });
+
+      if (!configs[MAIN_CURRENCY] && !configs[DEAL_CURRENCY]) {
+        return;
+      }
+
+      const PARAMS = {
+        startDate: NOW.toLocaleDateString("en-CA"),
+        endDate: NOW.toLocaleDateString("en-CA"),
+      };
+
+      const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+
+      const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+        method: "POST",
+      }).then((res) => res.json());
+
+      const { success, data = [] } = response || {};
+
+      if (!success || !data.length) {
+        return;
+      }
+
+      const exchangeRates: Record<string, string> = data[0] || {};
+
+      const rateCurrencies = configs[DEAL_CURRENCY] || [];
+
+      for (const [rateCurrency, rate] of Object.entries(exchangeRates)) {
+        if (!rateCurrency || !rate) {
+          continue;
+        }
+
+        if (rateCurrencies.includes(rateCurrency)) {
+          sendCoreMessage({
+            subdomain,
+            action: "exchangeRates.create",
+            data: {
+              mainCurrency: configs[MAIN_CURRENCY],
+              rateCurrency,
+              rate: Number(rate.replace(/,/g, "")),
+              date: NOW,
+            },
+          });
+        }
+      }
+    } catch (error) {
+      console.log("error", error.message);
path: packages/plugin-accountings-api/src/cronjobs/accountings.ts
user: login: sourcery-ai[bot]
body: **suggestion:** Error logging could provide more diagnostic detail.

Logging only the error message might omit useful debugging information (such as a stack trace). Consider logging the full error object to aid troubleshooting during failures in the cron job.

```suggestion
      console.error("error", error);
```
created_at: 2025-04-07T06:24:20Z
author_association: NONE
diff_hunk: @@ -0,0 +1,110 @@
+const DIGITS = [
+  { word: "тэг" },
+  { word: "нэг", prefix: "нэгэн" },
+  { word: "хоёр" },
+  { word: "гурав", prefix: "гурван" },
+  { word: "дөрөв", prefix: "дөрвөн" },
+  { word: "тав", prefix: "таван" },
+  { word: "зургаа", prefix: "зургаан" },
+  { word: "долоо", prefix: "долоон" },
+  { word: "найм", prefix: "найман" },
+  { word: "ес", prefix: "есөн" },
+];
+
+const TENS = [
+  { word: "арав", prefix: "арван" },
+  { word: "хорь", prefix: "хорин" },
+  { word: "гуч", prefix: "гучин" },
+  { word: "дөч", prefix: "дөчин" },
+  { word: "тавь", prefix: "тавин" },
+  { word: "жар", prefix: "жаран" },
+  { word: "дал", prefix: "далан" },
+  { word: "ная", prefix: "наян" },
+  { word: "ер", prefix: "ерэн" },
+];
+
+const SCALES = [
+  { word: "зуу", prefix: "зуун" },
+  { word: "мянга" },
+  { word: "сая" },
+  { word: "тэр бум" },
+  { word: "их наяд" },
+];
+
+const numberToWord = (num: number, usePrefix = false): string => {
path: packages/api-utils/src/numberUtils.ts
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider refactoring the `numberToWord` function by extracting the conversion logic for different numeric ranges into smaller, well-scoped helper functions to reduce complexity and improve readability. 

You might simplify `numberToWord` by splitting the conversion by numeric range into small helper functions. This reduces nested conditions and repeated ternary operations while keeping the functionality intact. For example, you could extract handling of single digits, tens, and hundreds into their own functions:

```ts
const convertDigit = (num: number, usePrefix: boolean): string =>
  usePrefix ? DIGITS[num].prefix || DIGITS[num].word : DIGITS[num].word;

const convertTens = (num: number, usePrefix: boolean): string => {
  const quotient = Math.floor(num / 10);
  const remainder = num % 10;
  if (remainder === 0) {
    return usePrefix ? TENS[quotient - 1].prefix : TENS[quotient - 1].word;
  }
  return `${TENS[quotient - 1].prefix} ${convertDigit(remainder, usePrefix)}`;
};

const convertHundreds = (num: number, usePrefix: boolean): string => {
  const quotient = Math.floor(num / 100);
  const remainder = num % 100;
  const hundredsWord = quotient === 1
    ? DIGITS[quotient].word
    : DIGITS[quotient].prefix || DIGITS[quotient].word;
  if (remainder === 0) {
    return `${hundredsWord} ${usePrefix ? SCALES[0].prefix || SCALES[0].word : SCALES[0].word}`;
  }
  return `${hundredsWord} ${SCALES[0].prefix} ${numberToWord(remainder, usePrefix)}`;
};

const numberToWord = (num: number, usePrefix = false): string => {
  if (num === 0) return DIGITS[0].word;
  if (num < 10) return convertDigit(num, usePrefix);
  if (num < 100) return convertTens(num, usePrefix);
  if (num < 1_000) return convertHundreds(num, usePrefix);

  for (let i = SCALES.length - 1; i >= 1; i--) {
    const scaleValue = 10 ** (i * 3);
    if (num >= scaleValue) {
      const quotient = Math.floor(num / scaleValue);
      const remainder = num % scaleValue;
      const quotientWord = numberToWord(quotient, quotient !== 1);
      const scaleWord = SCALES[i].word;
      return remainder === 0
        ? `${quotientWord} ${scaleWord}`
        : `${quotientWord} ${scaleWord} ${numberToWord(remainder)}`;
    }
  }

  return "Number is too large";
};
```

**Actionable steps:**

1. **Extract Helpers:** Create helper functions like `convertDigit`, `convertTens`, and `convertHundreds` to isolate logic for each numeric range.
2. **Reduce Nesting:** Replace deeply nested if/ternary operations with these well-scoped helpers.
3. **Improve Readability:** This modular approach makes the control flow more straightforward and easier to maintain.

These changes keep all functionality intact while reducing overall complexity.
created_at: 2025-04-07T06:24:20Z
author_association: NONE
diff_hunk: @@ -0,0 +1,110 @@
+const DIGITS = [
+  { word: "тэг" },
+  { word: "нэг", prefix: "нэгэн" },
+  { word: "хоёр" },
+  { word: "гурав", prefix: "гурван" },
+  { word: "дөрөв", prefix: "дөрвөн" },
+  { word: "тав", prefix: "таван" },
+  { word: "зургаа", prefix: "зургаан" },
+  { word: "долоо", prefix: "долоон" },
+  { word: "найм", prefix: "найман" },
+  { word: "ес", prefix: "есөн" },
+];
+
+const TENS = [
+  { word: "арав", prefix: "арван" },
+  { word: "хорь", prefix: "хорин" },
+  { word: "гуч", prefix: "гучин" },
+  { word: "дөч", prefix: "дөчин" },
+  { word: "тавь", prefix: "тавин" },
+  { word: "жар", prefix: "жаран" },
+  { word: "дал", prefix: "далан" },
+  { word: "ная", prefix: "наян" },
+  { word: "ер", prefix: "ерэн" },
+];
+
+const SCALES = [
+  { word: "зуу", prefix: "зуун" },
+  { word: "мянга" },
+  { word: "сая" },
+  { word: "тэр бум" },
+  { word: "их наяд" },
+];
+
+const numberToWord = (num: number, usePrefix = false): string => {
+  if (num === 0) return DIGITS[0].word;
path: packages/api-utils/src/numberUtils.ts
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-braces))

```suggestion
  if (num === 0) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-04-07T06:24:20Z
author_association: NONE
diff_hunk: @@ -0,0 +1,110 @@
+const DIGITS = [
+  { word: "тэг" },
+  { word: "нэг", prefix: "нэгэн" },
+  { word: "хоёр" },
+  { word: "гурав", prefix: "гурван" },
+  { word: "дөрөв", prefix: "дөрвөн" },
+  { word: "тав", prefix: "таван" },
+  { word: "зургаа", prefix: "зургаан" },
+  { word: "долоо", prefix: "долоон" },
+  { word: "найм", prefix: "найман" },
+  { word: "ес", prefix: "есөн" },
+];
+
+const TENS = [
+  { word: "арав", prefix: "арван" },
+  { word: "хорь", prefix: "хорин" },
+  { word: "гуч", prefix: "гучин" },
+  { word: "дөч", prefix: "дөчин" },
+  { word: "тавь", prefix: "тавин" },
+  { word: "жар", prefix: "жаран" },
+  { word: "дал", prefix: "далан" },
+  { word: "ная", prefix: "наян" },
+  { word: "ер", prefix: "ерэн" },
+];
+
+const SCALES = [
+  { word: "зуу", prefix: "зуун" },
+  { word: "мянга" },
+  { word: "сая" },
+  { word: "тэр бум" },
+  { word: "их наяд" },
+];
+
+const numberToWord = (num: number, usePrefix = false): string => {
+  if (num === 0) return DIGITS[0].word;
+
+  if (num < 10) {
+    // 1, 2, 3, 4, 5, 6, 7, 8, 9
+    return usePrefix
+      ? DIGITS[num].prefix || DIGITS[num].word
+      : DIGITS[num].word;
+  }
+
+  if (num < 100) {
+    // 10 - 99
+    const quotient = Math.floor(num / 10); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 10; // #1, #2, #3, #4, #5, #6, #7, #8, #9
+
+    if (remainder === 0) {
+      // 10, 20, 30, 40, 50, 60, 70, 80, 90
+      return usePrefix ? TENS[quotient - 1].prefix : TENS[quotient - 1].word;
+    }
+
+    return `${TENS[quotient - 1].prefix} ${
+      usePrefix
+        ? DIGITS[remainder].prefix || DIGITS[remainder].word
+        : DIGITS[remainder].word
+    }`;
+  }
+
+  if (num < 1_000) {
+    // 100 - 999
+    const quotient = Math.floor(num / 100); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 100; // ##, ##, ##, ##, ##, ##, ##, ##, ##
+
+    if (remainder === 0) {
+      // 100, 200, 300, 400, 500, 600, 700, 800, 900
+      return `${
+        quotient === 1
+          ? DIGITS[quotient].word
+          : DIGITS[quotient].prefix || DIGITS[quotient].word
+      } ${usePrefix ? SCALES[0].prefix || SCALES[0].word : SCALES[0].word}`;
+    }
+
+    return `${
+      quotient === 1
+        ? DIGITS[quotient].word
+        : DIGITS[quotient].prefix || DIGITS[quotient].word
+    } ${SCALES[0].prefix} ${numberToWord(remainder, usePrefix)}`;
path: packages/api-utils/src/numberUtils.ts
user: login: ellipsis-dev[bot]
body: Inconsistent use of the `usePrefix` flag: for numbers 100-999 with a remainder, the scale always uses `SCALES[0].prefix` instead of checking `usePrefix`.
```suggestion
    } ${usePrefix ? SCALES[0].prefix : SCALES[0].word} ${numberToWord(remainder, usePrefix)};
```
created_at: 2025-04-07T06:25:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,110 @@
+const DIGITS = [
+  { word: "тэг" },
+  { word: "нэг", prefix: "нэгэн" },
+  { word: "хоёр" },
+  { word: "гурав", prefix: "гурван" },
+  { word: "дөрөв", prefix: "дөрвөн" },
+  { word: "тав", prefix: "таван" },
+  { word: "зургаа", prefix: "зургаан" },
+  { word: "долоо", prefix: "долоон" },
+  { word: "найм", prefix: "найман" },
+  { word: "ес", prefix: "есөн" },
+];
+
+const TENS = [
+  { word: "арав", prefix: "арван" },
+  { word: "хорь", prefix: "хорин" },
+  { word: "гуч", prefix: "гучин" },
+  { word: "дөч", prefix: "дөчин" },
+  { word: "тавь", prefix: "тавин" },
+  { word: "жар", prefix: "жаран" },
+  { word: "дал", prefix: "далан" },
+  { word: "ная", prefix: "наян" },
+  { word: "ер", prefix: "ерэн" },
+];
+
+const SCALES = [
+  { word: "зуу", prefix: "зуун" },
+  { word: "мянга" },
+  { word: "сая" },
+  { word: "тэр бум" },
+  { word: "их наяд" },
+];
+
+const numberToWord = (num: number, usePrefix = false): string => {
+  if (num === 0) return DIGITS[0].word;
+
+  if (num < 10) {
+    // 1, 2, 3, 4, 5, 6, 7, 8, 9
+    return usePrefix
+      ? DIGITS[num].prefix || DIGITS[num].word
+      : DIGITS[num].word;
+  }
+
+  if (num < 100) {
+    // 10 - 99
+    const quotient = Math.floor(num / 10); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 10; // #1, #2, #3, #4, #5, #6, #7, #8, #9
+
+    if (remainder === 0) {
+      // 10, 20, 30, 40, 50, 60, 70, 80, 90
+      return usePrefix ? TENS[quotient - 1].prefix : TENS[quotient - 1].word;
+    }
+
+    return `${TENS[quotient - 1].prefix} ${
+      usePrefix
+        ? DIGITS[remainder].prefix || DIGITS[remainder].word
+        : DIGITS[remainder].word
+    }`;
+  }
+
+  if (num < 1_000) {
+    // 100 - 999
+    const quotient = Math.floor(num / 100); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 100; // ##, ##, ##, ##, ##, ##, ##, ##, ##
+
+    if (remainder === 0) {
+      // 100, 200, 300, 400, 500, 600, 700, 800, 900
+      return `${
+        quotient === 1
+          ? DIGITS[quotient].word
+          : DIGITS[quotient].prefix || DIGITS[quotient].word
+      } ${usePrefix ? SCALES[0].prefix || SCALES[0].word : SCALES[0].word}`;
+    }
+
+    return `${
+      quotient === 1
+        ? DIGITS[quotient].word
+        : DIGITS[quotient].prefix || DIGITS[quotient].word
+    } ${SCALES[0].prefix} ${numberToWord(remainder, usePrefix)}`;
+  }
+
+  for (let i = SCALES.length - 1; i >= 1; i--) {
+    const scaleValue = 10 ** (i * 3); // 1_000, 1_000_000, ...
+
+    if (num >= scaleValue) {
+      const quotient = Math.floor(num / scaleValue);
+      const remainder = num % scaleValue;
+
+      if (remainder === 0) {
+        return `${numberToWord(quotient, quotient !== 1)} ${SCALES[i].word}`;
+      }
+
+      return `${numberToWord(quotient, quotient !== 1)} ${
+        SCALES[i].word
+      } ${numberToWord(remainder)}`;
+    }
+  }
+
+  return "Number is too large";
+};
+
+const toWord = (num: number): string => {
path: packages/api-utils/src/numberUtils.ts
user: login: ellipsis-dev[bot]
body: Consider handling (or rejecting) negative numbers explicitly.
created_at: 2025-04-07T06:25:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,110 @@
+const DIGITS = [
+  { word: "тэг" },
+  { word: "нэг", prefix: "нэгэн" },
+  { word: "хоёр" },
+  { word: "гурав", prefix: "гурван" },
+  { word: "дөрөв", prefix: "дөрвөн" },
+  { word: "тав", prefix: "таван" },
+  { word: "зургаа", prefix: "зургаан" },
+  { word: "долоо", prefix: "долоон" },
+  { word: "найм", prefix: "найман" },
+  { word: "ес", prefix: "есөн" },
+];
+
+const TENS = [
+  { word: "арав", prefix: "арван" },
+  { word: "хорь", prefix: "хорин" },
+  { word: "гуч", prefix: "гучин" },
+  { word: "дөч", prefix: "дөчин" },
+  { word: "тавь", prefix: "тавин" },
+  { word: "жар", prefix: "жаран" },
+  { word: "дал", prefix: "далан" },
+  { word: "ная", prefix: "наян" },
+  { word: "ер", prefix: "ерэн" },
+];
+
+const SCALES = [
+  { word: "зуу", prefix: "зуун" },
+  { word: "мянга" },
+  { word: "сая" },
+  { word: "тэр бум" },
+  { word: "их наяд" },
+];
+
+const numberToWord = (num: number, usePrefix = false): string => {
+  if (num === 0) return DIGITS[0].word;
+
+  if (num < 10) {
+    // 1, 2, 3, 4, 5, 6, 7, 8, 9
+    return usePrefix
+      ? DIGITS[num].prefix || DIGITS[num].word
+      : DIGITS[num].word;
+  }
+
+  if (num < 100) {
+    // 10 - 99
+    const quotient = Math.floor(num / 10); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 10; // #1, #2, #3, #4, #5, #6, #7, #8, #9
+
+    if (remainder === 0) {
+      // 10, 20, 30, 40, 50, 60, 70, 80, 90
+      return usePrefix ? TENS[quotient - 1].prefix : TENS[quotient - 1].word;
+    }
+
+    return `${TENS[quotient - 1].prefix} ${
path: packages/api-utils/src/numberUtils.ts
user: login: ellipsis-dev[bot]
body: In the tens block for non-zero remainders, the code always uses `TENS[quotient - 1].prefix` regardless of the `usePrefix` flag. Consider using the non-prefix word when `usePrefix` is false.
```suggestion
    return `${usePrefix ? TENS[quotient - 1].prefix : TENS[quotient - 1].word} ${
```
created_at: 2025-04-07T06:25:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,110 @@
+const DIGITS = [
+  { word: "тэг" },
+  { word: "нэг", prefix: "нэгэн" },
+  { word: "хоёр" },
+  { word: "гурав", prefix: "гурван" },
+  { word: "дөрөв", prefix: "дөрвөн" },
+  { word: "тав", prefix: "таван" },
+  { word: "зургаа", prefix: "зургаан" },
+  { word: "долоо", prefix: "долоон" },
+  { word: "найм", prefix: "найман" },
+  { word: "ес", prefix: "есөн" },
+];
+
+const TENS = [
+  { word: "арав", prefix: "арван" },
+  { word: "хорь", prefix: "хорин" },
+  { word: "гуч", prefix: "гучин" },
+  { word: "дөч", prefix: "дөчин" },
+  { word: "тавь", prefix: "тавин" },
+  { word: "жар", prefix: "жаран" },
+  { word: "дал", prefix: "далан" },
+  { word: "ная", prefix: "наян" },
+  { word: "ер", prefix: "ерэн" },
+];
+
+const SCALES = [
+  { word: "зуу", prefix: "зуун" },
+  { word: "мянга" },
+  { word: "сая" },
+  { word: "тэр бум" },
+  { word: "их наяд" },
+];
+
+const numberToWord = (num: number, usePrefix = false): string => {
+  if (num === 0) return DIGITS[0].word;
+
+  if (num < 10) {
+    // 1, 2, 3, 4, 5, 6, 7, 8, 9
+    return usePrefix
+      ? DIGITS[num].prefix || DIGITS[num].word
+      : DIGITS[num].word;
+  }
+
+  if (num < 100) {
+    // 10 - 99
+    const quotient = Math.floor(num / 10); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 10; // #1, #2, #3, #4, #5, #6, #7, #8, #9
+
+    if (remainder === 0) {
+      // 10, 20, 30, 40, 50, 60, 70, 80, 90
+      return usePrefix ? TENS[quotient - 1].prefix : TENS[quotient - 1].word;
+    }
+
+    return `${TENS[quotient - 1].prefix} ${
+      usePrefix
+        ? DIGITS[remainder].prefix || DIGITS[remainder].word
+        : DIGITS[remainder].word
+    }`;
+  }
+
+  if (num < 1_000) {
+    // 100 - 999
+    const quotient = Math.floor(num / 100); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 100; // ##, ##, ##, ##, ##, ##, ##, ##, ##
+
+    if (remainder === 0) {
+      // 100, 200, 300, 400, 500, 600, 700, 800, 900
+      return `${
+        quotient === 1
+          ? DIGITS[quotient].word
+          : DIGITS[quotient].prefix || DIGITS[quotient].word
+      } ${usePrefix ? SCALES[0].prefix || SCALES[0].word : SCALES[0].word}`;
+    }
+
+    return `${
+      quotient === 1
+        ? DIGITS[quotient].word
+        : DIGITS[quotient].prefix || DIGITS[quotient].word
+    } ${SCALES[0].prefix} ${numberToWord(remainder, usePrefix)}`;
+  }
+
+  for (let i = SCALES.length - 1; i >= 1; i--) {
+    const scaleValue = 10 ** (i * 3); // 1_000, 1_000_000, ...
+
+    if (num >= scaleValue) {
+      const quotient = Math.floor(num / scaleValue);
+      const remainder = num % scaleValue;
+
+      if (remainder === 0) {
+        return `${numberToWord(quotient, quotient !== 1)} ${SCALES[i].word}`;
+      }
+
+      return `${numberToWord(quotient, quotient !== 1)} ${
+        SCALES[i].word
+      } ${numberToWord(remainder)}`;
+    }
+  }
+
+  return "Number is too large";
+};
+
+const toWord = (num: number): string => {
+  if (num > 1_000_000_000_000_000) {
+    throw new Error("Number is too large");
path: packages/api-utils/src/numberUtils.ts
user: login: ellipsis-dev[bot]
body: The toWord wrapper throws an error for numbers > 1_000_000_000_000_000, but if a number equals the threshold, numberToWord returns 'Number is too large'. Consider unifying the behavior.
created_at: 2025-04-07T06:25:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+import { sendCoreMessage } from "../messageBroker";
+
+export default {
+  handleDailyJob: async ({ subdomain }) => {
+    const NOW = new Date();
+    const MAIN_CURRENCY = "mainCurrency";
+    const DEAL_CURRENCY = "dealCurrency";
+
+    const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
+    try {
+      const configs = await sendCoreMessage({
+        subdomain,
+        action: "getConfigs",
+        data: {},
+        isRPC: true,
+        defaultValue: {},
+      });
+
+      if (!configs[MAIN_CURRENCY] && !configs[DEAL_CURRENCY]) {
+        return;
+      }
+
+      const PARAMS = {
+        startDate: NOW.toLocaleDateString("en-CA"),
+        endDate: NOW.toLocaleDateString("en-CA"),
+      };
+
+      const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+
+      const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
path: packages/plugin-accountings-api/src/cronjobs/accountings.ts
user: login: ellipsis-dev[bot]
body: Prefer using async/await consistently instead of mixing with .then(). Refactor the fetch call to use await for parsing the JSON.
created_at: 2025-04-07T06:25:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+import { sendCoreMessage } from "../messageBroker";
+
+export default {
+  handleDailyJob: async ({ subdomain }) => {
+    const NOW = new Date();
+    const MAIN_CURRENCY = "mainCurrency";
+    const DEAL_CURRENCY = "dealCurrency";
+
+    const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
+    try {
+      const configs = await sendCoreMessage({
+        subdomain,
+        action: "getConfigs",
+        data: {},
+        isRPC: true,
+        defaultValue: {},
+      });
+
+      if (!configs[MAIN_CURRENCY] && !configs[DEAL_CURRENCY]) {
+        return;
+      }
+
+      const PARAMS = {
+        startDate: NOW.toLocaleDateString("en-CA"),
+        endDate: NOW.toLocaleDateString("en-CA"),
+      };
+
+      const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+
+      const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+        method: "POST",
+      }).then((res) => res.json());
+
+      const { success, data = [] } = response || {};
+
+      if (!success || !data.length) {
+        return;
+      }
+
+      const exchangeRates: Record<string, string> = data[0] || {};
+
+      const rateCurrencies = configs[DEAL_CURRENCY] || [];
+
+      for (const [rateCurrency, rate] of Object.entries(exchangeRates)) {
+        if (!rateCurrency || !rate) {
+          continue;
+        }
+
+        if (rateCurrencies.includes(rateCurrency)) {
+          sendCoreMessage({
+            subdomain,
+            action: "exchangeRates.create",
+            data: {
+              mainCurrency: configs[MAIN_CURRENCY],
+              rateCurrency,
+              rate: Number(rate.replace(/,/g, "")),
+              date: NOW,
+            },
+          });
+        }
+      }
+    } catch (error) {
+      console.log("error", error.message);
path: packages/plugin-accountings-api/src/cronjobs/accountings.ts
user: login: ellipsis-dev[bot]
body: Use `console.error` instead of `console.log` in the catch block for proper error logging.
```suggestion
      console.error("error", error.message);
```
created_at: 2025-04-07T06:25:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,110 @@
+const DIGITS = [
+  { word: "тэг" },
+  { word: "нэг", prefix: "нэгэн" },
+  { word: "хоёр" },
+  { word: "гурав", prefix: "гурван" },
+  { word: "дөрөв", prefix: "дөрвөн" },
+  { word: "тав", prefix: "таван" },
+  { word: "зургаа", prefix: "зургаан" },
+  { word: "долоо", prefix: "долоон" },
+  { word: "найм", prefix: "найман" },
+  { word: "ес", prefix: "есөн" },
+];
+
+const TENS = [
+  { word: "арав", prefix: "арван" },
+  { word: "хорь", prefix: "хорин" },
+  { word: "гуч", prefix: "гучин" },
+  { word: "дөч", prefix: "дөчин" },
+  { word: "тавь", prefix: "тавин" },
+  { word: "жар", prefix: "жаран" },
+  { word: "дал", prefix: "далан" },
+  { word: "ная", prefix: "наян" },
+  { word: "ер", prefix: "ерэн" },
+];
+
+const SCALES = [
+  { word: "зуу", prefix: "зуун" },
+  { word: "мянга" },
+  { word: "сая" },
+  { word: "тэр бум" },
+  { word: "их наяд" },
+];
+
+const numberToWord = (num: number, usePrefix = false): string => {
+  if (num === 0) return DIGITS[0].word;
+
+  if (num < 10) {
+    // 1, 2, 3, 4, 5, 6, 7, 8, 9
+    return usePrefix
+      ? DIGITS[num].prefix || DIGITS[num].word
+      : DIGITS[num].word;
+  }
+
+  if (num < 100) {
+    // 10 - 99
+    const quotient = Math.floor(num / 10); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 10; // #1, #2, #3, #4, #5, #6, #7, #8, #9
+
+    if (remainder === 0) {
+      // 10, 20, 30, 40, 50, 60, 70, 80, 90
+      return usePrefix ? TENS[quotient - 1].prefix : TENS[quotient - 1].word;
+    }
+
+    return `${TENS[quotient - 1].prefix} ${
+      usePrefix
+        ? DIGITS[remainder].prefix || DIGITS[remainder].word
+        : DIGITS[remainder].word
+    }`;
+  }
+
+  if (num < 1_000) {
+    // 100 - 999
+    const quotient = Math.floor(num / 100); // 1#, 2#, 3#, 4#, 5#, 6#, 7#, 8#, 9#
+    const remainder = num % 100; // ##, ##, ##, ##, ##, ##, ##, ##, ##
+
+    if (remainder === 0) {
+      // 100, 200, 300, 400, 500, 600, 700, 800, 900
+      return `${
+        quotient === 1
+          ? DIGITS[quotient].word
+          : DIGITS[quotient].prefix || DIGITS[quotient].word
+      } ${usePrefix ? SCALES[0].prefix || SCALES[0].word : SCALES[0].word}`;
+    }
+
+    return `${
+      quotient === 1
+        ? DIGITS[quotient].word
+        : DIGITS[quotient].prefix || DIGITS[quotient].word
+    } ${SCALES[0].prefix} ${numberToWord(remainder, usePrefix)}`;
+  }
+
+  for (let i = SCALES.length - 1; i >= 1; i--) {
+    const scaleValue = 10 ** (i * 3); // 1_000, 1_000_000, ...
+
+    if (num >= scaleValue) {
+      const quotient = Math.floor(num / scaleValue);
+      const remainder = num % scaleValue;
+
+      if (remainder === 0) {
+        return `${numberToWord(quotient, quotient !== 1)} ${SCALES[i].word}`;
+      }
+
+      return `${numberToWord(quotient, quotient !== 1)} ${
+        SCALES[i].word
+      } ${numberToWord(remainder)}`;
+    }
+  }
+
+  return "Number is too large";
+};
+
+const toWord = (num: number): string => {
+  if (num > 1_000_000_000_000_000) {
+    throw new Error("Number is too large");
+  }
+
+  return numberToWord(num, false);
+};
path: packages/api-utils/src/numberUtils.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add validation for negative and non-integer numbers in `toWord`.**

The function currently only checks if the number is too large, but doesn't validate for negative numbers or non-integer values, which might produce unexpected results.


```diff
 const toWord = (num: number): string => {
+  // Validate input
+  if (num < 0) {
+    throw new Error("Negative numbers are not supported");
+  }
+  
+  if (!Number.isInteger(num)) {
+    throw new Error("Only integer numbers are supported");
+  }
+  
   if (num > 1_000_000_000_000_000) {
     throw new Error("Number is too large");
   }
 
   return numberToWord(num, false);
 };
```


<!-- 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 toWord = (num: number): string => {
  // Validate input
  if (num < 0) {
    throw new Error("Negative numbers are not supported");
  }
  
  if (!Number.isInteger(num)) {
    throw new Error("Only integer numbers are supported");
  }
  
  if (num > 1_000_000_000_000_000) {
    throw new Error("Number is too large");
  }

  return numberToWord(num, false);
};
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 4f0cb78 to e1a537e
created_at: 2025-04-07T06:28:10Z
author_association: NONE
diff_hunk: @@ -0,0 +1,67 @@
+import { sendCoreMessage } from "../messageBroker";
+
+export default {
+  handleDailyJob: async ({ subdomain }) => {
+    const NOW = new Date();
+    const MAIN_CURRENCY = "mainCurrency";
+    const DEAL_CURRENCY = "dealCurrency";
+
+    const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
+    try {
+      const configs = await sendCoreMessage({
+        subdomain,
+        action: "getConfigs",
+        data: {},
+        isRPC: true,
+        defaultValue: {},
+      });
+
+      if (!configs[MAIN_CURRENCY] && !configs[DEAL_CURRENCY]) {
+        return;
+      }
+
+      const PARAMS = {
+        startDate: NOW.toLocaleDateString("en-CA"),
+        endDate: NOW.toLocaleDateString("en-CA"),
+      };
+
+      const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+
+      const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+        method: "POST",
+      }).then((res) => res.json());
path: packages/plugin-accountings-api/src/cronjobs/accountings.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve the fetch response handling.**

The current implementation doesn't check the HTTP status code before trying to parse the JSON, which could lead to errors if the request fails.


```diff
-      const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+      const fetchResponse = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
         method: "POST",
-      }).then((res) => res.json());
+      });
+      
+      if (!fetchResponse.ok) {
+        console.error(`Failed to fetch exchange rates: ${fetchResponse.status} ${fetchResponse.statusText}`);
+        return;
+      }
+      
+      const response: any = await fetchResponse.json();
```


<!-- 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 fetchResponse = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
        method: "POST",
      });
      
      if (!fetchResponse.ok) {
        console.error(`Failed to fetch exchange rates: ${fetchResponse.status} ${fetchResponse.statusText}`);
        return;
      }
      
      const response: any = await fetchResponse.json();
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 876262f
created_at: 2025-04-07T06:28:10Z
author_association: NONE
diff_hunk: @@ -0,0 +1,67 @@
+import { sendCoreMessage } from "../messageBroker";
+
+export default {
+  handleDailyJob: async ({ subdomain }) => {
+    const NOW = new Date();
+    const MAIN_CURRENCY = "mainCurrency";
+    const DEAL_CURRENCY = "dealCurrency";
+
+    const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
+    try {
+      const configs = await sendCoreMessage({
+        subdomain,
+        action: "getConfigs",
+        data: {},
+        isRPC: true,
+        defaultValue: {},
+      });
+
+      if (!configs[MAIN_CURRENCY] && !configs[DEAL_CURRENCY]) {
+        return;
+      }
+
+      const PARAMS = {
+        startDate: NOW.toLocaleDateString("en-CA"),
+        endDate: NOW.toLocaleDateString("en-CA"),
+      };
+
+      const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+
+      const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+        method: "POST",
+      }).then((res) => res.json());
+
+      const { success, data = [] } = response || {};
+
+      if (!success || !data.length) {
+        return;
+      }
+
+      const exchangeRates: Record<string, string> = data[0] || {};
+
+      const rateCurrencies = configs[DEAL_CURRENCY] || [];
+
+      for (const [rateCurrency, rate] of Object.entries(exchangeRates)) {
+        if (!rateCurrency || !rate) {
+          continue;
+        }
+
+        if (rateCurrencies.includes(rateCurrency)) {
+          sendCoreMessage({
+            subdomain,
+            action: "exchangeRates.create",
+            data: {
+              mainCurrency: configs[MAIN_CURRENCY],
+              rateCurrency,
+              rate: Number(rate.replace(/,/g, "")),
+              date: NOW,
path: packages/plugin-accountings-api/src/cronjobs/accountings.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Ensure the rate value is properly parsed.**

The current implementation properly removes commas from the rate string before converting it to a number, but it doesn't handle other potential formatting issues that might cause `Number()` to return `NaN`.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the rate parsing is robust by printing out possible rate values from the API

echo "Simulating a sample rate value conversion:"
SAMPLE_RATE="3,456.78"
echo "Sample rate: $SAMPLE_RATE"
# Use node to parse the rate in the same way as the code
NODE_RESULT=$(node -e "console.log(Number('$SAMPLE_RATE'.replace(/,/g, '')))")
echo "Parsed result: $NODE_RESULT"

# Check for other potential formats that might be returned by the API
echo "Testing other potential formats:"
FORMATS=("3.456,78" "3 456,78" "3456.78" "3456,78")
for FORMAT in "${FORMATS[@]}"; do
  echo "Format: $FORMAT"
  NODE_RESULT=$(node -e "console.log(Number('$FORMAT'.replace(/,/g, '')))")
  echo "Parsed result: $NODE_RESULT"
done
```

Length of output: 1338

---



</details>

**Action Required: Enhance the rate parsing logic**

- In `packages/plugin-accountings-api/src/cronjobs/accountings.ts` (lines 57–58), the current implementation uses `Number(rate.replace(/,/g, ""))`. While this works for values like `"3,456.78"`, our tests show it does not correctly handle other formats.
  - For example, `"3456,78"` is converted to `345678` instead of the intended `3456.78`.
  - Similarly, `"3 456,78"` results in `NaN` after removing commas.
- **Recommendation:** Update the parsing logic to handle different international formats (e.g., handling spaces as thousand separators and commas as decimal marks) or consider using a robust library (such as numeral.js) to ensure correct numeric conversions.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-07T06:28:10Z
author_association: NONE
diff_hunk: @@ -0,0 +1,74 @@
+import { sendCoreMessage } from "../messageBroker";
+
+const mntRates = async (subdomain, NOW, rateCurrencies) => {
path: packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts
user: login: ellipsis-dev[bot]
body: Consider adding type annotations for function parameters and the response to improve type safety.
```suggestion
const mntRates = async (subdomain: string, NOW: Date, rateCurrencies: string[]): Promise<void> => {
```
created_at: 2025-04-09T04:01:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+import { sendCoreMessage } from "../messageBroker";
+
+const mntRates = async (subdomain, NOW, rateCurrencies) => {
+  const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
+  try {
+    const PARAMS = {
+      startDate: NOW.toLocaleDateString("en-CA"),
+      endDate: NOW.toLocaleDateString("en-CA"),
+    };
+
+    const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+
+    const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+      method: "POST",
+    }).then((res) => res.json());
+
+    const { success, data = [] } = response || {};
+
+    if (!success || !data.length) {
+      return;
+    }
+
+    const exchangeRates: Record<string, string> = data[0] || {};
+
+    for (const [rateCurrency, rate] of Object.entries(exchangeRates)) {
+      if (!rateCurrency || !rate) {
+        continue;
+      }
+
+      if (rateCurrencies.includes(rateCurrency)) {
+        sendCoreMessage({
+          subdomain,
+          action: "exchangeRates.create",
+          data: {
+            mainCurrency: 'MNT',
+            rateCurrency,
+            rate: Number(rate.replace(/,/g, "")),
+            date: NOW,
+          },
+        });
+      }
+    }
+  } catch (error) {
+    console.log("error", error.message);
path: packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts
user: login: ellipsis-dev[bot]
body: Replace `console.log` with a logging utility for consistent error logging.
```suggestion
    logger.error("error", error.message);
```
created_at: 2025-04-09T04:01:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+import { sendCoreMessage } from "../messageBroker";
+
+const mntRates = async (subdomain, NOW, rateCurrencies) => {
+  const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
+  try {
+    const PARAMS = {
+      startDate: NOW.toLocaleDateString("en-CA"),
+      endDate: NOW.toLocaleDateString("en-CA"),
+    };
+
+    const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+
+    const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+      method: "POST",
+    }).then((res) => res.json());
+
+    const { success, data = [] } = response || {};
+
+    if (!success || !data.length) {
+      return;
+    }
+
+    const exchangeRates: Record<string, string> = data[0] || {};
+
+    for (const [rateCurrency, rate] of Object.entries(exchangeRates)) {
+      if (!rateCurrency || !rate) {
+        continue;
+      }
+
+      if (rateCurrencies.includes(rateCurrency)) {
+        sendCoreMessage({
path: packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts
user: login: ellipsis-dev[bot]
body: sendCoreMessage calls inside the loop are not awaited. Consider awaiting or handling errors to ensure proper message delivery.
```suggestion
        await sendCoreMessage({
```
created_at: 2025-04-09T04:01:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+import { sendCoreMessage } from "../messageBroker";
+
+const mntRates = async (subdomain, NOW, rateCurrencies) => {
+  const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
+  try {
+    const PARAMS = {
+      startDate: NOW.toLocaleDateString("en-CA"),
+      endDate: NOW.toLocaleDateString("en-CA"),
+    };
+
+    const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+
+    const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+      method: "POST",
+    }).then((res) => res.json());
+
+    const { success, data = [] } = response || {};
+
+    if (!success || !data.length) {
+      return;
+    }
+
+    const exchangeRates: Record<string, string> = data[0] || {};
+
+    for (const [rateCurrency, rate] of Object.entries(exchangeRates)) {
+      if (!rateCurrency || !rate) {
+        continue;
+      }
+
+      if (rateCurrencies.includes(rateCurrency)) {
+        sendCoreMessage({
+          subdomain,
+          action: "exchangeRates.create",
+          data: {
+            mainCurrency: 'MNT',
+            rateCurrency,
+            rate: Number(rate.replace(/,/g, "")),
path: packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts
user: login: ellipsis-dev[bot]
body: Consider validating the numeric conversion as `Number(rate.replace(/,/g, ''))` may yield `NaN` if the rate is invalid.
```suggestion
            rate: isNaN(Number(rate.replace(/,/g, ""))) ? 0 : Number(rate.replace(/,/g, "")),
```
created_at: 2025-04-09T04:01:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,74 @@
+import { sendCoreMessage } from "../messageBroker";
+
+const mntRates = async (subdomain, NOW, rateCurrencies) => {
+  const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
+  try {
+    const PARAMS = {
+      startDate: NOW.toLocaleDateString("en-CA"),
+      endDate: NOW.toLocaleDateString("en-CA"),
+    };
+
+    const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+
+    const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+      method: "POST",
+    }).then((res) => res.json());
+
+    const { success, data = [] } = response || {};
+
+    if (!success || !data.length) {
+      return;
+    }
+
+    const exchangeRates: Record<string, string> = data[0] || {};
+
+    for (const [rateCurrency, rate] of Object.entries(exchangeRates)) {
+      if (!rateCurrency || !rate) {
+        continue;
+      }
+
+      if (rateCurrencies.includes(rateCurrency)) {
+        sendCoreMessage({
+          subdomain,
+          action: "exchangeRates.create",
+          data: {
+            mainCurrency: 'MNT',
+            rateCurrency,
+            rate: Number(rate.replace(/,/g, "")),
+            date: NOW,
+          },
+        });
+      }
+    }
+  } catch (error) {
+    console.log("error", error.message);
+  }
+}
path: packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add type definitions for function parameters**

The `mntRates` function lacks proper TypeScript type annotations for its parameters, which could lead to type-related bugs and reduces code maintainability.


```diff
-const mntRates = async (subdomain, NOW, rateCurrencies) => {
+const mntRates = async (subdomain: string, NOW: Date, rateCurrencies: string[]) => {
```


<!-- 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 mntRates = async (subdomain: string, NOW: Date, rateCurrencies: string[]) => {
  const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";

  try {
    const PARAMS = {
      startDate: NOW.toLocaleDateString("en-CA"),
      endDate: NOW.toLocaleDateString("en-CA"),
    };

    const QUERY_STRING = new URLSearchParams(PARAMS).toString();

    const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
      method: "POST",
    }).then((res) => res.json());

    const { success, data = [] } = response || {};

    if (!success || !data.length) {
      return;
    }

    const exchangeRates: Record<string, string> = data[0] || {};

    for (const [rateCurrency, rate] of Object.entries(exchangeRates)) {
      if (!rateCurrency || !rate) {
        continue;
      }

      if (rateCurrencies.includes(rateCurrency)) {
        sendCoreMessage({
          subdomain,
          action: "exchangeRates.create",
          data: {
            mainCurrency: 'MNT',
            rateCurrency,
            rate: Number(rate.replace(/,/g, "")),
            date: NOW,
          },
        });
      }
    }
  } catch (error) {
    console.log("error", error.message);
  }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit dcf7bcb
created_at: 2025-04-09T04:03:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,74 @@
+import { sendCoreMessage } from "../messageBroker";
+
+const mntRates = async (subdomain, NOW, rateCurrencies) => {
+  const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
+  try {
+    const PARAMS = {
+      startDate: NOW.toLocaleDateString("en-CA"),
+      endDate: NOW.toLocaleDateString("en-CA"),
+    };
+
+    const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+
+    const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+      method: "POST",
+    }).then((res) => res.json());
+
+    const { success, data = [] } = response || {};
+
+    if (!success || !data.length) {
+      return;
+    }
+
+    const exchangeRates: Record<string, string> = data[0] || {};
+
+    for (const [rateCurrency, rate] of Object.entries(exchangeRates)) {
+      if (!rateCurrency || !rate) {
+        continue;
+      }
+
+      if (rateCurrencies.includes(rateCurrency)) {
+        sendCoreMessage({
+          subdomain,
+          action: "exchangeRates.create",
+          data: {
+            mainCurrency: 'MNT',
+            rateCurrency,
+            rate: Number(rate.replace(/,/g, "")),
+            date: NOW,
+          },
+        });
+      }
+    }
+  } catch (error) {
+    console.log("error", error.message);
+  }
path: packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve error handling**

The current error handling only logs the error message. Consider adding more comprehensive error logging and proper error propagation.


```diff
-  } catch (error) {
-    console.log("error", error.message);
+  } catch (error) {
+    console.error("Error fetching exchange rates:", error);
+    // Consider adding more context to the error
+    // Consider proper error propagation or retry mechanism
```


<!-- 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
  } catch (error) {
    console.error("Error fetching exchange rates:", error);
    // Consider adding more context to the error
    // Consider proper error propagation or retry mechanism
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit dcf7bcb
created_at: 2025-04-09T04:03:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,74 @@
+import { sendCoreMessage } from "../messageBroker";
+
+const mntRates = async (subdomain, NOW, rateCurrencies) => {
+  const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
+  try {
+    const PARAMS = {
+      startDate: NOW.toLocaleDateString("en-CA"),
+      endDate: NOW.toLocaleDateString("en-CA"),
+    };
+
+    const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+
+    const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+      method: "POST",
+    }).then((res) => res.json());
+
+    const { success, data = [] } = response || {};
+
+    if (!success || !data.length) {
+      return;
+    }
+
+    const exchangeRates: Record<string, string> = data[0] || {};
+
+    for (const [rateCurrency, rate] of Object.entries(exchangeRates)) {
+      if (!rateCurrency || !rate) {
+        continue;
+      }
+
+      if (rateCurrencies.includes(rateCurrency)) {
+        sendCoreMessage({
+          subdomain,
+          action: "exchangeRates.create",
+          data: {
+            mainCurrency: 'MNT',
+            rateCurrency,
+            rate: Number(rate.replace(/,/g, "")),
+            date: NOW,
+          },
+        });
+      }
+    }
+  } catch (error) {
+    console.log("error", error.message);
+  }
+}
+
+export default {
+  handleDailyJob: async ({ subdomain }) => {
+    const NOW = new Date();
+    const MAIN_CURRENCY = "mainCurrency";
+    const DEAL_CURRENCY = "dealCurrency";
+
+    const configs = await sendCoreMessage({
+      subdomain,
+      action: "getConfigs",
+      data: {},
+      isRPC: true,
+      defaultValue: {},
+    });
+
+    if (
+      !configs[MAIN_CURRENCY] ||
+      !configs[DEAL_CURRENCY]?.length
+    ) {
+      return;
+    }
+
+    if (configs[MAIN_CURRENCY] === 'MNT') {
+      await mntRates(subdomain, NOW, configs[DEAL_CURRENCY])
+    }
+  },
path: packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Enhance the handleDailyJob function with better type definitions and logging**

The function lacks type definitions for configs and doesn't include logging for job execution status, which would aid debugging and monitoring.


```diff
-  handleDailyJob: async ({ subdomain }) => {
+  handleDailyJob: async ({ subdomain }: { subdomain: string }) => {
+    console.log(`Starting exchange rates daily job for subdomain: ${subdomain}`);
     const NOW = new Date();
     const MAIN_CURRENCY = "mainCurrency";
     const DEAL_CURRENCY = "dealCurrency";
 
+    interface Configs {
+      [key: string]: string | string[];
+    }
+
     const configs = await sendCoreMessage({
       subdomain,
       action: "getConfigs",
       data: {},
       isRPC: true,
       defaultValue: {},
-    });
+    }) as Configs;

     if (
       !configs[MAIN_CURRENCY] ||
       !configs[DEAL_CURRENCY]?.length
     ) {
+      console.log("Missing configuration for currency exchange rates, skipping job");
       return;
     }

     if (configs[MAIN_CURRENCY] === 'MNT') {
-      await mntRates(subdomain, NOW, configs[DEAL_CURRENCY])
+      try {
+        await mntRates(subdomain, NOW, configs[DEAL_CURRENCY] as string[]);
+        console.log("Successfully processed MNT exchange rates");
+      } catch (error) {
+        console.error("Failed to process MNT exchange rates:", error);
+      }
+    } else {
+      console.log(`Main currency is not MNT (found: ${configs[MAIN_CURRENCY]}), skipping job`);
     }
+    console.log("Completed exchange rates daily job");
   },
```


<!-- 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
  handleDailyJob: async ({ subdomain }: { subdomain: string }) => {
    console.log(`Starting exchange rates daily job for subdomain: ${subdomain}`);
    const NOW = new Date();
    const MAIN_CURRENCY = "mainCurrency";
    const DEAL_CURRENCY = "dealCurrency";

    interface Configs {
      [key: string]: string | string[];
    }

    const configs = await sendCoreMessage({
      subdomain,
      action: "getConfigs",
      data: {},
      isRPC: true,
      defaultValue: {},
    }) as Configs;

    if (
      !configs[MAIN_CURRENCY] ||
      !configs[DEAL_CURRENCY]?.length
    ) {
      console.log("Missing configuration for currency exchange rates, skipping job");
      return;
    }

    if (configs[MAIN_CURRENCY] === 'MNT') {
      try {
        await mntRates(subdomain, NOW, configs[DEAL_CURRENCY] as string[]);
        console.log("Successfully processed MNT exchange rates");
      } catch (error) {
        console.error("Failed to process MNT exchange rates:", error);
      }
    } else {
      console.log(`Main currency is not MNT (found: ${configs[MAIN_CURRENCY]}), skipping job`);
    }
    console.log("Completed exchange rates daily job");
  },
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit dcf7bcb
created_at: 2025-04-09T04:03:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,74 @@
+import { sendCoreMessage } from "../messageBroker";
+
+const mntRates = async (subdomain, NOW, rateCurrencies) => {
+  const BASE_URL = "https://www.mongolbank.mn/mn/currency-rates/data";
+
+  try {
+    const PARAMS = {
+      startDate: NOW.toLocaleDateString("en-CA"),
+      endDate: NOW.toLocaleDateString("en-CA"),
+    };
+
+    const QUERY_STRING = new URLSearchParams(PARAMS).toString();
+
+    const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+      method: "POST",
+    }).then((res) => res.json());
path: packages/plugin-accountings-api/src/cronjobs/exchangeRates.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve API request implementation**

The current implementation has two issues:
1. Using query parameters with a POST request is unusual - typically POST requests use a request body
2. The `any` type bypasses TypeScript's type checking



```diff
-    const response: any = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
-      method: "POST",
-    }).then((res) => res.json());
+    interface ExchangeRateResponse {
+      success: boolean;
+      data: Record<string, string>[];
+    }
+
+    const response = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+    });
+    
+    const responseData: ExchangeRateResponse = await response.json();
```


<!-- 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
    interface ExchangeRateResponse {
      success: boolean;
      data: Record<string, string>[];
    }
    
    const response = await fetch(`${BASE_URL}?${QUERY_STRING}`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
    });
    
    const responseData: ExchangeRateResponse = await response.json();
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-09T04:03:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,106 @@
+const DIGITS = [
+  { word: "тэг" },
+  { word: "нэг", prefix: "нэгэн" },
+  { word: "хоёр" },
+  { word: "гурав", prefix: "гурван" },
+  { word: "дөрөв", prefix: "дөрвөн" },
+  { word: "тав", prefix: "таван" },
+  { word: "зургаа", prefix: "зургаан" },
+  { word: "долоо", prefix: "долоон" },
+  { word: "найм", prefix: "найман" },
+  { word: "ес", prefix: "есөн" },
+];
+
+const TENS = [
+  { word: "арав", prefix: "арван" },
+  { word: "хорь", prefix: "хорин" },
+  { word: "гуч", prefix: "гучин" },
+  { word: "дөч", prefix: "дөчин" },
+  { word: "тавь", prefix: "тавин" },
+  { word: "жар", prefix: "жаран" },
+  { word: "дал", prefix: "далан" },
+  { word: "ная", prefix: "наян" },
+  { word: "ер", prefix: "ерэн" },
+];
+
+const SCALES = [
+  { word: "зуу", prefix: "зуун" },
+  { word: "мянга" },
+  { word: "сая" },
+  { word: "тэр бум" },
+  { word: "их наяд" },
+];
+
+const runner = (num: number, usePrefix = false): string => {
path: packages/api-utils/src/numberUtils.ts
user: login: ellipsis-dev[bot]
body: Rename the helper 'runner' to a more descriptive name to avoid confusion with the exported function.
created_at: 2025-04-09T04:16:25Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-04-07T06:23:18Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide by Sourcery

This pull request introduces two new features: a cron job for fetching and storing daily exchange rates and a utility function for converting numbers to Mongolian words. The cron job fetches data from the Mongol Bank API and stores it in the system. The number-to-word conversion utility supports numbers up to the quadrillions.

#### Sequence diagram for fetching and storing daily exchange rates

```mermaid
sequenceDiagram
    participant CronJob
    participant MongolBankAPI
    participant Core

    CronJob->>MongolBankAPI: Fetch exchange rates
    activate MongolBankAPI
    MongolBankAPI-->>CronJob: Returns exchange rates
    deactivate MongolBankAPI
    CronJob->>Core: Create exchange rates
    activate Core
    Core-->>CronJob: Success
    deactivate Core
```

#### Updated class diagram for api-utils

```mermaid
classDiagram
    class APIUtils {
      <<static>> dateToShortStr()
      <<static>> shortStrToDate()
      <<static>> afterQueryWrapper()
      <<static>> toWord(num: number)
    }

    class toWord {
      +toWord(num: number): string
    }

    APIUtils -- toWord : contains

    note for toWord "Converts numbers to Mongolian words"
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Introduced a cron job to fetch and store exchange rates daily. | <ul><li>Created a function to fetch exchange rates from an external API.</li><li>Scheduled the function to run daily.</li><li>Implemented logic to store the fetched exchange rates in the system.</li><li>Added configurations for main and deal currencies.</li><li>Utilized `node-fetch` to make HTTP requests to the external API.</li><li>Implemented error handling for API requests and data processing.</li></ul> | `packages/plugin-accountings-api/src/configs.ts`<br/>`packages/plugin-accountings-api/src/cronjobs/accountings.ts` |
| Added a utility function to convert numbers to Mongolian words. | <ul><li>Implemented a function that converts a given number into its Mongolian word representation.</li><li>The function supports numbers up to the quadrillions.</li><li>Added necessary word lists for digits, tens, and scales.</li><li>Included the new utility function in the `api-utils` package.</li><li>Added handling for numbers that are too large.</li></ul> | `packages/api-utils/src/index.ts`<br/>`packages/api-utils/src/numberUtils.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-07T06:23:22Z
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
This pull request introduces new functionality in two modules. In the API utilities package, a `numberToWord` function is added to convert numeric values into their Mongolian word representations, and this function is included in the module's default export. In the plugin accountings API, a new cronjob module is implemented to manage daily exchange rate updates, which fetches and processes currency rates from the Mongol Bank API. Both changes enhance the modules without modifying existing behaviors.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `packages/api-utils/.../index.ts`, `packages/api-utils/.../numberUtils.ts` | Added `numberToWord` function; introduced a new file with logic for converting numbers to their Mongolian word representations using constants and recursion. |
| `packages/plugin-accountings-api/.../configs.ts`, `packages/plugin-accountings-api/.../cronjobs/exchangeRates.ts` | Imported `cronjobs` in the configuration; added a new exchange rate update cronjob module that fetches, validates, and dispatches currency rate data. |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant S as Scheduler
    participant E as ExchangeRates Module
    participant API as Mongol Bank API
    participant M as Message Sender

    S->>E: Trigger handleDailyJob()
    E->>E: Retrieve configuration & check main currency
    alt Main currency is 'MNT'
        E->>E: Call mntRates(date, rateCurrencies)
        E->>API: Send POST request with date range
        API-->>E: Return exchange rates response
        loop For each valid rate
            E->>M: Execute sendCoreMessage(rate data)
        end
    else
        E-->>S: Skip exchange rate processing
    end
    E-->>S: Complete daily job execution
```

## Poem
> I'm a bunny, full of glee, 🐰  
> Hopping through code so merrily.  
> Numbers now whisper words so clear,  
> Rates update each day, oh dear!  
> Carrots and code, a jolly spree!  
> Huuray for changes in our API!

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpADK2VmgqyjcEj2wAbb5AokAI7Y0riQ4rjedJAARAyw+AFcDBT4GEKCADSQGE4ClOH4kADuibQxJWjIiNgCzOo09HKQ2IgFAtTMGADWsABMAMwALEPoGPSo8EoY4gBm8NHNucz5fACsAJwbAGwaMLCevPDMLvKCImKSnviz4QjItijIBCgzqbTYDJ5okLTa3vIUmkMgJILNEmCSLh4vAMERGNgKAEMAx5CQAB7xTCkfzUaRjegvY7cKJsGboFribzqeTgvhMDBSCjiOE5PKUZ5FUoUWjIWF3TwAWTSRHw1MwkG82OwaFIGiMAGkSLSSNREfi0LQlBMUd5sEoOEYoABBHIkYqU+DU3C07Ao8RpHJoNj0AAGy1W0HwAHUyq67tQFIzKLhkB6Oa8XsK4WL4BLubzssUEPEnihmCSSGTGq8BZB3eyKABVKmIDSh/3zKIEyAY7iJHP812wpTo8uIV3yk2/f6A1LpQRgu2XNJoa2ybJkBhobg1KWNrCujFYuEkGx4ssVpMp2CQ6EHZCYejYbh/GjIBiI5Go2uY2DYzxUc9g1LMPPR0XirAAIUw3XlBjGlq6jwKOvgTnmjywgwepKM8ByRiQRBPqBWA3HmQIDqCR55uGFBgAQYAJpa46RkUuAIRiSAsvCvD4BcYSwrMVCILgFCfLg6p7B4QbzEQiLUKhYIQhRniYSCfJ8rq+rRE2DJ8ZuHYtOMBSiegDBMHaNGQMaVgAJIAQAYhCsyIqJfBKLg/yINkancD4fgBMEoSMBK+TqV8iBtPQgZqeCvj4MUsLwkWNgADJcAA2rAuC4LOHAAPQJUQ6iwLUGhMMwCWUOi0jZRQuWIAl9m+Almw7AAugAFDFcWIIlyWpelmX5YVrV5SV3hlVs2wAJQARAYDmBYkAAKLotRwWQAA8sIojiFIyCOM4rjuOoUTJAkSSMP2ILZHhhQlGUkCQAYAByhZcOV2wnQYRZtBQXAdLgXS9IMIxGmAhgGCYUBkPQ6FoHghCkOQT7RJl2ZcLw/BzZci2QM0TBKFQqjqFoOj6D94BQHAkyHlgQOEaDyg5pD7BcFQFo1Ct8hI4oyho5o2i6F9RjY6YBjcGgDDdLKeUzvAYB4FaRWIBQDAJS2GLtoaMTywYI3GnpxBkKT0Q0ycrj8LcK6kIg7gIbQ8CzLcsJsYonwalgxINpArF4tmwl8GpBYrJQXq+jylbDg6WDMSweZu6sJai/6zCW9W5vkQhzYqW2FZglaJDcfc6DAX76ABcU8GeMHHs+n6Q72kJLzuSL1IAF7REFFH8mpEcfFEeyGWZBwUBHAS2bHeGe0Xpkl46kzSdquZqUosxA94YR1nb5zzdkJzdFN6gedIiDwAI1bJmQeaNz4niTBmDbRCQ3htDvASp6geueHWR655A+9RAA5MgA8jhgY40oj8jQYk9YUKsh+OQC0Fcf4f0zrXBIeAs40AoFNTAaIJqsSmt4fAKUGD8D4JA1C38bQASVtPUmqFOR5iUDBFwgk0jIHQrPZk0QIT2S3vALB7AQLSCNJAQUUIEg+S1HQLg+cKB929rmV03Neb8yKoLYWpYEri0ltLBOHYDDyxiJ9IaXMeZ831glWRFcxYSwSnhUO59ZZqIVkrFWJNwb0E1qcHWjB7yrgNoBM0Foqx51MaWds/p7wyBILvc27wra0G3Kw3cdEJBTHxLgsCP8XgMiZGEPCUkXiiXgPSRIARED1nGFNYiAReDSHYNQjAUluEiljJgVOngvG/BIPMcg8EAhiRoVZckLgqCyHqvmAAInpAA4npaA9hXTZFdNAUaZ1xnZBwq6ewABhY0YVRpzJKDuIMVl+TzzEMgE43BuBTWWJQVhY5IASDHCEMhmS+DETpMwAm9ASnzEKnUhQARJQYNYWmKcM45x4h1OEUoxdP59NdOxDAYN/QLN7oXb2AEeKQrtDCsFmcXG0CiI/ZJHIhLoTSbiOEU0A5vgAAyHSZPIKUFAcRXL1NIbI4DK5TQCJeCgG9HR0jZO7DliNlRpHoFsDYew9JhHiKIboj88JvyJTiHCDJWLsX2XmIpJASltBmOUxGVRGFYHHk02EEMOmYFDKKxiI98R5NECbX5mLqSsm5YS7F8EJQAEYyXZHyLgYoQSsAeprFseZ4x+U+r9ZAD1FKcLCvmcgX1vh0DvwhDS0gfBEDTmxZKeA3RPAUXwK0B+NZ6i+FIR8+JhNz5FH/hQQBG4KSvPgOifg3BM7cq+MybQWA0mTgwC4hgU01L5rirAjo3l+D6tjq0EgVgAhvP9NzKgbB4FIp7oWURtAfaDywA9BGVR621GpFg82lBJ5fGyPSqYglWSwnsmEF4ZAahfNXrQfA+IMD4BnpiIJ9A3WQGCJqBBJbHRVTdZkMl4GwMQfA566DkGyX9UgHpW4akb2wNQAQIoKaSDZFXhRVIOcxi1iRIkPY01zJBTaDh8VY5z75ihWil43AT2JDfGpXFHLUIAVGsRvg9qV58gzKSdgslFzwq9huw6Jwv44mXOvK4AJJQuBxN2zZyc96dqDEyTlWB0GYPEfRyg/pUBKo4uqQkRR7WeCuQg/NyAqCuJrI86gtDbiEqabMea8nZCEMsMaYhKEaGHQNZQgLFSnH0JzEwg9vz2HiE4QYKArct3p21EIxVqS10IvoAAXkgFVZYXA8K9S4Eq4KRnFySN0QLY5cjRYKOMT40WfjLEaIS5zSr0jip6hShgMAPNNIzGCogPrxz6uS3kvAIgik5ZWN8zYtWdj7ZOC1mcXWLj9aGzEut/EITLZfDzLbDtmdAYeKfpHbD+ZxKCA7NkUleZuYUXzBoBKV2BBFWXNt9c55Oz7FQJqY2mdUCzqiGIETIKigkh5tILg8wOW4Co2mQ7OZfIIQINwJxflk7BvsaIQVCPh4wRksC12S60D+j2WEdCakIvRAnlPcVaQ+ICT9h810pPydw1SYFbZnbH6ule0pOiTHmTyDHCKDeShbyTVZELkMCxqifF3Hu10umbv5iR2r10jy1cLOF/ULypCftnSKFRVBrIHX4l9V8gIEcpAAz4I3W1dAADcGFtvVDFGfQENDqIY4Qv9kCjoqexwF0Z4+zIazqCkoT7TY9Y7s9hgxHzOl/PlNuUbUQNK0/hfRIAyLfBmGHtrINuLbioBnXNOmPPKXBGXd2tdysr4nsvfr297Kd4HxfekOV/MHW9EkmwD1vrGl82DbhMNwWY2XuM8m4pV0CXrCpGFzaGvtA0ut6UvXePUIyeJ/mn7zwdOfCfur02PvHVuuwmHwNmiE/RuKOnxgBSzX1GaKMOfoqA+h/9dH7fkb8Ap8Bd29b4u9psWtFY5tVYwYgUltaYnFb43FTRQEk5qwlwO9VxQC/FIAAl+Vgk3g9s6AIlUxiQhMZh35fY8FSJuUpNZQpoPsHxcQaAWhTwNwPkmAvly1JkuhcBQCJlE1ZAURYB+xbMFMPN9x8R2Vrx5BwZ34m81IPwxRIBfwegdJ9IWgN5gFrBpp7BoB/Aggbl4cShUpyE8Q5VPA2g70Y4xIrx2AewaBzUgwTMVUfhnJtZStHU5CjZTCF1nQoQIwcI6JPI2hH5dI9I9C8kaELsL0zwB0EIagNJ14D89DnJWIaw7Jckpxrhbgzw0BRVbhojbxb5GCNQvlBAdlyBwkUBGJ4E608N80iBdxRJmBshNVjZWQ2AvJpFnZaweZdxwYAwwgThxDH5rV+15gIYbCUR5d1DYi85WillEgSAeFOjSBN1P4V16k1MItDxGlJ5j898xAedYREEn5eFFB8xLN+lewAApQQPg49FSegblP4K0ZBUQEWNIa+A5M4+gAINiBYBGCbfiULe2KEW/bohuDTHCJQC5SQqcaYmJH4H4dgzwDoxAfmPI9TfkOElEP+ZAF+QUM6aAF+fHRkfAHNPnbg3g9FISaBPMAIKIK5ckHwpdDkPYbjVIXjI8B1eEAnABRIGAl4XTMYPEmofEKcX/SgWnREWYvcVMfAYXcpDY7ZVIPwWYdBC0aCWCCQg4XmJNF2SiFBGgFELI3iSbZnISelG5NIuIz4IIpI0I8I/JSjYvR9WIwMNIBTAonItMQI9eaIHCTVTQYaFPGov2dPQ/TPKhcMnPPPRhAvaLNhEveXLhHhPNfhVLfMKoAQrBS4m4wQKqAAbyWwEBfROH5AAF9eoe8DU9jp5bxq90I0CQDWDE4F8ktP5V8hFsyUQn4ZhQCqoahSyWBO1sgzpppvRshwYllJj+1pBqzxF6CMDWzVFX82tvpfpi8AZbgiYQYFsYDyYZhKY0BqZltHF6YUYVA1BmZMY2ZNzIZ1AAB9KYRAR8gIGJc0OgR8h2CPLGDmKABgWgWYN1bYNYMlDYN1AADhIEgphO2DdVoDdQGDWA9TQDdVmFoAAHY0A1gYLMK3U3VMLJ4yUsEsZNySA3UcKBhMKKLZgBA+gyUSBtgBgGBJ5cKNhIKhhwNWK+gNg1g1gPMMKGA+g+gwLWZDBNzILMLtg+gZLZghhmLhLcKvg3UGBILQK1hhKMKhgBhCLIK+L6KyVZgBhaA+hxL/zfhWLMKBAGABBIKvhaAwLKK0BZgNI0A0BMK+hZgNgBgSATK+hsKmKBhYKYL6AyKcZIATLJ5RAyUhhcLZhPKaKyVMLaAhh3KGABhBgGA1haAZKSKhg7KSAOLUrzKLKHzcBnzeQ3ySAPzfVaBHz/pxKgA= -->

<!-- internal state 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 3dfaec045ef727e07d4aac323c5d620c4b8e98d4 and 531121632a53725ae236b2a8c610d1096bd04539.

</details>

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

* `packages/api-utils/src/numberUtils.ts` (1 hunks)

</details>

</details>
<!-- tips_start -->

---



<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=5996):

- 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 plan` to trigger planning for file edits and PR creation.
- `@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-09T04:50:22Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=erxes_erxes&pullRequest=5996) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=erxes_erxes&pullRequest=5996&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=erxes_erxes&pullRequest=5996&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=erxes_erxes&pullRequest=5996&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=erxes_erxes&pullRequest=5996&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=erxes_erxes&pullRequest=5996&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=erxes_erxes&pullRequest=5996)


author_association: NONE