@webitel/api-services 26.8.70 → 26.8.72

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 (55) hide show
  1. package/package.json +1 -1
  2. package/src/api/clients/outboundResources/outboundResources.ts +44 -81
  3. package/src/api/clients/outboundResources/resourceDisplays.ts +106 -32
  4. package/src/enums/ResourceCidType/ResourceCidType.ts +8 -0
  5. package/src/enums/ResourceIgnoreEarlyMedia/ResourceIgnoreEarlyMedia.ts +9 -0
  6. package/src/enums/index.ts +4 -0
  7. package/src/locale/en/en.ts +4 -0
  8. package/src/locale/es/es.ts +4 -0
  9. package/src/locale/kz/kz.ts +4 -0
  10. package/src/locale/pl/pl.ts +4 -0
  11. package/src/locale/ro/ro.ts +4 -0
  12. package/src/locale/ru/ru.ts +4 -0
  13. package/src/locale/uk/uk.ts +4 -0
  14. package/src/locale/uz/uz.ts +4 -0
  15. package/src/locale/vi/vi.ts +4 -0
  16. package/src/scripts/convertTimeUnits/__tests__/convertTimeUnits.spec.ts +19 -0
  17. package/src/scripts/convertTimeUnits/convertTimeUnits.ts +3 -0
  18. package/src/scripts/index.ts +3 -0
  19. package/src/validations/_shared/timeRange.validations.ts +103 -0
  20. package/src/validations/calendar/__tests__/calendar.validations.spec.ts +9 -9
  21. package/src/validations/calendar/calendar.validations.ts +10 -131
  22. package/src/validations/index.ts +5 -0
  23. package/src/validations/resource/__tests__/resource.validations.spec.ts +102 -0
  24. package/src/validations/resource/resource.validations.ts +28 -0
  25. package/src/validations/resourceDisplay/__tests__/resourceDisplay.validations.spec.ts +45 -0
  26. package/src/validations/resourceDisplay/resourceDisplay.validations.ts +9 -0
  27. package/src/validations/resourceGroup/__tests__/resourceGroup.validations.spec.ts +191 -0
  28. package/src/validations/resourceGroup/resourceGroup.validations.ts +36 -0
  29. package/src/validations/resourceInGroup/__tests__/resourceInGroup.validations.spec.ts +38 -0
  30. package/src/validations/resourceInGroup/resourceInGroup.validations.ts +16 -0
  31. package/types/.tsbuildinfo +1 -1
  32. package/types/api/clients/outboundResources/outboundResources.d.ts +2 -7
  33. package/types/api/clients/outboundResources/resourceDisplays.d.ts +7 -1
  34. package/types/enums/ResourceCidType/ResourceCidType.d.ts +6 -0
  35. package/types/enums/ResourceIgnoreEarlyMedia/ResourceIgnoreEarlyMedia.d.ts +7 -0
  36. package/types/enums/index.d.ts +3 -1
  37. package/types/locale/en/en.d.ts +3 -0
  38. package/types/locale/es/es.d.ts +3 -0
  39. package/types/locale/index.d.ts +27 -0
  40. package/types/locale/kz/kz.d.ts +3 -0
  41. package/types/locale/pl/pl.d.ts +3 -0
  42. package/types/locale/ro/ro.d.ts +3 -0
  43. package/types/locale/ru/ru.d.ts +3 -0
  44. package/types/locale/uk/uk.d.ts +3 -0
  45. package/types/locale/uz/uz.d.ts +3 -0
  46. package/types/locale/vi/vi.d.ts +3 -0
  47. package/types/scripts/convertTimeUnits/convertTimeUnits.d.ts +2 -0
  48. package/types/scripts/index.d.ts +2 -1
  49. package/types/validations/_shared/timeRange.validations.d.ts +18 -0
  50. package/types/validations/calendar/calendar.validations.d.ts +1 -7
  51. package/types/validations/index.d.ts +5 -0
  52. package/types/validations/resource/resource.validations.d.ts +26 -0
  53. package/types/validations/resourceDisplay/resourceDisplay.validations.d.ts +5 -0
  54. package/types/validations/resourceGroup/resourceGroup.validations.d.ts +17 -0
  55. package/types/validations/resourceInGroup/resourceInGroup.validations.d.ts +8 -0
@@ -0,0 +1,103 @@
1
+ import { z } from 'zod';
2
+
3
+ import { i18nIssue } from './i18nIssue';
4
+
5
+ export const dayMinuteSchema = z
6
+ .number()
7
+ .int()
8
+ .refine((value) => value >= 0 && value < 24 * 60, i18nIssue('hourRange'));
9
+
10
+ export type TimeRange = {
11
+ start: number;
12
+ end: number;
13
+ };
14
+
15
+ export type TimeRangeError = {
16
+ index: number;
17
+ prop: string;
18
+ key: string;
19
+ };
20
+
21
+ const RANGE_PROPS = [
22
+ 'start',
23
+ 'end',
24
+ ] as const;
25
+
26
+ const addRangeIssue = (
27
+ ctx: z.RefinementCtx,
28
+ key: string,
29
+ ...path: number[]
30
+ ) => {
31
+ RANGE_PROPS.forEach((prop) => {
32
+ ctx.addIssue({
33
+ code: 'custom',
34
+ path: [
35
+ ...path,
36
+ prop,
37
+ ],
38
+ ...i18nIssue(key),
39
+ });
40
+ });
41
+ };
42
+
43
+ export const refineTimeRangeStartLessThanEnd = (
44
+ item: TimeRange,
45
+ ctx: z.RefinementCtx,
46
+ ) => {
47
+ if (item.start >= item.end) addRangeIssue(ctx, 'timerangeStartLessThanEnd');
48
+ };
49
+
50
+ export const timeRangeSchema = z
51
+ .object({
52
+ start: dayMinuteSchema,
53
+ end: dayMinuteSchema,
54
+ })
55
+ .superRefine(refineTimeRangeStartLessThanEnd);
56
+
57
+ const isIntersecting = (a: TimeRange, b: TimeRange) =>
58
+ a.start <= b.end && b.start <= a.end;
59
+
60
+ export const refineTimeRangesNotIntersect =
61
+ <T extends TimeRange>(groupBy?: (item: T) => unknown) =>
62
+ (items: T[], ctx: z.RefinementCtx) => {
63
+ items.forEach((item, index) => {
64
+ const hasIntersection = items.some(
65
+ (other, otherIndex) =>
66
+ otherIndex !== index &&
67
+ groupBy?.(item) === groupBy?.(other) &&
68
+ isIntersecting(item, other),
69
+ );
70
+
71
+ if (hasIntersection) addRangeIssue(ctx, 'timerangeNotIntersect', index);
72
+ });
73
+ };
74
+
75
+ export const getTimeRangeErrors = (
76
+ schema: z.ZodType,
77
+ items: unknown,
78
+ ): TimeRangeError[] => {
79
+ const result = schema.safeParse(items);
80
+
81
+ if (result.success) return [];
82
+
83
+ return result.error.issues.flatMap((issue) => {
84
+ const [index, prop] = issue.path;
85
+ const key = issue.code === 'custom' ? issue.params?.i18nKey : undefined;
86
+
87
+ if (
88
+ typeof index !== 'number' ||
89
+ typeof prop !== 'string' ||
90
+ typeof key !== 'string'
91
+ ) {
92
+ return [];
93
+ }
94
+
95
+ return [
96
+ {
97
+ index,
98
+ prop,
99
+ key,
100
+ },
101
+ ];
102
+ });
103
+ };
@@ -4,7 +4,7 @@ import type { z } from 'zod';
4
4
  import * as validationsIndex from '../../index';
5
5
  import {
6
6
  calendarSchema,
7
- getCalendarDayRangeIssues,
7
+ getCalendarDayRangeErrors,
8
8
  } from '../calendar.validations';
9
9
 
10
10
  /** custom rules carry their message key in params, see `i18nIssue` */
@@ -668,7 +668,7 @@ describe('validations barrel export', () => {
668
668
  });
669
669
  });
670
670
 
671
- describe('getCalendarDayRangeIssues', () => {
671
+ describe('getCalendarDayRangeErrors', () => {
672
672
  const row = (day: number, start: number, end: number) => ({
673
673
  day,
674
674
  disabled: false,
@@ -678,7 +678,7 @@ describe('getCalendarDayRangeIssues', () => {
678
678
 
679
679
  it('reports nothing for valid rows', () => {
680
680
  expect(
681
- getCalendarDayRangeIssues([
681
+ getCalendarDayRangeErrors([
682
682
  row(0, 540, 541),
683
683
  row(0, 600, 660),
684
684
  row(1, 540, 1200),
@@ -688,7 +688,7 @@ describe('getCalendarDayRangeIssues', () => {
688
688
 
689
689
  it('reports both ends of a row whose start is not before its end', () => {
690
690
  expect(
691
- getCalendarDayRangeIssues([
691
+ getCalendarDayRangeErrors([
692
692
  row(0, 600, 540),
693
693
  ]),
694
694
  ).toEqual([
@@ -706,15 +706,15 @@ describe('getCalendarDayRangeIssues', () => {
706
706
  });
707
707
 
708
708
  it('reports every row of an overlap on the same day', () => {
709
- const issues = getCalendarDayRangeIssues([
709
+ const errors = getCalendarDayRangeErrors([
710
710
  row(0, 540, 720),
711
711
  row(0, 600, 780),
712
712
  ]);
713
713
 
714
- expect(issues.every(({ key }) => key === 'timerangeNotIntersect')).toBe(
714
+ expect(errors.every(({ key }) => key === 'timerangeNotIntersect')).toBe(
715
715
  true,
716
716
  );
717
- expect(issues.map(({ index, prop }) => `${index}.${prop}`).sort()).toEqual([
717
+ expect(errors.map(({ index, prop }) => `${index}.${prop}`).sort()).toEqual([
718
718
  '0.end',
719
719
  '0.start',
720
720
  '1.end',
@@ -724,7 +724,7 @@ describe('getCalendarDayRangeIssues', () => {
724
724
 
725
725
  it('does not treat the same hours on different days as an overlap', () => {
726
726
  expect(
727
- getCalendarDayRangeIssues([
727
+ getCalendarDayRangeErrors([
728
728
  row(0, 540, 720),
729
729
  row(2, 540, 720),
730
730
  ]),
@@ -733,7 +733,7 @@ describe('getCalendarDayRangeIssues', () => {
733
733
 
734
734
  it('reports a minute outside the day', () => {
735
735
  expect(
736
- getCalendarDayRangeIssues([
736
+ getCalendarDayRangeErrors([
737
737
  row(0, 0, 24 * 60),
738
738
  ]),
739
739
  ).toEqual([
@@ -1,67 +1,14 @@
1
1
  import type { EngineCalendar } from '@webitel/api-services/gen/models';
2
2
  import { z } from 'zod';
3
- import { i18nIssue } from '../_shared/i18nIssue';
4
3
  import { requiredLookupSchema } from '../_shared/lookup.validations';
4
+ import {
5
+ dayMinuteSchema,
6
+ getTimeRangeErrors,
7
+ refineTimeRangeStartLessThanEnd,
8
+ refineTimeRangesNotIntersect,
9
+ } from '../_shared/timeRange.validations';
5
10
  import type { ZodShape } from '../types';
6
11
 
7
- const HOUR_RANGE_KEY = 'hourRange';
8
- const TIMERANGE_START_LESS_THAN_END_KEY = 'timerangeStartLessThanEnd';
9
- const TIMERANGE_NOT_INTERSECT_KEY = 'timerangeNotIntersect';
10
-
11
- /** Legacy hourRange: value >= 0 && value < 1440 */
12
- const dayMinuteSchema = z
13
- .number()
14
- .int()
15
- .refine((value) => value >= 0 && value < 24 * 60, i18nIssue(HOUR_RANGE_KEY));
16
-
17
- type AcceptOfDayUi = {
18
- day: number;
19
- start: number;
20
- end: number;
21
- };
22
-
23
- const getIntersectingIndices = (items: AcceptOfDayUi[]) => {
24
- const indices = new Set<number>();
25
- const indicesByDay = new Map<number, number[]>();
26
-
27
- items.forEach((item, index) => {
28
- const dayIndices = indicesByDay.get(item.day) ?? [];
29
- dayIndices.push(index);
30
- indicesByDay.set(item.day, dayIndices);
31
- });
32
-
33
- indicesByDay.forEach((dayIndices) => {
34
- const ranges: Array<{
35
- start: number;
36
- end: number;
37
- index: number;
38
- }> = [];
39
-
40
- dayIndices.forEach((index) => {
41
- const current = items[index];
42
-
43
- ranges.forEach((range) => {
44
- if (
45
- (current.start >= range.start && current.end <= range.end) ||
46
- (current.start <= range.start && current.end >= range.start) ||
47
- (current.start <= range.end && current.end >= range.end)
48
- ) {
49
- indices.add(index);
50
- indices.add(range.index);
51
- }
52
- });
53
-
54
- ranges.push({
55
- start: current.start,
56
- end: current.end,
57
- index,
58
- });
59
- });
60
- });
61
-
62
- return indices;
63
- };
64
-
65
12
  /** UI shape: minutes as `start`/`end` (API maps to startTimeOfDay/endTimeOfDay). */
66
13
  const acceptOfDayUiSchema = z
67
14
  .object({
@@ -70,54 +17,11 @@ const acceptOfDayUiSchema = z
70
17
  start: dayMinuteSchema,
71
18
  end: dayMinuteSchema,
72
19
  })
73
- .superRefine((item, ctx) => {
74
- if (item.start >= item.end) {
75
- ctx.addIssue({
76
- code: 'custom',
77
- path: [
78
- 'start',
79
- ],
80
- ...i18nIssue(TIMERANGE_START_LESS_THAN_END_KEY),
81
- });
82
- ctx.addIssue({
83
- code: 'custom',
84
- path: [
85
- 'end',
86
- ],
87
- ...i18nIssue(TIMERANGE_START_LESS_THAN_END_KEY),
88
- });
89
- }
90
- });
20
+ .superRefine(refineTimeRangeStartLessThanEnd);
91
21
 
92
22
  const acceptsOfDayUiArraySchema = z
93
23
  .array(acceptOfDayUiSchema)
94
- .superRefine((items, ctx) => {
95
- getIntersectingIndices(items).forEach((index) => {
96
- ctx.addIssue({
97
- code: 'custom',
98
- path: [
99
- index,
100
- 'start',
101
- ],
102
- ...i18nIssue(TIMERANGE_NOT_INTERSECT_KEY),
103
- });
104
- ctx.addIssue({
105
- code: 'custom',
106
- path: [
107
- index,
108
- 'end',
109
- ],
110
- ...i18nIssue(TIMERANGE_NOT_INTERSECT_KEY),
111
- });
112
- });
113
- });
114
-
115
- export type CalendarDayRangeIssue = {
116
- index: number;
117
- prop: string;
118
- /** message key, resolve as `validation.<key>` */
119
- key: string;
120
- };
24
+ .superRefine(refineTimeRangesNotIntersect((item) => item.day));
121
25
 
122
26
  /**
123
27
  * The same rules as the card schema, reported per row so a form can mark the
@@ -127,33 +31,8 @@ export type CalendarDayRangeIssue = {
127
31
  * verdicts (overlapping ranges) are only re-derived by a full `$validate()`,
128
32
  * so a row the user has not touched keeps a message that is no longer true.
129
33
  */
130
- export const getCalendarDayRangeIssues = (
131
- items: unknown,
132
- ): CalendarDayRangeIssue[] => {
133
- const result = acceptsOfDayUiArraySchema.safeParse(items);
134
-
135
- if (result.success) return [];
136
-
137
- return result.error.issues.flatMap((issue) => {
138
- const [index, prop] = issue.path;
139
- const key =
140
- issue.code === 'custom' && typeof issue.params?.i18nKey === 'string'
141
- ? issue.params.i18nKey
142
- : undefined;
143
-
144
- if (typeof index !== 'number' || typeof prop !== 'string' || !key) {
145
- return [];
146
- }
147
-
148
- return [
149
- {
150
- index,
151
- prop,
152
- key,
153
- },
154
- ];
155
- });
156
- };
34
+ export const getCalendarDayRangeErrors = (items: unknown) =>
35
+ getTimeRangeErrors(acceptsOfDayUiArraySchema, items);
157
36
 
158
37
  export const calendarExceptSchema = z.object({
159
38
  name: z.string().min(1),
@@ -3,6 +3,7 @@ export * from './_shared/duration.validations';
3
3
  export * from './_shared/i18nIssue';
4
4
  export * from './_shared/lookup.validations';
5
5
  export * from './_shared/phoneNumber.validations';
6
+ export * from './_shared/timeRange.validations';
6
7
  export * from './_shared/variablePair.validations';
7
8
  export * from './adjunctType/adjunctType.validations';
8
9
  export * from './agent/agent.validations';
@@ -44,6 +45,10 @@ export * from './queueMember/memberCommunication.validations';
44
45
  export * from './queueMember/queueMember.validations';
45
46
  export * from './queueResGroup/queueResGroup.validations';
46
47
  export * from './queueSkill/queueSkill.validations';
48
+ export * from './resource/resource.validations';
49
+ export * from './resourceDisplay/resourceDisplay.validations';
50
+ export * from './resourceGroup/resourceGroup.validations';
51
+ export * from './resourceInGroup/resourceInGroup.validations';
47
52
  export * from './sla/sla.validations';
48
53
  export * from './slaCondition/slaCondition.validations';
49
54
  export * from './team/team.validations';
@@ -0,0 +1,102 @@
1
+ import { getDefaultsFromZodSchema } from '@webitel/api-services/gen/utils';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { resourceSchema } from '../resource.validations';
5
+
6
+ const filledResource = {
7
+ name: 'Resource',
8
+ gateway: {
9
+ id: 1,
10
+ name: 'Gateway',
11
+ },
12
+ rps: 10,
13
+ limit: 10,
14
+ maxSuccessivelyErrors: 2,
15
+ };
16
+
17
+ const issuePaths = (value: Record<string, unknown>) =>
18
+ resourceSchema
19
+ .safeParse(value)
20
+ .error?.issues.map((issue) => issue.path.join('.')) ?? [];
21
+
22
+ describe('resourceSchema', () => {
23
+ it('seeds a new resource with the defaults the form expects', () => {
24
+ expect(getDefaultsFromZodSchema(resourceSchema, {})).toMatchObject({
25
+ name: '',
26
+ rps: 10,
27
+ limit: 10,
28
+ description: '',
29
+ maxSuccessivelyErrors: 2,
30
+ errorIds: [],
31
+ patterns: [],
32
+ failureDialDelay: 0,
33
+ parameters: {
34
+ cidType: '',
35
+ ignoreEarlyMedia: '',
36
+ },
37
+ });
38
+ });
39
+
40
+ it('accepts a filled resource', () => {
41
+ expect(resourceSchema.safeParse(filledResource).success).toBe(true);
42
+ });
43
+
44
+ it('requires a name, a gateway, rps, limit and max successive errors', () => {
45
+ const paths = issuePaths({
46
+ name: '',
47
+ gateway: {},
48
+ rps: null,
49
+ limit: null,
50
+ maxSuccessivelyErrors: null,
51
+ });
52
+
53
+ expect(paths).toEqual(
54
+ expect.arrayContaining([
55
+ 'name',
56
+ 'gateway',
57
+ 'rps',
58
+ 'limit',
59
+ 'maxSuccessivelyErrors',
60
+ ]),
61
+ );
62
+ });
63
+
64
+ it('keeps rps and limit within their ranges', () => {
65
+ expect(
66
+ issuePaths({
67
+ ...filledResource,
68
+ rps: 1001,
69
+ limit: 5001,
70
+ }),
71
+ ).toEqual([
72
+ 'rps',
73
+ 'limit',
74
+ ]);
75
+ expect(
76
+ issuePaths({
77
+ ...filledResource,
78
+ rps: -2,
79
+ limit: -2,
80
+ }),
81
+ ).toEqual([
82
+ 'rps',
83
+ 'limit',
84
+ ]);
85
+ expect(
86
+ issuePaths({
87
+ ...filledResource,
88
+ rps: -1,
89
+ limit: -1,
90
+ }),
91
+ ).toEqual([]);
92
+ });
93
+
94
+ it('lets the failure dial delay be cleared', () => {
95
+ expect(
96
+ resourceSchema.safeParse({
97
+ ...filledResource,
98
+ failureDialDelay: null,
99
+ }).success,
100
+ ).toBe(true);
101
+ });
102
+ });
@@ -0,0 +1,28 @@
1
+ import type { EngineOutboundResource } from '@webitel/api-services/gen/models';
2
+ import { z } from 'zod';
3
+
4
+ import { clearableNumberSchema } from '../_shared/clearableNumber.validations';
5
+ import { filledLookupSchema } from '../_shared/lookup.validations';
6
+ import type { ZodShape } from '../types';
7
+
8
+ export const resourceSchema = z.object<ZodShape<EngineOutboundResource>>({
9
+ name: z.string().min(1).default(''),
10
+ gateway: filledLookupSchema,
11
+ rps: z.number().min(-1).max(1000).default(10),
12
+ limit: z.number().min(-1).max(5000).default(10),
13
+ description: z.string().optional().default(''),
14
+ maxSuccessivelyErrors: z.number().default(2),
15
+ errorIds: z.array(z.string()).optional().default([]),
16
+ patterns: z.array(z.string()).optional().default([]),
17
+ failureDialDelay: clearableNumberSchema.default(0),
18
+ parameters: z
19
+ .object({
20
+ cidType: z.string().optional().default(''),
21
+ ignoreEarlyMedia: z.string().optional().default(''),
22
+ })
23
+ .optional()
24
+ .default({
25
+ cidType: '',
26
+ ignoreEarlyMedia: '',
27
+ }),
28
+ });
@@ -0,0 +1,45 @@
1
+ import { getDefaultsFromZodSchema } from '@webitel/api-services/gen/utils';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { resourceDisplaySchema } from '../resourceDisplay.validations';
5
+
6
+ const issuePaths = (value: Record<string, unknown>) =>
7
+ resourceDisplaySchema
8
+ .safeParse(value)
9
+ .error?.issues.map((issue) => issue.path.join('.')) ?? [];
10
+
11
+ describe('resourceDisplaySchema', () => {
12
+ it('seeds a new number with an empty display', () => {
13
+ expect(getDefaultsFromZodSchema(resourceDisplaySchema, {})).toMatchObject({
14
+ display: '',
15
+ });
16
+ });
17
+
18
+ it('requires a number', () => {
19
+ expect(
20
+ issuePaths({
21
+ display: '',
22
+ }),
23
+ ).toEqual([
24
+ 'display',
25
+ ]);
26
+ });
27
+
28
+ it('rejects symbols a number cannot dial', () => {
29
+ expect(
30
+ issuePaths({
31
+ display: '380 44 123',
32
+ }),
33
+ ).toEqual([
34
+ 'display',
35
+ ]);
36
+ });
37
+
38
+ it('accepts a dialable number', () => {
39
+ expect(
40
+ resourceDisplaySchema.safeParse({
41
+ display: '+380441234567',
42
+ }).success,
43
+ ).toBe(true);
44
+ });
45
+ });
@@ -0,0 +1,9 @@
1
+ import type { EngineResourceDisplay } from '@webitel/api-services/gen/models';
2
+ import { z } from 'zod';
3
+
4
+ import { phoneNumberSchema } from '../_shared/phoneNumber.validations';
5
+ import type { ZodShape } from '../types';
6
+
7
+ export const resourceDisplaySchema = z.object<ZodShape<EngineResourceDisplay>>({
8
+ display: phoneNumberSchema.default(''),
9
+ });