@adatechnology/meta-whatsapp-module 0.2.0-rc.9 → 0.2.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.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
2
2
  import { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core';
3
3
  import * as _adatechnology_meta_whatsapp_contracts from '@adatechnology/meta-whatsapp-contracts';
4
- import { SessionState, SessionMode, ConversationSummary, FlowGraphData, FlowGraphSummary, LiveFlowPosition, WhatsAppSettings, MessageDirection, MessageSender, MessageStatus, RealtimeNotifierInterface, ChannelAdapterInterface, ObjectStorageInterface, FlowActionKind, FlowActionHandler, ConversationSession, MetaWhatsAppHooks, SubjectResolverInterface, CatalogPort } from '@adatechnology/meta-whatsapp-contracts';
4
+ import { TranscriptionMode, SessionState, SessionMode, ConversationSummary, MessageDirection, MessageSender, MessageStatus, CacheInterface, FlowGraphData, FlowGraphSummary, LiveFlowPosition, WhatsAppSettings, RealtimeNotifierInterface, ChannelAdapterInterface, ObjectStorageInterface, FlowActionKind, FlowActionHandler, ConversationSession, WhatsAppMessage, WhatsAppStatus, MetaWhatsAppHooks, SubjectResolverInterface, CatalogPort, SendChannelMediaParams } from '@adatechnology/meta-whatsapp-contracts';
5
+ export { PREVIEW_MEDIA_ID_PREFIX, TranscriptionMode, resolvePreviewUploadId, toPreviewMediaId } from '@adatechnology/meta-whatsapp-contracts';
5
6
  import { WhatsAppMessageProvider } from '@adatechnology/meta-whatsapp-provider';
7
+ export { WEBHOOK_CLAIM_TTL_SECONDS, WEBHOOK_NONCE_TTL_SECONDS } from '@adatechnology/meta-graph-core';
6
8
 
7
9
  type MetaWhatsAppDatabase = PgDatabase<PgQueryResultHKT, any, any>;
8
10
  type DrizzleMigrateFunction = (db: never, config: {
@@ -10,6 +12,72 @@ type DrizzleMigrateFunction = (db: never, config: {
10
12
  migrationsTable?: string;
11
13
  }) => Promise<void>;
12
14
 
15
+ /**
16
+ * Vocabulário de transcrição de áudio do módulo.
17
+ *
18
+ * Fica em arquivo próprio porque o schema e os dois use-cases (ingestão automática e sob demanda)
19
+ * precisam dos mesmos tipos, e pendurá-los em qualquer um dos três faria os outros dois importarem
20
+ * de dentro de uma camada que não é a deles.
21
+ */
22
+
23
+ declare const TRANSCRIPTION_STATUS: {
24
+ /** Falhou de forma retriável (cota, rede, 5xx) — vai sair quando alguém tentar de novo. */
25
+ readonly PENDING: "pending";
26
+ /** Processado. Texto vazio aqui é áudio em silêncio, e NÃO deve ser reprocessado. */
27
+ readonly DONE: "done";
28
+ /** Falha definitiva do engine (credencial, áudio corrompido, arquivo grande demais). */
29
+ readonly FAILED: "failed";
30
+ /** Nenhum engine da cadeia aceita o formato. Retentar não conserta codec. */
31
+ readonly UNSUPPORTED: "unsupported";
32
+ };
33
+ type TranscriptionStatus = (typeof TRANSCRIPTION_STATUS)[keyof typeof TRANSCRIPTION_STATUS];
34
+ /**
35
+ * Quando transcrever.
36
+ *
37
+ * `auto` transcreve durante a ingestão da mídia, onde o buffer do áudio JÁ está em memória — não
38
+ * custa um segundo download do storage. `onDemand` só transcreve quando o atendente pede, o que
39
+ * troca latência na interface por não gastar cota com áudio que ninguém vai ler.
40
+ *
41
+ * O tipo vem do contrato (o painel escolhe, a API transporta, o módulo obedece) e `satisfies`
42
+ * garante em tempo de compilação que estes valores continuam sendo exatamente os de lá — sem isso,
43
+ * um modo novo no contrato passaria despercebido aqui.
44
+ */
45
+ declare const TRANSCRIPTION_MODE: {
46
+ readonly AUTO: "auto";
47
+ readonly ON_DEMAND: "onDemand";
48
+ };
49
+
50
+ /**
51
+ * O contrato mínimo de `@adatechnology/audio-transcription-provider`, declarado aqui em vez de
52
+ * importado — mesma decisão do `MessageModerator` em `LogMessage.use-case.ts`.
53
+ *
54
+ * Assim o módulo não ganha dependência de pacote por um recurso opcional, e transcrição não fica
55
+ * amarrada a WhatsApp: o que atravessa esta fronteira é um buffer de áudio e um mime.
56
+ */
57
+ type AudioTranscriber = {
58
+ readonly name: string;
59
+ transcribe: (input: {
60
+ buffer: Buffer;
61
+ mimeType: string;
62
+ languageHint?: string;
63
+ }) => Promise<{
64
+ text: string;
65
+ language?: string;
66
+ durationSeconds?: number;
67
+ engine: string;
68
+ }>;
69
+ };
70
+ /**
71
+ * Recorte do erro do provider que o módulo precisa ler para escolher entre `'pending'` e
72
+ * `'failed'`. Estrutural, não `instanceof`: o provider é opcional e pode nem estar instalado, e um
73
+ * `instanceof` contra classe ausente não compila.
74
+ */
75
+ declare function isRetriableTranscriptionError(error: unknown): boolean;
76
+ declare function isUnsupportedTranscriptionError(error: unknown): boolean;
77
+ declare function transcriptionRetryAfterSeconds(error: unknown): number | undefined;
78
+ /** Mime de áudio? Só áudio é transcrito — vídeo, imagem e documento passam sem tocar no engine. */
79
+ declare function isAudioMimeType(mimeType: string | undefined | null): boolean;
80
+
13
81
  declare const metaWhatsAppSchema: drizzle_orm_pg_core.PgSchema<"meta_whatsapp">;
14
82
  declare const sessions: drizzle_orm_pg_core.PgTableWithColumns<{
15
83
  name: "sessions";
@@ -560,6 +628,81 @@ declare const messages: drizzle_orm_pg_core.PgTableWithColumns<{
560
628
  }, {}, {
561
629
  $type: string[];
562
630
  }>;
631
+ transcriptionStatus: drizzle_orm_pg_core.PgColumn<{
632
+ name: "transcription_status";
633
+ tableName: "messages";
634
+ dataType: "string";
635
+ columnType: "PgVarchar";
636
+ data: TranscriptionStatus;
637
+ driverParam: string;
638
+ notNull: false;
639
+ hasDefault: false;
640
+ isPrimaryKey: false;
641
+ isAutoincrement: false;
642
+ hasRuntimeDefault: false;
643
+ enumValues: [string, ...string[]];
644
+ baseColumn: never;
645
+ identity: undefined;
646
+ generated: undefined;
647
+ }, {}, {
648
+ length: 16;
649
+ $type: TranscriptionStatus;
650
+ }>;
651
+ transcriptionText: drizzle_orm_pg_core.PgColumn<{
652
+ name: "transcription_text";
653
+ tableName: "messages";
654
+ dataType: "string";
655
+ columnType: "PgText";
656
+ data: string;
657
+ driverParam: string;
658
+ notNull: false;
659
+ hasDefault: false;
660
+ isPrimaryKey: false;
661
+ isAutoincrement: false;
662
+ hasRuntimeDefault: false;
663
+ enumValues: [string, ...string[]];
664
+ baseColumn: never;
665
+ identity: undefined;
666
+ generated: undefined;
667
+ }, {}, {}>;
668
+ transcriptionLanguage: drizzle_orm_pg_core.PgColumn<{
669
+ name: "transcription_language";
670
+ tableName: "messages";
671
+ dataType: "string";
672
+ columnType: "PgVarchar";
673
+ data: string;
674
+ driverParam: string;
675
+ notNull: false;
676
+ hasDefault: false;
677
+ isPrimaryKey: false;
678
+ isAutoincrement: false;
679
+ hasRuntimeDefault: false;
680
+ enumValues: [string, ...string[]];
681
+ baseColumn: never;
682
+ identity: undefined;
683
+ generated: undefined;
684
+ }, {}, {
685
+ length: 32;
686
+ }>;
687
+ transcriptionEngine: drizzle_orm_pg_core.PgColumn<{
688
+ name: "transcription_engine";
689
+ tableName: "messages";
690
+ dataType: "string";
691
+ columnType: "PgVarchar";
692
+ data: string;
693
+ driverParam: string;
694
+ notNull: false;
695
+ hasDefault: false;
696
+ isPrimaryKey: false;
697
+ isAutoincrement: false;
698
+ hasRuntimeDefault: false;
699
+ enumValues: [string, ...string[]];
700
+ baseColumn: never;
701
+ identity: undefined;
702
+ generated: undefined;
703
+ }, {}, {
704
+ length: 32;
705
+ }>;
563
706
  createdAt: drizzle_orm_pg_core.PgColumn<{
564
707
  name: "created_at";
565
708
  tableName: "messages";
@@ -580,13 +723,25 @@ declare const messages: drizzle_orm_pg_core.PgTableWithColumns<{
580
723
  };
581
724
  dialect: "pg";
582
725
  }>;
583
- declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
584
- name: "flow_graphs";
726
+ /**
727
+ * Biblioteca de arquivos da conversa.
728
+ *
729
+ * Tabela própria, e não derivada de `messages.payload`: o painel precisa de `source` e `linkedAt`,
730
+ * que não existem no payload; busca por nome de arquivo quer índice, não varredura de jsonb; e
731
+ * `messageId` anulável é o que permite o atendente anexar documento à conversa sem que exista uma
732
+ * mensagem correspondente.
733
+ *
734
+ * `sessionId` em cascata apaga a LINHA junto com a conversa, mas não o binário no storage — quem
735
+ * apaga objeto é passo de aplicação (listar `uploadId` → apagar no storage → apagar a sessão).
736
+ * Confiar só na FK deixaria objeto órfão sendo cobrado para sempre.
737
+ */
738
+ declare const documents: drizzle_orm_pg_core.PgTableWithColumns<{
739
+ name: "documents";
585
740
  schema: "meta_whatsapp";
586
741
  columns: {
587
742
  id: drizzle_orm_pg_core.PgColumn<{
588
743
  name: "id";
589
- tableName: "flow_graphs";
744
+ tableName: "documents";
590
745
  dataType: "string";
591
746
  columnType: "PgUUID";
592
747
  data: string;
@@ -603,7 +758,7 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
603
758
  }, {}, {}>;
604
759
  companyId: drizzle_orm_pg_core.PgColumn<{
605
760
  name: "company_id";
606
- tableName: "flow_graphs";
761
+ tableName: "documents";
607
762
  dataType: "string";
608
763
  columnType: "PgUUID";
609
764
  data: string;
@@ -618,11 +773,11 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
618
773
  identity: undefined;
619
774
  generated: undefined;
620
775
  }, {}, {}>;
621
- key: drizzle_orm_pg_core.PgColumn<{
622
- name: "key";
623
- tableName: "flow_graphs";
776
+ sessionId: drizzle_orm_pg_core.PgColumn<{
777
+ name: "session_id";
778
+ tableName: "documents";
624
779
  dataType: "string";
625
- columnType: "PgVarchar";
780
+ columnType: "PgUUID";
626
781
  data: string;
627
782
  driverParam: string;
628
783
  notNull: true;
@@ -630,16 +785,31 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
630
785
  isPrimaryKey: false;
631
786
  isAutoincrement: false;
632
787
  hasRuntimeDefault: false;
633
- enumValues: [string, ...string[]];
788
+ enumValues: undefined;
634
789
  baseColumn: never;
635
790
  identity: undefined;
636
791
  generated: undefined;
637
- }, {}, {
638
- length: 64;
639
- }>;
640
- label: drizzle_orm_pg_core.PgColumn<{
641
- name: "label";
642
- tableName: "flow_graphs";
792
+ }, {}, {}>;
793
+ messageId: drizzle_orm_pg_core.PgColumn<{
794
+ name: "message_id";
795
+ tableName: "documents";
796
+ dataType: "string";
797
+ columnType: "PgUUID";
798
+ data: string;
799
+ driverParam: string;
800
+ notNull: false;
801
+ hasDefault: false;
802
+ isPrimaryKey: false;
803
+ isAutoincrement: false;
804
+ hasRuntimeDefault: false;
805
+ enumValues: undefined;
806
+ baseColumn: never;
807
+ identity: undefined;
808
+ generated: undefined;
809
+ }, {}, {}>;
810
+ uploadId: drizzle_orm_pg_core.PgColumn<{
811
+ name: "upload_id";
812
+ tableName: "documents";
643
813
  dataType: "string";
644
814
  columnType: "PgVarchar";
645
815
  data: string;
@@ -654,11 +824,11 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
654
824
  identity: undefined;
655
825
  generated: undefined;
656
826
  }, {}, {
657
- length: 120;
827
+ length: 256;
658
828
  }>;
659
- startNodeId: drizzle_orm_pg_core.PgColumn<{
660
- name: "start_node_id";
661
- tableName: "flow_graphs";
829
+ filename: drizzle_orm_pg_core.PgColumn<{
830
+ name: "filename";
831
+ tableName: "documents";
662
832
  dataType: "string";
663
833
  columnType: "PgVarchar";
664
834
  data: string;
@@ -673,36 +843,36 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
673
843
  identity: undefined;
674
844
  generated: undefined;
675
845
  }, {}, {
676
- length: 64;
846
+ length: 512;
677
847
  }>;
678
- nodes: drizzle_orm_pg_core.PgColumn<{
679
- name: "nodes";
680
- tableName: "flow_graphs";
681
- dataType: "json";
682
- columnType: "PgJsonb";
683
- data: Record<string, unknown>;
684
- driverParam: unknown;
848
+ mimeType: drizzle_orm_pg_core.PgColumn<{
849
+ name: "mime_type";
850
+ tableName: "documents";
851
+ dataType: "string";
852
+ columnType: "PgVarchar";
853
+ data: string;
854
+ driverParam: string;
685
855
  notNull: true;
686
- hasDefault: true;
856
+ hasDefault: false;
687
857
  isPrimaryKey: false;
688
858
  isAutoincrement: false;
689
859
  hasRuntimeDefault: false;
690
- enumValues: undefined;
860
+ enumValues: [string, ...string[]];
691
861
  baseColumn: never;
692
862
  identity: undefined;
693
863
  generated: undefined;
694
864
  }, {}, {
695
- $type: Record<string, unknown>;
865
+ length: 128;
696
866
  }>;
697
- version: drizzle_orm_pg_core.PgColumn<{
698
- name: "version";
699
- tableName: "flow_graphs";
867
+ sizeBytes: drizzle_orm_pg_core.PgColumn<{
868
+ name: "size_bytes";
869
+ tableName: "documents";
700
870
  dataType: "number";
701
871
  columnType: "PgInteger";
702
872
  data: number;
703
873
  driverParam: string | number;
704
874
  notNull: true;
705
- hasDefault: true;
875
+ hasDefault: false;
706
876
  isPrimaryKey: false;
707
877
  isAutoincrement: false;
708
878
  hasRuntimeDefault: false;
@@ -711,31 +881,33 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
711
881
  identity: undefined;
712
882
  generated: undefined;
713
883
  }, {}, {}>;
714
- showInMenu: drizzle_orm_pg_core.PgColumn<{
715
- name: "show_in_menu";
716
- tableName: "flow_graphs";
717
- dataType: "boolean";
718
- columnType: "PgBoolean";
719
- data: boolean;
720
- driverParam: boolean;
721
- notNull: true;
722
- hasDefault: true;
884
+ sha256: drizzle_orm_pg_core.PgColumn<{
885
+ name: "sha256";
886
+ tableName: "documents";
887
+ dataType: "string";
888
+ columnType: "PgVarchar";
889
+ data: string;
890
+ driverParam: string;
891
+ notNull: false;
892
+ hasDefault: false;
723
893
  isPrimaryKey: false;
724
894
  isAutoincrement: false;
725
895
  hasRuntimeDefault: false;
726
- enumValues: undefined;
896
+ enumValues: [string, ...string[]];
727
897
  baseColumn: never;
728
898
  identity: undefined;
729
899
  generated: undefined;
730
- }, {}, {}>;
731
- menuOptionLabel: drizzle_orm_pg_core.PgColumn<{
732
- name: "menu_option_label";
733
- tableName: "flow_graphs";
900
+ }, {}, {
901
+ length: 64;
902
+ }>;
903
+ source: drizzle_orm_pg_core.PgColumn<{
904
+ name: "source";
905
+ tableName: "documents";
734
906
  dataType: "string";
735
907
  columnType: "PgVarchar";
736
908
  data: string;
737
909
  driverParam: string;
738
- notNull: false;
910
+ notNull: true;
739
911
  hasDefault: false;
740
912
  isPrimaryKey: false;
741
913
  isAutoincrement: false;
@@ -745,11 +917,11 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
745
917
  identity: undefined;
746
918
  generated: undefined;
747
919
  }, {}, {
748
- length: 64;
920
+ length: 12;
749
921
  }>;
750
- createdAt: drizzle_orm_pg_core.PgColumn<{
751
- name: "created_at";
752
- tableName: "flow_graphs";
922
+ linkedAt: drizzle_orm_pg_core.PgColumn<{
923
+ name: "linked_at";
924
+ tableName: "documents";
753
925
  dataType: "date";
754
926
  columnType: "PgTimestamp";
755
927
  data: Date;
@@ -764,16 +936,23 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
764
936
  identity: undefined;
765
937
  generated: undefined;
766
938
  }, {}, {}>;
767
- updatedAt: drizzle_orm_pg_core.PgColumn<{
768
- name: "updated_at";
939
+ };
940
+ dialect: "pg";
941
+ }>;
942
+ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
943
+ name: "flow_graphs";
944
+ schema: "meta_whatsapp";
945
+ columns: {
946
+ id: drizzle_orm_pg_core.PgColumn<{
947
+ name: "id";
769
948
  tableName: "flow_graphs";
770
- dataType: "date";
771
- columnType: "PgTimestamp";
772
- data: Date;
949
+ dataType: "string";
950
+ columnType: "PgUUID";
951
+ data: string;
773
952
  driverParam: string;
774
953
  notNull: true;
775
954
  hasDefault: true;
776
- isPrimaryKey: false;
955
+ isPrimaryKey: true;
777
956
  isAutoincrement: false;
778
957
  hasRuntimeDefault: false;
779
958
  enumValues: undefined;
@@ -781,23 +960,16 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
781
960
  identity: undefined;
782
961
  generated: undefined;
783
962
  }, {}, {}>;
784
- };
785
- dialect: "pg";
786
- }>;
787
- declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
788
- name: "settings";
789
- schema: "meta_whatsapp";
790
- columns: {
791
963
  companyId: drizzle_orm_pg_core.PgColumn<{
792
964
  name: "company_id";
793
- tableName: "settings";
965
+ tableName: "flow_graphs";
794
966
  dataType: "string";
795
967
  columnType: "PgUUID";
796
968
  data: string;
797
969
  driverParam: string;
798
970
  notNull: true;
799
971
  hasDefault: false;
800
- isPrimaryKey: true;
972
+ isPrimaryKey: false;
801
973
  isAutoincrement: false;
802
974
  hasRuntimeDefault: false;
803
975
  enumValues: undefined;
@@ -805,14 +977,14 @@ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
805
977
  identity: undefined;
806
978
  generated: undefined;
807
979
  }, {}, {}>;
808
- templateName: drizzle_orm_pg_core.PgColumn<{
809
- name: "template_name";
810
- tableName: "settings";
980
+ key: drizzle_orm_pg_core.PgColumn<{
981
+ name: "key";
982
+ tableName: "flow_graphs";
811
983
  dataType: "string";
812
984
  columnType: "PgVarchar";
813
985
  data: string;
814
986
  driverParam: string;
815
- notNull: false;
987
+ notNull: true;
816
988
  hasDefault: false;
817
989
  isPrimaryKey: false;
818
990
  isAutoincrement: false;
@@ -822,17 +994,17 @@ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
822
994
  identity: undefined;
823
995
  generated: undefined;
824
996
  }, {}, {
825
- length: 128;
997
+ length: 64;
826
998
  }>;
827
- templateLanguage: drizzle_orm_pg_core.PgColumn<{
828
- name: "template_language";
829
- tableName: "settings";
999
+ label: drizzle_orm_pg_core.PgColumn<{
1000
+ name: "label";
1001
+ tableName: "flow_graphs";
830
1002
  dataType: "string";
831
1003
  columnType: "PgVarchar";
832
1004
  data: string;
833
1005
  driverParam: string;
834
1006
  notNull: true;
835
- hasDefault: true;
1007
+ hasDefault: false;
836
1008
  isPrimaryKey: false;
837
1009
  isAutoincrement: false;
838
1010
  hasRuntimeDefault: false;
@@ -841,31 +1013,488 @@ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
841
1013
  identity: undefined;
842
1014
  generated: undefined;
843
1015
  }, {}, {
844
- length: 16;
1016
+ length: 120;
845
1017
  }>;
846
- templateVariables: drizzle_orm_pg_core.PgColumn<{
847
- name: "template_variables";
848
- tableName: "settings";
849
- dataType: "json";
850
- columnType: "PgJsonb";
851
- data: string[];
852
- driverParam: unknown;
1018
+ startNodeId: drizzle_orm_pg_core.PgColumn<{
1019
+ name: "start_node_id";
1020
+ tableName: "flow_graphs";
1021
+ dataType: "string";
1022
+ columnType: "PgVarchar";
1023
+ data: string;
1024
+ driverParam: string;
853
1025
  notNull: true;
854
- hasDefault: true;
1026
+ hasDefault: false;
855
1027
  isPrimaryKey: false;
856
1028
  isAutoincrement: false;
857
1029
  hasRuntimeDefault: false;
858
- enumValues: undefined;
1030
+ enumValues: [string, ...string[]];
859
1031
  baseColumn: never;
860
1032
  identity: undefined;
861
1033
  generated: undefined;
862
1034
  }, {}, {
863
- $type: string[];
1035
+ length: 64;
864
1036
  }>;
865
- welcomeMessage: drizzle_orm_pg_core.PgColumn<{
866
- name: "welcome_message";
867
- tableName: "settings";
868
- dataType: "string";
1037
+ nodes: drizzle_orm_pg_core.PgColumn<{
1038
+ name: "nodes";
1039
+ tableName: "flow_graphs";
1040
+ dataType: "json";
1041
+ columnType: "PgJsonb";
1042
+ data: Record<string, unknown>;
1043
+ driverParam: unknown;
1044
+ notNull: true;
1045
+ hasDefault: true;
1046
+ isPrimaryKey: false;
1047
+ isAutoincrement: false;
1048
+ hasRuntimeDefault: false;
1049
+ enumValues: undefined;
1050
+ baseColumn: never;
1051
+ identity: undefined;
1052
+ generated: undefined;
1053
+ }, {}, {
1054
+ $type: Record<string, unknown>;
1055
+ }>;
1056
+ version: drizzle_orm_pg_core.PgColumn<{
1057
+ name: "version";
1058
+ tableName: "flow_graphs";
1059
+ dataType: "number";
1060
+ columnType: "PgInteger";
1061
+ data: number;
1062
+ driverParam: string | number;
1063
+ notNull: true;
1064
+ hasDefault: true;
1065
+ isPrimaryKey: false;
1066
+ isAutoincrement: false;
1067
+ hasRuntimeDefault: false;
1068
+ enumValues: undefined;
1069
+ baseColumn: never;
1070
+ identity: undefined;
1071
+ generated: undefined;
1072
+ }, {}, {}>;
1073
+ showInMenu: drizzle_orm_pg_core.PgColumn<{
1074
+ name: "show_in_menu";
1075
+ tableName: "flow_graphs";
1076
+ dataType: "boolean";
1077
+ columnType: "PgBoolean";
1078
+ data: boolean;
1079
+ driverParam: boolean;
1080
+ notNull: true;
1081
+ hasDefault: true;
1082
+ isPrimaryKey: false;
1083
+ isAutoincrement: false;
1084
+ hasRuntimeDefault: false;
1085
+ enumValues: undefined;
1086
+ baseColumn: never;
1087
+ identity: undefined;
1088
+ generated: undefined;
1089
+ }, {}, {}>;
1090
+ menuOptionLabel: drizzle_orm_pg_core.PgColumn<{
1091
+ name: "menu_option_label";
1092
+ tableName: "flow_graphs";
1093
+ dataType: "string";
1094
+ columnType: "PgVarchar";
1095
+ data: string;
1096
+ driverParam: string;
1097
+ notNull: false;
1098
+ hasDefault: false;
1099
+ isPrimaryKey: false;
1100
+ isAutoincrement: false;
1101
+ hasRuntimeDefault: false;
1102
+ enumValues: [string, ...string[]];
1103
+ baseColumn: never;
1104
+ identity: undefined;
1105
+ generated: undefined;
1106
+ }, {}, {
1107
+ length: 64;
1108
+ }>;
1109
+ createdAt: drizzle_orm_pg_core.PgColumn<{
1110
+ name: "created_at";
1111
+ tableName: "flow_graphs";
1112
+ dataType: "date";
1113
+ columnType: "PgTimestamp";
1114
+ data: Date;
1115
+ driverParam: string;
1116
+ notNull: true;
1117
+ hasDefault: true;
1118
+ isPrimaryKey: false;
1119
+ isAutoincrement: false;
1120
+ hasRuntimeDefault: false;
1121
+ enumValues: undefined;
1122
+ baseColumn: never;
1123
+ identity: undefined;
1124
+ generated: undefined;
1125
+ }, {}, {}>;
1126
+ updatedAt: drizzle_orm_pg_core.PgColumn<{
1127
+ name: "updated_at";
1128
+ tableName: "flow_graphs";
1129
+ dataType: "date";
1130
+ columnType: "PgTimestamp";
1131
+ data: Date;
1132
+ driverParam: string;
1133
+ notNull: true;
1134
+ hasDefault: true;
1135
+ isPrimaryKey: false;
1136
+ isAutoincrement: false;
1137
+ hasRuntimeDefault: false;
1138
+ enumValues: undefined;
1139
+ baseColumn: never;
1140
+ identity: undefined;
1141
+ generated: undefined;
1142
+ }, {}, {}>;
1143
+ };
1144
+ dialect: "pg";
1145
+ }>;
1146
+ /**
1147
+ * Biblioteca de mídia do bot: arquivos que um nó `action` de `send_media` dispara ao chegar nele.
1148
+ *
1149
+ * Tabela separada de `documents` de propósito, e não por simetria: `documents` é o acervo DE UMA
1150
+ * CONVERSA (tem `sessionId` obrigatório) e tem índice único em (companyId, uploadId), porque lá o
1151
+ * mesmo binário chegando duas vezes é reentrega de job. Aqui é o oposto — o mesmo arquivo é
1152
+ * enviado para todo cliente que passar pelo nó, então aquele único bloquearia o segundo envio.
1153
+ *
1154
+ * Sem FK para `flow_graphs.id`: o vínculo natural é (companyId, flowKey), que é justamente o
1155
+ * índice único de lá. `nodeId` não tem como ser FK — nós vivem dentro do jsonb `nodes` — então
1156
+ * apagar um nó no editor deixa a linha órfã; quem lista sempre parte de um nó existente, e a
1157
+ * limpeza é passo de aplicação ao salvar o grafo.
1158
+ */
1159
+ declare const flowMedia: drizzle_orm_pg_core.PgTableWithColumns<{
1160
+ name: "flow_media";
1161
+ schema: "meta_whatsapp";
1162
+ columns: {
1163
+ id: drizzle_orm_pg_core.PgColumn<{
1164
+ name: "id";
1165
+ tableName: "flow_media";
1166
+ dataType: "string";
1167
+ columnType: "PgUUID";
1168
+ data: string;
1169
+ driverParam: string;
1170
+ notNull: true;
1171
+ hasDefault: true;
1172
+ isPrimaryKey: true;
1173
+ isAutoincrement: false;
1174
+ hasRuntimeDefault: false;
1175
+ enumValues: undefined;
1176
+ baseColumn: never;
1177
+ identity: undefined;
1178
+ generated: undefined;
1179
+ }, {}, {}>;
1180
+ companyId: drizzle_orm_pg_core.PgColumn<{
1181
+ name: "company_id";
1182
+ tableName: "flow_media";
1183
+ dataType: "string";
1184
+ columnType: "PgUUID";
1185
+ data: string;
1186
+ driverParam: string;
1187
+ notNull: true;
1188
+ hasDefault: false;
1189
+ isPrimaryKey: false;
1190
+ isAutoincrement: false;
1191
+ hasRuntimeDefault: false;
1192
+ enumValues: undefined;
1193
+ baseColumn: never;
1194
+ identity: undefined;
1195
+ generated: undefined;
1196
+ }, {}, {}>;
1197
+ flowKey: drizzle_orm_pg_core.PgColumn<{
1198
+ name: "flow_key";
1199
+ tableName: "flow_media";
1200
+ dataType: "string";
1201
+ columnType: "PgVarchar";
1202
+ data: string;
1203
+ driverParam: string;
1204
+ notNull: true;
1205
+ hasDefault: false;
1206
+ isPrimaryKey: false;
1207
+ isAutoincrement: false;
1208
+ hasRuntimeDefault: false;
1209
+ enumValues: [string, ...string[]];
1210
+ baseColumn: never;
1211
+ identity: undefined;
1212
+ generated: undefined;
1213
+ }, {}, {
1214
+ length: 64;
1215
+ }>;
1216
+ nodeId: drizzle_orm_pg_core.PgColumn<{
1217
+ name: "node_id";
1218
+ tableName: "flow_media";
1219
+ dataType: "string";
1220
+ columnType: "PgVarchar";
1221
+ data: string;
1222
+ driverParam: string;
1223
+ notNull: true;
1224
+ hasDefault: false;
1225
+ isPrimaryKey: false;
1226
+ isAutoincrement: false;
1227
+ hasRuntimeDefault: false;
1228
+ enumValues: [string, ...string[]];
1229
+ baseColumn: never;
1230
+ identity: undefined;
1231
+ generated: undefined;
1232
+ }, {}, {
1233
+ length: 64;
1234
+ }>;
1235
+ uploadId: drizzle_orm_pg_core.PgColumn<{
1236
+ name: "upload_id";
1237
+ tableName: "flow_media";
1238
+ dataType: "string";
1239
+ columnType: "PgVarchar";
1240
+ data: string;
1241
+ driverParam: string;
1242
+ notNull: true;
1243
+ hasDefault: false;
1244
+ isPrimaryKey: false;
1245
+ isAutoincrement: false;
1246
+ hasRuntimeDefault: false;
1247
+ enumValues: [string, ...string[]];
1248
+ baseColumn: never;
1249
+ identity: undefined;
1250
+ generated: undefined;
1251
+ }, {}, {
1252
+ length: 256;
1253
+ }>;
1254
+ filename: drizzle_orm_pg_core.PgColumn<{
1255
+ name: "filename";
1256
+ tableName: "flow_media";
1257
+ dataType: "string";
1258
+ columnType: "PgVarchar";
1259
+ data: string;
1260
+ driverParam: string;
1261
+ notNull: true;
1262
+ hasDefault: false;
1263
+ isPrimaryKey: false;
1264
+ isAutoincrement: false;
1265
+ hasRuntimeDefault: false;
1266
+ enumValues: [string, ...string[]];
1267
+ baseColumn: never;
1268
+ identity: undefined;
1269
+ generated: undefined;
1270
+ }, {}, {
1271
+ length: 512;
1272
+ }>;
1273
+ mimeType: drizzle_orm_pg_core.PgColumn<{
1274
+ name: "mime_type";
1275
+ tableName: "flow_media";
1276
+ dataType: "string";
1277
+ columnType: "PgVarchar";
1278
+ data: string;
1279
+ driverParam: string;
1280
+ notNull: true;
1281
+ hasDefault: false;
1282
+ isPrimaryKey: false;
1283
+ isAutoincrement: false;
1284
+ hasRuntimeDefault: false;
1285
+ enumValues: [string, ...string[]];
1286
+ baseColumn: never;
1287
+ identity: undefined;
1288
+ generated: undefined;
1289
+ }, {}, {
1290
+ length: 128;
1291
+ }>;
1292
+ sizeBytes: drizzle_orm_pg_core.PgColumn<{
1293
+ name: "size_bytes";
1294
+ tableName: "flow_media";
1295
+ dataType: "number";
1296
+ columnType: "PgInteger";
1297
+ data: number;
1298
+ driverParam: string | number;
1299
+ notNull: true;
1300
+ hasDefault: false;
1301
+ isPrimaryKey: false;
1302
+ isAutoincrement: false;
1303
+ hasRuntimeDefault: false;
1304
+ enumValues: undefined;
1305
+ baseColumn: never;
1306
+ identity: undefined;
1307
+ generated: undefined;
1308
+ }, {}, {}>;
1309
+ caption: drizzle_orm_pg_core.PgColumn<{
1310
+ name: "caption";
1311
+ tableName: "flow_media";
1312
+ dataType: "string";
1313
+ columnType: "PgText";
1314
+ data: string;
1315
+ driverParam: string;
1316
+ notNull: false;
1317
+ hasDefault: false;
1318
+ isPrimaryKey: false;
1319
+ isAutoincrement: false;
1320
+ hasRuntimeDefault: false;
1321
+ enumValues: [string, ...string[]];
1322
+ baseColumn: never;
1323
+ identity: undefined;
1324
+ generated: undefined;
1325
+ }, {}, {}>;
1326
+ sortOrder: drizzle_orm_pg_core.PgColumn<{
1327
+ name: "sort_order";
1328
+ tableName: "flow_media";
1329
+ dataType: "number";
1330
+ columnType: "PgInteger";
1331
+ data: number;
1332
+ driverParam: string | number;
1333
+ notNull: true;
1334
+ hasDefault: true;
1335
+ isPrimaryKey: false;
1336
+ isAutoincrement: false;
1337
+ hasRuntimeDefault: false;
1338
+ enumValues: undefined;
1339
+ baseColumn: never;
1340
+ identity: undefined;
1341
+ generated: undefined;
1342
+ }, {}, {}>;
1343
+ active: drizzle_orm_pg_core.PgColumn<{
1344
+ name: "active";
1345
+ tableName: "flow_media";
1346
+ dataType: "boolean";
1347
+ columnType: "PgBoolean";
1348
+ data: boolean;
1349
+ driverParam: boolean;
1350
+ notNull: true;
1351
+ hasDefault: true;
1352
+ isPrimaryKey: false;
1353
+ isAutoincrement: false;
1354
+ hasRuntimeDefault: false;
1355
+ enumValues: undefined;
1356
+ baseColumn: never;
1357
+ identity: undefined;
1358
+ generated: undefined;
1359
+ }, {}, {}>;
1360
+ metaMediaIds: drizzle_orm_pg_core.PgColumn<{
1361
+ name: "meta_media_ids";
1362
+ tableName: "flow_media";
1363
+ dataType: "json";
1364
+ columnType: "PgJsonb";
1365
+ data: Record<string, string>;
1366
+ driverParam: unknown;
1367
+ notNull: true;
1368
+ hasDefault: true;
1369
+ isPrimaryKey: false;
1370
+ isAutoincrement: false;
1371
+ hasRuntimeDefault: false;
1372
+ enumValues: undefined;
1373
+ baseColumn: never;
1374
+ identity: undefined;
1375
+ generated: undefined;
1376
+ }, {}, {
1377
+ $type: Record<string, string>;
1378
+ }>;
1379
+ createdAt: drizzle_orm_pg_core.PgColumn<{
1380
+ name: "created_at";
1381
+ tableName: "flow_media";
1382
+ dataType: "date";
1383
+ columnType: "PgTimestamp";
1384
+ data: Date;
1385
+ driverParam: string;
1386
+ notNull: true;
1387
+ hasDefault: true;
1388
+ isPrimaryKey: false;
1389
+ isAutoincrement: false;
1390
+ hasRuntimeDefault: false;
1391
+ enumValues: undefined;
1392
+ baseColumn: never;
1393
+ identity: undefined;
1394
+ generated: undefined;
1395
+ }, {}, {}>;
1396
+ updatedAt: drizzle_orm_pg_core.PgColumn<{
1397
+ name: "updated_at";
1398
+ tableName: "flow_media";
1399
+ dataType: "date";
1400
+ columnType: "PgTimestamp";
1401
+ data: Date;
1402
+ driverParam: string;
1403
+ notNull: true;
1404
+ hasDefault: true;
1405
+ isPrimaryKey: false;
1406
+ isAutoincrement: false;
1407
+ hasRuntimeDefault: false;
1408
+ enumValues: undefined;
1409
+ baseColumn: never;
1410
+ identity: undefined;
1411
+ generated: undefined;
1412
+ }, {}, {}>;
1413
+ };
1414
+ dialect: "pg";
1415
+ }>;
1416
+ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
1417
+ name: "settings";
1418
+ schema: "meta_whatsapp";
1419
+ columns: {
1420
+ companyId: drizzle_orm_pg_core.PgColumn<{
1421
+ name: "company_id";
1422
+ tableName: "settings";
1423
+ dataType: "string";
1424
+ columnType: "PgUUID";
1425
+ data: string;
1426
+ driverParam: string;
1427
+ notNull: true;
1428
+ hasDefault: false;
1429
+ isPrimaryKey: true;
1430
+ isAutoincrement: false;
1431
+ hasRuntimeDefault: false;
1432
+ enumValues: undefined;
1433
+ baseColumn: never;
1434
+ identity: undefined;
1435
+ generated: undefined;
1436
+ }, {}, {}>;
1437
+ templateName: drizzle_orm_pg_core.PgColumn<{
1438
+ name: "template_name";
1439
+ tableName: "settings";
1440
+ dataType: "string";
1441
+ columnType: "PgVarchar";
1442
+ data: string;
1443
+ driverParam: string;
1444
+ notNull: false;
1445
+ hasDefault: false;
1446
+ isPrimaryKey: false;
1447
+ isAutoincrement: false;
1448
+ hasRuntimeDefault: false;
1449
+ enumValues: [string, ...string[]];
1450
+ baseColumn: never;
1451
+ identity: undefined;
1452
+ generated: undefined;
1453
+ }, {}, {
1454
+ length: 128;
1455
+ }>;
1456
+ templateLanguage: drizzle_orm_pg_core.PgColumn<{
1457
+ name: "template_language";
1458
+ tableName: "settings";
1459
+ dataType: "string";
1460
+ columnType: "PgVarchar";
1461
+ data: string;
1462
+ driverParam: string;
1463
+ notNull: true;
1464
+ hasDefault: true;
1465
+ isPrimaryKey: false;
1466
+ isAutoincrement: false;
1467
+ hasRuntimeDefault: false;
1468
+ enumValues: [string, ...string[]];
1469
+ baseColumn: never;
1470
+ identity: undefined;
1471
+ generated: undefined;
1472
+ }, {}, {
1473
+ length: 16;
1474
+ }>;
1475
+ templateVariables: drizzle_orm_pg_core.PgColumn<{
1476
+ name: "template_variables";
1477
+ tableName: "settings";
1478
+ dataType: "json";
1479
+ columnType: "PgJsonb";
1480
+ data: string[];
1481
+ driverParam: unknown;
1482
+ notNull: true;
1483
+ hasDefault: true;
1484
+ isPrimaryKey: false;
1485
+ isAutoincrement: false;
1486
+ hasRuntimeDefault: false;
1487
+ enumValues: undefined;
1488
+ baseColumn: never;
1489
+ identity: undefined;
1490
+ generated: undefined;
1491
+ }, {}, {
1492
+ $type: string[];
1493
+ }>;
1494
+ welcomeMessage: drizzle_orm_pg_core.PgColumn<{
1495
+ name: "welcome_message";
1496
+ tableName: "settings";
1497
+ dataType: "string";
869
1498
  columnType: "PgText";
870
1499
  data: string;
871
1500
  driverParam: string;
@@ -896,6 +1525,43 @@ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
896
1525
  identity: undefined;
897
1526
  generated: undefined;
898
1527
  }, {}, {}>;
1528
+ transcriptionEnabled: drizzle_orm_pg_core.PgColumn<{
1529
+ name: "transcription_enabled";
1530
+ tableName: "settings";
1531
+ dataType: "boolean";
1532
+ columnType: "PgBoolean";
1533
+ data: boolean;
1534
+ driverParam: boolean;
1535
+ notNull: false;
1536
+ hasDefault: false;
1537
+ isPrimaryKey: false;
1538
+ isAutoincrement: false;
1539
+ hasRuntimeDefault: false;
1540
+ enumValues: undefined;
1541
+ baseColumn: never;
1542
+ identity: undefined;
1543
+ generated: undefined;
1544
+ }, {}, {}>;
1545
+ transcriptionMode: drizzle_orm_pg_core.PgColumn<{
1546
+ name: "transcription_mode";
1547
+ tableName: "settings";
1548
+ dataType: "string";
1549
+ columnType: "PgVarchar";
1550
+ data: TranscriptionMode;
1551
+ driverParam: string;
1552
+ notNull: false;
1553
+ hasDefault: false;
1554
+ isPrimaryKey: false;
1555
+ isAutoincrement: false;
1556
+ hasRuntimeDefault: false;
1557
+ enumValues: [string, ...string[]];
1558
+ baseColumn: never;
1559
+ identity: undefined;
1560
+ generated: undefined;
1561
+ }, {}, {
1562
+ length: 16;
1563
+ $type: TranscriptionMode;
1564
+ }>;
899
1565
  createdAt: drizzle_orm_pg_core.PgColumn<{
900
1566
  name: "created_at";
901
1567
  tableName: "settings";
@@ -941,6 +1607,10 @@ type MessageRow = typeof messages.$inferSelect;
941
1607
  type NewMessageRow = typeof messages.$inferInsert;
942
1608
  type FlowGraphRow = typeof flowGraphs.$inferSelect;
943
1609
  type NewFlowGraphRow = typeof flowGraphs.$inferInsert;
1610
+ type DocumentRow = typeof documents.$inferSelect;
1611
+ type NewDocumentRow = typeof documents.$inferInsert;
1612
+ type FlowMediaRow = typeof flowMedia.$inferSelect;
1613
+ type NewFlowMediaRow = typeof flowMedia.$inferInsert;
944
1614
 
945
1615
  interface ListConversationsFilters {
946
1616
  page?: number;
@@ -953,13 +1623,23 @@ declare class SessionRepository {
953
1623
  constructor(db: MetaWhatsAppDatabase);
954
1624
  getContext(companyId: string, whatsappNumber: string): Promise<SessionRow | undefined>;
955
1625
  getOrCreate(companyId: string, whatsappNumber: string, startState: SessionState): Promise<SessionRow>;
956
- setState(companyId: string, whatsappNumber: string, state: SessionState, context?: Record<string, unknown>): Promise<void>;
1626
+ setState<TSessionContext extends Record<string, unknown> = Record<string, unknown>>(companyId: string, whatsappNumber: string, state: SessionState, context?: TSessionContext): Promise<void>;
1627
+ patchContext<TSessionContext extends Record<string, unknown> = Record<string, unknown>>(companyId: string, whatsappNumber: string, patch: Partial<TSessionContext>): Promise<void>;
1628
+ readContext<TSessionContext extends Record<string, unknown> = Record<string, unknown>>(companyId: string, whatsappNumber: string): Promise<TSessionContext | undefined>;
957
1629
  setFlowPosition(companyId: string, whatsappNumber: string, flowKey: string | null, currentNodeId: string | null): Promise<void>;
958
1630
  touchInbound(companyId: string, whatsappNumber: string): Promise<void>;
959
1631
  hoursSinceLastInbound(companyId: string, whatsappNumber: string): Promise<number | undefined>;
960
1632
  setMode(companyId: string, whatsappNumber: string, mode: SessionMode, assignedUserId?: string | null): Promise<void>;
961
1633
  takeover(companyId: string, whatsappNumber: string, agentUserId: string): Promise<void>;
962
1634
  release(companyId: string, whatsappNumber: string): Promise<void>;
1635
+ /**
1636
+ * Apaga a sessão; a cascata das FKs leva mensagens e documentos.
1637
+ *
1638
+ * Não apaga o binário no storage — isso é passo de aplicação, e é por isso que este método é
1639
+ * chamado por `DeleteConversationUseCase` e não diretamente pelo host. Chamar daqui sem apagar os
1640
+ * objetos antes deixa mídia órfã sendo cobrada para sempre.
1641
+ */
1642
+ deleteByNumber(companyId: string, whatsappNumber: string): Promise<void>;
963
1643
  requestHuman(companyId: string, whatsappNumber: string): Promise<void>;
964
1644
  markRead(companyId: string, whatsappNumber: string): Promise<void>;
965
1645
  markAllRead(companyId: string, userId: string): Promise<number>;
@@ -998,41 +1678,15 @@ declare class SessionRepository {
998
1678
  readAt: Date | null;
999
1679
  moderationFlagged: boolean | null;
1000
1680
  moderationTerms: string[] | null;
1681
+ transcriptionStatus: TranscriptionStatus | null;
1682
+ transcriptionText: string | null;
1683
+ transcriptionLanguage: string | null;
1684
+ transcriptionEngine: string | null;
1001
1685
  createdAt: Date;
1002
1686
  }[];
1003
1687
  } | null>;
1004
1688
  }
1005
1689
 
1006
- declare class InvalidFlowGraphError extends Error {
1007
- readonly validationMessage: string;
1008
- constructor(key: string, validationMessage: string);
1009
- }
1010
- declare class OptimisticLockError extends Error {
1011
- constructor(key: string);
1012
- }
1013
- declare class FlowGraphRepository {
1014
- private readonly db;
1015
- constructor(db: MetaWhatsAppDatabase);
1016
- get(companyId: string, key: string): Promise<FlowGraphData | undefined>;
1017
- list(companyId: string): Promise<FlowGraphSummary[]>;
1018
- private assertValidNodes;
1019
- create(companyId: string, graph: Omit<FlowGraphData, 'version'> & {
1020
- showInMenu?: boolean;
1021
- menuOptionLabel?: string;
1022
- }): Promise<FlowGraphData>;
1023
- save(companyId: string, graph: FlowGraphData, expectedVersion: number): Promise<FlowGraphData>;
1024
- delete(companyId: string, key: string): Promise<void>;
1025
- getLiveFlowPositions(companyId: string): Promise<LiveFlowPosition[]>;
1026
- }
1027
-
1028
- declare class SettingsRepository {
1029
- private readonly db;
1030
- constructor(db: MetaWhatsAppDatabase);
1031
- get(companyId: string): Promise<WhatsAppSettings>;
1032
- save(companyId: string, update: Partial<WhatsAppSettings>): Promise<WhatsAppSettings>;
1033
- resolveTemplateVariables(companyId: string, context: Record<string, unknown>): Promise<string[]>;
1034
- }
1035
-
1036
1690
  interface InsertMessageParams {
1037
1691
  companyId: string;
1038
1692
  sessionId: string;
@@ -1055,14 +1709,106 @@ interface ListMessagesParams$1 {
1055
1709
  limit?: number;
1056
1710
  before?: string;
1057
1711
  }
1712
+ interface SaveTranscriptionByWaMessageIdParams extends Omit<SaveTranscriptionParams, 'messageId'> {
1713
+ /**
1714
+ * Id da mensagem na Meta. É o único que quem processa o webhook conhece — o id do módulo só
1715
+ * existe depois da gravação, e obrigar o host a descobri-lo faria cada um escrever a própria
1716
+ * consulta por `wa_message_id`.
1717
+ */
1718
+ waMessageId: string;
1719
+ }
1720
+ interface SaveTranscriptionParams {
1721
+ companyId: string;
1722
+ messageId: string;
1723
+ status: TranscriptionStatus;
1724
+ /** Ausente em `pending`/`failed`/`unsupported`; vazio em `done` é silêncio já processado. */
1725
+ text?: string | null;
1726
+ language?: string | null;
1727
+ engine?: string | null;
1728
+ }
1058
1729
  declare class MessageRepository {
1059
1730
  private readonly db;
1060
1731
  constructor(db: MetaWhatsAppDatabase);
1061
1732
  insertMessage(params: InsertMessageParams): Promise<MessageRow | undefined>;
1062
1733
  updateMessageStatus(companyId: string, waMessageId: string, status: MessageStatus): Promise<MessageRow | undefined>;
1734
+ /**
1735
+ * Grava a transcrição endereçando pelo id da Meta, para quem só tem esse.
1736
+ *
1737
+ * Serve ao caso em que a transcrição acontece no próprio webhook — o grafo precisa do texto para
1738
+ * responder ao cliente, e jogar fora o que ele já pagou para transcrever significaria transcrever
1739
+ * o mesmo áudio uma segunda vez só para o painel ver.
1740
+ *
1741
+ * Devolve `undefined` quando não achou a mensagem: entrega duplicada e mensagem apagada são
1742
+ * corridas normais, não erro.
1743
+ */
1744
+ saveTranscriptionByWaMessageId(params: SaveTranscriptionByWaMessageIdParams): Promise<MessageRow | undefined>;
1745
+ findById(companyId: string, messageId: string): Promise<MessageRow | undefined>;
1746
+ /**
1747
+ * Grava o resultado da transcrição. Devolve `undefined` quando a mensagem não existe (apagada
1748
+ * entre o enfileiramento e a execução do job) — não é erro, é corrida normal.
1749
+ *
1750
+ * `text`/`language`/`engine` só são tocados quando informados: uma retentativa que volta a falhar
1751
+ * atualiza o status sem apagar a transcrição parcial de uma tentativa anterior que tenha vindo de
1752
+ * outro engine da cadeia.
1753
+ */
1754
+ saveTranscription(params: SaveTranscriptionParams): Promise<MessageRow | undefined>;
1063
1755
  listByConversation(params: ListMessagesParams$1): Promise<MessageRow[]>;
1064
1756
  }
1065
1757
 
1758
+ declare const DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS = 300;
1759
+ /**
1760
+ * Cache de leitura dos grafos de fluxo, com invalidação na publicação.
1761
+ *
1762
+ * TTL **e** invalidação explícita, e não um dos dois: só TTL deixaria o cliente andando no grafo
1763
+ * antigo até a chave expirar, logo depois de alguém corrigir o fluxo no editor; só invalidação
1764
+ * deixaria cache envenenado para sempre se um `delete` se perdesse (Redis reiniciando, deploy no
1765
+ * meio da escrita).
1766
+ *
1767
+ * Toda operação é tolerante a falha por decisão de projeto: cache é aceleração, não dependência.
1768
+ * Redis fora do ar tem que degradar para leitura no banco — que é exatamente o comportamento de
1769
+ * quem não configura cache nenhum — em vez de derrubar a conversa do cliente.
1770
+ */
1771
+ declare class FlowGraphCache {
1772
+ private readonly provider;
1773
+ private readonly ttlSeconds;
1774
+ constructor(provider: CacheInterface, ttlSeconds?: number);
1775
+ private keyFor;
1776
+ read(companyId: string, flowKey: string): Promise<FlowGraphData | undefined>;
1777
+ write(companyId: string, graph: FlowGraphData): Promise<void>;
1778
+ invalidate(companyId: string, flowKey: string): Promise<void>;
1779
+ }
1780
+
1781
+ declare class InvalidFlowGraphError extends Error {
1782
+ readonly validationMessage: string;
1783
+ constructor(key: string, validationMessage: string);
1784
+ }
1785
+ declare class OptimisticLockError extends Error {
1786
+ constructor(key: string);
1787
+ }
1788
+ declare class FlowGraphRepository {
1789
+ private readonly db;
1790
+ private readonly cache?;
1791
+ constructor(db: MetaWhatsAppDatabase, cache?: FlowGraphCache | undefined);
1792
+ get(companyId: string, key: string): Promise<FlowGraphData | undefined>;
1793
+ list(companyId: string): Promise<FlowGraphSummary[]>;
1794
+ private assertValidNodes;
1795
+ create(companyId: string, graph: Omit<FlowGraphData, 'version'> & {
1796
+ showInMenu?: boolean;
1797
+ menuOptionLabel?: string;
1798
+ }): Promise<FlowGraphData>;
1799
+ save(companyId: string, graph: FlowGraphData, expectedVersion: number): Promise<FlowGraphData>;
1800
+ delete(companyId: string, key: string): Promise<void>;
1801
+ getLiveFlowPositions(companyId: string): Promise<LiveFlowPosition[]>;
1802
+ }
1803
+
1804
+ declare class SettingsRepository {
1805
+ private readonly db;
1806
+ constructor(db: MetaWhatsAppDatabase);
1807
+ get(companyId: string): Promise<WhatsAppSettings>;
1808
+ save(companyId: string, update: Partial<WhatsAppSettings>): Promise<WhatsAppSettings>;
1809
+ resolveTemplateVariables(companyId: string, context: Record<string, unknown>): Promise<string[]>;
1810
+ }
1811
+
1066
1812
  type LogMessageParams = Omit<InsertMessageParams, 'sessionId'> & {
1067
1813
  startState: SessionState;
1068
1814
  };
@@ -1089,6 +1835,100 @@ declare class LogMessageUseCase {
1089
1835
  private moderationOf;
1090
1836
  }
1091
1837
 
1838
+ interface LinkDocumentParams {
1839
+ companyId: string;
1840
+ sessionId: string;
1841
+ messageId?: string | null;
1842
+ uploadId: string;
1843
+ filename: string;
1844
+ mimeType: string;
1845
+ sizeBytes: number;
1846
+ sha256?: string | null;
1847
+ source: string;
1848
+ }
1849
+ interface ListDocumentsParams {
1850
+ companyId: string;
1851
+ sessionId: string;
1852
+ search?: string;
1853
+ /**
1854
+ * Origens aceitas, como lista explícita (`['agent', 'bot']`) e não como apelido de UI.
1855
+ *
1856
+ * Agrupamento tipo "Equipe" é vocabulário de tela e muda por produto; traduzir aqui obrigaria o
1857
+ * módulo a conhecer o rótulo de cada host. Quem recebe o apelido na borda é a rota.
1858
+ */
1859
+ sources?: readonly string[];
1860
+ sortDirection?: 'asc' | 'desc';
1861
+ page?: number;
1862
+ limit?: number;
1863
+ }
1864
+ interface ListCompanyDocumentsParams$1 {
1865
+ companyId: string;
1866
+ search?: string;
1867
+ sources?: readonly string[];
1868
+ sortDirection?: 'asc' | 'desc';
1869
+ page?: number;
1870
+ limit?: number;
1871
+ }
1872
+ interface CompanyDocumentRow {
1873
+ id: string;
1874
+ uploadId: string;
1875
+ filename: string;
1876
+ mimeType: string;
1877
+ sizeBytes: number;
1878
+ source: string;
1879
+ linkedAt: Date;
1880
+ whatsappNumber: string;
1881
+ }
1882
+ interface ListCompanyDocumentsResult {
1883
+ rows: CompanyDocumentRow[];
1884
+ total: number;
1885
+ }
1886
+ interface ListDocumentsResult {
1887
+ rows: DocumentRow[];
1888
+ /** Total no servidor, ANTES do corte de página — é o que permite calcular a última página. */
1889
+ total: number;
1890
+ }
1891
+ declare class DocumentRepository {
1892
+ private readonly db;
1893
+ constructor(db: MetaWhatsAppDatabase);
1894
+ /**
1895
+ * Idempotente por (companyId, uploadId), garantido pelo índice único e não por SELECT prévio: o
1896
+ * job de ingestão é reentregue por retry e duas tentativas concorrentes passariam as duas por uma
1897
+ * checagem, duplicando a linha no painel.
1898
+ *
1899
+ * Devolve `undefined` quando o documento já estava linkado.
1900
+ */
1901
+ link(params: LinkDocumentParams): Promise<DocumentRow | undefined>;
1902
+ listByConversation(params: ListDocumentsParams): Promise<ListDocumentsResult>;
1903
+ /**
1904
+ * A biblioteca da EMPRESA inteira, não de uma conversa.
1905
+ *
1906
+ * A busca casa nome do arquivo OU telefone da conversa — ver `companyDocumentSearch`.
1907
+ *
1908
+ * Faz join com `sessions` para carregar de qual conversa cada arquivo veio — numa lista global,
1909
+ * arquivo sem essa referência é inútil: o atendente vê "comprovante.pdf" e não sabe de quem.
1910
+ *
1911
+ * Ordena por `linkedAt` apoiada no índice `idx_documents_company_linked`, que já existia para a
1912
+ * varredura de retenção.
1913
+ */
1914
+ listByCompany(params: ListCompanyDocumentsParams$1): Promise<ListCompanyDocumentsResult>;
1915
+ /**
1916
+ * Um documento pela key do objeto. Serve para recuperar o nome original na hora de assinar o
1917
+ * download: a key é caminho no bucket e salvaria o arquivo com o id da Meta.
1918
+ */
1919
+ findByUploadId(companyId: string, uploadId: string): Promise<DocumentRow | undefined>;
1920
+ /**
1921
+ * Os objetos a apagar no storage antes de a linha sumir.
1922
+ *
1923
+ * Existe porque a cascata da FK apaga a linha e deixa o binário órfão: quem for apagar a conversa
1924
+ * precisa desta lista primeiro, senão paga armazenamento para sempre por arquivo inalcançável.
1925
+ */
1926
+ listUploadIdsBySession(companyId: string, sessionId: string): Promise<string[]>;
1927
+ /** Varredura de retenção por idade — o par é o mesmo cuidado com o objeto no storage. */
1928
+ listExpired(companyId: string, olderThan: Date, limit?: number): Promise<DocumentRow[]>;
1929
+ deleteById(companyId: string, id: string): Promise<void>;
1930
+ }
1931
+
1092
1932
  type SendTextParams = {
1093
1933
  companyId: string;
1094
1934
  whatsappNumber: string;
@@ -1123,7 +1963,8 @@ declare class SendMessageUseCase {
1123
1963
  private readonly sessionRepository;
1124
1964
  private readonly logMessage;
1125
1965
  private readonly objectStorage?;
1126
- constructor(channel: ChannelAdapterInterface, sessionRepository: SessionRepository, logMessage: LogMessageUseCase, objectStorage?: ObjectStorageInterface | undefined);
1966
+ private readonly documentRepository?;
1967
+ constructor(channel: ChannelAdapterInterface, sessionRepository: SessionRepository, logMessage: LogMessageUseCase, objectStorage?: ObjectStorageInterface | undefined, documentRepository?: DocumentRepository | undefined);
1127
1968
  private assertWithinWindow;
1128
1969
  sendText(params: SendTextParams): Promise<MessageRow | undefined>;
1129
1970
  sendMedia(params: SendMediaParams): Promise<MessageRow | undefined>;
@@ -1176,6 +2017,143 @@ declare class ListMessagesUseCase {
1176
2017
  execute(params: ListMessagesParams): Promise<MessageRow[]>;
1177
2018
  }
1178
2019
 
2020
+ type ListConversationDocumentsParams = {
2021
+ companyId: string;
2022
+ whatsappNumber: string;
2023
+ search?: string;
2024
+ /** Origens explícitas. O apelido de UI ("Equipe") é traduzido na borda HTTP, não aqui. */
2025
+ sources?: readonly string[];
2026
+ sortDirection?: 'asc' | 'desc';
2027
+ page?: number;
2028
+ limit?: number;
2029
+ };
2030
+ /**
2031
+ * O que o `ConversationDocumentsPanel` do conversations-ui consome. O shape é o do pacote de UI
2032
+ * (`ConversationDocument`), com `linkedAt` já em ISO para não obrigar cada host a serializar.
2033
+ */
2034
+ type ConversationDocumentView = {
2035
+ /**
2036
+ * O `uploadId` (key no storage), e NÃO o id da linha.
2037
+ *
2038
+ * É este valor que o consumidor devolve para pedir a URL assinada ou montar o zip, então expor o
2039
+ * UUID da tabela aqui fazia o download apontar para um objeto inexistente — falha que só aparece
2040
+ * no clique, porque a assinatura é gerada sem consultar o bucket.
2041
+ */
2042
+ id: string;
2043
+ filename: string;
2044
+ mimeType: string;
2045
+ sizeBytes: number;
2046
+ source: string;
2047
+ linkedAt: string;
2048
+ };
2049
+ /** Espelha o `ConversationDocumentPage` do conversations-ui: lista da página + total no servidor. */
2050
+ type ConversationDocumentsPage = {
2051
+ documents: ConversationDocumentView[];
2052
+ total: number;
2053
+ };
2054
+ declare class ListConversationDocumentsUseCase {
2055
+ private readonly sessionRepository;
2056
+ private readonly documentRepository;
2057
+ constructor(sessionRepository: SessionRepository, documentRepository: DocumentRepository);
2058
+ execute(params: ListConversationDocumentsParams): Promise<ConversationDocumentsPage>;
2059
+ }
2060
+
2061
+ type ListCompanyDocumentsParams = {
2062
+ companyId: string;
2063
+ search?: string;
2064
+ /** Origens explícitas. O apelido de UI ("Equipe") é traduzido na borda HTTP, não aqui. */
2065
+ sources?: readonly string[];
2066
+ sortDirection?: 'asc' | 'desc';
2067
+ page?: number;
2068
+ limit?: number;
2069
+ };
2070
+ /**
2071
+ * Um arquivo na biblioteca da empresa. Igual ao da conversa, mais `conversationId` — sem saber de
2072
+ * quem veio, uma lista global de anexos não responde nenhuma pergunta útil.
2073
+ */
2074
+ type CompanyDocumentView = {
2075
+ /** O `uploadId` (key no storage), pelo mesmo motivo do `ConversationDocumentView`. */
2076
+ id: string;
2077
+ conversationId: string;
2078
+ filename: string;
2079
+ mimeType: string;
2080
+ sizeBytes: number;
2081
+ source: string;
2082
+ linkedAt: string;
2083
+ };
2084
+ type CompanyDocumentsPage = {
2085
+ documents: CompanyDocumentView[];
2086
+ total: number;
2087
+ };
2088
+ /**
2089
+ * Biblioteca de arquivos de todas as conversas da empresa.
2090
+ *
2091
+ * Existe separada de `ListConversationDocumentsUseCase` porque a pergunta é outra: aquela parte de
2092
+ * uma conversa conhecida, esta varre a empresa e por isso precisa dizer de qual conversa cada
2093
+ * arquivo veio. Reaproveitar a primeira exigiria um `sessionId` opcional que muda o significado do
2094
+ * retorno — dois nomes claros custam menos que um parâmetro que dobra o comportamento.
2095
+ */
2096
+ declare class ListCompanyDocumentsUseCase {
2097
+ private readonly documentRepository;
2098
+ constructor(documentRepository: DocumentRepository);
2099
+ execute(params: ListCompanyDocumentsParams): Promise<CompanyDocumentsPage>;
2100
+ }
2101
+
2102
+ type DeleteConversationParams = {
2103
+ companyId: string;
2104
+ whatsappNumber: string;
2105
+ };
2106
+ type DeleteConversationResult = {
2107
+ /** Objetos efetivamente apagados no storage. */
2108
+ deletedObjects: number;
2109
+ /** Objetos que o storage recusou. A conversa NÃO é apagada quando isto é maior que zero. */
2110
+ failedObjects: readonly string[];
2111
+ };
2112
+ /**
2113
+ * Apaga a conversa e a mídia dela.
2114
+ *
2115
+ * A ordem é o ponto: **storage primeiro, banco depois**. A FK de `documents.session_id` é
2116
+ * `on delete cascade`, então apagar a sessão primeiro derrubaria as linhas e levaria embora a única
2117
+ * lista de `uploadId` existente — os binários ficariam órfãos, cobrados para sempre e inalcançáveis.
2118
+ *
2119
+ * Se algum objeto falhar, a conversa é preservada e o chamador recebe a lista. Apagar as linhas
2120
+ * "mesmo assim" transformaria uma falha visível e reexecutável em lixo silencioso no storage.
2121
+ */
2122
+ declare class DeleteConversationUseCase {
2123
+ private readonly sessionRepository;
2124
+ private readonly documentRepository;
2125
+ private readonly objectStorage?;
2126
+ constructor(sessionRepository: SessionRepository, documentRepository: DocumentRepository, objectStorage?: ObjectStorageInterface | undefined);
2127
+ execute(params: DeleteConversationParams): Promise<DeleteConversationResult>;
2128
+ }
2129
+
2130
+ type PurgeExpiredDocumentsParams = {
2131
+ companyId: string;
2132
+ /** Dias de retenção. O produto configura; o módulo não escolhe política de dado pessoal. */
2133
+ retentionDays: number;
2134
+ /** Teto por execução, para o job não segurar conexão nem storage por tempo indefinido. */
2135
+ batchSize?: number;
2136
+ /** Instante de referência — injetado para o teste não depender do relógio. */
2137
+ now?: Date;
2138
+ };
2139
+ type PurgeExpiredDocumentsResult = {
2140
+ purged: number;
2141
+ failed: readonly string[];
2142
+ };
2143
+ /**
2144
+ * Apaga documento vencido: objeto no storage primeiro, linha depois — a mesma ordem do
2145
+ * `DeleteConversationUseCase`, e pelo mesmo motivo.
2146
+ *
2147
+ * A linha só cai quando o objeto caiu. Contar como purgado sem ter apagado o binário deixaria lixo
2148
+ * pago no storage sem nenhum registro que o alcance depois: a linha era o único ponteiro.
2149
+ */
2150
+ declare class PurgeExpiredDocumentsUseCase {
2151
+ private readonly documentRepository;
2152
+ private readonly objectStorage?;
2153
+ constructor(documentRepository: DocumentRepository, objectStorage?: ObjectStorageInterface | undefined);
2154
+ execute(params: PurgeExpiredDocumentsParams): Promise<PurgeExpiredDocumentsResult>;
2155
+ }
2156
+
1179
2157
  type ExportConversationParams = {
1180
2158
  companyId: string;
1181
2159
  whatsappNumber: string;
@@ -1297,10 +2275,77 @@ declare class FlowInterpreter {
1297
2275
  }): Promise<FlowRunResult>;
1298
2276
  }
1299
2277
 
2278
+ type FlowMediaLocation = {
2279
+ companyId: string;
2280
+ flowKey: string;
2281
+ nodeId: string;
2282
+ };
2283
+ type AttachFlowMediaParams = FlowMediaLocation & {
2284
+ uploadId: string;
2285
+ filename: string;
2286
+ mimeType: string;
2287
+ sizeBytes: number;
2288
+ caption?: string;
2289
+ sortOrder?: number;
2290
+ };
2291
+ type UpdateFlowMediaParams = {
2292
+ companyId: string;
2293
+ id: string;
2294
+ caption?: string | null;
2295
+ sortOrder?: number;
2296
+ active?: boolean;
2297
+ };
2298
+ declare class FlowMediaRepository {
2299
+ private readonly db;
2300
+ constructor(db: MetaWhatsAppDatabase);
2301
+ private locationFilter;
2302
+ listActive(location: FlowMediaLocation): Promise<FlowMediaRow[]>;
2303
+ listAll(location: FlowMediaLocation): Promise<FlowMediaRow[]>;
2304
+ /**
2305
+ * Anexa um arquivo já existente no storage ao nó.
2306
+ *
2307
+ * `onConflictDoUpdate` em vez de deixar estourar: reanexar o mesmo arquivo é clique repetido no
2308
+ * editor, e o esperado ali é atualizar a legenda/ordem — não um erro de índice único na cara de
2309
+ * quem está montando o fluxo.
2310
+ */
2311
+ attach(params: AttachFlowMediaParams): Promise<FlowMediaRow>;
2312
+ update(params: UpdateFlowMediaParams): Promise<FlowMediaRow | undefined>;
2313
+ /**
2314
+ * Desanexa do nó. Não toca no storage de propósito: o mesmo `uploadId` pode estar anexado a
2315
+ * outro nó ou a outro fluxo, e apagar o binário aqui quebraria os demais. Quem apaga objeto é o
2316
+ * host, que é dono da biblioteca de arquivos.
2317
+ */
2318
+ detach(params: {
2319
+ companyId: string;
2320
+ id: string;
2321
+ }): Promise<void>;
2322
+ detachRemovedNodes(params: {
2323
+ companyId: string;
2324
+ flowKey: string;
2325
+ existingNodeIds: string[];
2326
+ }): Promise<void>;
2327
+ }
2328
+
2329
+ /**
2330
+ * Copyright (c) 2026 Ada Technology. MIT License.
2331
+ *
2332
+ * O algoritmo mora em `meta-graph-core`, compartilhado com os outros objetos que a Meta assina.
2333
+ * Aqui fica só a tradução para o vocabulário deste domínio: o erro lançado e o namespace do nonce.
2334
+ */
2335
+
1300
2336
  interface NonceStoreInterface {
1301
2337
  setIfAbsent(key: string, ttlSeconds: number): Promise<boolean>;
2338
+ /**
2339
+ * Estende a chave já reivindicada para a janela cheia. Um SET simples, sem NX.
2340
+ *
2341
+ * Opcional só por compatibilidade com hosts que ainda não o implementam: sem ele o claim curto
2342
+ * expira, e a Meta pode reentregar uma entrega já processada — trabalho repetido, que a dedupe
2343
+ * por `waMessageId` ainda segura antes de virar efeito visível ao cliente. Implementar é o
2344
+ * caminho correto.
2345
+ */
2346
+ confirm?(key: string, ttlSeconds: number): Promise<void>;
1302
2347
  }
1303
- declare const WEBHOOK_NONCE_TTL_SECONDS = 300;
2348
+
1304
2349
  declare function verifyWebhookChallenge(params: {
1305
2350
  mode: string | null;
1306
2351
  token: string | null;
@@ -1312,11 +2357,101 @@ declare function verifyWebhookSignature(params: {
1312
2357
  signatureHeader: string | null | undefined;
1313
2358
  appSecret: string;
1314
2359
  }): void;
2360
+ /**
2361
+ * Reivindica a entrega por pouco tempo. O par obrigatório é `confirmWebhookDelivery` ao fim do
2362
+ * processamento — ver `WEBHOOK_CLAIM_TTL_SECONDS` para o porquê dos dois tempos.
2363
+ */
1315
2364
  declare function claimWebhookDelivery(params: {
1316
2365
  nonceStore: NonceStoreInterface;
1317
2366
  signatureHeader: string;
1318
2367
  ttlSeconds?: number;
1319
2368
  }): Promise<boolean>;
2369
+ /** Só depois da entrega processada por inteiro: é isto que fecha a janela anti-replay. */
2370
+ declare function confirmWebhookDelivery(params: {
2371
+ nonceStore: NonceStoreInterface;
2372
+ signatureHeader: string;
2373
+ ttlSeconds?: number;
2374
+ }): Promise<void>;
2375
+
2376
+ /**
2377
+ * Copyright (c) 2026 Ada Technology. MIT License.
2378
+ *
2379
+ * Separa a entrega do webhook em duas metades: o que precisa acontecer antes de responder à Meta
2380
+ * (persistir a mensagem, que é escrita local e rápida) e o que pode esperar alguns milissegundos
2381
+ * (os hooks do host, que costumam falar com rede — motor de fluxo, IA, integrações).
2382
+ *
2383
+ * A segunda metade rodava dentro da requisição do webhook. Um deploy no meio de uma conversa
2384
+ * matava a chamada em voo, e o cliente ficava sem resposta sem que nada registrasse a perda. Com
2385
+ * uma fila no host, derrubar o processo vira atraso: o job espera e é retomado.
2386
+ */
2387
+
2388
+ type InboundMediaDescriptor = {
2389
+ sourceMediaId: string;
2390
+ mimeType: string;
2391
+ filename?: string;
2392
+ };
2393
+ type InboundMessageEffectsJob = {
2394
+ kind: 'message';
2395
+ companyId: string;
2396
+ message: WhatsAppMessage;
2397
+ savedMessageId: string;
2398
+ media?: InboundMediaDescriptor;
2399
+ receivedAt: number;
2400
+ };
2401
+ type InboundStatusEffectsJob = {
2402
+ kind: 'status';
2403
+ companyId: string;
2404
+ status: WhatsAppStatus;
2405
+ whatsappNumber: string;
2406
+ receivedAt: number;
2407
+ };
2408
+ type InboundDispatchJob = InboundMessageEffectsJob | InboundStatusEffectsJob;
2409
+ /**
2410
+ * Porta de fila. O módulo não escolhe a tecnologia — BullMQ, SQS, o que o host já tiver — mas
2411
+ * exige dela duas garantias, e sem as duas o padrão não entrega o que promete:
2412
+ *
2413
+ * 1. **Durabilidade**: o job sobrevive à morte do processo que o enfileirou. Uma fila em memória
2414
+ * reintroduz exatamente a perda que este desenho existe para evitar.
2415
+ * 2. **Retentativa com backoff**: o destino dos hooks (n8n, IA, API de terceiro) também cai em
2416
+ * deploy. Sem retry, o job falha uma vez e a conversa morre do mesmo jeito.
2417
+ *
2418
+ * `jobId` é estável e derivado da mensagem: reentrega da Meta e re-enfileiramento produzem o mesmo
2419
+ * id, e a fila descarta o segundo em vez de rodar o efeito duas vezes.
2420
+ */
2421
+ interface InboundDispatchQueueInterface {
2422
+ enqueue(job: InboundDispatchJob, options: {
2423
+ jobId: string;
2424
+ }): Promise<void>;
2425
+ }
2426
+ declare function buildInboundJobId(job: InboundDispatchJob): string;
2427
+ declare function toSessionContract(row: SessionRow): ConversationSession;
2428
+ /**
2429
+ * Os efeitos de host de uma entrega já persistida. Vive fora do `ReceiveWebhookUseCase` porque
2430
+ * roda nos dois lados: inline, quando o host não configurou fila, e dentro do worker, quando
2431
+ * configurou. Um só corpo de regra para os dois caminhos — duas cópias divergiriam.
2432
+ */
2433
+ declare class InboundEffectsDispatcher {
2434
+ private readonly params;
2435
+ constructor(params: {
2436
+ sessionRepository: SessionRepository;
2437
+ hooks?: MetaWhatsAppHooks;
2438
+ realtime?: RealtimeNotifierInterface;
2439
+ });
2440
+ run(job: InboundDispatchJob): Promise<void>;
2441
+ runMessageEffects(job: InboundMessageEffectsJob): Promise<void>;
2442
+ runStatusEffects(job: InboundStatusEffectsJob): Promise<void>;
2443
+ }
2444
+ /**
2445
+ * Ponto de entrada do worker do host: recebe o job que a fila devolveu e roda os efeitos.
2446
+ *
2447
+ * Deixe a exceção propagar. É ela que faz a fila contar a tentativa e reagendar com backoff;
2448
+ * capturar aqui para logar transforma falha recuperável em mensagem perdida em silêncio.
2449
+ */
2450
+ declare class ProcessInboundDispatchUseCase {
2451
+ private readonly dispatcher;
2452
+ constructor(dispatcher: InboundEffectsDispatcher);
2453
+ execute(job: InboundDispatchJob): Promise<void>;
2454
+ }
1320
2455
 
1321
2456
  type ReceiveWebhookParams = {
1322
2457
  companyId: string;
@@ -1327,11 +2462,15 @@ type ReceiveWebhookResult = {
1327
2462
  duplicate: boolean;
1328
2463
  messagesProcessed: number;
1329
2464
  statusesProcessed: number;
2465
+ ignoredForeignNumber: number;
2466
+ accountEventsProcessed: number;
2467
+ unhandledEvents: number;
1330
2468
  };
1331
2469
  declare class ReceiveWebhookUseCase {
1332
2470
  private readonly params;
1333
2471
  constructor(params: {
1334
2472
  appSecret: string;
2473
+ phoneNumberId: string;
1335
2474
  nonceStore: NonceStoreInterface;
1336
2475
  sessionRepository: SessionRepository;
1337
2476
  messageRepository: MessageRepository;
@@ -1339,12 +2478,53 @@ declare class ReceiveWebhookUseCase {
1339
2478
  startState: SessionState;
1340
2479
  hooks?: MetaWhatsAppHooks;
1341
2480
  realtime?: RealtimeNotifierInterface;
2481
+ /**
2482
+ * Sem fila o módulo se comporta como sempre: os hooks rodam dentro da requisição do webhook.
2483
+ * Configurá-la é o que faz a conversa sobreviver a um deploy no meio do atendimento.
2484
+ */
2485
+ inboundQueue?: InboundDispatchQueueInterface;
1342
2486
  });
2487
+ private readonly dispatcher;
1343
2488
  execute(input: ReceiveWebhookParams): Promise<ReceiveWebhookResult>;
2489
+ private handleAccountEvent;
1344
2490
  private handleMessage;
1345
2491
  private handleStatus;
2492
+ private dispatch;
1346
2493
  }
1347
2494
 
2495
+ /**
2496
+ * Política efetiva de transcrição de uma empresa.
2497
+ *
2498
+ * A separação que este arquivo existe para manter: **ambiente decide se é POSSÍVEL, settings decide
2499
+ * se é para FAZER.** A capacidade (engine, chave, storage que sabe reler) é injetada pelo host e é
2500
+ * por deploy — chave de API não vai para tabela de configuração de tenant. Já "transcrever ou não" e
2501
+ * "automático ou sob demanda" são decisão de operação de cada empresa, e pedir deploy para mudar
2502
+ * isso é o que transforma um interruptor em ticket.
2503
+ */
2504
+ type TranscriptionPolicy = {
2505
+ readonly isEnabled: boolean;
2506
+ readonly mode: TranscriptionMode;
2507
+ };
2508
+ type TranscriptionPolicyDefaults = {
2509
+ /** O que vale quando o painel não decidiu. Tipicamente vem do ambiente do host. */
2510
+ readonly isEnabled: boolean;
2511
+ readonly mode: TranscriptionMode;
2512
+ };
2513
+ type ResolveTranscriptionPolicyDependencies = {
2514
+ readonly settingsRepository: SettingsRepository;
2515
+ readonly defaults: TranscriptionPolicyDefaults;
2516
+ };
2517
+ /**
2518
+ * Resolve a política por empresa, com o padrão do host como base.
2519
+ *
2520
+ * Uma consulta a `settings` por áudio transcrito. Não é cacheado de propósito: é uma leitura por
2521
+ * chave primária, acontece uma vez por nota de voz (não por mensagem), e cachear introduziria a
2522
+ * pergunta "por quanto tempo o operador continua vendo o interruptor antigo depois de mexer nele" —
2523
+ * custo real, para economizar um índice único.
2524
+ */
2525
+ declare function createTranscriptionPolicyResolver(dependencies: ResolveTranscriptionPolicyDependencies): (companyId: string) => Promise<TranscriptionPolicy>;
2526
+ type TranscriptionPolicyResolver = ReturnType<typeof createTranscriptionPolicyResolver>;
2527
+
1348
2528
  type IngestInboundMediaParams = {
1349
2529
  companyId: string;
1350
2530
  messageId: string;
@@ -1355,13 +2535,52 @@ type IngestInboundMediaParams = {
1355
2535
  type IngestInboundMediaResult = {
1356
2536
  uploadId: string;
1357
2537
  alreadyIngested: boolean;
2538
+ /**
2539
+ * Só presente quando a transcrição automática rodou nesta execução. Ausente é o normal: mídia que
2540
+ * não é áudio, transcrição desligada, modo sob demanda, ou mídia já ingerida antes.
2541
+ */
2542
+ transcription?: {
2543
+ status: TranscriptionStatus;
2544
+ };
2545
+ };
2546
+ /**
2547
+ * Transcrição durante a ingestão — o modo `auto`.
2548
+ *
2549
+ * Entra aqui, e não em use-case separado, por um motivo só: neste ponto o buffer do áudio ACABOU de
2550
+ * ser baixado e está em memória. Transcrever fora daqui custaria um segundo download do storage por
2551
+ * áudio, e o `TranscribeAudioUseCase` existe justamente para esse caso (sob demanda e retomada).
2552
+ */
2553
+ type IngestTranscriptionOptions = {
2554
+ transcriber: AudioTranscriber;
2555
+ messageRepository: MessageRepository;
2556
+ /**
2557
+ * Política POR EMPRESA, resolvida a cada áudio. Não é um `mode` fixo porque o interruptor mora nas
2558
+ * configurações da empresa: um valor capturado na construção do use-case congelaria a escolha até
2559
+ * o próximo deploy, e o worker é um processo longo — o operador mexeria no painel e nada mudaria.
2560
+ */
2561
+ resolvePolicy: TranscriptionPolicyResolver;
2562
+ languageHint?: string;
2563
+ hooks?: Pick<MetaWhatsAppHooks, 'onTranscriptionDeferred'>;
1358
2564
  };
1359
2565
  declare class IngestInboundMediaUseCase {
1360
2566
  private readonly db;
1361
2567
  private readonly channel;
1362
2568
  private readonly objectStorage;
1363
- constructor(db: MetaWhatsAppDatabase, channel: ChannelAdapterInterface, objectStorage: ObjectStorageInterface);
2569
+ private readonly documentRepository?;
2570
+ private readonly transcription?;
2571
+ constructor(db: MetaWhatsAppDatabase, channel: ChannelAdapterInterface, objectStorage: ObjectStorageInterface, documentRepository?: DocumentRepository | undefined, transcription?: IngestTranscriptionOptions | undefined);
1364
2572
  execute(params: IngestInboundMediaParams): Promise<IngestInboundMediaResult>;
2573
+ /**
2574
+ * Transcreve o áudio recém-baixado, quando o modo é `auto`.
2575
+ *
2576
+ * **Nunca propaga erro.** Neste ponto o binário já está no storage e já entrou na biblioteca da
2577
+ * conversa: deixar uma falha de transcrição subir marcaria a ingestão inteira como falha, e o
2578
+ * retry do host baixaria de novo da Meta um arquivo que está salvo — gastando banda para reproduzir
2579
+ * um efeito que já aconteceu. O status fica gravado na mensagem e o `onTranscriptionDeferred`
2580
+ * avisa quem sabe reenfileirar.
2581
+ */
2582
+ private transcribeIfAuto;
2583
+ private recordTranscriptionFailure;
1365
2584
  }
1366
2585
  declare function extractMediaDescriptor(message: MessageRow): {
1367
2586
  sourceMediaId: string;
@@ -1369,6 +2588,105 @@ declare function extractMediaDescriptor(message: MessageRow): {
1369
2588
  filename?: string;
1370
2589
  } | undefined;
1371
2590
 
2591
+ /**
2592
+ * Storage com leitura garantida. `getObject` é opcional no contrato, então quem monta este use-case
2593
+ * precisa provar que o método existe — sem os bytes não há o que transcrever, e um use-case que
2594
+ * sempre falha é pior do que a ausência ser visível no tipo.
2595
+ */
2596
+ type ReadableObjectStorage = ObjectStorageInterface & {
2597
+ getObject: NonNullable<ObjectStorageInterface['getObject']>;
2598
+ };
2599
+ type TranscribeAudioParams = {
2600
+ companyId: string;
2601
+ messageId: string;
2602
+ /**
2603
+ * Refaz mesmo com transcrição já salva. Serve ao "transcrever de novo" depois de trocar de engine
2604
+ * — sem isto, um resultado ruim do engine antigo ficaria congelado para sempre.
2605
+ */
2606
+ force?: boolean;
2607
+ };
2608
+ type TranscribeAudioResult = {
2609
+ status: TranscriptionStatus;
2610
+ text: string | null;
2611
+ language: string | null;
2612
+ engine: string | null;
2613
+ /** `true` quando devolveu o que já estava salvo, sem gastar cota. */
2614
+ alreadyTranscribed: boolean;
2615
+ };
2616
+ type TranscribeAudioDependencies = {
2617
+ messageRepository: MessageRepository;
2618
+ objectStorage: ReadableObjectStorage;
2619
+ transcriber: AudioTranscriber;
2620
+ /**
2621
+ * Política por empresa. Ausente, a transcrição sob demanda não consulta configuração nenhuma e
2622
+ * atende sempre — que é o comportamento de quem controla o liga/desliga só por ambiente.
2623
+ */
2624
+ resolvePolicy?: TranscriptionPolicyResolver;
2625
+ /** ISO 639-1 do produto. Informar corta a detecção do Whisper e evita pt-BR curto virar espanhol. */
2626
+ languageHint?: string;
2627
+ hooks?: Pick<MetaWhatsAppHooks, 'onTranscriptionDeferred'>;
2628
+ };
2629
+ /**
2630
+ * Transcreve o áudio de UMA mensagem já persistida e ingerida.
2631
+ *
2632
+ * Serve aos dois modos: é o que o painel chama no botão "transcrever" (`onDemand`) e é o que o host
2633
+ * chama ao retomar um `pending` reenfileirado. O modo `auto` não passa por aqui — ele transcreve
2634
+ * dentro da ingestão, onde o buffer já está em memória e não custa um segundo download.
2635
+ *
2636
+ * Idempotente por `transcription_status`: chamar de novo num `'done'` devolve o que está salvo em
2637
+ * vez de gastar cota transcrevendo o mesmo áudio.
2638
+ */
2639
+ declare class TranscribeAudioUseCase {
2640
+ private readonly dependencies;
2641
+ constructor(dependencies: TranscribeAudioDependencies);
2642
+ execute(params: TranscribeAudioParams): Promise<TranscribeAudioResult>;
2643
+ private transcribeBuffer;
2644
+ /**
2645
+ * Carimba o motivo antes de propagar. O status é o que impede os dois desperdícios simétricos:
2646
+ * reprocessar para sempre um codec impossível, e desistir de um áudio que só esbarrou na cota.
2647
+ */
2648
+ private persistFailure;
2649
+ }
2650
+ declare function resolveFailureStatus(error: unknown): TranscriptionStatus;
2651
+
2652
+ /**
2653
+ * Guarda um arquivo gravado no simulador e devolve o id que o webhook vai referenciar.
2654
+ *
2655
+ * Existe como use-case para o host só precisar da ROTA: receber o corpo, chamar isto, devolver o
2656
+ * `mediaId`. A parte que erra — onde gravar, com que chave, como marcar o id para o adaptador
2657
+ * reconhecer depois — fica aqui, num lugar só, e não em cada produto.
2658
+ *
2659
+ * O SDK para exatamente na porta do HTTP: registrar endpoint é do host, e um pacote que abrisse rota
2660
+ * no servidor de quem o instala decidiria caminho, autenticação e versionamento no lugar dele.
2661
+ */
2662
+ type StorePreviewMediaParams = {
2663
+ companyId: string;
2664
+ /** Bytes do arquivo. Quem converte de base64 é a rota — o use-case não conhece transporte. */
2665
+ buffer: Buffer;
2666
+ mimeType: string;
2667
+ filename?: string;
2668
+ };
2669
+ type StorePreviewMediaResult = {
2670
+ /** Já com o prefixo: é isto que o simulador manda no webhook. */
2671
+ mediaId: string;
2672
+ uploadId: string;
2673
+ };
2674
+ declare class StorePreviewMediaUseCase {
2675
+ private readonly objectStorage;
2676
+ /**
2677
+ * Fonte do sufixo único da chave. Injetada porque o módulo não escolhe gerador de id — e porque
2678
+ * um teste precisa de chave previsível.
2679
+ */
2680
+ private readonly generateKeySuffix;
2681
+ constructor(objectStorage: ObjectStorageInterface,
2682
+ /**
2683
+ * Fonte do sufixo único da chave. Injetada porque o módulo não escolhe gerador de id — e porque
2684
+ * um teste precisa de chave previsível.
2685
+ */
2686
+ generateKeySuffix: () => string);
2687
+ execute(params: StorePreviewMediaParams): Promise<StorePreviewMediaResult>;
2688
+ }
2689
+
1372
2690
  interface MetaWhatsAppModuleConfig {
1373
2691
  phoneNumberId: string;
1374
2692
  accessToken: string;
@@ -1377,16 +2695,66 @@ interface MetaWhatsAppModuleConfig {
1377
2695
  wabaId?: string;
1378
2696
  apiVersion?: string;
1379
2697
  baseUrl?: string;
2698
+ /**
2699
+ * Catálogo do Meta Commerce que a vitrine no chat oferece. Sem ele — ou sem `providers.catalog`
2700
+ * — a action `send_product_list` não é registrada: nó que o editor oferece e que em silêncio não
2701
+ * faz nada é pior do que nó que não existe.
2702
+ */
2703
+ catalogId?: string;
1380
2704
  }
1381
2705
  interface MetaWhatsAppModuleFeatures {
1382
2706
  flowEngine?: boolean;
2707
+ flowGraphCache?: boolean | {
2708
+ ttlSeconds?: number;
2709
+ };
2710
+ /**
2711
+ * Aceita mídia do simulador de conversa — o que faz o microfone aparecer no preview do cliente.
2712
+ *
2713
+ * **Desligado por omissão, e a decisão é consciente.** Ligado, o canal passa a aceitar id que não
2714
+ * veio da Meta: `preview-upload:<chave>` faz o servidor ler aquele objeto do storage. Em ambiente
2715
+ * de simulação isso é o recurso; em produção é leitura arbitrária do bucket por webhook forjado.
2716
+ *
2717
+ * Exige `providers.objectStorage` com `getObject` — sem os bytes não há o que devolver, e o flag é
2718
+ * ignorado em vez de produzir um canal que falha na primeira nota de voz.
2719
+ */
2720
+ previewMedia?: boolean;
2721
+ }
2722
+ /**
2723
+ * Transcrição de áudio. Ausente = desligada, e as colunas ficam nulas ("não avaliado").
2724
+ *
2725
+ * `mode` é a decisão de produto que este objeto existe para carregar: `auto` transcreve toda nota de
2726
+ * voz recebida, na ingestão, onde o buffer já está em memória; `onDemand` só transcreve quando o
2727
+ * atendente pede, gastando cota apenas com áudio que alguém vai ler de fato.
2728
+ */
2729
+ interface MetaWhatsAppTranscriptionConfig {
2730
+ transcriber: AudioTranscriber;
2731
+ /**
2732
+ * PADRÃO, não decisão final: vale para as empresas que não mexeram no interruptor do painel. As
2733
+ * configurações por empresa (`settings.transcriptionMode`) têm precedência.
2734
+ *
2735
+ * Padrão `onDemand` — o modo que não gasta cota sem alguém pedir.
2736
+ */
2737
+ mode?: TranscriptionMode;
2738
+ /**
2739
+ * PADRÃO de ligado/desligado para empresas sem escolha registrada. `true` — injetar o transcritor
2740
+ * já é a declaração de que o host quer o recurso; quem controla por empresa usa o painel.
2741
+ */
2742
+ isEnabledByDefault?: boolean;
2743
+ /** ISO 639-1 do produto (ex.: `'pt'`). Corta a detecção de idioma do engine. */
2744
+ languageHint?: string;
1383
2745
  }
1384
2746
  interface MetaWhatsAppModuleProviders {
1385
2747
  objectStorage?: ObjectStorageInterface;
2748
+ cache?: CacheInterface;
1386
2749
  realtime?: RealtimeNotifierInterface;
1387
2750
  subjectResolver?: SubjectResolverInterface;
1388
2751
  catalog?: CatalogPort;
1389
2752
  moderator?: MessageModerator;
2753
+ /**
2754
+ * Converte nota de voz em texto. Exige `objectStorage` com `getObject` para o modo sob demanda —
2755
+ * transcrever um áudio já salvo significa ler os bytes de volta.
2756
+ */
2757
+ transcription?: MetaWhatsAppTranscriptionConfig;
1390
2758
  }
1391
2759
  interface CreateMetaWhatsAppModuleParams {
1392
2760
  db: MetaWhatsAppDatabase;
@@ -1407,10 +2775,31 @@ declare function createMetaWhatsAppModule(params: CreateMetaWhatsAppModuleParams
1407
2775
  release: ReleaseConversationUseCase;
1408
2776
  list: ListConversationsUseCase;
1409
2777
  listMessages: ListMessagesUseCase;
2778
+ listDocuments: ListConversationDocumentsUseCase;
2779
+ listCompanyDocuments: ListCompanyDocumentsUseCase;
2780
+ delete: DeleteConversationUseCase;
2781
+ purgeExpiredDocuments: PurgeExpiredDocumentsUseCase;
1410
2782
  export: ExportConversationUseCase;
2783
+ transcribeAudio: TranscribeAudioUseCase | undefined;
1411
2784
  repository: SessionRepository;
2785
+ messageRepository: MessageRepository;
2786
+ documentRepository: DocumentRepository;
1412
2787
  };
2788
+ /**
2789
+ * `undefined` = o host não injetou transcritor, e nenhuma configuração de empresa muda isso: a
2790
+ * capacidade não existe. Presente, `resolvePolicy` responde o que vale para uma empresa —
2791
+ * é o que a rota de configurações usa para dizer ao painel se desenha o interruptor.
2792
+ */
2793
+ transcription: {
2794
+ defaultMode: TranscriptionMode;
2795
+ resolvePolicy: (companyId: string) => Promise<TranscriptionPolicy>;
2796
+ } | undefined;
1413
2797
  settings: SettingsRepository;
2798
+ /**
2799
+ * `undefined` quando o recurso não está ligado (ou falta storage legível). O host consulta a
2800
+ * ausência para não registrar a rota de upload — e o preview, sem a rota, esconde o microfone.
2801
+ */
2802
+ previewMedia: StorePreviewMediaUseCase | undefined;
1414
2803
  webhook: {
1415
2804
  receive: ReceiveWebhookUseCase;
1416
2805
  verifyChallenge: (query: {
@@ -1429,26 +2818,49 @@ declare function createMetaWhatsAppModule(params: CreateMetaWhatsAppModuleParams
1429
2818
  delete: DeleteFlowGraphUseCase;
1430
2819
  livePositions: GetLiveFlowPositionsUseCase;
1431
2820
  repository: FlowGraphRepository;
2821
+ mediaRepository: FlowMediaRepository;
1432
2822
  } | undefined;
1433
2823
  catalog: CatalogPort | undefined;
1434
2824
  };
1435
2825
  type MetaWhatsAppModule = ReturnType<typeof createMetaWhatsAppModule>;
1436
2826
 
2827
+ /**
2828
+ * Leitura de mídia do simulador de conversa.
2829
+ *
2830
+ * **Atrás de flag de propósito, e não ligado por omissão.** Aceitar id que não veio da Meta é
2831
+ * exatamente o que um webhook forjado exploraria: bastaria mandar `preview-upload:<chave>` para
2832
+ * fazer o servidor ler um objeto arbitrário do storage e devolvê-lo. Num ambiente de simulação isso
2833
+ * é o recurso; em produção é leitura arbitrária. Quem liga assume, e a decisão fica visível no
2834
+ * lugar onde o módulo é montado.
2835
+ */
2836
+ type PreviewMediaSupport = {
2837
+ readonly isEnabled: boolean;
2838
+ readonly objectStorage: ObjectStorageInterface & {
2839
+ getObject: NonNullable<ObjectStorageInterface['getObject']>;
2840
+ };
2841
+ /** Mime a devolver, já que o storage guarda bytes e não o tipo. Padrão `audio/ogg`. */
2842
+ readonly defaultMimeType?: string;
2843
+ };
1437
2844
  declare class WhatsAppChannelAdapter implements ChannelAdapterInterface {
1438
2845
  private readonly messages;
1439
- constructor(messages: WhatsAppMessageProvider);
2846
+ /**
2847
+ * Ausente, o adaptador se comporta como sempre: todo id vai para a Graph API. É o que garante
2848
+ * que atualizar o pacote não abre nada em quem não pediu.
2849
+ */
2850
+ private readonly previewMedia?;
2851
+ constructor(messages: WhatsAppMessageProvider,
2852
+ /**
2853
+ * Ausente, o adaptador se comporta como sempre: todo id vai para a Graph API. É o que garante
2854
+ * que atualizar o pacote não abre nada em quem não pediu.
2855
+ */
2856
+ previewMedia?: PreviewMediaSupport | undefined);
1440
2857
  private translateErrors;
1441
2858
  sendText(to: string, body: string): Promise<{
1442
2859
  externalMessageId: string | null;
1443
2860
  }>;
1444
- sendMedia(params: {
1445
- to: string;
1446
- buffer: Buffer;
1447
- mimeType: string;
1448
- filename: string;
1449
- caption?: string;
1450
- }): Promise<{
2861
+ sendMedia(params: SendChannelMediaParams): Promise<{
1451
2862
  externalMessageId: string | null;
2863
+ mediaId?: string | undefined;
1452
2864
  }>;
1453
2865
  sendTemplate(params: {
1454
2866
  to: string;
@@ -1469,6 +2881,34 @@ declare class WhatsAppChannelAdapter implements ChannelAdapterInterface {
1469
2881
  }): Promise<{
1470
2882
  externalMessageId: string | null;
1471
2883
  }>;
2884
+ sendInteractiveButtons(params: {
2885
+ to: string;
2886
+ body: string;
2887
+ buttons: {
2888
+ id: string;
2889
+ title: string;
2890
+ }[];
2891
+ }): Promise<{
2892
+ externalMessageId: string | null;
2893
+ }>;
2894
+ sendProductList(params: {
2895
+ to: string;
2896
+ headerText: string;
2897
+ body: string;
2898
+ footerText?: string;
2899
+ sections: {
2900
+ title: string;
2901
+ retailerIds: string[];
2902
+ }[];
2903
+ }): Promise<{
2904
+ externalMessageId: string | null;
2905
+ }>;
2906
+ /**
2907
+ * Busca o binário da mídia — da Meta, ou do storage quando o id é do simulador.
2908
+ *
2909
+ * O desvio acontece ANTES de qualquer chamada de rede: id do simulador não existe na Meta, e
2910
+ * tentar buscá-lo lá renderia um 404 confuso em vez do áudio que o operador acabou de gravar.
2911
+ */
1472
2912
  fetchMediaAsBase64(mediaId: string): Promise<{
1473
2913
  data: string;
1474
2914
  mimeType: string;
@@ -1512,4 +2952,132 @@ declare function redeemSseTicket(store: TicketStoreInterface, ticket: string): P
1512
2952
  whatsappNumber: string;
1513
2953
  } | null>;
1514
2954
 
1515
- export { type CreateFlowGraphParams, CreateFlowGraphUseCase, type CreateMetaWhatsAppModuleParams, DeleteFlowGraphUseCase, type DrizzleMigrateFunction, type ExportConversationParams, type ExportConversationResult, ExportConversationUseCase, FlowGraphRepository, type FlowGraphRow, FlowInterpreter, type FlowRunResult, type FlowStepInput, type FlowStepResult, GetFlowGraphUseCase, GetLiveFlowPositionsUseCase, type IngestInboundMediaParams, type IngestInboundMediaResult, IngestInboundMediaUseCase, type InsertMessageParams, InvalidFlowGraphError, type ListConversationsFilters, type ListConversationsParams, ListConversationsUseCase, ListFlowGraphsUseCase, type ListMessagesParams, ListMessagesUseCase, type LogMessageParams, LogMessageUseCase, META_WHATSAPP_MIGRATIONS_TABLE, type MessageModerator, MessageRepository, type MessageRow, type MetaWhatsAppDatabase, type MetaWhatsAppModule, type MetaWhatsAppModuleConfig, type MetaWhatsAppModuleFeatures, type MetaWhatsAppModuleProviders, type NewFlowGraphRow, type NewMessageRow, type NewSessionRow, type NewSettingsRow, type NonceStoreInterface, OptimisticLockError, type RealtimeRelay, type ReceiveWebhookParams, type ReceiveWebhookResult, ReceiveWebhookUseCase, type ReleaseConversationParams, ReleaseConversationUseCase, type ListMessagesParams$1 as RepositoryListMessagesParams, type RunMetaWhatsAppMigrationsParams, type SaveFlowGraphParams, SaveFlowGraphUseCase, type SendMediaParams, SendMessageUseCase, type SendTemplateParams, type SendTextParams, SessionRepository, type SessionRow, SettingsRepository, type SettingsRow, SseHub, type SseListener, type TakeoverConversationParams, TakeoverConversationUseCase, type TicketStoreInterface, WEBHOOK_NONCE_TTL_SECONDS, WhatsAppChannelAdapter, claimWebhookDelivery, createMetaWhatsAppModule, extractMediaDescriptor, flowGraphs, issueSseTicket, messages, metaWhatsAppMigrationsFolder, metaWhatsAppSchema, redeemSseTicket, runMetaWhatsAppMigrations, sessions, settings, verifyWebhookChallenge, verifyWebhookSignature };
2955
+ /**
2956
+ * Porta de escrita no transcript, e não a classe `LogMessageUseCase`.
2957
+ *
2958
+ * Exigir a classe amarraria a action a quem já guarda as mensagens nas tabelas do módulo — um host
2959
+ * em migração, com o transcript ainda no schema dele, não conseguiria usar a única action built-in
2960
+ * sem gravar o envio numa tabela que o painel dele não lê. O que a action precisa é só de um lugar
2961
+ * para registrar o que saiu.
2962
+ */
2963
+ /**
2964
+ * Onde guardar o `mediaId` que a Meta devolveu para cada arquivo da biblioteca.
2965
+ *
2966
+ * Porta, e não tabela: o módulo não sabe se o host guarda isso numa coluna, no Redis ou em memória.
2967
+ * O que ele precisa é reaproveitar o id em vez de ressubir o mesmo binário para cada cliente que
2968
+ * passa pelo nó — a Meta aceita reusar por 30 dias.
2969
+ */
2970
+ type FlowMediaIdStore = {
2971
+ get(params: {
2972
+ flowMediaId: string;
2973
+ senderKey: string;
2974
+ }): Promise<string | undefined>;
2975
+ set(params: {
2976
+ flowMediaId: string;
2977
+ senderKey: string;
2978
+ mediaId: string;
2979
+ }): Promise<void>;
2980
+ clear(params: {
2981
+ flowMediaId: string;
2982
+ senderKey: string;
2983
+ }): Promise<void>;
2984
+ };
2985
+ /**
2986
+ * O `senderKey` vem junto do store, e não solto, para não existir a forma inválida.
2987
+ *
2988
+ * O `mediaId` é escopado ao número remetente (`phone_number_id`) na Meta: cachear sem separar por
2989
+ * número faz o id de um número ser mandado pelo outro, e o envio falha. Exigi-lo dentro do mesmo
2990
+ * objeto torna "liguei o cache e esqueci o número" impossível de escrever.
2991
+ */
2992
+ type FlowMediaIdCache = {
2993
+ readonly store: FlowMediaIdStore;
2994
+ readonly senderKey: string;
2995
+ };
2996
+ type FlowMediaTranscriptLogger = {
2997
+ execute(params: LogMessageParams): Promise<unknown>;
2998
+ };
2999
+ type CreateSendMediaActionParams = {
3000
+ flowMediaRepository: FlowMediaRepository;
3001
+ objectStorage: ObjectStorageInterface & {
3002
+ getObject: NonNullable<ObjectStorageInterface['getObject']>;
3003
+ };
3004
+ logMessage: FlowMediaTranscriptLogger;
3005
+ /**
3006
+ * Reaproveitamento do arquivo já subido. Capacidade por ausência: sem o cache, cada envio ressobe
3007
+ * o binário — exatamente o comportamento anterior, sem flag para desligar.
3008
+ */
3009
+ mediaIdCache?: FlowMediaIdCache;
3010
+ startState: SessionState;
3011
+ onError?: (error: unknown, details: {
3012
+ flowKey: string;
3013
+ nodeId: string;
3014
+ uploadId: string;
3015
+ }) => void;
3016
+ };
3017
+ declare function createSendMediaAction(params: CreateSendMediaActionParams): FlowActionHandler;
3018
+
3019
+ /** Tetos da Meta para `interactive.product_list`. Acima disso a mensagem inteira é recusada. */
3020
+ declare const PRODUCT_LIST_LIMIT: {
3021
+ readonly ITEMS: 30;
3022
+ readonly SECTIONS: 10;
3023
+ };
3024
+ type CreateSendProductListActionParams = {
3025
+ catalog: CatalogPort;
3026
+ catalogId: string;
3027
+ logMessage: FlowMediaTranscriptLogger;
3028
+ startState: SessionState;
3029
+ /** Falha de UMA vitrine não pode travar a conversa; o host observa por aqui. */
3030
+ onError?: (error: unknown, details: {
3031
+ flowKey: string;
3032
+ nodeId: string;
3033
+ }) => void;
3034
+ };
3035
+ /**
3036
+ * Action `send_product_list`: ao passar pelo nó, envia a vitrine do catálogo publicado na Meta.
3037
+ *
3038
+ * O nó guarda **critério**, não a lista de produtos. Congelar `retailerId` em `actionParams` faria
3039
+ * o fluxo continuar oferecendo o item esgotado — ou o item excluído, que a Meta recusa junto com a
3040
+ * mensagem inteira. O que o operador edita é o texto e o filtro; o estoque manda no resto.
3041
+ *
3042
+ * Só produto em estoque entra. Vitrine que mostra o que não tem produz a pior conversa possível:
3043
+ * o cliente escolhe, responde, e recebe "acabou".
3044
+ */
3045
+ declare function createSendProductListAction(params: CreateSendProductListActionParams): FlowActionHandler;
3046
+
3047
+ /**
3048
+ * Store padrão do cache de `mediaId`, na própria linha da biblioteca de mídia.
3049
+ *
3050
+ * A porta `FlowMediaIdStore` existe para o host poder escolher Redis ou memória. Esta é a
3051
+ * implementação que o módulo já traz pronta, para o caso comum não exigir nada de quem instala: a
3052
+ * tabela é do módulo, a coluna é do módulo, e sem isso todo host reescreveria o mesmo repositório.
3053
+ *
3054
+ * Um mapa por `phone_number_id` na mesma linha, e não uma tabela à parte: o id não existe sem o
3055
+ * arquivo, morre com ele, e a linha é lida de qualquer forma no envio.
3056
+ */
3057
+ declare class FlowMediaIdRepository implements FlowMediaIdStore {
3058
+ private readonly db;
3059
+ constructor(db: MetaWhatsAppDatabase);
3060
+ get(params: {
3061
+ flowMediaId: string;
3062
+ senderKey: string;
3063
+ }): Promise<string | undefined>;
3064
+ /**
3065
+ * Grava só a chave deste número.
3066
+ *
3067
+ * `jsonb_set` no banco, e não ler-alterar-escrever na aplicação: dois clientes passando pelo nó
3068
+ * ao mesmo tempo com números diferentes leriam o mesmo mapa e o último gravaria por cima,
3069
+ * apagando o id do outro. A escrita atômica não tem essa janela.
3070
+ */
3071
+ set(params: {
3072
+ flowMediaId: string;
3073
+ senderKey: string;
3074
+ mediaId: string;
3075
+ }): Promise<void>;
3076
+ /** Remove só a chave deste número — o id do outro número continua válido. */
3077
+ clear(params: {
3078
+ flowMediaId: string;
3079
+ senderKey: string;
3080
+ }): Promise<void>;
3081
+ }
3082
+
3083
+ export { type AttachFlowMediaParams, type AudioTranscriber, type CompanyDocumentView, type CompanyDocumentsPage, type ConversationDocumentView, type ConversationDocumentsPage, type CreateFlowGraphParams, CreateFlowGraphUseCase, type CreateMetaWhatsAppModuleParams, type CreateSendMediaActionParams, type CreateSendProductListActionParams, DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS, type DeleteConversationParams, type DeleteConversationResult, DeleteConversationUseCase, DeleteFlowGraphUseCase, DocumentRepository, type DocumentRow, type DrizzleMigrateFunction, type ExportConversationParams, type ExportConversationResult, ExportConversationUseCase, FlowGraphCache, FlowGraphRepository, type FlowGraphRow, FlowInterpreter, type FlowMediaIdCache, FlowMediaIdRepository, type FlowMediaIdStore, type FlowMediaLocation, FlowMediaRepository, type FlowMediaRow, type FlowMediaTranscriptLogger, type FlowRunResult, type FlowStepInput, type FlowStepResult, GetFlowGraphUseCase, GetLiveFlowPositionsUseCase, type InboundDispatchJob, type InboundDispatchQueueInterface, InboundEffectsDispatcher, type InboundMediaDescriptor, type InboundMessageEffectsJob, type InboundStatusEffectsJob, type IngestInboundMediaParams, type IngestInboundMediaResult, IngestInboundMediaUseCase, type IngestTranscriptionOptions, type InsertMessageParams, InvalidFlowGraphError, type LinkDocumentParams, type ListCompanyDocumentsParams, ListCompanyDocumentsUseCase, type ListConversationDocumentsParams, ListConversationDocumentsUseCase, type ListConversationsFilters, type ListConversationsParams, ListConversationsUseCase, type ListDocumentsParams, type ListDocumentsResult, ListFlowGraphsUseCase, type ListMessagesParams, ListMessagesUseCase, type LogMessageParams, LogMessageUseCase, META_WHATSAPP_MIGRATIONS_TABLE, type MessageModerator, MessageRepository, type MessageRow, type MetaWhatsAppDatabase, type MetaWhatsAppModule, type MetaWhatsAppModuleConfig, type MetaWhatsAppModuleFeatures, type MetaWhatsAppModuleProviders, type MetaWhatsAppTranscriptionConfig, type NewDocumentRow, type NewFlowGraphRow, type NewFlowMediaRow, type NewMessageRow, type NewSessionRow, type NewSettingsRow, type NonceStoreInterface, OptimisticLockError, PRODUCT_LIST_LIMIT, type PreviewMediaSupport, ProcessInboundDispatchUseCase, type PurgeExpiredDocumentsParams, type PurgeExpiredDocumentsResult, PurgeExpiredDocumentsUseCase, type RealtimeRelay, type ReceiveWebhookParams, type ReceiveWebhookResult, ReceiveWebhookUseCase, type ReleaseConversationParams, ReleaseConversationUseCase, type ListMessagesParams$1 as RepositoryListMessagesParams, type ResolveTranscriptionPolicyDependencies, type RunMetaWhatsAppMigrationsParams, type SaveFlowGraphParams, SaveFlowGraphUseCase, type SaveTranscriptionByWaMessageIdParams, type SaveTranscriptionParams, type SendMediaParams, SendMessageUseCase, type SendTemplateParams, type SendTextParams, SessionRepository, type SessionRow, SettingsRepository, type SettingsRow, SseHub, type SseListener, type StorePreviewMediaParams, type StorePreviewMediaResult, StorePreviewMediaUseCase, TRANSCRIPTION_MODE, TRANSCRIPTION_STATUS, type TakeoverConversationParams, TakeoverConversationUseCase, type TicketStoreInterface, type TranscribeAudioDependencies, type TranscribeAudioParams, type TranscribeAudioResult, TranscribeAudioUseCase, type TranscriptionPolicy, type TranscriptionPolicyDefaults, type TranscriptionPolicyResolver, type TranscriptionStatus, type UpdateFlowMediaParams, WhatsAppChannelAdapter, buildInboundJobId, claimWebhookDelivery, confirmWebhookDelivery, createMetaWhatsAppModule, createSendMediaAction, createSendProductListAction, createTranscriptionPolicyResolver, documents, extractMediaDescriptor, flowGraphs, flowMedia, isAudioMimeType, isRetriableTranscriptionError, isUnsupportedTranscriptionError, issueSseTicket, messages, metaWhatsAppMigrationsFolder, metaWhatsAppSchema, redeemSseTicket, resolveFailureStatus, runMetaWhatsAppMigrations, sessions, settings, toSessionContract, transcriptionRetryAfterSeconds, verifyWebhookChallenge, verifyWebhookSignature };