@andrey4emk/npm-app-back-b24 3.7.0 → 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 */
@@ -57,12 +60,56 @@ export class ChatApp {
57
60
  private auth: ChatAppAuthParam;
58
61
  private type: ChatAppTypeParam;
59
62
  private updateToken: boolean;
63
+ private readonly whatsAppType: string | null;
64
+ private readonly telegramType: string | null;
65
+
66
+ /**
67
+ * Тип мессенджера из конфига. Массив приходит из внешнего источника
68
+ * (config/chatApp.json обновляется данными API ChatApp), поэтому проверяем:
69
+ * пустой messenger дал бы URL с undefined и «отправку в никуда».
70
+ * Не бросаем: экземпляр создаётся на уровне модуля, и бросок уронил бы
71
+ * импорт всего модуля отправки вместе с остальными каналами. Отказ локальный —
72
+ * его возвращают методы того мессенджера, который не разрешился.
73
+ */
74
+ private static resolveMessengerType(config: MessengerConfig | undefined): string | null {
75
+ const first = config?.messenger?.[0];
76
+ if (typeof first?.type !== "string" || !first.type.trim()) {
77
+ return null;
78
+ }
79
+ return first.type;
80
+ }
60
81
 
61
82
  constructor(makeParam: ChatAppMakeParam, authParam: ChatAppAuthParam, typeParam: ChatAppTypeParam) {
62
83
  this.make = makeParam;
63
84
  this.auth = authParam;
64
85
  this.type = typeParam;
65
86
  this.updateToken = false;
87
+ this.whatsAppType = ChatApp.resolveMessengerType(typeParam?.whatsApp);
88
+ this.telegramType = ChatApp.resolveMessengerType(typeParam?.telegram);
89
+ }
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 };
66
113
  }
67
114
 
68
115
  async sendMessageChatApp(messangerType: MessengerType, messageData: MessageData): Promise<ChatAppResult> {
@@ -73,6 +120,11 @@ export class ChatApp {
73
120
  if (!messageData.phone || !messageData.message) {
74
121
  return { error: true, message: "Отсутствует phone или message", data: null };
75
122
  }
123
+ // Тип мессенджера не разрешился из конфига — отправлять некуда
124
+ const baseUrl = this.resolveBaseUrl(messangerType);
125
+ if (!baseUrl) {
126
+ return ChatApp.denyMessenger(messangerType);
127
+ }
76
128
 
77
129
  const check = await this.checkTokenChatApp();
78
130
  if (check.error) {
@@ -82,25 +134,42 @@ export class ChatApp {
82
134
  const phone = messageData.phone;
83
135
  const message = messageData.message;
84
136
 
85
- let url: string | undefined;
86
- if (messangerType === "whatsApp") {
87
- url = `https://api.chatapp.online/v1/licenses/${this.type.whatsApp.licenseId}/messengers/${this.type.whatsApp.messenger[0].type}/chats/${phone}/messages/text`;
88
- }
89
- if (messangerType === "telegram") {
90
- url = `https://api.chatapp.online/v1/licenses/${this.type.telegram.licenseId}/messengers/${this.type.telegram.messenger[0].type}/chats/${phone}/messages/text`;
91
- }
137
+ const url = `${baseUrl}/chats/${phone}/messages/text`;
92
138
 
93
- const res = await fetch(url!, {
94
- method: "POST",
95
- headers: {
96
- Lang: "en",
97
- "Content-Type": "application/json",
98
- Accept: "application/json",
99
- Authorization: this.auth.accessToken,
100
- },
101
- body: JSON.stringify({ text: message }),
102
- });
103
- 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
+ }
104
173
 
105
174
  if (!data.success) {
106
175
  return { error: true, message: `Ошибка при отправке сообщения в ChatApp через ${messangerType}`, data };
@@ -123,6 +192,11 @@ export class ChatApp {
123
192
  if (!messageData.phone || !messageData.message) {
124
193
  return { error: true, message: "Отсутствует phone или message", data: null };
125
194
  }
195
+ // Тип мессенджера не разрешился из конфига — отправлять некуда
196
+ const baseUrl = this.resolveBaseUrl(messangerType);
197
+ if (!baseUrl) {
198
+ return ChatApp.denyMessenger(messangerType);
199
+ }
126
200
 
127
201
  const check = await this.checkTokenChatApp();
128
202
  if (check.error) {
@@ -134,25 +208,38 @@ export class ChatApp {
134
208
  const file = messageData.fileUrl;
135
209
  const fileName = messageData.fileName;
136
210
 
137
- let url: string | undefined;
138
- if (messangerType === "whatsApp") {
139
- url = `https://api.chatapp.online/v1/licenses/${this.type.whatsApp.licenseId}/messengers/${this.type.whatsApp.messenger[0].type}/chats/${phone}/messages/file`;
140
- }
141
- if (messangerType === "telegram") {
142
- url = `https://api.chatapp.online/v1/licenses/${this.type.telegram.licenseId}/messengers/${this.type.telegram.messenger[0].type}/chats/${phone}/messages/file`;
143
- }
211
+ const url = `${baseUrl}/chats/${phone}/messages/file`;
144
212
 
145
- const res = await fetch(url!, {
146
- method: "POST",
147
- headers: {
148
- Lang: "en",
149
- "Content-Type": "application/json",
150
- Accept: "application/json",
151
- Authorization: this.auth.accessToken,
152
- },
153
- body: JSON.stringify({ file, fileName, caption }),
154
- });
155
- const data: any = await res.json();
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;
234
+
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
+ }
156
243
 
157
244
  if (!data.success) {
158
245
  return { error: true, message: `Ошибка при отправке сообщения в ChatApp через ${messangerType}`, data };
@@ -167,33 +254,56 @@ export class ChatApp {
167
254
  return { error: false, message: `Сообщение успешно отправлено в ChatApp через ${messangerType}`, data, auth: newAuth };
168
255
  }
169
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
+ */
170
266
  async phoneCheckChatApp(messangerType: MessengerType, phone: string): Promise<ChatAppResult> {
171
267
  if (!["whatsApp", "telegram"].includes(messangerType)) {
172
268
  return { error: true, message: "Неверный тип мессенджера", data: null };
173
269
  }
270
+ // Тип мессенджера не разрешился из конфига — проверять телефон негде
271
+ const baseUrl = this.resolveBaseUrl(messangerType);
272
+ if (!baseUrl) {
273
+ return ChatApp.denyMessenger(messangerType);
274
+ }
174
275
 
175
276
  const check = await this.checkTokenChatApp();
176
277
  if (check.error) {
177
278
  return { error: true, message: `Ошибка с токеном ChatApp в функции phoneCheckChatApp класса ChatApp\n${check.message}`, data: check };
178
279
  }
179
280
 
180
- let url: string | undefined;
181
- if (messangerType === "whatsApp") {
182
- url = `https://api.chatapp.online/v1/licenses/${this.type.whatsApp.licenseId}/messengers/${this.type.whatsApp.messenger[0].type}/phones/${phone}/check`;
183
- }
184
- if (messangerType === "telegram") {
185
- url = `https://api.chatapp.online/v1/licenses/${this.type.telegram.licenseId}/messengers/${this.type.telegram.messenger[0].type}/phones/${phone}/check`;
186
- }
281
+ const url = `${baseUrl}/phones/${phone}/check`;
187
282
 
188
- const res = await fetch(url!, {
189
- method: "GET",
190
- headers: {
191
- Lang: "en",
192
- Accept: "application/json",
193
- Authorization: this.auth.accessToken,
194
- },
195
- });
196
- 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
+ }
197
307
 
198
308
  if (!data.success) {
199
309
  return { error: true, message: `Ошибка при проверке телефона в ChatApp через ${messangerType}`, data };
@@ -205,20 +315,30 @@ export class ChatApp {
205
315
  newAuth = this.auth;
206
316
  this.updateToken = false;
207
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
+
208
324
  data.check = data.data.exist;
209
325
  return { error: false, message: `Телефон успешно проверен в ChatApp через ${messangerType}`, data, auth: newAuth };
210
326
  }
211
327
 
212
328
  async checkTokenChatApp(): Promise<{ error: boolean; message: string; data?: null }> {
213
329
  try {
214
- const response = await fetch("https://api.chatapp.online/v1/tokens/check", {
215
- method: "GET",
216
- headers: {
217
- Lang: "en",
218
- Accept: "application/json",
219
- 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
+ },
220
339
  },
221
- });
340
+ FETCH_TIMEOUTS.quick
341
+ );
222
342
 
223
343
  const data: any = await response.json();
224
344
  if (!data.success) {
@@ -235,19 +355,23 @@ export class ChatApp {
235
355
  async makeTokenChatApp(): Promise<{ error: boolean; message?: string; data?: unknown }> {
236
356
  console.warn("makeTokenChatApp called");
237
357
  try {
238
- const response = await fetch("https://api.chatapp.online/v1/tokens", {
239
- method: "POST",
240
- headers: {
241
- Lang: "en",
242
- "Content-Type": "application/json",
243
- 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
+ }),
244
372
  },
245
- body: JSON.stringify({
246
- email: this.make.email,
247
- password: this.make.pass,
248
- appId: this.make.appId,
249
- }),
250
- });
373
+ FETCH_TIMEOUTS.quick
374
+ );
251
375
 
252
376
  const data: any = await response.json();
253
377
  if (!data.success) {
@@ -268,15 +392,19 @@ export class ChatApp {
268
392
 
269
393
  async refreshTokenChatApp(): Promise<{ error: boolean; message?: string; data?: unknown }> {
270
394
  try {
271
- const response = await fetch("https://api.chatapp.online/v1/tokens/refresh", {
272
- method: "POST",
273
- headers: {
274
- Lang: "en",
275
- "Content-Type": "application/json",
276
- 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 }),
277
405
  },
278
- body: JSON.stringify({ refreshToken: this.auth.refreshToken }),
279
- });
406
+ FETCH_TIMEOUTS.quick
407
+ );
280
408
 
281
409
  const data: any = await response.json();
282
410
  if (!data.success) {
@@ -301,14 +429,18 @@ export class ChatApp {
301
429
 
302
430
  async getLicensesChatApp(): Promise<{ error: boolean; message: string; data: unknown }> {
303
431
  try {
304
- const response = await fetch("https://api.chatapp.online/v1/licenses", {
305
- method: "GET",
306
- headers: {
307
- Lang: "en",
308
- Accept: "application/json",
309
- 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
+ },
310
441
  },
311
- });
442
+ FETCH_TIMEOUTS.api
443
+ );
312
444
  const data: any = await response.json();
313
445
 
314
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
  }