# yemot-router2 — תיעוד מלא

ספריית Node.js לבניית שרתי IVR לשירות ימות המשיח.

```js
import { YemotRouter } from 'yemot-router2';
```

---

## איך המנגנון עובד מבפנים

זה הלב של הספרייה — חשוב להבין אותו.

### הבעיה שהספרייה פותרת

ימות המשיח עובד כ-webhook: על כל פעולה (פתיחת שיחה, קבלת קלט מהמשתמש) ימות שולח **בקשת HTTP חדשה**. מבלי לספרייה, צריך לנהל state ידנית בין הבקשות. הספרייה מסתירה את זה לגמרי.

### מה קורה בפועל — מחזור חיים של שיחה

**בקשה ראשונה (שיחה חדשה):**
1. הרוטר בודק שהפרמטרים הנדרשים קיימים (`ApiCallId` וכו')
2. לא קיים `ApiCallId` ב-`activeCalls` → שיחה חדשה
3. יוצר אובייקט `Call` חדש ומכניס ל-`activeCalls[callId]`
4. קורא ל-`makeNewCall(fn, callId, call)` **ללא `await`** — הפונקציה של המשתמש רצה ברקע
5. הfn של המשתמש מגיעה לקריאת `call.read(...)` ראשונה:
   - בונה את מחרוזת הפקודה לימות (למשל `read=t-הקש ספרה=val_1,...`)
   - שולחת אותה לימות דרך `res.send(responseText)` — כאן ה-HTTP response יוצא
   - קוראת ל-`blockRunningUntilNextRequest()` שרושם listener חד-פעמי על `EventEmitter` ומחכה
   - ה-`Promise` לא נפתר — הקוד **תקוע כאן** עד לבקשה הבאה

**בקשה שנייה (המשתמש הקיש):**
1. ימות שולח בקשה חדשה עם אותו `ApiCallId` + הפרמטר `val_1=5` (הקלט שהמשתמש הכניס)
2. הרוטר מוצא את ה-`Call` הקיים ב-`activeCalls`
3. קורא ל-`call.setReqValues(req, res)` — מעדכן את `call.req`, `call.res`, ו-`call.values` לבקשה **החדשה**
4. פולט `eventsEmitter.emit(callId, false)` — זה מפעיל את ה-listener ב-`blockRunningUntilNextRequest`
5. ה-Promise נפתר → הקוד שתקוע ב-`await call.read()` מתחיל לרוץ שוב
6. `call.values['val_1']` מכיל עכשיו `'5'`, `call.read()` מחזירה את הערך
7. הקוד ממשיך לפעולה הבאה...

**ניתוק:**
- ימות שולח בקשה עם `hangup=yes`
- **אם השיחה הייתה ב-`activeCalls`** (כלומר היה אמצע קוד שמחכה לקלט) — `eventsEmitter.emit(callId, true)` → `blockRunningUntilNextRequest` עושה `reject(new HangupError(call))`, השרת מחזיר `{ message: 'hangup' }`, השגיאה מתפשטת ל-`uncaughtErrorHandler` ואז `deleteCall`
- **אם זו בקשה ראשונה עם `hangup=yes`** (לא הייתה שיחה פעילה) — הספרייה רק פולטת `call_hangup` event ומחזירה `{ message: 'hangup' }`, ללא קריאה ל-handler

**תרשים:**
```
ימות         שרת (Express)        handler function
  |               |                      |
  |--GET /path?-->|                      |
  |               |---makeNewCall()----->|
  |               |                      |--- await call.read() ------>
  |<--read=t-...--|  (res.send)          |    [תקוע - מחכה לevent]
  |               |                      |
  |--GET /path?   |                      |
  |  val_1=5 ---->|                      |
  |               |--setReqValues()      |
  |               |--emit(callId) ------>|    [Promise נפתר]
  |               |                      |<--- val = call.values.val_1
  |               |                      |
  |               |                      |--- call.id_list_message() --
  |<--id_list_...--|  (res.send)          |    [ExitError נזרק]
  |               |                      X
```

### `#responsesTextQueue` — מנגנון `prependToNextAction`

כשקוראים ל-`id_list_message(..., { prependToNextAction: true })`:
- הפקודה **לא נשלחת** מיד אלא נצבר ב-queue פנימי
- בקריאה הבאה ל-`send()` (מ-`read()` או `go_to_folder()`), ה-queue מתרוקן ומתווסף **לפני** הפקודה החדשה
- כך ימות מקבל שתי פקודות בתגובה אחת: `id_list_message=...&read=...`

### `shiftDuplicatedValues` — ערכים כפולים

אם ימות שולח אותו פרמטר פעמיים, רק **הערך האחרון** נשמר.

### פורמט תגובה לימות

הספרייה בונה מחרוזות בפורמט שימות מצפה לו:

| פעולה | תבנית |
|-------|-------|
| `read` (tap) | `read=t-[msg]=val_1,no,,1,7,No,no,no,,,,,None,` |
| `read` (stt) | `read=t-[msg]=val_1,no,voice,,,,,,` |
| `read` (record) | `read=t-[msg]=val_1,no,record,...` |
| `id_list_message` | `id_list_message=t-[msg1].t-[msg2]` |
| `go_to_folder` | `go_to_folder=[target]` |
| `routing_yemot` | `routing_yemot=[number]` |

ההודעות מקודדות לפי טיפוס: `t-` (text), `f-` (file), `d-` (digits), `n-` (number), `s-` (speech), `a-` (alpha), `m-` (system_message), `g-` (go_to_folder), `z-` (zmanim), `h-` (music_on_hold), `date-`, `dateH-`. פריטים מרובים מחוברים ב-`.`.

פקודות מצטברות (מ-`prependToNextAction`) מחוברות ב-`&`.

---

## פרמטרים נדרשים בכל בקשה מימות

ימות שולח GET (query string) או POST (body). **ארבעת הפרמטרים האלה חייבים להיות נוכחים** (גם אם ריקים) — חסר מפתח אחד → `200 OK` עם JSON: `{ message: 'the request is not valid yemot request' }`:

| פרמטר | תיאור |
|-------|-------|
| `ApiCallId` | מזהה ייחודי לשיחה (קבוע לאורך כל השיחה) |
| `ApiPhone` | מספר הטלפון של המתקשר |
| `ApiDID` | המספר הראשי של המערכת |
| `ApiExtension` | השלוחה הנוכחית |

פרמטרים נוספים שימות עשוי לשלוח:

| פרמטר | תיאור |
|-------|-------|
| `ApiRealDID` | המספר שאליו חייג (עשוי להיות שונה מ-`ApiDID`) |
| `ApiTime` | זמן epoch בשניות |
| `ApiYFCallId` | מזהה לAPI של ימות |
| `ApiEnterID` | סוג + ID של התחברות אישית (אם בוצעה) |
| `ApiEnterIDName` | שם משויך לזיהוי האישי |
| `hangup=yes` | ימות שולח כשהמשתמש ניתק |
| `[val_name]` | הקלט שהמשתמש הכניס — בשם שהוגדר ב-`val_name` |

---

## יצירת Router

```js
const router = YemotRouter({
    printLog: true,             // לוג מפורט (ברירת מחדל: false)
    timeout: 5 * 60 * 1000,    // timeout לקבלת קלט — ms או string ('5m'). ברירת מחדל: אין (0)
    removeInvalidChars: false,  // הסרת תווים לא חוקיים מ-TTS. ברירת מחדל: false
    uncaughtErrorHandler: async (error, call) => { /* ... */ },
    defaults: {                 // ברירות מחדל ברמת router (ראה טבלת defaults)
        read: {
            tap: { sec_wait: 10 },
        }
    }
});
```

> ⚠️ `prependToNextAction` **לא נתמך** כ-default — יזרוק שגיאה.

### מתודות Router

```js
router.get('/path', async (call) => { })    // GET בלבד
router.post('/path', async (call) => { })   // POST בלבד (api_url_post=yes)
router.all('/path', async (call) => { })    // GET + POST
router.use(...)                              // Express middleware
router.deleteCall(callId)                   // הסרת שיחה מ-activeCalls. מחזיר boolean.
router.activeCalls                          // { [callId]: Call } — שיחות פעילות כרגע
router.asExpressRouter                      // לצורך app.use(router.asExpressRouter)
```

### הרכבה ב-Express

```js
import express from 'express';
const app = express();

// POST דורש urlencoded — לא json!
app.use(express.urlencoded({ extended: true }));

app.use('/my-ivr', router.asExpressRouter);
app.listen(3000);
```

> ⚠️ לתמיכה ב-POST (`api_url_post=yes`) חובה `express.urlencoded({ extended: true })`, **לא** `express.json()`.

---

## ברירות מחדל מובנות (defaults.js)

```js
{
    printLog: false,
    removeInvalidChars: false,
    read: {
        timeout: 0,              // 0 = אין timeout
        re_enter_if_exists: false,
        removeInvalidChars: false,
        tap: {
            min_digits: 1,
            max_digits: '',      // '' = ללא הגבלה
            sec_wait: 7,
            typing_playback_mode: 'No',
            block_asterisk_key: false,
            block_zero_key: false,
            replace_char: '',
            digits_allowed: '',  // '' = הכל מותר
            amount_attempts: '', // '' = ברירת מחדל ימות
            allow_empty: false,
            empty_val: 'None',
            block_change_keyboard: false
        },
        stt: {
            lang: '',            // '' = שפת השלוחה
            block_typing: false,
            max_digits: '',
            quiet_max: '',
            max_length: '',
            use_records_recognition_engine: false
        },
        record: {
            path: '',
            file_name: '',
            no_confirm_menu: false,
            save_on_hangup: false,
            append_to_existing_file: false,
            min_length: '',
            max_length: ''
        }
    },
    id_list_message: {
        removeInvalidChars: false
    }
}
```

---

## אובייקט Call

### Properties

| Property | קיצור ל | תיאור |
|----------|---------|-------|
| `call.callId` | `ApiCallId` | מזהה ייחודי לשיחה |
| `call.phone` | `ApiPhone` | מספר המתקשר |
| `call.did` | `ApiDID` | המספר הראשי של המערכת |
| `call.real_did` | `ApiRealDID` | המספר שאליו חייג |
| `call.extension` | `ApiExtension` | השלוחה הנוכחית |
| `call.values` | — | `Readonly<Record<string,string>>` — כל פרמטרי הבקשה הנוכחית מימות |
| `call.req` | — | Express Request המקורי (מתעדכן בכל בקשה!) |
| `call.defaults` | — | ברירות מחדל ברמת שיחה (ניתן לדריסה, דורסות ברירות ה-router) |
| `call.ApiEnterID` | — | נתוני זיהוי אישי (אם בוצע) |
| `call.ApiTime` | — | epoch בשניות |

> `call.req` ו-`call.values` **מתעדכנים בכל בקשה חדשה** מימות. אל תשמור reference לישן.

> `call.query`, `call.body`, `call.params` — **deprecated**, יזרקו שגיאה. השתמש ב-`call.values` ו-`call.req.params`.

---

## מתודות Call

### `call.read(messages, mode, options)` — קבלת קלט

מחזירה `Promise<string>`. הערך מאוחסן גם ב-`call.values[val_name]`.

**`messages` חייב להיות מערך** של אובייקטי `Msg` (ראה סעיף Msg).

#### אפשרויות משותפות לכל המצבים

| אפשרות | תיאור | ברירת מחדל |
|--------|-------|-------------|
| `val_name` | שם הפרמטר שיחזור מימות | `val_1`, `val_2`... (לפי סדר קריאות ה-read בשיחה) |
| `re_enter_if_exists` | `false` (ברירת מחדל) = ימות ישאל שוב גם אם הערך קיים. `true` = ימות ישתמש בערך הקיים ולא ישאל | `false` |
| `removeInvalidChars` | הסרת תווים לא חוקיים מהודעות TTS | `false` |
| `timeout` | override ל-timeout ברמת קריאה בודדת (ms) | timeout של ה-router |

#### מצב `tap` — הקשות DTMF

```js
await call.read(messages, 'tap', {
    max_digits?: number,         // ספרות מקסימלי (ברירת מחדל: '' = ללא הגבלה)
    min_digits?: number,         // ספרות מינימלי (ברירת מחדל: 1)
    sec_wait?: number,           // שניות המתנה (ברירת מחדל: 7)
    // הערכים האלה דורסים min_digits/max_digits אוטומטית במקור:
    //   'Date'|'HebrewDate' → 8–8 ספרות, 'Time' → 4–4, 'TeudatZehut' → 8–9, 'Phone' → 9–10
    // ('Date' ו-'HebrewDate' עובדים במקור אך לא מופיעים בטיפוסי TypeScript)
    typing_playback_mode?: 'Number'|'Digits'|'File'|'TTS'|'Alpha'|'No'
                               | 'HebrewKeyboard'|'EmailKeyboard'|'EnglishKeyboard'
                               | 'DigitsKeyboard'|'TeudatZehut'|'Price'|'Time'|'Phone'
                               | 'Date'|'HebrewDate',
    block_asterisk_key?: boolean,    // חסימת מקש * (ברירת מחדל: false)
    block_zero_key?: boolean,        // חסימת מקש 0 (ברירת מחדל: false)
    block_change_keyboard?: boolean, // חסימת שינוי שפת הקלדה (ברירת מחדל: false)
    replace_char?: string,           // החלפת תו — 2 תווים: [מה→במה]. לדוגמה: '*/'-  * יוחלף ב-/
    digits_allowed?: Array<number|string>, // ספרות מותרות (ברירת מחדל: '' = הכל)
    amount_attempts?: number,        // ניסיונות לפני "ריק" (ברירת מחדל: '' = ברירת מחדל ימות)
    allow_empty?: boolean,           // אפשר ערך ריק (ברירת מחדל: false)
    empty_val?: any,                 // ערך כשלא הוקש כלום (ברירת מחדל: 'None')
});
```

#### מצב `stt` — זיהוי דיבור

```js
await call.read(messages, 'stt', {
    lang?: string,                           // שפה (ברירת מחדל: '' = שפת השלוחה, בד"כ 'he')
    block_typing?: boolean,                  // חסום הקשה — דיבור בלבד (ברירת מחדל: false)
                                             // ⚠️ הגדרתו (גם false) זורקת שגיאה עם use_records_recognition_engine: true
    max_digits?: number,                     // ספרות מקסימלי אם המשתמש מקיש
    use_records_recognition_engine?: boolean,// מנוע לטקסטים ארוכים (ברירת מחדל: false)
    quiet_max?: number,                      // שניות שקט לסיום — ⚠️ אסור ללא use_records_recognition_engine: true
    max_length?: number,                     // שניות מקסימלי (רלוונטי בעיקר ל-records_recognition_engine)
});
```

#### מצב `record` — הקלטת הודעה

מחזירה נתיב לקובץ ההקלטה.

```js
await call.read(messages, 'record', {
    path?: string,                     // תיקייה לשמירה (ברירת מחדל: '' = תיקיית השלוחה)
    file_name?: string,                // שם קובץ ללא סיומת (ברירת מחדל: מספור אוטומטי)
    no_confirm_menu?: boolean,         // שמירה ישירה ללא תפריט אישור (ברירת מחדל: false)
    save_on_hangup?: boolean,          // שמירה גם בניתוק (ברירת מחדל: false)
    append_to_existing_file?: boolean, // צירוף לקובץ קיים (ברירת מחדל: false)
    min_length?: number,               // שניות מינימלי
    max_length?: number,               // שניות מקסימלי
});
```

---

### `call.id_list_message(messages, options)` — השמעת הודעה

> ⚠️ **ללא `prependToNextAction: true`** — אחרי ההשמעה הספרייה **זורקת `ExitError`** ויוצאת מהשלוחה. הקוד שאחרי הקריאה לא רץ. אין צורך ב-`return`.

```js
// השמעה ויציאה מהשלוחה (ExitError נזרק)
call.id_list_message([{ type: 'text', data: 'תודה על פנייתך' }]);
// הקוד כאן לא רץ

// השמעה והמשך לפעולה הבאה (נצבר ב-queue, נשלח עם ה-read הבא)
call.id_list_message([{ type: 'text', data: 'ברוך הבא' }], { prependToNextAction: true });
const choice = await call.read([{ type: 'text', data: 'הקש 1 או 2' }], 'tap', { max_digits: 1 });
```

**אפשרויות:**

| אפשרות | תיאור | ברירת מחדל |
|--------|-------|-------------|
| `prependToNextAction` | שרשור לפעולה הבאה (לא ניתן כ-default) | `false` |
| `removeInvalidChars` | הסרת תווים לא חוקיים | `false` |

> אם `messages` מכיל `{ type: 'go_to_folder' }` — חייב להיות הפריט האחרון במערך, ואסור עם `prependToNextAction: true`.

---

### שאר מתודות ה-Call

```js
// מעבר לשלוחה: '100' (יחסי), '/100' (מהשרש), 'hangup'
// שולח go_to_folder=target לימות, ואז זורק ExitError
call.go_to_folder(target)

// ניתוק — קיצור ל-go_to_folder('hangup'), זורק HangupError
call.hangup()

// הפעלה מחדש של השלוחה הנוכחית — קיצור ל-go_to_folder('/' + call.extension)
call.restart_ext()

// העברה למערכת ימות אחרת (ללא עלות יחידות), זורק ExitError
call.routing_yemot(number)

// שליחת מחרוזת גולמית לימות ללא עיבוד — לפונקציונליות שאינה נתמכת בספרייה
// (מחזיר את Express Response — אחרי קריאה זו ה-HTTP response נסגר)
call.send(rawString)

// המתנה לבקשה הבאה מימות (בד"כ בשימוש אחרי call.send)
await call.blockRunningUntilNextRequest(timeoutMs?)
```

---

## סוגי הודעות (Msg)

`messages` תמיד הוא **מערך** — גם אם הודעה אחת.

```js
[
    { type: 'text',           data: 'ברוך הבא למערכת' },   // קריאת טקסט (TTS)
    { type: 'file',           data: '5/1.wav' },            // קובץ שמע — נתיב יחסי לשלוחה
    { type: 'speech',         data: 'greeting' },            // נאום מוקלט
    { type: 'digits',         data: '123' },                 // קריאת ספרות בנפרד (1, 2, 3)
    { type: 'number',         data: 1234 },                  // קריאת מספר שלם ("אלף מאתיים...")
    { type: 'alpha',          data: 'ABC' },                 // איות אותיות לועזיות
    { type: 'system_message', data: 'M0001' },               // הודעת מערכת ימות
    { type: 'go_to_folder',   data: '100' },                 // מעבר שלוחה כחלק מהודעה
    { type: 'date',           data: '28/05/2026' },          // תאריך לועזי DD/MM/YYYY
    { type: 'dateH',          data: '01/09/5786' },          // תאריך עברי
    { type: 'zmanim',         data: { time?, zone?, difference? } },
    { type: 'music_on_hold',  data: { musicName: 'music1', maxSec?: 30 } },
]
```

כל סוג הודעה מקבל גם `removeInvalidChars?: boolean`.

**תווים לא חוקיים ב-TTS (type: 'text'):** `. - ' " & |` — גורמים לשגיאה אלא אם `removeInvalidChars: true`.

---

## מערכת אירועים

```js
router.events.on('new_call', (call) => { })       // שיחה חדשה נפתחה
router.events.on('call_hangup', (call) => { })    // המשתמש ניתק
router.events.on('call_continue', (call) => { })  // בקשה נוספת מאותה שיחה קיימת
```

---

## שגיאות

```js
import { CallError, ExitError, HangupError, TimeoutError } from 'yemot-router2';
```

| שגיאה | מתי נזרקת | מאפיינים נוספים |
|-------|-----------|-----------------|
| `HangupError` | ימות שולח `hangup=yes` / `call.hangup()` | — |
| `TimeoutError` | לא התקבל קלט בזמן | `error.timeout` — זמן ה-timeout ב-ms |
| `ExitError` | `go_to_folder` / `id_list_message` ללא prependToNextAction / `routing_yemot` | `error.context = { caller, target }` |
| `CallError` | שגיאה לוגית בשימוש בAPI | — |

כולן יורשות מ-`CallError` עם `error.call` (אובייקט השיחה) ו-`error.date` (Date).

**דפוס מומלץ:**

```js
const router = YemotRouter({
    uncaughtErrorHandler: async (error, call) => {
        if (error instanceof HangupError) return;
        if (error instanceof TimeoutError) return;
        if (error instanceof ExitError) return;
        console.error(error);
        call.id_list_message([{ type: 'system_message', data: 'M0001' }]);
    }
});
```

> אם `uncaughtErrorHandler` עצמו זורק `ExitError` — מטופל בשקט. כל שגיאה אחרת ממנו קורסת את התהליך.

---

## אפשרויות deprecated (יזרקו שגיאה)

| שם ישן | שם חדש |
|--------|--------|
| `play_ok_mode` | `typing_playback_mode` |
| `read_none` | `allow_empty` |
| `read_none_var` | `empty_val` |
| `block_change_type_lang` | `block_change_keyboard` |
| `min` / `max` | `min_digits` / `max_digits` |
| `block_zero` | `block_zero_key` |
| `block_asterisk` | `block_asterisk_key` |
| `record_ok` | `no_confirm_menu` |
| `record_hangup` | `save_on_hangup` |
| `record_attach` | `append_to_existing_file` |
| `allow_typing` | `block_typing` |
| `use_records_engine` | `use_records_recognition_engine` |
| `lenght_min` / `length_min` | `min_length` |
| `lenght_max` / `length_max` | `max_length` |
| `uncaughtErrorsHandler` | `uncaughtErrorHandler` |
| `router.add_fn` | `router.get/post/all` |
| `call.query` / `call.body` / `call.params` | `call.values` / `call.req.params` |

---

## כתיבת טסטים

### CallSimulator — סימולציית שיחת ימות

זהו דפוס הסימולציה המשמש בטסטים של הספרייה עצמה (`test/utils.js`):

```js
import request from 'supertest';
import qs from 'qs';
import crypto from 'crypto';

class CallSimulator {
    #port;
    values = {
        ApiCallId:    crypto.randomBytes(20).toString('hex'),  // 40 hex chars
        ApiYFCallId:  crypto.randomBytes(20).toString('hex'),
        ApiDID:       '0772222770',
        ApiRealDID:   '07722225555',
        ApiPhone:     '0527000000',
        ApiExtension: '',                                       // ⚠️ נדרש (גם אם ריק)
        ApiTime:      String(Date.now()),
    };

    constructor(port) { this.#port = port; }

    get(path) {
        return request(`http://localhost:${this.#port}`)
            .get(`${path}?${qs.stringify(this.values)}`);
    }

    post(path) {
        return request(`http://localhost:${this.#port}`)
            .post(`${path}?${qs.stringify(this.values)}`);
    }
}
```

> ⚠️ **`ApiExtension` חייב להיות במפתחות** גם אם הערך ריק — `Object.prototype.hasOwnProperty.call` בודק קיום מפתח, לא ערך.

### דפוס טסט

```js
import express from 'express';
import { YemotRouter } from 'yemot-router2';

let server, port;

beforeAll((done) => {
    const app = express();
    app.use(express.urlencoded({ extended: true }));

    const router = YemotRouter({ printLog: false });
    router.get('/', async (call) => {
        await call.read(
            [{ type: 'text', data: 'הקש ספרה' }],
            'tap',
            { max_digits: 1, val_name: 'choice' }
        );
        call.id_list_message([{ type: 'text', data: `בחרת ${call.values.choice}` }]);
    });

    app.use(router.asExpressRouter);
    server = app.listen(0, () => {
        port = server.address().port;
        done();
    });
});

afterAll((done) => server.close(done));

it('מעביר קלט ומחזיר הודעה', async () => {
    const call = new CallSimulator(port);

    // בקשה 1 — ימות מגיע לראשונה, הספרייה שולחת פקודת read ומחכה
    const res1 = await call.get('/');
    expect(res1.status).toBe(200);
    expect(res1.text).toContain('read=');

    // סימולציית קלט — מוסיפים את val_name לvalues
    call.values.choice = '3';

    // בקשה 2 — הספרייה מתעוררת, הקוד ממשיך, שולח id_list_message
    const res2 = await call.get('/');
    expect(res2.status).toBe(200);
    expect(res2.text).toContain('id_list_message=');
});
```

### כללי טסטים

1. **שתי בקשות לכל `call.read`** — הראשונה מחזירה פקודת read, השנייה (עם הערך ב-`values`) ממשיכה את הקוד
2. **אותו `CallSimulator`** לכל בקשות השיחה — `ApiCallId` חייב להיות זהה
3. **`val_name` ברירת מחדל:** `val_1`, `val_2`... לפי סדר קריאות ה-`read` בשיחה (counter per-Call)
4. **סימולציית ניתוק:** `call.values.hangup = 'yes'` ואז בקשה נוספת
5. **בקשה לא חוקית** (חסרים ApiCallId וכו') מחזירה `{ message: 'the request is not valid yemot request' }`
6. **port 0** — מאפשר ל-OS לבחור פורט פנוי אוטומטית

---

## הערות חשובות

- `call.id_list_message` **ללא** `prependToNextAction` → זורק `ExitError`, הקוד שאחריה **לא רץ**
- `call.go_to_folder` / `call.hangup` → זורקים שגיאה מיד, הקוד שאחריהם **לא רץ**
- `call.values` הוא `Readonly` — אין לשנות ישירות
- `call.req` ו-`call.values` מתעדכנים בכל בקשה — אל תשמור reference
- `makeNewCall` רץ **ללא await** — מאפשר שיחות מרובות במקביל
- POST דורש `express.urlencoded({ extended: true })`, לא `express.json()`

---

## קישורים

- [GitHub](https://github.com/ShlomoCode/yemot-router2)
- [פורום ימות המשיח — תיעוד read](https://f2.freeivr.co.il/post/78283)
