@webitel/api-services 26.8.37 → 26.8.39

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": "@webitel/api-services",
3
- "version": "26.8.37",
3
+ "version": "26.8.39",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,179 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const time = 123;
4
+
5
+ const searchHistoryCallPost = vi.fn(() =>
6
+ Promise.resolve({
7
+ data: {
8
+ items: [
9
+ {
10
+ created_at: time,
11
+ joined_at: time,
12
+ duration: 60,
13
+ },
14
+ ],
15
+ },
16
+ }),
17
+ );
18
+
19
+ vi.mock('../../../../gen-wire', async (importOriginal) => ({
20
+ ...(await importOriginal<Record<string, unknown>>()),
21
+ getCallService: () => ({
22
+ searchHistoryCallPost,
23
+ }),
24
+ }));
25
+
26
+ const { FormatDateMode } = await import('../../../../enums');
27
+ const { convertDuration } = await import('../../../../scripts');
28
+ const { formatDate } = await import('../../../../utils');
29
+ const { AgentCallsAPI } = await import('../agentCalls');
30
+
31
+ // Expected output mirrors the source `listHandler`: createdAt -> DATETIME,
32
+ // joinedAt -> TIME, durations via convertDuration, every other field null,
33
+ // files grouped into an (empty) object.
34
+ const expectItems = [
35
+ {
36
+ createdAt: formatDate(+time, FormatDateMode.DATETIME),
37
+ joinedAt: formatDate(+time, FormatDateMode.TIME),
38
+ duration: convertDuration(60),
39
+ answeredAt: null,
40
+ bridgedAt: null,
41
+ queueBridgedAt: null,
42
+ leavingAt: null,
43
+ hangupAt: null,
44
+ reportingAt: null,
45
+ holdSec: null,
46
+ waitSec: null,
47
+ billSec: null,
48
+ talkSec: null,
49
+ reportingSec: null,
50
+ queueWaitSec: null,
51
+ queueDurationSec: null,
52
+ scoreRequired: null,
53
+ files: {},
54
+ },
55
+ ];
56
+
57
+ describe('AgentCallsAPI.getList', () => {
58
+ beforeEach(() => searchHistoryCallPost.mockClear());
59
+
60
+ it('correctly processes the response', async () => {
61
+ const response = await AgentCallsAPI.getList({});
62
+ expect(searchHistoryCallPost).toHaveBeenCalled();
63
+ expect(response).toEqual({
64
+ next: false,
65
+ items: expectItems,
66
+ });
67
+ });
68
+
69
+ it('sends the request body in snake_case', async () => {
70
+ await AgentCallsAPI.getList({
71
+ userId: '2',
72
+ queueId: '3',
73
+ skipParent: true,
74
+ });
75
+ const [body] = searchHistoryCallPost.mock.calls.at(-1);
76
+ expect(Object.keys(body)).toEqual(
77
+ expect.arrayContaining([
78
+ 'user_id',
79
+ 'queue_id',
80
+ 'skip_parent',
81
+ ]),
82
+ );
83
+ expect(body).not.toHaveProperty('userId');
84
+ expect(body).not.toHaveProperty('queueId');
85
+ expect(body).not.toHaveProperty('skipParent');
86
+ });
87
+
88
+ // EngineSearchHistoryCallRequest's id-ish fields (user_id, agent_id, ...)
89
+ // are string[] on the wire — a bare id string trips the backend's proto
90
+ // parser (400 "unexpected token" on the id's own characters).
91
+ it('wraps single-value id filters into arrays for the wire', async () => {
92
+ await AgentCallsAPI.getList({
93
+ userId: '2',
94
+ agentId: '3',
95
+ queueId: '4',
96
+ teamId: '5',
97
+ contactId: '6',
98
+ ownerId: '7',
99
+ gatewayId: '8',
100
+ ratedBy: '9',
101
+ });
102
+ const [body] = searchHistoryCallPost.mock.calls.at(-1);
103
+ expect(body.user_id).toEqual([
104
+ '2',
105
+ ]);
106
+ expect(body.agent_id).toEqual([
107
+ '3',
108
+ ]);
109
+ expect(body.queue_id).toEqual([
110
+ '4',
111
+ ]);
112
+ expect(body.team_id).toEqual([
113
+ '5',
114
+ ]);
115
+ expect(body.contact_id).toEqual([
116
+ '6',
117
+ ]);
118
+ expect(body.owner_id).toEqual([
119
+ '7',
120
+ ]);
121
+ expect(body.gateway_id).toEqual([
122
+ '8',
123
+ ]);
124
+ expect(body.rated_by).toEqual([
125
+ '9',
126
+ ]);
127
+ });
128
+
129
+ it('leaves id filters undefined when not provided', async () => {
130
+ await AgentCallsAPI.getList({});
131
+ const [body] = searchHistoryCallPost.mock.calls.at(-1);
132
+ expect(body.user_id).toBeUndefined();
133
+ expect(body.agent_id).toBeUndefined();
134
+ });
135
+
136
+ it('normalizes a {from,to} createdAt range for the wire', async () => {
137
+ await AgentCallsAPI.getList({
138
+ createdAt: {
139
+ from: 100,
140
+ to: 200,
141
+ },
142
+ });
143
+ const [body] = searchHistoryCallPost.mock.calls.at(-1);
144
+ expect(body.created_at).toEqual({
145
+ from: 100,
146
+ to: 200,
147
+ });
148
+ });
149
+
150
+ it('omits created_at when neither from nor to is set', async () => {
151
+ await AgentCallsAPI.getList({});
152
+ const [body] = searchHistoryCallPost.mock.calls.at(-1);
153
+ expect(body.created_at).toBeUndefined();
154
+ });
155
+
156
+ // the rated filter's value field emits a real boolean — rated: false is a
157
+ // legitimate, deliberate filter value, not "no filter applied"
158
+ it('sends rated: true as-is', async () => {
159
+ await AgentCallsAPI.getList({
160
+ rated: true,
161
+ });
162
+ const [body] = searchHistoryCallPost.mock.calls.at(-1);
163
+ expect(body.rated).toBe(true);
164
+ });
165
+
166
+ it('sends rated: false as-is, not omitted or flipped', async () => {
167
+ await AgentCallsAPI.getList({
168
+ rated: false,
169
+ });
170
+ const [body] = searchHistoryCallPost.mock.calls.at(-1);
171
+ expect(body.rated).toBe(false);
172
+ });
173
+
174
+ it('omits rated when the filter is not applied', async () => {
175
+ await AgentCallsAPI.getList({});
176
+ const [body] = searchHistoryCallPost.mock.calls.at(-1);
177
+ expect(body).not.toHaveProperty('rated');
178
+ });
179
+ });
@@ -0,0 +1,178 @@
1
+ import { FormatDateMode } from '../../../enums';
2
+ import { getCallService } from '../../../gen-wire';
3
+ import { convertDuration, normalizeDatetimeRange } from '../../../scripts';
4
+ import { formatDate } from '../../../utils';
5
+ import { getDefaultGetListResponse, getDefaultGetParams } from '../../defaults';
6
+ import {
7
+ applyTransform,
8
+ camelToSnake,
9
+ merge,
10
+ notify,
11
+ snakeToCamel,
12
+ starToSearch,
13
+ } from '../../transformers';
14
+ import type { ApiParams } from '../_shared/types';
15
+
16
+ const calcTime = (time?: number | string | null) =>
17
+ time ? formatDate(+time, FormatDateMode.TIME) : null;
18
+ const calcDuration = (duration?: number | string | null) =>
19
+ duration ? convertDuration(Number(duration)) : null;
20
+
21
+ // EngineSearchHistoryCallRequest's id-ish fields (agent_id, queue_id, etc.)
22
+ // are all string[] on the wire, even though the filters/store send a single id.
23
+ const toIdArray = (value: unknown): string[] | undefined => {
24
+ if (value == null || value === '') return undefined;
25
+ return Array.isArray(value)
26
+ ? value
27
+ : [
28
+ String(value),
29
+ ];
30
+ };
31
+
32
+ const groupFilesByType = (files?: ApiParams[]) => {
33
+ if (!files) return {};
34
+ return files.reduce((acc: Record<string, ApiParams[]>, file: ApiParams) => {
35
+ acc[file.type] = acc[file.type] || [];
36
+ acc[file.type].push(file);
37
+ return acc;
38
+ }, {});
39
+ };
40
+
41
+ const listHandler = (items: ApiParams[]) =>
42
+ items.map((item) => ({
43
+ ...item,
44
+ createdAt: item.createdAt
45
+ ? formatDate(+item.createdAt, FormatDateMode.DATETIME)
46
+ : null,
47
+ answeredAt: calcTime(item.answeredAt),
48
+ bridgedAt: calcTime(item.bridgedAt),
49
+ queueBridgedAt: calcTime(item.queueBridgedAt),
50
+ joinedAt: calcTime(item.joinedAt),
51
+ leavingAt: calcTime(item.leavingAt),
52
+ hangupAt: calcTime(item.hangupAt),
53
+ reportingAt: calcTime(item.reportingAt),
54
+ duration: calcDuration(item.duration),
55
+ holdSec: calcDuration(item.holdSec),
56
+ waitSec: calcDuration(item.waitSec),
57
+ billSec: calcDuration(item.billSec),
58
+ talkSec: calcDuration(item.talkSec),
59
+ reportingSec: calcDuration(item.reportingSec),
60
+ queueWaitSec: calcDuration(item.queueWaitSec),
61
+ queueDurationSec: calcDuration(item.queueDurationSec),
62
+ scoreRequired: item.scoreRequired ? (+item.scoreRequired).toFixed(2) : null,
63
+ files: groupFilesByType(item.files),
64
+ }));
65
+
66
+ /** An agent's own call history, shown on their card's "Calls" tab. */
67
+ const getAgentCallsList = async ({ options, ...params }: ApiParams) => {
68
+ const defaultParams = {
69
+ search: '',
70
+ sort: '-created_at',
71
+ fields: [],
72
+ skipParent: true,
73
+ };
74
+
75
+ const normalized = applyTransform(params, [
76
+ merge(getDefaultGetParams()),
77
+ merge(defaultParams),
78
+ starToSearch('search'),
79
+ ]);
80
+
81
+ const {
82
+ page,
83
+ size,
84
+ search,
85
+ sort,
86
+ fields,
87
+ createdAt,
88
+ agentId,
89
+ userId,
90
+ rated,
91
+ ratedBy,
92
+ direction,
93
+ directions,
94
+ hasFile,
95
+ hasTranscript,
96
+ queueId,
97
+ teamId,
98
+ contactId,
99
+ domainId,
100
+ ownerId,
101
+ gatewayId,
102
+ number,
103
+ missed,
104
+ skipParent,
105
+ } = normalized;
106
+
107
+ const postBody: Record<string, unknown> = {
108
+ page,
109
+ size,
110
+ sort,
111
+ q: search || undefined,
112
+ createdAt: normalizeDatetimeRange(createdAt),
113
+ fields: (fields || []).concat([
114
+ 'id',
115
+ 'files',
116
+ ]),
117
+ agentId: toIdArray(agentId),
118
+ userId: toIdArray(userId),
119
+ queueId: toIdArray(queueId),
120
+ teamId: toIdArray(teamId),
121
+ contactId: toIdArray(contactId),
122
+ ownerId: toIdArray(ownerId),
123
+ gatewayId: toIdArray(gatewayId),
124
+ ratedBy: toIdArray(ratedBy),
125
+ direction,
126
+ directions,
127
+ hasFile,
128
+ hasTranscript,
129
+ number,
130
+ missed,
131
+ domainId,
132
+ skipParent,
133
+ };
134
+ if (rated != null) postBody.rated = rated;
135
+
136
+ try {
137
+ const response = await getCallService().searchHistoryCallPost(
138
+ applyTransform(postBody, [
139
+ camelToSnake(),
140
+ ]),
141
+ options,
142
+ );
143
+
144
+ const { items, next } = applyTransform(response.data, [
145
+ snakeToCamel(),
146
+ merge(getDefaultGetListResponse()),
147
+ ]);
148
+
149
+ return {
150
+ items: applyTransform(items, [
151
+ listHandler,
152
+ ]),
153
+ next,
154
+ };
155
+ } catch (err) {
156
+ throw applyTransform(err, [
157
+ notify,
158
+ ]);
159
+ }
160
+ };
161
+
162
+ const getAgentCallsLookup = (params: ApiParams) =>
163
+ getAgentCallsList({
164
+ ...params,
165
+ fields: params?.fields || [
166
+ 'id',
167
+ 'destination',
168
+ 'state',
169
+ 'created_at',
170
+ 'files',
171
+ ],
172
+ size: params?.size ?? 20,
173
+ });
174
+
175
+ export const AgentCallsAPI = {
176
+ getList: getAgentCallsList,
177
+ getLookup: getAgentCallsLookup,
178
+ };
@@ -104,7 +104,7 @@ const AGENT_DEFAULTS = {
104
104
  supervisor: [],
105
105
  auditor: [],
106
106
  region: {},
107
- progressiveCount: 0,
107
+ progressiveCount: null,
108
108
  chatCount: 0,
109
109
  taskCount: 0,
110
110
  isSupervisor: false,
@@ -1,5 +1,6 @@
1
1
  export * from './_shared/generatePermissionsApi';
2
2
  export * from './activeCalls/activeCalls';
3
+ export * from './agentCalls/agentCalls';
3
4
  export * from './agentPauseCauses/agentPauseCauses';
4
5
  export * from './agentSkills/agentSkills';
5
6
  export * from './agents/agentAbsence';