tychat-contracts 1.6.52 → 1.6.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/legal-terms/default-legal-term-document.d.ts +23 -0
  2. package/dist/legal-terms/default-legal-term-document.d.ts.map +1 -0
  3. package/dist/legal-terms/default-legal-term-document.js +170 -0
  4. package/dist/legal-terms/index.d.ts +4 -0
  5. package/dist/legal-terms/index.d.ts.map +1 -1
  6. package/dist/legal-terms/index.js +4 -0
  7. package/dist/legal-terms/legal-term-body-format.d.ts +1 -1
  8. package/dist/legal-terms/legal-term-body-format.d.ts.map +1 -1
  9. package/dist/legal-terms/legal-term-body-format.js +1 -1
  10. package/dist/legal-terms/legal-term-document-compile.d.ts +3 -0
  11. package/dist/legal-terms/legal-term-document-compile.d.ts.map +1 -0
  12. package/dist/legal-terms/legal-term-document-compile.js +166 -0
  13. package/dist/legal-terms/legal-term-document-compile.spec.d.ts +2 -0
  14. package/dist/legal-terms/legal-term-document-compile.spec.d.ts.map +1 -0
  15. package/dist/legal-terms/legal-term-document-compile.spec.js +49 -0
  16. package/dist/legal-terms/legal-term-document.dto.d.ts +21 -0
  17. package/dist/legal-terms/legal-term-document.dto.d.ts.map +1 -0
  18. package/dist/legal-terms/legal-term-document.dto.js +127 -0
  19. package/dist/legal-terms/legal-term-document.types.d.ts +30 -0
  20. package/dist/legal-terms/legal-term-document.types.d.ts.map +1 -0
  21. package/dist/legal-terms/legal-term-document.types.js +14 -0
  22. package/dist/legal-terms/legal-term-preview.dto.d.ts +2 -0
  23. package/dist/legal-terms/legal-term-preview.dto.d.ts.map +1 -1
  24. package/dist/legal-terms/legal-term-preview.dto.js +14 -1
  25. package/dist/legal-terms/legal-term-template.dto.d.ts +4 -1
  26. package/dist/legal-terms/legal-term-template.dto.d.ts.map +1 -1
  27. package/dist/legal-terms/legal-term-template.dto.js +25 -6
  28. package/dist/notifications/index.d.ts +2 -1
  29. package/dist/notifications/index.d.ts.map +1 -1
  30. package/dist/notifications/index.js +5 -1
  31. package/dist/notifications/mark-all-notifications-viewed.dto.d.ts +14 -0
  32. package/dist/notifications/mark-all-notifications-viewed.dto.d.ts.map +1 -0
  33. package/dist/notifications/mark-all-notifications-viewed.dto.js +53 -0
  34. package/dist/notifications/notifications-kafka-topics.d.ts +2 -0
  35. package/dist/notifications/notifications-kafka-topics.d.ts.map +1 -1
  36. package/dist/notifications/notifications-kafka-topics.js +3 -1
  37. package/package.json +1 -1
  38. package/src/legal-terms/default-legal-term-document.ts +170 -0
  39. package/src/legal-terms/index.ts +4 -0
  40. package/src/legal-terms/legal-term-body-format.ts +1 -1
  41. package/src/legal-terms/legal-term-document-compile.spec.ts +53 -0
  42. package/src/legal-terms/legal-term-document-compile.ts +192 -0
  43. package/src/legal-terms/legal-term-document.dto.ts +100 -0
  44. package/src/legal-terms/legal-term-document.types.ts +42 -0
  45. package/src/legal-terms/legal-term-preview.dto.ts +13 -1
  46. package/src/legal-terms/legal-term-template.dto.ts +23 -7
  47. package/src/notifications/index.ts +5 -0
  48. package/src/notifications/mark-all-notifications-viewed.dto.ts +33 -0
  49. package/src/notifications/notifications-kafka-topics.ts +3 -0
@@ -0,0 +1,192 @@
1
+ import type {
2
+ LegalTermDocument,
3
+ LegalTermDocumentBlock,
4
+ LegalTermSectionLevel,
5
+ LegalTermTextAlign,
6
+ } from './legal-term-document.types';
7
+
8
+ type TextStyleOpts = {
9
+ align?: LegalTermTextAlign;
10
+ bold?: boolean;
11
+ italic?: boolean;
12
+ underline?: boolean;
13
+ };
14
+
15
+ function escapeHtmlText(s: string): string {
16
+ return s
17
+ .replace(/&/g, '&')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ .replace(/"/g, '&quot;');
21
+ }
22
+
23
+ function cssTextStyle(opts: TextStyleOpts, defaults: TextStyleOpts = {}): string {
24
+ const align = opts.align ?? defaults.align ?? 'left';
25
+ const bold = opts.bold ?? defaults.bold ?? false;
26
+ const italic = opts.italic ?? defaults.italic ?? false;
27
+ const underline = opts.underline ?? defaults.underline ?? false;
28
+ const parts = [`text-align:${align}`];
29
+ if (bold) parts.push('font-weight:700');
30
+ if (italic) parts.push('font-style:italic');
31
+ if (underline) parts.push('text-decoration:underline');
32
+ return parts.join(';');
33
+ }
34
+
35
+ function textToHtmlParagraphs(text: string, opts: TextStyleOpts = {}): string {
36
+ const trimmed = text.trim();
37
+ if (!trimmed) return '';
38
+ const style = cssTextStyle(opts, { align: 'left' });
39
+ const lines = trimmed.split(/\r?\n/);
40
+ return lines
41
+ .map((line) => {
42
+ const t = line.trim();
43
+ if (!t) return '<br/>';
44
+ return `<p style="margin:0.4em 0;line-height:1.45;${style}">${escapeHtmlText(t)}</p>`;
45
+ })
46
+ .join('');
47
+ }
48
+
49
+ function formatSectionNumber(counters: [number, number, number], level: LegalTermSectionLevel): string {
50
+ if (level === 1) return `${counters[0]}.`;
51
+ if (level === 2) return `${counters[0]}.${counters[1]}.`;
52
+ return `${counters[0]}.${counters[1]}.${counters[2]}.`;
53
+ }
54
+
55
+ function bumpSectionCounters(
56
+ counters: [number, number, number],
57
+ level: LegalTermSectionLevel,
58
+ ): [number, number, number] {
59
+ const next: [number, number, number] = [...counters] as [number, number, number];
60
+ if (level === 1) {
61
+ next[0] += 1;
62
+ next[1] = 0;
63
+ next[2] = 0;
64
+ } else if (level === 2) {
65
+ next[1] += 1;
66
+ next[2] = 0;
67
+ } else {
68
+ next[2] += 1;
69
+ }
70
+ return next;
71
+ }
72
+
73
+ function compileBlock(block: LegalTermDocumentBlock, sectionCounters: [number, number, number]): {
74
+ html: string;
75
+ counters: [number, number, number];
76
+ } {
77
+ let counters = sectionCounters;
78
+ const wrap = (inner: string, extraStyle = '') =>
79
+ `<div class="legal-term-block" data-block-type="${block.type}" style="margin-bottom:0.75em;${extraStyle}">${inner}</div>`;
80
+
81
+ switch (block.type) {
82
+ case 'document_title': {
83
+ const title = (block.title ?? block.body ?? '').trim();
84
+ const style = cssTextStyle(
85
+ {
86
+ align: block.titleAlign,
87
+ bold: block.titleBold,
88
+ italic: block.titleItalic,
89
+ underline: block.titleUnderline,
90
+ },
91
+ { align: 'center', bold: true },
92
+ );
93
+ const inner = title
94
+ ? `<h1 style="font-size:1.25em;margin:0 0 0.5em;${style}">${escapeHtmlText(title)}</h1>`
95
+ : '';
96
+ return { html: wrap(inner), counters };
97
+ }
98
+ case 'legal_reference': {
99
+ const ref = (block.body ?? block.title ?? '').trim();
100
+ const inner = ref
101
+ ? textToHtmlParagraphs(ref, {
102
+ align: block.bodyAlign ?? 'center',
103
+ bold: block.bodyBold,
104
+ italic: block.bodyItalic,
105
+ underline: block.bodyUnderline,
106
+ })
107
+ : '';
108
+ return { html: wrap(inner), counters };
109
+ }
110
+ case 'section': {
111
+ const level: LegalTermSectionLevel = block.level === 2 || block.level === 3 ? block.level : 1;
112
+ counters = bumpSectionCounters(counters, level);
113
+ const num = formatSectionNumber(counters, level);
114
+ const title = (block.title ?? '').trim();
115
+ const titleStyle = cssTextStyle(
116
+ {
117
+ align: block.titleAlign,
118
+ bold: block.titleBold,
119
+ italic: block.titleItalic,
120
+ underline: block.titleUnderline,
121
+ },
122
+ { align: 'left', bold: true },
123
+ );
124
+ const bodyHtml = textToHtmlParagraphs(block.body ?? '', {
125
+ align: block.bodyAlign,
126
+ bold: block.bodyBold,
127
+ italic: block.bodyItalic,
128
+ underline: block.bodyUnderline,
129
+ });
130
+ const heading = title
131
+ ? `<h2 style="font-size:${level === 1 ? '1.05em' : '1em'};margin:0.75em 0 0.35em;${titleStyle}">${escapeHtmlText(`${num} ${title}`)}</h2>`
132
+ : '';
133
+ return { html: wrap(`${heading}${bodyHtml}`), counters };
134
+ }
135
+ case 'paragraph': {
136
+ return {
137
+ html: wrap(
138
+ textToHtmlParagraphs(block.body ?? '', {
139
+ align: block.bodyAlign,
140
+ bold: block.bodyBold,
141
+ italic: block.bodyItalic,
142
+ underline: block.bodyUnderline,
143
+ }),
144
+ ),
145
+ counters,
146
+ };
147
+ }
148
+ case 'table_side_effects': {
149
+ const intro = textToHtmlParagraphs(block.body ?? '', {
150
+ align: block.bodyAlign,
151
+ bold: block.bodyBold,
152
+ italic: block.bodyItalic,
153
+ underline: block.bodyUnderline,
154
+ });
155
+ const token = '<p>{{ table_side_effects_en_list }}</p>';
156
+ return { html: wrap(`${intro}${token}`), counters };
157
+ }
158
+ case 'signature_footer': {
159
+ return {
160
+ html: wrap(
161
+ textToHtmlParagraphs(block.body ?? '', {
162
+ align: block.bodyAlign ?? 'left',
163
+ bold: block.bodyBold,
164
+ italic: block.bodyItalic,
165
+ underline: block.bodyUnderline,
166
+ }),
167
+ 'page-break-inside:avoid;margin-top:1.5em;padding-top:0.5em;border-top:1px solid #ddd',
168
+ ),
169
+ counters,
170
+ };
171
+ }
172
+ default:
173
+ return { html: '', counters };
174
+ }
175
+ }
176
+
177
+ export function compileLegalTermDocumentToHtml(document: LegalTermDocument): string {
178
+ if (!document?.blocks?.length) {
179
+ return '<p></p>';
180
+ }
181
+ let sectionCounters: [number, number, number] = [0, 0, 0];
182
+ const parts: string[] = [
183
+ '<div class="legal-term-document" style="font-family:Roboto,Helvetica,Arial,sans-serif;font-size:11pt;color:#111">',
184
+ ];
185
+ for (const block of document.blocks) {
186
+ const { html, counters } = compileBlock(block, sectionCounters);
187
+ sectionCounters = counters;
188
+ if (html) parts.push(html);
189
+ }
190
+ parts.push('</div>');
191
+ return parts.join('').trim() || '<p></p>';
192
+ }
@@ -0,0 +1,100 @@
1
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2
+ import { Type } from 'class-transformer';
3
+ import {
4
+ IsArray,
5
+ IsIn,
6
+ IsInt,
7
+ IsOptional,
8
+ IsString,
9
+ Max,
10
+ MaxLength,
11
+ Min,
12
+ MinLength,
13
+ ValidateNested,
14
+ } from 'class-validator';
15
+ import {
16
+ LEGAL_TERM_BLOCK_TYPES,
17
+ LEGAL_TERM_DOCUMENT_VERSION,
18
+ type LegalTermBlockType,
19
+ type LegalTermDocument,
20
+ type LegalTermSectionLevel,
21
+ } from './legal-term-document.types';
22
+
23
+ export class LegalTermDocumentBlockDto {
24
+ @ApiProperty({ description: 'Identificador estável do bloco (UUID ou id preset CFM)' })
25
+ @IsString()
26
+ @MinLength(1)
27
+ @MaxLength(64)
28
+ id: string;
29
+
30
+ @ApiProperty({ enum: LEGAL_TERM_BLOCK_TYPES })
31
+ @IsIn([...LEGAL_TERM_BLOCK_TYPES])
32
+ type: LegalTermBlockType;
33
+
34
+ @ApiPropertyOptional({ enum: [1, 2, 3] })
35
+ @IsOptional()
36
+ @IsInt()
37
+ @Min(1)
38
+ @Max(3)
39
+ level?: LegalTermSectionLevel;
40
+
41
+ @ApiPropertyOptional({ maxLength: 500 })
42
+ @IsOptional()
43
+ @IsString()
44
+ @MaxLength(500)
45
+ title?: string;
46
+
47
+ @ApiPropertyOptional({ maxLength: 50_000 })
48
+ @IsOptional()
49
+ @IsString()
50
+ @MaxLength(50_000)
51
+ body?: string;
52
+
53
+ @ApiPropertyOptional({ enum: ['left', 'center', 'right'] })
54
+ @IsOptional()
55
+ @IsIn(['left', 'center', 'right'])
56
+ titleAlign?: 'left' | 'center' | 'right';
57
+
58
+ @ApiPropertyOptional()
59
+ @IsOptional()
60
+ titleBold?: boolean;
61
+
62
+ @ApiPropertyOptional()
63
+ @IsOptional()
64
+ titleItalic?: boolean;
65
+
66
+ @ApiPropertyOptional()
67
+ @IsOptional()
68
+ titleUnderline?: boolean;
69
+
70
+ @ApiPropertyOptional({ enum: ['left', 'center', 'right'] })
71
+ @IsOptional()
72
+ @IsIn(['left', 'center', 'right'])
73
+ bodyAlign?: 'left' | 'center' | 'right';
74
+
75
+ @ApiPropertyOptional()
76
+ @IsOptional()
77
+ bodyBold?: boolean;
78
+
79
+ @ApiPropertyOptional()
80
+ @IsOptional()
81
+ bodyItalic?: boolean;
82
+
83
+ @ApiPropertyOptional()
84
+ @IsOptional()
85
+ bodyUnderline?: boolean;
86
+ }
87
+
88
+ export class LegalTermDocumentDto implements LegalTermDocument {
89
+ @ApiProperty({ enum: [LEGAL_TERM_DOCUMENT_VERSION] })
90
+ @IsInt()
91
+ @Min(1)
92
+ @Max(1)
93
+ version: 1;
94
+
95
+ @ApiProperty({ type: [LegalTermDocumentBlockDto] })
96
+ @IsArray()
97
+ @ValidateNested({ each: true })
98
+ @Type(() => LegalTermDocumentBlockDto)
99
+ blocks: LegalTermDocumentBlockDto[];
100
+ }
@@ -0,0 +1,42 @@
1
+ /** Blocos do editor visual do termo jurídico (sem HTML do usuário). */
2
+ export const LEGAL_TERM_BLOCK_TYPES = [
3
+ 'document_title',
4
+ 'legal_reference',
5
+ 'section',
6
+ 'paragraph',
7
+ 'table_side_effects',
8
+ 'signature_footer',
9
+ ] as const;
10
+
11
+ export type LegalTermBlockType = (typeof LEGAL_TERM_BLOCK_TYPES)[number];
12
+
13
+ export type LegalTermSectionLevel = 1 | 2 | 3;
14
+
15
+ export const LEGAL_TERM_TEXT_ALIGNS = ['left', 'center', 'right'] as const;
16
+ export type LegalTermTextAlign = (typeof LEGAL_TERM_TEXT_ALIGNS)[number];
17
+
18
+ export type LegalTermDocumentBlock = {
19
+ id: string;
20
+ type: LegalTermBlockType;
21
+ /** Nível de numeração para `section` (1 → 1., 2 → 1.1., 3 → 1.1.1.). */
22
+ level?: LegalTermSectionLevel;
23
+ /** Título visível (seção) ou ignorado em tipos sem título. */
24
+ title?: string;
25
+ /** Texto com placeholders `{{ token }}`; vazio em blocos só estruturais. */
26
+ body?: string;
27
+ titleAlign?: LegalTermTextAlign;
28
+ titleBold?: boolean;
29
+ titleItalic?: boolean;
30
+ titleUnderline?: boolean;
31
+ bodyAlign?: LegalTermTextAlign;
32
+ bodyBold?: boolean;
33
+ bodyItalic?: boolean;
34
+ bodyUnderline?: boolean;
35
+ };
36
+
37
+ export type LegalTermDocument = {
38
+ version: 1;
39
+ blocks: LegalTermDocumentBlock[];
40
+ };
41
+
42
+ export const LEGAL_TERM_DOCUMENT_VERSION = 1 as const;
@@ -11,8 +11,11 @@ import {
11
11
  MaxLength,
12
12
  MinLength,
13
13
  ValidateIf,
14
+ ValidateNested,
14
15
  } from 'class-validator';
16
+ import { Type } from 'class-transformer';
15
17
  import { LEGAL_TERM_BODY_FORMATS, type LegalTermBodyFormat } from './legal-term-body-format';
18
+ import { LegalTermDocumentDto } from './legal-term-document.dto';
16
19
 
17
20
  const LEGAL_TERM_SIGNATURE_METHODS = ['digital', 'printed'] as const;
18
21
  export type LegalTermSignatureMethodDto = (typeof LEGAL_TERM_SIGNATURE_METHODS)[number];
@@ -20,7 +23,16 @@ export type LegalTermSignatureMethodDto = (typeof LEGAL_TERM_SIGNATURE_METHODS)[
20
23
  /** Valores explícitos para substituição na pré-visualização (sem inferência heurística). */
21
24
  export class LegalTermPreviewRequestDto {
22
25
  @ApiPropertyOptional({
23
- description: 'HTML atual do editor (rascunho); se omitido, usa o modelo salvo no banco',
26
+ description: 'Rascunho do documento visual; se omitido, usa o modelo salvo',
27
+ type: LegalTermDocumentDto,
28
+ })
29
+ @IsOptional()
30
+ @ValidateNested()
31
+ @Type(() => LegalTermDocumentDto)
32
+ body_document?: LegalTermDocumentDto;
33
+
34
+ @ApiPropertyOptional({
35
+ description: 'HTML legado (rascunho); ignorado se body_document for enviado',
24
36
  maxLength: 500_000,
25
37
  })
26
38
  @IsOptional()
@@ -1,4 +1,5 @@
1
1
  import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2
+ import { Type } from 'class-transformer';
2
3
  import {
3
4
  IsArray,
4
5
  IsIn,
@@ -7,24 +8,32 @@ import {
7
8
  IsUUID,
8
9
  MaxLength,
9
10
  MinLength,
11
+ ValidateNested,
10
12
  } from 'class-validator';
11
13
  import { LEGAL_TERM_BODY_FORMATS, type LegalTermBodyFormat } from './legal-term-body-format';
14
+ import { LegalTermDocumentDto } from './legal-term-document.dto';
12
15
 
13
16
  export class LegalTermTemplateDto {
14
17
  @ApiProperty({ format: 'uuid' })
15
18
  id: string;
16
19
 
17
20
  @ApiProperty({
18
- description: 'Conteúdo do modelo (HTML, Markdown ou texto conforme body_format)',
21
+ description: 'HTML compilado a partir de body_document (cache para PDF)',
19
22
  })
20
23
  body_html: string;
21
24
 
22
25
  @ApiProperty({
23
26
  enum: LEGAL_TERM_BODY_FORMATS,
24
- description: 'Como interpretar body_html antes de gerar PDF',
27
+ description: 'structured: editor visual; demais formatos legados',
25
28
  })
26
29
  body_format: LegalTermBodyFormat;
27
30
 
31
+ @ApiProperty({
32
+ description: 'Documento estruturado do editor visual',
33
+ type: LegalTermDocumentDto,
34
+ })
35
+ body_document: LegalTermDocumentDto;
36
+
28
37
  @ApiProperty({
29
38
  type: [String],
30
39
  format: 'uuid',
@@ -41,18 +50,25 @@ export class LegalTermTemplateDto {
41
50
 
42
51
  export class UpsertLegalTermTemplateDto {
43
52
  @ApiProperty({
44
- description: 'Corpo do termo (HTML, Markdown ou texto puro conforme body_format)',
45
- minLength: 1,
53
+ description: 'Documento do editor visual (obrigatório no fluxo structured)',
54
+ type: LegalTermDocumentDto,
55
+ })
56
+ @ValidateNested()
57
+ @Type(() => LegalTermDocumentDto)
58
+ body_document: LegalTermDocumentDto;
59
+
60
+ @ApiPropertyOptional({
61
+ description: 'Ignorado no fluxo structured; servidor compila body_html',
46
62
  maxLength: 500_000,
47
63
  })
64
+ @IsOptional()
48
65
  @IsString()
49
- @MinLength(1)
50
66
  @MaxLength(500_000)
51
- body_html: string;
67
+ body_html?: string;
52
68
 
53
69
  @ApiPropertyOptional({
54
70
  enum: LEGAL_TERM_BODY_FORMATS,
55
- description: 'Padrão: html',
71
+ description: 'Padrão: structured',
56
72
  })
57
73
  @IsOptional()
58
74
  @IsIn(LEGAL_TERM_BODY_FORMATS)
@@ -10,6 +10,7 @@ export {
10
10
  TOPIC_NOTIFICATIONS_REGISTER_PUSH_TOKEN,
11
11
  TOPIC_NOTIFICATIONS_UNREGISTER_PUSH_TOKEN,
12
12
  TOPIC_NOTIFICATIONS_UPDATE_VIEWED,
13
+ TOPIC_NOTIFICATIONS_MARK_ALL_VIEWED,
13
14
  TOPIC_NOTIFICATIONS_SEND_PUSH_TO_USER,
14
15
  EVENT_NOTIFICATIONS_ADMIN_BROADCAST_PUSH,
15
16
  EVENT_NOTIFICATIONS_CONVERSATION_HANDOFF_PUSH,
@@ -40,3 +41,7 @@ export {
40
41
  NotificationsUpdateViewedKafkaDto,
41
42
  } from './update-notification-viewed.dto';
42
43
  export { NotificationsSendPushToUserKafkaDto } from './notifications-send-push-to-user.dto';
44
+ export {
45
+ MarkAllNotificationsViewedResultDto,
46
+ NotificationsMarkAllViewedKafkaDto,
47
+ } from './mark-all-notifications-viewed.dto';
@@ -0,0 +1,33 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsInt, IsNotEmpty, IsString, IsUUID, Min } from 'class-validator';
3
+
4
+ /**
5
+ * Resposta HTTP PUT /notifications/mark-all-viewed e RPC `notifications.markAllViewed`.
6
+ */
7
+ export class MarkAllNotificationsViewedResultDto {
8
+ @ApiProperty({
9
+ description: 'Quantidade de notificações atualizadas (viewed_at preenchido).',
10
+ example: 12,
11
+ minimum: 0,
12
+ })
13
+ @IsInt()
14
+ @Min(0)
15
+ updatedCount: number;
16
+ }
17
+
18
+ /**
19
+ * Payload RPC `notifications.markAllViewed` (sem body HTTP — só tenant + utilizador).
20
+ */
21
+ export class NotificationsMarkAllViewedKafkaDto {
22
+ @ApiProperty({ example: 'default' })
23
+ @IsString()
24
+ @IsNotEmpty()
25
+ tenant: string;
26
+
27
+ @ApiProperty({
28
+ description: 'ID do utilizador (ex.: JWT sub).',
29
+ format: 'uuid',
30
+ })
31
+ @IsUUID('4')
32
+ userId: string;
33
+ }
@@ -10,6 +10,9 @@ export const TOPIC_NOTIFICATIONS_UNREGISTER_PUSH_TOKEN =
10
10
  /** Kafka request–reply: atualizar visualização de uma notificação persistida. */
11
11
  export const TOPIC_NOTIFICATIONS_UPDATE_VIEWED = 'notifications.updateViewed';
12
12
 
13
+ /** Kafka request–reply: marcar todas as notificações não visualizadas do utilizador. */
14
+ export const TOPIC_NOTIFICATIONS_MARK_ALL_VIEWED = 'notifications.markAllViewed';
15
+
13
16
  /** RabbitMQ request–reply: enviar push FCM a um utilizador (serviços internos). */
14
17
  export const TOPIC_NOTIFICATIONS_SEND_PUSH_TO_USER = 'notifications.sendPushToUser';
15
18