@hed-hog/core 0.0.400 → 0.0.402

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.
Files changed (274) hide show
  1. package/dist/access-log/access-log.interceptor.d.ts +10 -0
  2. package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
  3. package/dist/access-log/access-log.interceptor.js +15 -1
  4. package/dist/access-log/access-log.interceptor.js.map +1 -1
  5. package/dist/ai/ai.controller.d.ts +2 -0
  6. package/dist/ai/ai.controller.d.ts.map +1 -1
  7. package/dist/ai/ai.service.d.ts +86 -2
  8. package/dist/ai/ai.service.d.ts.map +1 -1
  9. package/dist/ai/ai.service.js +304 -23
  10. package/dist/ai/ai.service.js.map +1 -1
  11. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts +12 -0
  12. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts.map +1 -1
  13. package/dist/ai-instruction/ai-prompt-resolver.service.js +26 -0
  14. package/dist/ai-instruction/ai-prompt-resolver.service.js.map +1 -1
  15. package/dist/core.module.d.ts.map +1 -1
  16. package/dist/core.module.js +3 -0
  17. package/dist/core.module.js.map +1 -1
  18. package/dist/document-kit/document-kit.module.d.ts +3 -0
  19. package/dist/document-kit/document-kit.module.d.ts.map +1 -0
  20. package/dist/document-kit/document-kit.module.js +24 -0
  21. package/dist/document-kit/document-kit.module.js.map +1 -0
  22. package/dist/document-kit/document-kit.registry.d.ts +55 -0
  23. package/dist/document-kit/document-kit.registry.d.ts.map +1 -0
  24. package/dist/document-kit/document-kit.registry.js +91 -0
  25. package/dist/document-kit/document-kit.registry.js.map +1 -0
  26. package/dist/document-kit/document-kit.service.d.ts +49 -0
  27. package/dist/document-kit/document-kit.service.d.ts.map +1 -0
  28. package/dist/document-kit/document-kit.service.js +135 -0
  29. package/dist/document-kit/document-kit.service.js.map +1 -0
  30. package/dist/file/file-upload-ticket.service.d.ts +1 -1
  31. package/dist/file/file.controller.d.ts +3 -3
  32. package/dist/file/file.service.d.ts +136 -13
  33. package/dist/file/file.service.d.ts.map +1 -1
  34. package/dist/file/file.service.js +297 -27
  35. package/dist/file/file.service.js.map +1 -1
  36. package/dist/file/mcp-file-source.schema.d.ts.map +1 -1
  37. package/dist/file/mcp-file-source.schema.js +8 -5
  38. package/dist/file/mcp-file-source.schema.js.map +1 -1
  39. package/dist/file/provider/abstract.provider.d.ts +33 -0
  40. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  41. package/dist/file/provider/abstract.provider.js +9 -0
  42. package/dist/file/provider/abstract.provider.js.map +1 -1
  43. package/dist/file/provider/s3-multipart.util.d.ts +17 -0
  44. package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
  45. package/dist/file/provider/s3-multipart.util.js +88 -0
  46. package/dist/file/provider/s3-multipart.util.js.map +1 -0
  47. package/dist/file/provider/s3.provider.d.ts +9 -1
  48. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  49. package/dist/file/provider/s3.provider.js +16 -0
  50. package/dist/file/provider/s3.provider.js.map +1 -1
  51. package/dist/file/provider/spaces.provider.d.ts +10 -1
  52. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  53. package/dist/file/provider/spaces.provider.js +17 -0
  54. package/dist/file/provider/spaces.provider.js.map +1 -1
  55. package/dist/index.d.ts +5 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +5 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
  60. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  61. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  62. package/dist/integration-profile/integration-profile.secrets.js +26 -0
  63. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  64. package/dist/integration-profile/integration-profile.service.d.ts +1 -0
  65. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  66. package/dist/integration-profile/integration-profile.service.js +3 -3
  67. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  68. package/dist/integration-profile/testers/ai.testers.d.ts.map +1 -1
  69. package/dist/integration-profile/testers/ai.testers.js +46 -8
  70. package/dist/integration-profile/testers/ai.testers.js.map +1 -1
  71. package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
  72. package/dist/integration-profile/testers/http-probe.js +5 -3
  73. package/dist/integration-profile/testers/http-probe.js.map +1 -1
  74. package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
  75. package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
  76. package/dist/mail/mail.service.d.ts.map +1 -1
  77. package/dist/mail/mail.service.js +16 -1
  78. package/dist/mail/mail.service.js.map +1 -1
  79. package/dist/mcp/mcp-server.service.d.ts.map +1 -1
  80. package/dist/mcp/mcp-server.service.js +10 -1
  81. package/dist/mcp/mcp-server.service.js.map +1 -1
  82. package/dist/mcp/mcp.module.d.ts.map +1 -1
  83. package/dist/mcp/mcp.module.js +2 -0
  84. package/dist/mcp/mcp.module.js.map +1 -1
  85. package/dist/mcp/tools/core-access-log.mcp-tools.d.ts +17 -0
  86. package/dist/mcp/tools/core-access-log.mcp-tools.d.ts.map +1 -0
  87. package/dist/mcp/tools/core-access-log.mcp-tools.js +74 -0
  88. package/dist/mcp/tools/core-access-log.mcp-tools.js.map +1 -0
  89. package/dist/mcp/tools/core-files.mcp-tools.js +4 -4
  90. package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -1
  91. package/dist/meeting/meeting-provider.registry.d.ts +72 -0
  92. package/dist/meeting/meeting-provider.registry.d.ts.map +1 -1
  93. package/dist/meeting/meeting-provider.registry.js.map +1 -1
  94. package/dist/oauth/oauth-token-broker.service.d.ts +50 -1
  95. package/dist/oauth/oauth-token-broker.service.d.ts.map +1 -1
  96. package/dist/oauth/oauth-token-broker.service.js +105 -3
  97. package/dist/oauth/oauth-token-broker.service.js.map +1 -1
  98. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +6 -0
  99. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  100. package/dist/oauth/providers/microsoft-entra-id.provider.js +30 -4
  101. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  102. package/dist/pdf/document/charts/chart-bar.d.ts +24 -0
  103. package/dist/pdf/document/charts/chart-bar.d.ts.map +1 -0
  104. package/dist/pdf/document/charts/chart-bar.js +117 -0
  105. package/dist/pdf/document/charts/chart-bar.js.map +1 -0
  106. package/dist/pdf/document/charts/chart-donut.d.ts +16 -0
  107. package/dist/pdf/document/charts/chart-donut.d.ts.map +1 -0
  108. package/dist/pdf/document/charts/chart-donut.js +89 -0
  109. package/dist/pdf/document/charts/chart-donut.js.map +1 -0
  110. package/dist/pdf/document/charts/chart-gauge.d.ts +15 -0
  111. package/dist/pdf/document/charts/chart-gauge.d.ts.map +1 -0
  112. package/dist/pdf/document/charts/chart-gauge.js +60 -0
  113. package/dist/pdf/document/charts/chart-gauge.js.map +1 -0
  114. package/dist/pdf/document/charts/chart-line.d.ts +29 -0
  115. package/dist/pdf/document/charts/chart-line.d.ts.map +1 -0
  116. package/dist/pdf/document/charts/chart-line.js +89 -0
  117. package/dist/pdf/document/charts/chart-line.js.map +1 -0
  118. package/dist/pdf/document/charts/chart-primitives.d.ts +64 -0
  119. package/dist/pdf/document/charts/chart-primitives.d.ts.map +1 -0
  120. package/dist/pdf/document/charts/chart-primitives.js +110 -0
  121. package/dist/pdf/document/charts/chart-primitives.js.map +1 -0
  122. package/dist/pdf/document/charts/chart-radar.d.ts +13 -0
  123. package/dist/pdf/document/charts/chart-radar.d.ts.map +1 -0
  124. package/dist/pdf/document/charts/chart-radar.js +83 -0
  125. package/dist/pdf/document/charts/chart-radar.js.map +1 -0
  126. package/dist/pdf/document/charts/chart-scale.d.ts +39 -0
  127. package/dist/pdf/document/charts/chart-scale.d.ts.map +1 -0
  128. package/dist/pdf/document/charts/chart-scale.js +118 -0
  129. package/dist/pdf/document/charts/chart-scale.js.map +1 -0
  130. package/dist/pdf/document/charts/index.d.ts +8 -0
  131. package/dist/pdf/document/charts/index.d.ts.map +1 -0
  132. package/dist/pdf/document/charts/index.js +24 -0
  133. package/dist/pdf/document/charts/index.js.map +1 -0
  134. package/dist/pdf/document/index.d.ts +8 -0
  135. package/dist/pdf/document/index.d.ts.map +1 -0
  136. package/dist/pdf/document/index.js +24 -0
  137. package/dist/pdf/document/index.js.map +1 -0
  138. package/dist/pdf/document/pdf-document.chrome.d.ts +20 -0
  139. package/dist/pdf/document/pdf-document.chrome.d.ts.map +1 -0
  140. package/dist/pdf/document/pdf-document.chrome.js +36 -0
  141. package/dist/pdf/document/pdf-document.chrome.js.map +1 -0
  142. package/dist/pdf/document/pdf-document.css.d.ts +20 -0
  143. package/dist/pdf/document/pdf-document.css.d.ts.map +1 -0
  144. package/dist/pdf/document/pdf-document.css.js +327 -0
  145. package/dist/pdf/document/pdf-document.css.js.map +1 -0
  146. package/dist/pdf/document/pdf-document.renderer.d.ts +10 -0
  147. package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -0
  148. package/dist/pdf/document/pdf-document.renderer.js +226 -0
  149. package/dist/pdf/document/pdf-document.renderer.js.map +1 -0
  150. package/dist/pdf/document/pdf-document.service.d.ts +27 -0
  151. package/dist/pdf/document/pdf-document.service.d.ts.map +1 -0
  152. package/dist/pdf/document/pdf-document.service.js +81 -0
  153. package/dist/pdf/document/pdf-document.service.js.map +1 -0
  154. package/dist/pdf/document/pdf-document.theme.d.ts +41 -0
  155. package/dist/pdf/document/pdf-document.theme.d.ts.map +1 -0
  156. package/dist/pdf/document/pdf-document.theme.js +105 -0
  157. package/dist/pdf/document/pdf-document.theme.js.map +1 -0
  158. package/dist/pdf/document/pdf-document.types.d.ts +164 -0
  159. package/dist/pdf/document/pdf-document.types.d.ts.map +1 -0
  160. package/dist/pdf/document/pdf-document.types.js +18 -0
  161. package/dist/pdf/document/pdf-document.types.js.map +1 -0
  162. package/dist/pdf/pdf.module.d.ts.map +1 -1
  163. package/dist/pdf/pdf.module.js +3 -2
  164. package/dist/pdf/pdf.module.js.map +1 -1
  165. package/dist/pdf/pdf.service.d.ts +75 -1
  166. package/dist/pdf/pdf.service.d.ts.map +1 -1
  167. package/dist/pdf/pdf.service.js +167 -63
  168. package/dist/pdf/pdf.service.js.map +1 -1
  169. package/dist/pdf/templates/commercial-proposal.template.d.ts +96 -0
  170. package/dist/pdf/templates/commercial-proposal.template.d.ts.map +1 -0
  171. package/dist/pdf/templates/commercial-proposal.template.js +276 -0
  172. package/dist/pdf/templates/commercial-proposal.template.js.map +1 -0
  173. package/dist/setting/favicon.controller.d.ts +26 -0
  174. package/dist/setting/favicon.controller.d.ts.map +1 -0
  175. package/dist/setting/favicon.controller.js +130 -0
  176. package/dist/setting/favicon.controller.js.map +1 -0
  177. package/dist/setting/setting.module.d.ts.map +1 -1
  178. package/dist/setting/setting.module.js +2 -1
  179. package/dist/setting/setting.module.js.map +1 -1
  180. package/dist/setting/setting.service.d.ts.map +1 -1
  181. package/dist/setting/setting.service.js +3 -0
  182. package/dist/setting/setting.service.js.map +1 -1
  183. package/dist/task/task.service.d.ts +6 -0
  184. package/dist/task/task.service.d.ts.map +1 -1
  185. package/dist/task/task.service.js +44 -13
  186. package/dist/task/task.service.js.map +1 -1
  187. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
  188. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
  189. package/dist/whatsapp-sender/whatsapp-sender.service.js +107 -22
  190. package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
  191. package/hedhog/data/integration_type.yaml +6 -0
  192. package/hedhog/data/route.yaml +8 -0
  193. package/hedhog/data/setting_group.yaml +52 -0
  194. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
  195. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
  196. package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
  197. package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
  198. package/hedhog/frontend/messages/en.json +81 -2
  199. package/hedhog/frontend/messages/pt.json +81 -2
  200. package/hedhog/table/file.yaml +3 -1
  201. package/package.json +5 -5
  202. package/src/access-log/access-log.interceptor.spec.ts +50 -0
  203. package/src/access-log/access-log.interceptor.ts +18 -0
  204. package/src/ai/ai-attachment.fragments.spec.ts +56 -3
  205. package/src/ai/ai.service.responses.spec.ts +429 -0
  206. package/src/ai/ai.service.ts +409 -27
  207. package/src/ai-instruction/ai-prompt-resolver.service.spec.ts +44 -0
  208. package/src/ai-instruction/ai-prompt-resolver.service.ts +29 -0
  209. package/src/core.module.ts +3 -0
  210. package/src/document-kit/document-kit.module.ts +12 -0
  211. package/src/document-kit/document-kit.registry.ts +125 -0
  212. package/src/document-kit/document-kit.service.spec.ts +156 -0
  213. package/src/document-kit/document-kit.service.ts +173 -0
  214. package/src/file/file-direct-upload.spec.ts +344 -0
  215. package/src/file/file.service.spec.ts +168 -1
  216. package/src/file/file.service.ts +435 -36
  217. package/src/file/mcp-file-source.schema.ts +8 -5
  218. package/src/file/provider/abstract.provider.ts +57 -0
  219. package/src/file/provider/s3-multipart.spec.ts +123 -0
  220. package/src/file/provider/s3-multipart.util.ts +126 -0
  221. package/src/file/provider/s3.provider.ts +65 -1
  222. package/src/file/provider/spaces.provider.ts +67 -1
  223. package/src/index.ts +5 -0
  224. package/src/integration-profile/integration-profile.secrets.ts +26 -0
  225. package/src/integration-profile/integration-profile.service.ts +1 -0
  226. package/src/integration-profile/testers/ai.testers.ts +70 -5
  227. package/src/integration-profile/testers/http-probe.ts +5 -3
  228. package/src/integration-profile/testers/integration-tester.types.ts +5 -0
  229. package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
  230. package/src/integration-profile/testers/remaining.testers.spec.ts +65 -1
  231. package/src/integration-profile/testers/tester-coverage.spec.ts +12 -0
  232. package/src/mail/mail.service.spec.ts +25 -0
  233. package/src/mail/mail.service.ts +18 -1
  234. package/src/mcp/mcp-server.service.spec.ts +16 -0
  235. package/src/mcp/mcp-server.service.ts +10 -1
  236. package/src/mcp/mcp.module.ts +2 -0
  237. package/src/mcp/tools/core-access-log.mcp-tools.ts +66 -0
  238. package/src/mcp/tools/core-files.mcp-tools.ts +4 -4
  239. package/src/meeting/meeting-provider.registry.ts +80 -0
  240. package/src/oauth/oauth-token-broker.service.spec.ts +108 -0
  241. package/src/oauth/oauth-token-broker.service.ts +129 -2
  242. package/src/oauth/providers/microsoft-entra-id.provider.spec.ts +133 -0
  243. package/src/oauth/providers/microsoft-entra-id.provider.ts +29 -2
  244. package/src/pdf/document/charts/chart-bar.ts +217 -0
  245. package/src/pdf/document/charts/chart-donut.ts +163 -0
  246. package/src/pdf/document/charts/chart-gauge.ts +94 -0
  247. package/src/pdf/document/charts/chart-line.ts +159 -0
  248. package/src/pdf/document/charts/chart-primitives.ts +188 -0
  249. package/src/pdf/document/charts/chart-radar.ts +136 -0
  250. package/src/pdf/document/charts/chart-scale.spec.ts +93 -0
  251. package/src/pdf/document/charts/chart-scale.ts +141 -0
  252. package/src/pdf/document/charts/charts.spec.ts +157 -0
  253. package/src/pdf/document/charts/index.ts +7 -0
  254. package/src/pdf/document/index.ts +7 -0
  255. package/src/pdf/document/pdf-document.chrome.ts +38 -0
  256. package/src/pdf/document/pdf-document.css.ts +326 -0
  257. package/src/pdf/document/pdf-document.renderer.spec.ts +172 -0
  258. package/src/pdf/document/pdf-document.renderer.ts +317 -0
  259. package/src/pdf/document/pdf-document.service.ts +77 -0
  260. package/src/pdf/document/pdf-document.theme.ts +146 -0
  261. package/src/pdf/document/pdf-document.types.ts +149 -0
  262. package/src/pdf/pdf.module.ts +3 -2
  263. package/src/pdf/pdf.service.spec.ts +131 -0
  264. package/src/pdf/pdf.service.ts +256 -82
  265. package/src/pdf/templates/commercial-proposal.template.spec.ts +177 -0
  266. package/src/pdf/templates/commercial-proposal.template.ts +400 -0
  267. package/src/setting/favicon.controller.spec.ts +92 -0
  268. package/src/setting/favicon.controller.ts +135 -0
  269. package/src/setting/setting.module.ts +2 -1
  270. package/src/setting/setting.service.ts +3 -0
  271. package/src/task/task.service.spec.ts +77 -1
  272. package/src/task/task.service.ts +58 -14
  273. package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +263 -0
  274. package/src/whatsapp-sender/whatsapp-sender.service.ts +154 -16
@@ -32,8 +32,17 @@ export type WhatsappSendInput = {
32
32
  phoneNumberId?: string;
33
33
  /** Template aprovado pela Meta — sem ele o envio só chega na janela de 24h. */
34
34
  template?: { name: string; language?: string; params?: string[] };
35
+ /**
36
+ * Providers aceitos para este envio. Vazio aceita qualquer um. Quem manda em
37
+ * lote para alunos restringe ao oficial: disparo em massa pela Evolution
38
+ * (WhatsApp Web) derruba e bane o número.
39
+ */
40
+ providers?: readonly string[];
35
41
  };
36
42
 
43
+ /** Só a Cloud API da Meta — para envio em volume a quem não é da equipe. */
44
+ export const WHATSAPP_OFFICIAL_ONLY: readonly string[] = ['whatsapp-official'];
45
+
37
46
  type ResolvedProfile = {
38
47
  id: number;
39
48
  slug: string;
@@ -46,13 +55,27 @@ type ResolvedProfile = {
46
55
  * resultado diferente. Um HTTP 4xx da Graph API (id inexistente, sem
47
56
  * permissão, token ruim) não muda em cinco minutos; um 5xx ou falha de rede
48
57
  * pode ser uma instabilidade passageira do provedor.
58
+ *
59
+ * `isTransient` repete o `retryable` no formato que o `TransientError.is` da
60
+ * fila reconhece por duck-typing (o core não depende do queue): com ele as
61
+ * tentativas intermediárias vão ao Sentry como warning, e só a última como erro.
62
+ *
63
+ * `reason` marca as falhas que valem para a instância inteira, não para a
64
+ * mensagem: com a sessão fechada ou a credencial recusada, a próxima mensagem
65
+ * do mesmo lote tem o mesmo destino, e quem envia em série pode parar ali.
49
66
  */
67
+ export type WhatsappSendErrorReason = 'session_closed' | 'unauthorized';
68
+
50
69
  export class WhatsappSendError extends BadRequestException {
70
+ readonly isTransient: boolean;
71
+
51
72
  constructor(
52
73
  message: string,
53
74
  public readonly retryable: boolean,
75
+ public readonly reason?: WhatsappSendErrorReason,
54
76
  ) {
55
77
  super(message);
78
+ this.isTransient = retryable;
56
79
  }
57
80
  }
58
81
 
@@ -73,7 +96,7 @@ export class WhatsappSenderService {
73
96
  ) {}
74
97
 
75
98
  async send(input: WhatsappSendInput): Promise<void> {
76
- const profile = await this.resolveProfile(input.profileSlug);
99
+ const profile = await this.resolveProfile(input.profileSlug, input.providers);
77
100
 
78
101
  switch (profile.providerSlug) {
79
102
  case 'evolution-api':
@@ -88,9 +111,12 @@ export class WhatsappSenderService {
88
111
  }
89
112
 
90
113
  /** Diz se há perfil utilizável, para o chamador decidir por um fallback sem tentar o envio. */
91
- async isConfigured(profileSlug?: string): Promise<boolean> {
114
+ async isConfigured(
115
+ profileSlug?: string,
116
+ providers?: readonly string[],
117
+ ): Promise<boolean> {
92
118
  try {
93
- const profile = await this.resolveProfile(profileSlug);
119
+ const profile = await this.resolveProfile(profileSlug, providers);
94
120
 
95
121
  if (profile.providerSlug === 'evolution-api') {
96
122
  const { baseUrl, token, instance } = this.readEvolutionConfig(profile);
@@ -150,8 +176,12 @@ export class WhatsappSenderService {
150
176
  return normalized;
151
177
  }
152
178
 
153
- private async resolveProfile(profileSlug?: string): Promise<ResolvedProfile> {
179
+ private async resolveProfile(
180
+ profileSlug?: string,
181
+ providers?: readonly string[],
182
+ ): Promise<ResolvedProfile> {
154
183
  const slug = String(profileSlug ?? '').trim();
184
+ const allowed = providers?.length ? [...providers] : null;
155
185
 
156
186
  const profile = slug
157
187
  ? await this.prisma.integration_profile.findUnique({
@@ -162,7 +192,11 @@ export class WhatsappSenderService {
162
192
  },
163
193
  })
164
194
  : await this.prisma.integration_profile.findFirst({
165
- where: { is_active: true, integration_type: { slug: 'whatsapp' } },
195
+ where: {
196
+ is_active: true,
197
+ integration_type: { slug: 'whatsapp' },
198
+ ...(allowed ? { integration_provider: { slug: { in: allowed } } } : {}),
199
+ },
166
200
  orderBy: { updated_at: 'desc' },
167
201
  include: {
168
202
  integration_provider: { select: { slug: true } },
@@ -174,7 +208,9 @@ export class WhatsappSenderService {
174
208
  throw new NotFoundException(
175
209
  slug
176
210
  ? `Perfil de integração "${slug}" não encontrado.`
177
- : 'Nenhum perfil de integração de WhatsApp ativo encontrado.',
211
+ : allowed
212
+ ? `Nenhum perfil de WhatsApp ativo do provider ${allowed.join(' ou ')} encontrado.`
213
+ : 'Nenhum perfil de integração de WhatsApp ativo encontrado.',
178
214
  );
179
215
  }
180
216
 
@@ -184,10 +220,21 @@ export class WhatsappSenderService {
184
220
  );
185
221
  }
186
222
 
223
+ const providerSlug = String((profile as any).integration_provider?.slug ?? '');
224
+
225
+ // Perfil escolhido à mão que não é do provider exigido: é configuração, não
226
+ // instabilidade — reenviar daria no mesmo.
227
+ if (allowed && !allowed.includes(providerSlug)) {
228
+ throw new WhatsappSendError(
229
+ `Perfil de integração "${profile.slug}" usa o provider "${providerSlug}", e este envio só aceita ${allowed.join(' ou ')}.`,
230
+ false,
231
+ );
232
+ }
233
+
187
234
  return {
188
235
  id: profile.id,
189
236
  slug: profile.slug,
190
- providerSlug: String((profile as any).integration_provider?.slug ?? ''),
237
+ providerSlug,
191
238
  config: ((profile.config ?? {}) as Record<string, unknown>),
192
239
  };
193
240
  }
@@ -242,17 +289,62 @@ export class WhatsappSenderService {
242
289
  const detail = await response.text().catch(() => '');
243
290
 
244
291
  if (response.status === 401) {
245
- throw new BadRequestException(
292
+ throw new WhatsappSendError(
246
293
  `Credenciais do WhatsApp inválidas (401). Verifique o token do perfil "${profile.slug}".`,
294
+ false,
295
+ 'unauthorized',
247
296
  );
248
297
  }
249
298
 
250
- throw new BadRequestException(
299
+ // Número sem conta no WhatsApp: a Evolution responde 400 com
300
+ // `exists: false`. É dado do cadastro, não instabilidade — reenviar não
301
+ // muda nada, e o chamador precisa saber disso para não insistir.
302
+ if (isEvolutionNumberMissing(detail)) {
303
+ throw new WhatsappSendError(
304
+ `O número ${target} não tem WhatsApp (Evolution: exists=false).`,
305
+ false,
306
+ );
307
+ }
308
+
309
+ // Sessão da instância caída (celular deslogado ou Baileys reconectando):
310
+ // a Evolution responde 500 "Connection Closed". Segue retentável porque a
311
+ // reconexão costuma ser sozinha; se persistir, quem ler a falha precisa
312
+ // saber que a ação é reconectar a instância, não mexer no envio.
313
+ if (isEvolutionSessionClosed(detail)) {
314
+ throw new WhatsappSendError(
315
+ `A instância "${instance}" da Evolution está desconectada do WhatsApp (Connection Closed). Reconecte a instância (QR code) do perfil "${profile.slug}".`,
316
+ true,
317
+ 'session_closed',
318
+ );
319
+ }
320
+
321
+ // Mesmo critério do provider oficial: 4xx é recusa estrutural, 5xx e
322
+ // 429 podem passar numa próxima tentativa.
323
+ throw new WhatsappSendError(
251
324
  `Evolution API recusou o envio (HTTP ${response.status}): ${detail.slice(0, 300)}`,
325
+ response.status >= 500 || response.status === 429,
252
326
  );
253
327
  }
254
328
  }
255
329
 
330
+ /**
331
+ * Número padrão do perfil, mantido pela library `whatsapp` (sync com o WABA
332
+ * ou registro na primeira mensagem recebida).
333
+ *
334
+ * A tabela não é do core: num projeto sem a library `whatsapp` o delegate não
335
+ * existe, e o envio segue sem padrão — cai no erro de phone_number_id ausente.
336
+ */
337
+ private async resolveDefaultPhoneNumberId(profileId: number): Promise<string> {
338
+ const delegate = (this.prisma as any).whatsapp_phone_number;
339
+ if (!delegate?.findFirst) return '';
340
+
341
+ const found = await delegate.findFirst({
342
+ where: { integration_profile_id: profileId, is_default: true, status: 'active' },
343
+ select: { phone_number_id: true },
344
+ });
345
+ return String(found?.phone_number_id ?? '').trim();
346
+ }
347
+
256
348
  private async sendWithOfficial(
257
349
  profile: ResolvedProfile,
258
350
  input: WhatsappSendInput,
@@ -278,15 +370,16 @@ export class WhatsappSenderService {
278
370
  );
279
371
  }
280
372
 
281
- // O phone_number_id não vive no perfil: um WABA pode ter vários números,
282
- // então quem envia precisa dizer por qual deles a mensagem sai.
283
- const phoneNumberId = String(
284
- input.phoneNumberId ?? profile.config.phone_number_id ?? '',
285
- ).trim();
373
+ // O phone_number_id não vive no perfil: um WABA pode ter vários números.
374
+ // Quem envia pode dizer por qual deles a mensagem sai; sem isso, vale o
375
+ // número padrão do perfil.
376
+ const phoneNumberId =
377
+ String(input.phoneNumberId ?? profile.config.phone_number_id ?? '').trim() ||
378
+ (await this.resolveDefaultPhoneNumberId(profile.id));
286
379
 
287
380
  if (!phoneNumberId) {
288
381
  throw new BadRequestException(
289
- 'O provider whatsapp-official exige o phone_number_id do número remetente.',
382
+ `Perfil de WhatsApp "${profile.slug}" não tem número padrão: informe o phone_number_id do número remetente ou mande uma mensagem para o número, que ele é registrado.`,
290
383
  );
291
384
  }
292
385
 
@@ -308,7 +401,7 @@ export class WhatsappSenderService {
308
401
  type: 'body',
309
402
  parameters: (input.template.params ?? [input.text]).map((text) => ({
310
403
  type: 'text',
311
- text,
404
+ text: toTemplateParam(text),
312
405
  })),
313
406
  },
314
407
  ],
@@ -359,6 +452,51 @@ export class WhatsappSenderService {
359
452
  }
360
453
  }
361
454
 
455
+ /**
456
+ * Reconhece o 400 da Evolution para destino inexistente:
457
+ * `{"response":{"message":[{"exists":false,"number":"55..."}]}}`.
458
+ */
459
+ function isEvolutionNumberMissing(detail: string): boolean {
460
+ try {
461
+ const messages = JSON.parse(detail)?.response?.message;
462
+ return (
463
+ Array.isArray(messages) &&
464
+ messages.some((item) => item && typeof item === 'object' && item.exists === false)
465
+ );
466
+ } catch {
467
+ return false;
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Reconhece o 500 da Evolution com a sessão do WhatsApp fechada:
473
+ * `{"response":{"message":"Connection Closed"}}` (às vezes com a string num array).
474
+ */
475
+ function isEvolutionSessionClosed(detail: string): boolean {
476
+ try {
477
+ const message = JSON.parse(detail)?.response?.message;
478
+ const texts = Array.isArray(message) ? message : [message];
479
+ return texts.some(
480
+ (item) => typeof item === 'string' && /connection closed/i.test(item),
481
+ );
482
+ } catch {
483
+ return false;
484
+ }
485
+ }
486
+
487
+ /**
488
+ * A Meta recusa parâmetro de template com quebra de linha, tab ou mais de
489
+ * quatro espaços seguidos (erro 132018) — e o texto que os módulos montam para
490
+ * o WhatsApp vem em parágrafos. Vazio também é recusado.
491
+ */
492
+ export function toTemplateParam(text: unknown): string {
493
+ const flat = String(text ?? '')
494
+ .replace(/\s*[\r\n\t]+\s*/g, ' · ')
495
+ .replace(/ {2,}/g, ' ')
496
+ .trim();
497
+ return flat || '-';
498
+ }
499
+
362
500
  /** Evolution espera base64 puro; um data URL vindo do chamador quebraria o envio. */
363
501
  function stripDataUrlPrefix(image: string): string {
364
502
  const match = /^data:[^;]+;base64,(.*)$/i.exec(image);