@andrey4emk/npm-app-back-b24 3.7.1 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,6 @@
1
+ import { fetchWithTimeout, isTimeoutError, FETCH_TIMEOUTS } from "../utils/fetchRetry.ts";
2
+ import { logs } from "../logs/logs.ts";
3
+
1
4
  // ==================== Типы ====================
2
5
 
3
6
  /** Параметры для создания токена ChatApp */
@@ -85,6 +88,30 @@ export class ChatApp {
85
88
  this.telegramType = ChatApp.resolveMessengerType(typeParam?.telegram);
86
89
  }
87
90
 
91
+ /**
92
+ * Общая часть URL до раздела запроса: лицензия и разрешённый тип мессенджера.
93
+ * `null` — тип из конфига не разрешился, обращаться некуда. Проверка типа идёт
94
+ * первой: при отсутствующей секции конфига `this.type.whatsApp` тоже undefined,
95
+ * и до чтения licenseId дело не доходит.
96
+ */
97
+ private resolveBaseUrl(messangerType: MessengerType): string | null {
98
+ const resolvedType = messangerType === "whatsApp" ? this.whatsAppType : this.telegramType;
99
+ if (!resolvedType) {
100
+ return null;
101
+ }
102
+ const licenseId = messangerType === "whatsApp" ? this.type.whatsApp.licenseId : this.type.telegram.licenseId;
103
+ return `https://api.chatapp.online/v1/licenses/${licenseId}/messengers/${resolvedType}`;
104
+ }
105
+
106
+ /**
107
+ * Отказ гейта — один текст на все три публичных метода.
108
+ * Не бросаем: экземпляр создаётся на уровне модуля, и бросок уронил бы импорт
109
+ * всего модуля отправки вместе с остальными каналами.
110
+ */
111
+ private static denyMessenger(messangerType: MessengerType): ChatAppResult {
112
+ return { error: true, message: `ChatApp: не разрешён тип мессенджера ${messangerType} (нет конфига или пуст messenger)`, data: null };
113
+ }
114
+
88
115
  async sendMessageChatApp(messangerType: MessengerType, messageData: MessageData): Promise<ChatAppResult> {
89
116
  // Проверяем что верно указан messangerType и в messageData есть phone и message
90
117
  if (!["whatsApp", "telegram"].includes(messangerType)) {
@@ -94,9 +121,9 @@ export class ChatApp {
94
121
  return { error: true, message: "Отсутствует phone или message", data: null };
95
122
  }
96
123
  // Тип мессенджера не разрешился из конфига — отправлять некуда
97
- const resolvedType = messangerType === "whatsApp" ? this.whatsAppType : this.telegramType;
98
- if (!resolvedType) {
99
- return { error: true, message: `ChatApp: не разрешён тип мессенджера ${messangerType} (нет конфига или пуст messenger)`, data: null };
124
+ const baseUrl = this.resolveBaseUrl(messangerType);
125
+ if (!baseUrl) {
126
+ return ChatApp.denyMessenger(messangerType);
100
127
  }
101
128
 
102
129
  const check = await this.checkTokenChatApp();
@@ -107,25 +134,42 @@ export class ChatApp {
107
134
  const phone = messageData.phone;
108
135
  const message = messageData.message;
109
136
 
110
- let url: string | undefined;
111
- if (messangerType === "whatsApp") {
112
- url = `https://api.chatapp.online/v1/licenses/${this.type.whatsApp.licenseId}/messengers/${resolvedType}/chats/${phone}/messages/text`;
113
- }
114
- if (messangerType === "telegram") {
115
- url = `https://api.chatapp.online/v1/licenses/${this.type.telegram.licenseId}/messengers/${resolvedType}/chats/${phone}/messages/text`;
116
- }
137
+ const url = `${baseUrl}/chats/${phone}/messages/text`;
117
138
 
118
- const res = await fetch(url!, {
119
- method: "POST",
120
- headers: {
121
- Lang: "en",
122
- "Content-Type": "application/json",
123
- Accept: "application/json",
124
- Authorization: this.auth.accessToken,
125
- },
126
- body: JSON.stringify({ text: message }),
127
- });
128
- const data: any = await res.json();
139
+ let data: any;
140
+ try {
141
+ const res = await fetchWithTimeout(
142
+ url,
143
+ {
144
+ method: "POST",
145
+ headers: {
146
+ Lang: "en",
147
+ "Content-Type": "application/json",
148
+ Accept: "application/json",
149
+ Authorization: this.auth.accessToken,
150
+ },
151
+ body: JSON.stringify({ text: message }),
152
+ },
153
+ FETCH_TIMEOUTS.send
154
+ );
155
+ data = await res.json();
156
+ } catch (error: unknown) {
157
+ // Метод исторически без try/catch — перехватываем только таймаут, который сами
158
+ // и вводим. Остальные ошибки летят наружу как раньше
159
+ if (!isTimeoutError(error)) throw error;
160
+
161
+ // Уровень error — единственный, который уходит в чат B24. До появления
162
+ // таймаута зависание долетало исключением до потребителя и там порождало
163
+ // задачу через errorB24(); перехватив ошибку здесь, мы обязаны сами оставить
164
+ // след, иначе правка сделала бы отказ отправки тише, чем он был
165
+ logs.add(`[${phone}] ChatApp: таймаут ${FETCH_TIMEOUTS.send}мс при отправке сообщения через ${messangerType} — статус неизвестен, повторять нельзя`, "error");
166
+
167
+ return {
168
+ error: true,
169
+ message: `ChatApp: таймаут ${FETCH_TIMEOUTS.send}мс при отправке сообщения через ${messangerType} — статус неизвестен`,
170
+ data: null,
171
+ };
172
+ }
129
173
 
130
174
  if (!data.success) {
131
175
  return { error: true, message: `Ошибка при отправке сообщения в ChatApp через ${messangerType}`, data };
@@ -149,9 +193,9 @@ export class ChatApp {
149
193
  return { error: true, message: "Отсутствует phone или message", data: null };
150
194
  }
151
195
  // Тип мессенджера не разрешился из конфига — отправлять некуда
152
- const resolvedType = messangerType === "whatsApp" ? this.whatsAppType : this.telegramType;
153
- if (!resolvedType) {
154
- return { error: true, message: `ChatApp: не разрешён тип мессенджера ${messangerType} (нет конфига или пуст messenger)`, data: null };
196
+ const baseUrl = this.resolveBaseUrl(messangerType);
197
+ if (!baseUrl) {
198
+ return ChatApp.denyMessenger(messangerType);
155
199
  }
156
200
 
157
201
  const check = await this.checkTokenChatApp();
@@ -164,25 +208,38 @@ export class ChatApp {
164
208
  const file = messageData.fileUrl;
165
209
  const fileName = messageData.fileName;
166
210
 
167
- let url: string | undefined;
168
- if (messangerType === "whatsApp") {
169
- url = `https://api.chatapp.online/v1/licenses/${this.type.whatsApp.licenseId}/messengers/${resolvedType}/chats/${phone}/messages/file`;
170
- }
171
- if (messangerType === "telegram") {
172
- url = `https://api.chatapp.online/v1/licenses/${this.type.telegram.licenseId}/messengers/${resolvedType}/chats/${phone}/messages/file`;
173
- }
211
+ const url = `${baseUrl}/chats/${phone}/messages/file`;
212
+
213
+ let data: any;
214
+ try {
215
+ // Файл по ссылке забирает сам ChatApp, поэтому ответа можно ждать дольше — бюджет transfer
216
+ const res = await fetchWithTimeout(
217
+ url,
218
+ {
219
+ method: "POST",
220
+ headers: {
221
+ Lang: "en",
222
+ "Content-Type": "application/json",
223
+ Accept: "application/json",
224
+ Authorization: this.auth.accessToken,
225
+ },
226
+ body: JSON.stringify({ file, fileName, caption }),
227
+ },
228
+ FETCH_TIMEOUTS.transfer
229
+ );
230
+ data = await res.json();
231
+ } catch (error: unknown) {
232
+ // Перехватываем только таймаут, остальные ошибки ведут себя как раньше
233
+ if (!isTimeoutError(error)) throw error;
174
234
 
175
- const res = await fetch(url!, {
176
- method: "POST",
177
- headers: {
178
- Lang: "en",
179
- "Content-Type": "application/json",
180
- Accept: "application/json",
181
- Authorization: this.auth.accessToken,
182
- },
183
- body: JSON.stringify({ file, fileName, caption }),
184
- });
185
- const data: any = await res.json();
235
+ logs.add(`[${phone}] ChatApp: таймаут ${FETCH_TIMEOUTS.transfer}мс при отправке файла через ${messangerType} — статус неизвестен, повторять нельзя`, "error");
236
+
237
+ return {
238
+ error: true,
239
+ message: `ChatApp: таймаут ${FETCH_TIMEOUTS.transfer}мс при отправке файла через ${messangerType} — статус неизвестен`,
240
+ data: null,
241
+ };
242
+ }
186
243
 
187
244
  if (!data.success) {
188
245
  return { error: true, message: `Ошибка при отправке сообщения в ChatApp через ${messangerType}`, data };
@@ -197,14 +254,23 @@ export class ChatApp {
197
254
  return { error: false, message: `Сообщение успешно отправлено в ChatApp через ${messangerType}`, data, auth: newAuth };
198
255
  }
199
256
 
257
+ /**
258
+ * Проверка телефона. Результат — поле `check` в `data` (значение `data.data.exist` ответа API).
259
+ *
260
+ * Изменение поведения от 16.08.2026: ответ с `success: true`, но без объекта `data`
261
+ * раньше давал `TypeError` наружу (метод не обёрнут в try/catch). Теперь такой ответ
262
+ * возвращается как обычный для этого класса отказ `{ error: true, ... }` с сырым ответом
263
+ * в `data`. Молча подставлять `check: undefined` нельзя: «телефон не найден» и «ответ
264
+ * непонятной формы» — разные факты, и вызывающий отличил бы их только по исключению.
265
+ */
200
266
  async phoneCheckChatApp(messangerType: MessengerType, phone: string): Promise<ChatAppResult> {
201
267
  if (!["whatsApp", "telegram"].includes(messangerType)) {
202
268
  return { error: true, message: "Неверный тип мессенджера", data: null };
203
269
  }
204
270
  // Тип мессенджера не разрешился из конфига — проверять телефон негде
205
- const resolvedType = messangerType === "whatsApp" ? this.whatsAppType : this.telegramType;
206
- if (!resolvedType) {
207
- return { error: true, message: `ChatApp: не разрешён тип мессенджера ${messangerType} (нет конфига или пуст messenger)`, data: null };
271
+ const baseUrl = this.resolveBaseUrl(messangerType);
272
+ if (!baseUrl) {
273
+ return ChatApp.denyMessenger(messangerType);
208
274
  }
209
275
 
210
276
  const check = await this.checkTokenChatApp();
@@ -212,23 +278,32 @@ export class ChatApp {
212
278
  return { error: true, message: `Ошибка с токеном ChatApp в функции phoneCheckChatApp класса ChatApp\n${check.message}`, data: check };
213
279
  }
214
280
 
215
- let url: string | undefined;
216
- if (messangerType === "whatsApp") {
217
- url = `https://api.chatapp.online/v1/licenses/${this.type.whatsApp.licenseId}/messengers/${resolvedType}/phones/${phone}/check`;
218
- }
219
- if (messangerType === "telegram") {
220
- url = `https://api.chatapp.online/v1/licenses/${this.type.telegram.licenseId}/messengers/${resolvedType}/phones/${phone}/check`;
221
- }
281
+ const url = `${baseUrl}/phones/${phone}/check`;
222
282
 
223
- const res = await fetch(url!, {
224
- method: "GET",
225
- headers: {
226
- Lang: "en",
227
- Accept: "application/json",
228
- Authorization: this.auth.accessToken,
229
- },
230
- });
231
- const data: any = await res.json();
283
+ let data: any;
284
+ try {
285
+ const res = await fetchWithTimeout(
286
+ url,
287
+ {
288
+ method: "GET",
289
+ headers: {
290
+ Lang: "en",
291
+ Accept: "application/json",
292
+ Authorization: this.auth.accessToken,
293
+ },
294
+ },
295
+ FETCH_TIMEOUTS.api
296
+ );
297
+ data = await res.json();
298
+ } catch (error: unknown) {
299
+ // Перехватываем только таймаут, остальные ошибки ведут себя как раньше
300
+ if (!isTimeoutError(error)) throw error;
301
+
302
+ // warn, а не error: проверка номера ничего не меняет, повторить её безопасно
303
+ logs.add(`[${phone}] ChatApp: таймаут ${FETCH_TIMEOUTS.api}мс при проверке телефона через ${messangerType}`, "warn");
304
+
305
+ return { error: true, message: `ChatApp: таймаут ${FETCH_TIMEOUTS.api}мс при проверке телефона через ${messangerType}`, data: null };
306
+ }
232
307
 
233
308
  if (!data.success) {
234
309
  return { error: true, message: `Ошибка при проверке телефона в ChatApp через ${messangerType}`, data };
@@ -240,20 +315,30 @@ export class ChatApp {
240
315
  newAuth = this.auth;
241
316
  this.updateToken = false;
242
317
  }
318
+ // Ответ разбирается как any — noUncheckedIndexedAccess здесь ничего не ловит,
319
+ // а обращение к data.data.exist без проверки давало TypeError мимо всякого try/catch
320
+ if (typeof data.data !== "object" || data.data === null) {
321
+ return { error: true, message: `Ответ ChatApp без поля data при проверке телефона через ${messangerType}`, data, auth: newAuth };
322
+ }
323
+
243
324
  data.check = data.data.exist;
244
325
  return { error: false, message: `Телефон успешно проверен в ChatApp через ${messangerType}`, data, auth: newAuth };
245
326
  }
246
327
 
247
328
  async checkTokenChatApp(): Promise<{ error: boolean; message: string; data?: null }> {
248
329
  try {
249
- const response = await fetch("https://api.chatapp.online/v1/tokens/check", {
250
- method: "GET",
251
- headers: {
252
- Lang: "en",
253
- Accept: "application/json",
254
- Authorization: this.auth.accessToken,
330
+ const response = await fetchWithTimeout(
331
+ "https://api.chatapp.online/v1/tokens/check",
332
+ {
333
+ method: "GET",
334
+ headers: {
335
+ Lang: "en",
336
+ Accept: "application/json",
337
+ Authorization: this.auth.accessToken,
338
+ },
255
339
  },
256
- });
340
+ FETCH_TIMEOUTS.quick
341
+ );
257
342
 
258
343
  const data: any = await response.json();
259
344
  if (!data.success) {
@@ -270,19 +355,23 @@ export class ChatApp {
270
355
  async makeTokenChatApp(): Promise<{ error: boolean; message?: string; data?: unknown }> {
271
356
  console.warn("makeTokenChatApp called");
272
357
  try {
273
- const response = await fetch("https://api.chatapp.online/v1/tokens", {
274
- method: "POST",
275
- headers: {
276
- Lang: "en",
277
- "Content-Type": "application/json",
278
- Accept: "application/json",
358
+ const response = await fetchWithTimeout(
359
+ "https://api.chatapp.online/v1/tokens",
360
+ {
361
+ method: "POST",
362
+ headers: {
363
+ Lang: "en",
364
+ "Content-Type": "application/json",
365
+ Accept: "application/json",
366
+ },
367
+ body: JSON.stringify({
368
+ email: this.make.email,
369
+ password: this.make.pass,
370
+ appId: this.make.appId,
371
+ }),
279
372
  },
280
- body: JSON.stringify({
281
- email: this.make.email,
282
- password: this.make.pass,
283
- appId: this.make.appId,
284
- }),
285
- });
373
+ FETCH_TIMEOUTS.quick
374
+ );
286
375
 
287
376
  const data: any = await response.json();
288
377
  if (!data.success) {
@@ -303,15 +392,19 @@ export class ChatApp {
303
392
 
304
393
  async refreshTokenChatApp(): Promise<{ error: boolean; message?: string; data?: unknown }> {
305
394
  try {
306
- const response = await fetch("https://api.chatapp.online/v1/tokens/refresh", {
307
- method: "POST",
308
- headers: {
309
- Lang: "en",
310
- "Content-Type": "application/json",
311
- Accept: "application/json",
395
+ const response = await fetchWithTimeout(
396
+ "https://api.chatapp.online/v1/tokens/refresh",
397
+ {
398
+ method: "POST",
399
+ headers: {
400
+ Lang: "en",
401
+ "Content-Type": "application/json",
402
+ Accept: "application/json",
403
+ },
404
+ body: JSON.stringify({ refreshToken: this.auth.refreshToken }),
312
405
  },
313
- body: JSON.stringify({ refreshToken: this.auth.refreshToken }),
314
- });
406
+ FETCH_TIMEOUTS.quick
407
+ );
315
408
 
316
409
  const data: any = await response.json();
317
410
  if (!data.success) {
@@ -336,14 +429,18 @@ export class ChatApp {
336
429
 
337
430
  async getLicensesChatApp(): Promise<{ error: boolean; message: string; data: unknown }> {
338
431
  try {
339
- const response = await fetch("https://api.chatapp.online/v1/licenses", {
340
- method: "GET",
341
- headers: {
342
- Lang: "en",
343
- Accept: "application/json",
344
- Authorization: this.auth.accessToken,
432
+ const response = await fetchWithTimeout(
433
+ "https://api.chatapp.online/v1/licenses",
434
+ {
435
+ method: "GET",
436
+ headers: {
437
+ Lang: "en",
438
+ Accept: "application/json",
439
+ Authorization: this.auth.accessToken,
440
+ },
345
441
  },
346
- });
442
+ FETCH_TIMEOUTS.api
443
+ );
347
444
  const data: any = await response.json();
348
445
 
349
446
  if (!data.success) {
@@ -1,4 +1,5 @@
1
1
  import nodemailer from "nodemailer";
2
+ import { fetchWithTimeout, FETCH_TIMEOUTS } from "../utils/fetchRetry.ts";
2
3
 
3
4
  // ==================== Типы ====================
4
5
 
@@ -42,6 +43,12 @@ export class Email {
42
43
  host: "smtp.yandex.ru",
43
44
  port: 465, // 465 = SMTPS, 587 = STARTTLS
44
45
  secure: true, // true для 465
46
+ // Дефолты nodemailer (соединение 2 мин, приветствие 30с, сокет 10 мин) слишком щедры:
47
+ // письмо с вложением уходит на Яндекс за секунды, а 10 минут молчания сокета
48
+ // блокируют вызывающую очередь
49
+ connectionTimeout: 15_000,
50
+ greetingTimeout: 10_000,
51
+ socketTimeout: 120_000,
45
52
  });
46
53
  }
47
54
 
@@ -51,7 +58,9 @@ export class Email {
51
58
  if (dataMail.fileUrl) {
52
59
  const resDownloadFile = await this.downloadFileToUrl(dataMail.fileUrl);
53
60
  if (resDownloadFile.error) {
54
- return { error: true, message: resDownloadFile.message };
61
+ // Подстановка вместо возможного undefined: при exactOptionalPropertyTypes
62
+ // у потребителя явный undefined не подходит полю message?: string
63
+ return { error: true, message: resDownloadFile.message ?? "Не удалось скачать файл по fileUrl" };
55
64
  }
56
65
  dataMail.attachments = [
57
66
  {
@@ -80,7 +89,7 @@ export class Email {
80
89
 
81
90
  private async downloadFileToUrl(fileUrl: string): Promise<{ error: boolean; message?: string; buffer?: Buffer }> {
82
91
  try {
83
- const response = await fetch(fileUrl);
92
+ const response = await fetchWithTimeout(fileUrl, undefined, FETCH_TIMEOUTS.transfer);
84
93
  if (!response.ok) {
85
94
  throw new Error(`HTTP error! status: ${response.status}`);
86
95
  }
@@ -1,5 +1,6 @@
1
1
  import { getResultData, type B24MethodCaller } from "../bitrix24/b24.ts";
2
- import { fetchRetry } from "../utils/fetchRetry.ts";
2
+ import { fetchRetry, fetchWithTimeout, isTimeoutError, FETCH_TIMEOUTS } from "../utils/fetchRetry.ts";
3
+ import { logs } from "../logs/logs.ts";
3
4
 
4
5
  // ==================== Типы ====================
5
6
 
@@ -17,6 +18,15 @@ interface SmsMessageData {
17
18
  fileName?: string;
18
19
  }
19
20
 
21
+ /**
22
+ * Папка на диске Bitrix24, куда кладётся вложение перед отправкой SMS.
23
+ * Значение — папка «contract_company» проекта «Документы и договора» портала
24
+ * repacheb.bitrix24.ru. Для опубликованной библиотеки это чужая константа,
25
+ * поэтому она вынесена в параметр конструктора; здесь она осталась дефолтом,
26
+ * иначе смена была бы ломающей для всех текущих потребителей.
27
+ */
28
+ const DEFAULT_UPLOAD_FOLDER_ID = 2792881;
29
+
20
30
  /** Результат отправки SMS */
21
31
  interface SmsResult {
22
32
  error: boolean;
@@ -27,6 +37,12 @@ interface SmsResult {
27
37
  data?: string;
28
38
  errorText?: string;
29
39
  } | null;
40
+ /**
41
+ * Выставляется только при таймауте ожидания ответа SMSGold: запрос ушёл, ответ не пришёл.
42
+ * Означает «сообщение могло уйти» — не «не отправлено». Повторять отправку нельзя,
43
+ * решение принимает человек.
44
+ */
45
+ unknownDelivery?: boolean;
30
46
  }
31
47
 
32
48
  // ==================== Класс ====================
@@ -37,11 +53,14 @@ export class Smsgold {
37
53
  // Структурный тип, а не B24OAuth: объявляет callMethod сам, поэтому переживёт
38
54
  // удаление метода из SDK. Сигнатуру конструктора не сужает — она стала шире
39
55
  private b24: B24MethodCaller | null;
56
+ // ID папки на диске B24 для вложений SMS — параметр, а не константа модуля
57
+ private uploadFolderId: number;
40
58
 
41
- constructor(authParam: SmsgoldAuthParam, b24: B24MethodCaller | null = null) {
59
+ constructor(authParam: SmsgoldAuthParam, b24: B24MethodCaller | null = null, uploadFolderId: number = DEFAULT_UPLOAD_FOLDER_ID) {
42
60
  this.user = authParam.user;
43
61
  this.pass = authParam.pass;
44
62
  this.b24 = b24;
63
+ this.uploadFolderId = uploadFolderId;
45
64
  }
46
65
 
47
66
  async sendSms(messageData: SmsMessageData): Promise<SmsResult> {
@@ -97,13 +116,20 @@ export class Smsgold {
97
116
  sender: "potolkiRepa",
98
117
  }).toString();
99
118
 
100
- const result = await fetch("https://web.smsgold.ru/http2/", {
101
- method: "POST",
102
- headers: {
103
- "Content-Type": "application/x-www-form-urlencoded",
119
+ // Отправка неидемпотентна — повторов нет, единственная защита от зависшего
120
+ // соединения — бюджет времени. 30с взяты с запасом, чтобы «неизвестный статус»
121
+ // случался как можно реже
122
+ const result = await fetchWithTimeout(
123
+ "https://web.smsgold.ru/http2/",
124
+ {
125
+ method: "POST",
126
+ headers: {
127
+ "Content-Type": "application/x-www-form-urlencoded",
128
+ },
129
+ body,
104
130
  },
105
- body,
106
- });
131
+ FETCH_TIMEOUTS.send
132
+ );
107
133
 
108
134
  if (!result.ok) {
109
135
  const errorText = await result.text();
@@ -126,6 +152,23 @@ export class Smsgold {
126
152
  },
127
153
  };
128
154
  } catch (error: unknown) {
155
+ // Ответа не дождались: запрос уже ушёл, сообщение могло быть доставлено.
156
+ // Это не «ошибка отправки» — статус неизвестен, и решить это кодом нельзя.
157
+ // Единственное место в классе, где пишем в лог: уровень error уходит в чат B24,
158
+ // потому что разбираться должен человек
159
+ if (isTimeoutError(error)) {
160
+ logs.add(
161
+ `[${phone}] SMS: таймаут ожидания ответа SMSGold ${FETCH_TIMEOUTS.send}мс — статус неизвестен, повторная отправка запрещена`,
162
+ "error"
163
+ );
164
+ return {
165
+ error: true,
166
+ message: `SMSGold не ответил за ${FETCH_TIMEOUTS.send / 1000}с — статус отправки неизвестен: сообщение могло уйти, повторять нельзя`,
167
+ result: null,
168
+ unknownDelivery: true,
169
+ };
170
+ }
171
+
129
172
  const errMsg = error instanceof Error ? error.message : String(error);
130
173
  return { error: true, message: errMsg, result: null };
131
174
  }
@@ -138,7 +181,7 @@ export class Smsgold {
138
181
 
139
182
  // Используем метод disk.folder.uploadfile
140
183
  const res = await this.b24!.callMethod("disk.folder.uploadfile", {
141
- id: 2792881, // Id папки "contract_company" из проекта "Документы и договора"
184
+ id: this.uploadFolderId,
142
185
  data: { NAME: fileName },
143
186
  fileContent: [fileName, base64File],
144
187
  });
@@ -153,9 +196,11 @@ export class Smsgold {
153
196
  private async downloadFileToUrl(fileUrl: string): Promise<{ error: boolean; message?: string; buffer?: Buffer }> {
154
197
  try {
155
198
  // Скачивание файла идемпотентно — повтор при сетевом сбое безопасен.
156
- // Отправка SMS (fetch выше по коду) намеренно остаётся без retry:
157
- // при потерянном ответе повтор отправил бы сообщение второй раз
158
- const response = await fetchRetry(fileUrl);
199
+ // Отправка SMS (fetchWithTimeout выше по коду) намеренно остаётся без retry:
200
+ // при потерянном ответе повтор отправил бы сообщение второй раз.
201
+ // Попыток 3, а не 5: при бюджете 60с на попытку пять попыток дали бы до пяти
202
+ // минут ожидания перед самой отправкой SMS
203
+ const response = await fetchRetry(fileUrl, undefined, 3, 500, FETCH_TIMEOUTS.transfer);
159
204
  if (!response.ok) {
160
205
  throw new Error(`HTTP error! status: ${response.status}`);
161
206
  }