@silexpert/core 2.0.123-beta.0 → 2.0.123

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silexpert/core",
3
- "version": "2.0.123-beta.0",
3
+ "version": "2.0.123",
4
4
  "description": "Silex Core perform HTTP Request and object binding. Silex core helps developers to retrieve and return formatted object",
5
5
  "keywords": [
6
6
  "accounting",
@@ -1,16 +1,17 @@
1
1
  import {
2
+ CountUnreadZendeskTickets,
3
+ CountUnreadZendeskTicketsResponse,
2
4
  CreateZendeskTicket,
3
5
  UpdateZendeskTicket,
4
6
  ListZendeskTickets,
5
- TicketZendeskResponse,
6
- SearchZendeskResponse,
7
+ ListZendeskTicketsResponse,
7
8
  TicketFormEnriched,
8
9
  TicketZendesk,
9
10
  } from '../../../index.js';
10
11
  import { Resource } from '../../Resource.js';
11
12
 
12
13
  export class ZendeskTicket extends Resource {
13
- getPaginate(query: ListZendeskTickets): Promise<TicketZendeskResponse | SearchZendeskResponse> {
14
+ getPaginate(query: ListZendeskTickets): Promise<ListZendeskTicketsResponse> {
14
15
  return this.axios.$get('connectors/zendesk/tickets', { params: query });
15
16
  }
16
17
 
@@ -18,6 +19,10 @@ export class ZendeskTicket extends Resource {
18
19
  return this.axios.$get('connectors/zendesk/tickets/forms');
19
20
  }
20
21
 
22
+ getUnreadCount(query: CountUnreadZendeskTickets): Promise<CountUnreadZendeskTicketsResponse> {
23
+ return this.axios.$get('connectors/zendesk/tickets/unread-count', { params: query });
24
+ }
25
+
21
26
  get(id: number): Promise<TicketZendesk> {
22
27
  return this.axios.$get(`connectors/zendesk/tickets/${id}`);
23
28
  }
@@ -43,6 +48,10 @@ export class ZendeskTicket extends Resource {
43
48
  return this.axios.$put(`connectors/zendesk/tickets/${id}`, data);
44
49
  }
45
50
 
51
+ markAsRead(id: number): Promise<void> {
52
+ return this.axios.$put(`connectors/zendesk/tickets/${id}/read`);
53
+ }
54
+
46
55
  delete(id: number): Promise<void> {
47
56
  return this.axios.$delete(`connectors/zendesk/tickets/${id}`);
48
57
  }
@@ -125,3 +125,31 @@ export const ZendeskTicketCategoryList: Array<{ id: ZendeskTicketCategoryEnum; l
125
125
  { id: ZendeskTicketCategoryEnum.RELANCE_BILAN, label: 'Relance bilan' },
126
126
  { id: ZendeskTicketCategoryEnum.SUPPORT_APPLICATION, label: 'Support application' },
127
127
  ];
128
+
129
+ export type ZendeskTicketStatusGroup = 'information_required' | 'in_progress' | 'received' | 'resolved' | 'closed';
130
+
131
+ export enum ZendeskTicketStatusGroupEnum {
132
+ INFORMATION_REQUIRED = 'information_required',
133
+ IN_PROGRESS = 'in_progress',
134
+ RECEIVED = 'received',
135
+ RESOLVED = 'resolved',
136
+ CLOSED = 'closed',
137
+ }
138
+
139
+ export const ZendeskTicketStatusGroupByStatus: Record<ZendeskTicketStatusEnum, ZendeskTicketStatusGroupEnum> = {
140
+ [ZendeskTicketStatusEnum.NEW]: ZendeskTicketStatusGroupEnum.RECEIVED,
141
+ [ZendeskTicketStatusEnum.OPEN]: ZendeskTicketStatusGroupEnum.IN_PROGRESS,
142
+ [ZendeskTicketStatusEnum.HOLD]: ZendeskTicketStatusGroupEnum.IN_PROGRESS,
143
+ [ZendeskTicketStatusEnum.PENDING]: ZendeskTicketStatusGroupEnum.INFORMATION_REQUIRED,
144
+ [ZendeskTicketStatusEnum.SOLVED]: ZendeskTicketStatusGroupEnum.RESOLVED,
145
+ [ZendeskTicketStatusEnum.CLOSED]: ZendeskTicketStatusGroupEnum.CLOSED,
146
+ };
147
+
148
+ // Display order of the support page: groups needing customer action come first.
149
+ export const ZendeskTicketStatusGroupList: Array<{ id: ZendeskTicketStatusGroupEnum; label: string }> = [
150
+ { id: ZendeskTicketStatusGroupEnum.INFORMATION_REQUIRED, label: 'Information requise' },
151
+ { id: ZendeskTicketStatusGroupEnum.IN_PROGRESS, label: 'En cours' },
152
+ { id: ZendeskTicketStatusGroupEnum.RECEIVED, label: 'Demande reçue' },
153
+ { id: ZendeskTicketStatusGroupEnum.RESOLVED, label: 'Résolu' },
154
+ { id: ZendeskTicketStatusGroupEnum.CLOSED, label: 'Archivé' },
155
+ ];
@@ -10,6 +10,7 @@ import {
10
10
  Min,
11
11
  Max,
12
12
  IsDateString,
13
+ IsBoolean,
13
14
  } from 'class-validator';
14
15
  import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
15
16
  import {
@@ -21,9 +22,13 @@ import {
21
22
  ZendeskTicketPriorityEnum,
22
23
  type ZendeskTicketStatus,
23
24
  ZendeskTicketStatusEnum,
25
+ type ZendeskTicketStatusGroup,
26
+ ZendeskTicketStatusGroupEnum,
24
27
  type ZendeskTicketType,
25
28
  ZendeskTicketTypeEnum,
26
29
  } from '../../../../types/Enum/Zendesk.js';
30
+ import { ToBoolean } from '../../../../utils/transforms/boolean.transform.js';
31
+ import { ToStringArray } from '../../../../utils/transforms/array.transform.js';
27
32
 
28
33
  export class CreateZendeskTicket {
29
34
  @ApiProperty()
@@ -193,13 +198,36 @@ export class ListZendeskTickets {
193
198
  @IsDateString()
194
199
  endDate?: string;
195
200
 
201
+ /**
202
+ * @deprecated Tickets are now sorted by status group, unread state and last public message date.
203
+ * Only `sortOrder` is taken into account.
204
+ */
196
205
  @ApiPropertyOptional()
197
206
  @IsOptional()
198
207
  @IsString()
199
208
  sortBy?: string;
200
209
 
201
- @ApiPropertyOptional()
210
+ @ApiPropertyOptional({ description: 'Order applied to the last public message date' })
202
211
  @IsOptional()
203
212
  @IsEnum(['asc', 'desc'])
204
213
  sortOrder?: 'asc' | 'desc';
214
+
215
+ @ApiPropertyOptional({ enum: ZendeskTicketStatusGroupEnum, isArray: true })
216
+ @IsOptional()
217
+ @ToStringArray()
218
+ @IsArray()
219
+ @IsEnum(ZendeskTicketStatusGroupEnum, { each: true })
220
+ statusGroups?: ZendeskTicketStatusGroup[];
221
+
222
+ @ApiPropertyOptional()
223
+ @IsOptional()
224
+ @ToBoolean()
225
+ @IsBoolean()
226
+ isUnread?: boolean;
227
+ }
228
+
229
+ export class CountUnreadZendeskTickets {
230
+ @ApiProperty()
231
+ @IsEnum(ZendeskTicketChannelEnum)
232
+ channel: ZendeskTicketChannel;
205
233
  }
@@ -2,8 +2,10 @@ import {
2
2
  ZendeskTicketChannel,
3
3
  ZendeskTicketPriority,
4
4
  ZendeskTicketStatus,
5
+ ZendeskTicketStatusGroup,
5
6
  ZendeskTicketType,
6
7
  } from '../../../Enum/Zendesk.js';
8
+ import type { BasePaginate } from '../BasePaginate.js';
7
9
 
8
10
  export interface TicketZendesk {
9
11
  id: number;
@@ -22,6 +24,19 @@ export interface TicketZendesk {
22
24
  organizationId: number | null;
23
25
  createdAt: string;
24
26
  updatedAt: string;
27
+ // Read-tracking data, only computed on the paginated list
28
+ statusGroup?: ZendeskTicketStatusGroup;
29
+ isUnread?: boolean;
30
+ lastActivityAt?: string;
31
+ }
32
+
33
+ export interface ListZendeskTicketsResponse extends BasePaginate<TicketZendesk> {
34
+ // Unread tickets of the channel whatever the filters, to refresh the unread badge with the list.
35
+ unreadCount: number;
36
+ }
37
+
38
+ export interface CountUnreadZendeskTicketsResponse {
39
+ count: number;
25
40
  }
26
41
 
27
42
  export interface SearchZendeskResponse {
@@ -23,6 +23,7 @@ export enum Code {
23
23
  CONNECTION_ALREADY_EXIST = 'connection_already_exist',
24
24
  firstConnectionFromVpn = 'first_connection_from_vpn',
25
25
  ENTITY_ALREADY_EXIST = 'entity_already_exist',
26
+ tooManyRequest = 'too_many_requests',
26
27
 
27
28
  // Annotation Error
28
29
  failedCheckBalance = 'failed_check_balance',
@@ -0,0 +1,15 @@
1
+ import type { IUser } from './IUser.js';
2
+
3
+ export interface IZendeskTicketRead {
4
+ id?: number;
5
+ idUser: number;
6
+ idTicket: number;
7
+ readAt?: Date | null;
8
+ // Activity snapshot of the Zendesk ticket, refreshed only when `ticketUpdatedAt` changes
9
+ ticketUpdatedAt?: Date | null;
10
+ lastPublicCommentAt?: Date | null;
11
+ lastSupportCommentAt?: Date | null;
12
+ createdAt?: Date;
13
+ updatedAt?: Date;
14
+ user?: IUser;
15
+ }
package/ts/types/index.ts CHANGED
@@ -290,6 +290,7 @@ export * from './Interface/models/IRevenueSnapshot.js';
290
290
  export * from './Interface/models/IEInvoicingEligibilityRequest.js';
291
291
  export * from './Interface/models/IUsersHistory.js';
292
292
  export * from './Interface/api/teamSystemSignature/index.js';
293
+ export * from './Interface/models/IZendeskTicketRead.js';
293
294
  export * from './Interface/models/IEInvoicingKycAndMandate.js';
294
295
  export * from './Interface/models/IConnectorTracking.js';
295
296
  export * from './Interface/models/IEInvoicing.js';
@@ -171,28 +171,85 @@ export function generateCommercialDocument(
171
171
  : payload?.settings?.freeInvoiceText;
172
172
 
173
173
  return {
174
- header: ejs.render(commonTemplate.header, data),
175
- body: ejs.render(bodyTemplate, {
176
- ...data,
177
- comment:
178
- data.comment && typeof data.comment === 'string'
179
- ? sanitize(data.comment?.replace(/\n/g, '<br/>'), { allowedTags: ['b', 'i', 'em', 'br', 'strong'] })
180
- : null,
181
- products: data.products.map((p) => ({
182
- ...p,
174
+ header: ejs.render(commonTemplate.header, data, {}),
175
+ body: ejs.render(
176
+ bodyTemplate,
177
+ {
178
+ ...data,
179
+ cgv:
180
+ data.cgv && typeof data.cgv === 'string'
181
+ ? sanitize(data.cgv, {
182
+ allowedTags: [
183
+ 'b',
184
+ 'i',
185
+ 'em',
186
+ 'br',
187
+ 'strong',
188
+ 'p',
189
+ 'span',
190
+ 'li',
191
+ 'ul',
192
+ 'ol',
193
+ 'h1',
194
+ 'h2',
195
+ 'h3',
196
+ 'u',
197
+ 's',
198
+ 'a',
199
+ 'blockquote',
200
+ ],
201
+ allowedAttributes: {
202
+ a: ['href', 'name', 'target', 'data-list'],
203
+ li: ['data-list'],
204
+ '*': ['class', 'style', 'dir'],
205
+ },
206
+ allowedClasses: {
207
+ '*': [/^ql-/],
208
+ },
209
+ allowedStyles: {
210
+ '*': {
211
+ color: [/^[a-z]+$/i, /^#(?:[0-9a-f]{3}){1,2}$/i, /^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/i],
212
+ 'background-color': [
213
+ /^[a-z]+$/i,
214
+ /^#(?:[0-9a-f]{3}){1,2}$/i,
215
+ /^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/i,
216
+ ],
217
+ 'text-align': [/^left$/i, /^right$/i, /^center$/i, /^justify$/i],
218
+ 'white-space': [/^pre-wrap$/i],
219
+ },
220
+ },
221
+ })
222
+ : null,
183
223
  comment:
184
- p.comment && typeof p.comment === 'string'
185
- ? sanitize(p.comment?.replace(/\n/g, '<br/>'), { allowedTags: ['b', 'i', 'em', 'br', 'strong'] })
224
+ data.comment && typeof data.comment === 'string'
225
+ ? sanitize(data.comment?.replace(/\n/g, '<br/>'), { allowedTags: ['b', 'i', 'em', 'br', 'strong'] })
186
226
  : null,
187
- // @ts-expect-error
188
- unitFormatted: internationalizationConsistency(locale, p.unitFormatted, 'unit'),
189
- })),
190
- vats: forcedVats ?? vats,
191
- }),
192
- footer: ejs.render(commonTemplate.footer, {
193
- ...data,
194
- freeText: documentFreeText ?? null,
195
- }),
227
+ products: data.products.map((p) => ({
228
+ ...p,
229
+ comment:
230
+ p.comment && typeof p.comment === 'string'
231
+ ? sanitize(p.comment?.replace(/\n/g, '<br/>'), { allowedTags: ['b', 'i', 'em', 'br', 'strong'] })
232
+ : null,
233
+ // @ts-expect-error
234
+ unitFormatted: internationalizationConsistency(locale, p.unitFormatted, 'unit'),
235
+ })),
236
+ vats: forcedVats ?? vats,
237
+ },
238
+ {},
239
+ ),
240
+ footer: ejs.render(
241
+ commonTemplate.footer,
242
+ {
243
+ ...data,
244
+ freeText:
245
+ documentFreeText && typeof documentFreeText === 'string'
246
+ ? sanitize(documentFreeText?.replace(/\n/g, '<br/>'), {
247
+ allowedTags: ['b', 'i', 'em', 'br', 'strong', 'p'],
248
+ })
249
+ : null,
250
+ },
251
+ {},
252
+ ),
196
253
  data,
197
254
  };
198
255
  }
@@ -227,7 +284,7 @@ export function generateMailHeader(
227
284
  const mailTemplate = new CommercialDocumentMailHTML();
228
285
 
229
286
  return {
230
- header: ejs.render(mailTemplate.header, data),
287
+ header: ejs.render(mailTemplate.header, data, {}),
231
288
  };
232
289
  }
233
290
 
@@ -334,7 +334,7 @@ export class CommercialDocumentMailHTML {
334
334
  </style>
335
335
  <div style="background-color: <%= brandColors.primary50 %> !important; color: black; width: calc(100% - 48px); display: grid; border-radius: 16px; padding: 24px;">
336
336
  <div style="width: max-content; margin: auto; font-size: 16px; font-family: 'circular std', sans-serif; font-weight: 100; line-height: 140%; position: relative;">
337
- <div style="color: black;"><span style="font-weight: 500;"><%- companyName %></span> <%= languageMail.mail.common.send %>
337
+ <div style="color: black;"><span style="font-weight: 500;"><%= companyName %></span> <%= languageMail.mail.common.send %>
338
338
  <% if(idEstimateInvoiceType) { %>
339
339
  <% if ([${EstimateInvoiceType.FACTURE}, ${EstimateInvoiceType.RESTE}].includes(idEstimateInvoiceType)) { %>
340
340
  <%= languageMail.mail.invoice.title %>
@@ -370,7 +370,7 @@ export class CommercialDocumentMailHTML {
370
370
  <% } else { %>
371
371
  <%= languageMail.mail.default.subTitle %>
372
372
  <% } %>
373
- <%- reference %>
373
+ <%= reference %>
374
374
  </div>
375
375
  <div style="; color: <%= brandColors.primary500 %>; font-size: 14px;">
376
376
  <%= languageMail.mail.common.dueDate %> : <%- dueDateFormatted %>
package/ts/utils/email.ts CHANGED
@@ -366,25 +366,30 @@ export function generateSignature(params: any): string {
366
366
  user.calendlyUrl = calendlyUrl;
367
367
  }
368
368
 
369
- return ejs.render(signatureTemplate, {
370
- forceRole,
371
- isFacturation: user.roles ? user.roles.some((r: any) => r.id === Role.FACTURATION) : false,
372
- isDataOfficer: user.roles ? user.roles.some((r: any) => r.id === Role.DATA_OFFICER) : true,
373
- brandName,
374
- brandPostalAddress,
375
- brandCity,
376
- idTypeBrand,
377
- commercialName,
378
- iban,
379
- bic,
380
- hostS3,
381
- user,
382
- roleSignature,
383
- mailSignature,
384
- phoneSignature: commercialName !== 'Comptalib' && phoneSignature ? phoneSignature.replace(/\s+/gm, '&nbsp;') : null,
385
- standardPhoneSignature: commercialName !== 'Comptalib' ? standardPhoneSignature.replace(/\s+/gm, '&nbsp;') : null,
386
- horaires,
387
- primaryColor,
388
- supportMail,
389
- });
369
+ return ejs.render(
370
+ signatureTemplate,
371
+ {
372
+ forceRole,
373
+ isFacturation: user.roles ? user.roles.some((r: any) => r.id === Role.FACTURATION) : false,
374
+ isDataOfficer: user.roles ? user.roles.some((r: any) => r.id === Role.DATA_OFFICER) : true,
375
+ brandName,
376
+ brandPostalAddress,
377
+ brandCity,
378
+ idTypeBrand,
379
+ commercialName,
380
+ iban,
381
+ bic,
382
+ hostS3,
383
+ user,
384
+ roleSignature,
385
+ mailSignature,
386
+ phoneSignature:
387
+ commercialName !== 'Comptalib' && phoneSignature ? phoneSignature.replace(/\s+/gm, '&nbsp;') : null,
388
+ standardPhoneSignature: commercialName !== 'Comptalib' ? standardPhoneSignature.replace(/\s+/gm, '&nbsp;') : null,
389
+ horaires,
390
+ primaryColor,
391
+ supportMail,
392
+ },
393
+ {},
394
+ );
390
395
  }
@@ -24,7 +24,7 @@ const payloadWithoutSellerAddress = {
24
24
  reference: '202609/94',
25
25
  capital: 0,
26
26
  isDraft: false,
27
- cgv: '<p><br></p>',
27
+ cgv: '<p>Test avec une liste</p><ol><li data-list="bullet"><span class="ql-ui"></span>1</li><li data-list="bullet"><span class="ql-ui"></span>2</li></ol></p>',
28
28
  logoUrl: null,
29
29
  disableVat: false,
30
30
  nafCode: null,
@@ -94,7 +94,8 @@ describe('generateCommercialDocument', () => {
94
94
  test.each([true, false])(
95
95
  'should render CGV HTML only when invoiceCgv is enabled (%s)',
96
96
  (invoiceCgv: boolean): void => {
97
- const cgv = '<p>Conditions de vente <strong>applicables</strong></p>';
97
+ const cgv =
98
+ '<p>Test avec une liste</p><ol><li data-list="bullet"><span class="ql-ui"></span>1</li><li data-list="bullet"><span class="ql-ui"></span>2</li></ol>';
98
99
  const payload: CreateInvoiceDocument = {
99
100
  ...payloadWithoutSellerAddress,
100
101
  cgv,
@@ -0,0 +1,35 @@
1
+ import { Transform } from 'class-transformer';
2
+
3
+ const valueToStringArray = (value: any) => {
4
+ if (value === null || value === undefined || value === '') {
5
+ return undefined;
6
+ }
7
+ if (Array.isArray(value)) {
8
+ return value.map((v) => String(v).trim());
9
+ }
10
+ return String(value)
11
+ .split(',')
12
+ .map((v) => v.trim())
13
+ .filter((v) => v !== '');
14
+ };
15
+
16
+ // Accepts repeated query params (`a=x&a=y`) as well as comma-separated values (`a=x,y`).
17
+ const ToStringArray = () => {
18
+ const toPlain = Transform(
19
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
20
+ ({ value }) => value,
21
+ {
22
+ toPlainOnly: true,
23
+ },
24
+ );
25
+ const toClass = (target: any, key: string) =>
26
+ Transform(({ obj }) => valueToStringArray(obj[key]), {
27
+ toClassOnly: true,
28
+ })(target, key);
29
+ return (target: any, key: string) => {
30
+ toPlain(target, key);
31
+ toClass(target, key);
32
+ };
33
+ };
34
+
35
+ export { ToStringArray };