tango-app-api-audio-analytics 1.0.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/.eslintrc.cjs +41 -0
- package/API_EXAMPLES.md +310 -0
- package/COHORT_API.md +513 -0
- package/COHORT_API_EXAMPLES.sh +235 -0
- package/COHORT_API_IMPLEMENTATION.md +296 -0
- package/COHORT_API_QUICKSTART.md +387 -0
- package/index.js +6 -0
- package/package.json +36 -0
- package/src/controllers/audioAnalytics.controller.js +116 -0
- package/src/controllers/cohort.controller.js +357 -0
- package/src/controllers/cohortAnalytics.controller.js +46 -0
- package/src/controllers/conversationAnalytics.controller.js +92 -0
- package/src/dtos/audioAnalytics.dtos.js +537 -0
- package/src/middlewares/validation.middleware.js +624 -0
- package/src/routes/audioAnalytics.routes.js +18 -0
- package/src/services/cohort.service.js +332 -0
- package/src/validations/cohort.validation.js +113 -0
package/COHORT_API.md
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
# Cohort API Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
The Cohort API manages cohort creation, retrieval, and analytics for the Tango Audio Analytics system. Cohorts are templates that define specific evaluation criteria and metrics for analyzing audio conversations.
|
|
5
|
+
|
|
6
|
+
## Base URL
|
|
7
|
+
```
|
|
8
|
+
https://testtangoretail-api.tangoeye.ai/v3/audio-analitics
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Database
|
|
12
|
+
- **Search Engine**: OpenSearch
|
|
13
|
+
- **Index Name**: `tango-audio-cohort`
|
|
14
|
+
- **Document ID**: Auto-generated UUID
|
|
15
|
+
|
|
16
|
+
## Validation
|
|
17
|
+
All requests are validated using Joi schema validation from the `tango-app-api-middleware` package.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Endpoints
|
|
22
|
+
|
|
23
|
+
### 1. Create Cohort
|
|
24
|
+
Create a new cohort with defined metrics and contexts.
|
|
25
|
+
|
|
26
|
+
**Endpoint**
|
|
27
|
+
```
|
|
28
|
+
POST /cohorts
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Request Body**
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"clientId": "11",
|
|
35
|
+
"cohortId": "cohort_photochromatic_001",
|
|
36
|
+
"cohortName": "Photochromatic",
|
|
37
|
+
"cohortDescription": "A strict evaluation of the photochromatic sales journey, focusing on pitch quality, technical accuracy, and customer sentiment",
|
|
38
|
+
"metrics": [
|
|
39
|
+
{
|
|
40
|
+
"metricId": "pitch_quality_001",
|
|
41
|
+
"metricName": "Standard Pitch Quality",
|
|
42
|
+
"metricDescription": "Identify which specific value propositions were explicitly used by the staff",
|
|
43
|
+
"hasContext": true,
|
|
44
|
+
"isNumneric": false,
|
|
45
|
+
"contexts": [
|
|
46
|
+
{
|
|
47
|
+
"contextName": "ONE_GLASS_ALL_NEEDS",
|
|
48
|
+
"priority": 2,
|
|
49
|
+
"description": "Convenience of one pair for all lighting"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"contextName": "TRAVEL_CONVENIENCE",
|
|
53
|
+
"priority": 2,
|
|
54
|
+
"description": "Specific benefits for bikers/commuters"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"contextName": "LIGHT_ADAPTATION",
|
|
58
|
+
"priority": 2,
|
|
59
|
+
"description": "Technical explanation of UV reaction"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"contextName": "REDUCES_EYE_STRAIN",
|
|
63
|
+
"priority": 2,
|
|
64
|
+
"description": "Glare and fatigue protection"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"metricId": "technical_depth_score_001",
|
|
70
|
+
"metricName": "Technical Depth Score",
|
|
71
|
+
"metricDescription": "A numeric evaluation of the staff's product knowledge during the pitch",
|
|
72
|
+
"hasContext": true,
|
|
73
|
+
"isNumneric": true,
|
|
74
|
+
"contexts": [
|
|
75
|
+
{
|
|
76
|
+
"minValue": 0,
|
|
77
|
+
"maxValue": 100
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"metricId": "customer_sentiment_001",
|
|
83
|
+
"metricName": "Initial Customer Sentiment",
|
|
84
|
+
"metricDescription": "The immediate reaction of the customer when the lens was introduced",
|
|
85
|
+
"hasContext": true,
|
|
86
|
+
"isNumneric": false,
|
|
87
|
+
"contexts": [
|
|
88
|
+
{
|
|
89
|
+
"contextName": "POSITIVE",
|
|
90
|
+
"priority": 2,
|
|
91
|
+
"description": "High interest or curiosity"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"contextName": "NEUTRAL",
|
|
95
|
+
"priority": 1,
|
|
96
|
+
"description": "Acknowledgment or price-focused only"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"contextName": "NEGATIVE",
|
|
100
|
+
"priority": 0,
|
|
101
|
+
"description": "Dismissive or disinterested"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"metricId": "staff_responsiveness_001",
|
|
107
|
+
"metricName": "Staff Responsiveness",
|
|
108
|
+
"metricDescription": "How effectively were customer doubts addressed?",
|
|
109
|
+
"hasContext": true,
|
|
110
|
+
"isNumneric": false,
|
|
111
|
+
"contexts": [
|
|
112
|
+
{
|
|
113
|
+
"contextName": "FULLY_RESPONSIVE",
|
|
114
|
+
"priority": 2,
|
|
115
|
+
"description": "All doubts cleared with evidence"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"contextName": "PARTIALLY_RESPONSIVE",
|
|
119
|
+
"priority": 1,
|
|
120
|
+
"description": "Some doubts addressed, others ignored"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"contextName": "UNRESPONSIVE",
|
|
124
|
+
"priority": 0,
|
|
125
|
+
"description": "Customer doubts were dismissed"
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"metricId": "sales_outcome_001",
|
|
131
|
+
"metricName": "Final Sales Outcome",
|
|
132
|
+
"metricDescription": "The final commitment level reached at the end of the conversation",
|
|
133
|
+
"hasContext": true,
|
|
134
|
+
"isNumneric": false,
|
|
135
|
+
"contexts": [
|
|
136
|
+
{
|
|
137
|
+
"contextName": "BOUGHT_PHOTOCHROMATIC",
|
|
138
|
+
"priority": 2,
|
|
139
|
+
"description": "Customer agreed to the upgrade"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"contextName": "BOUGHT_SOMETHING_ELSE",
|
|
143
|
+
"priority": 0,
|
|
144
|
+
"description": "Bought standard lenses/frames only"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"contextName": "NO_SALE",
|
|
148
|
+
"priority": 0,
|
|
149
|
+
"description": "No purchase commitment"
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"metricId": "pitch_timing_summary_001",
|
|
155
|
+
"metricName": "Pitch Timing & Context",
|
|
156
|
+
"metricDescription": "Narrative summary of when and how the pitch was introduced",
|
|
157
|
+
"hasContext": false,
|
|
158
|
+
"isNumneric": false
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Response (201 Created)**
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"status": "success",
|
|
168
|
+
"message": "Cohort created successfully",
|
|
169
|
+
"data": {
|
|
170
|
+
"cohortId": "cohort_photochromatic_001",
|
|
171
|
+
"cohortName": "Photochromatic",
|
|
172
|
+
"clientId": "11",
|
|
173
|
+
"documentId": "550e8400-e29b-41d4-a716-446655440000",
|
|
174
|
+
"createdAt": "2026-03-03T10:30:00Z",
|
|
175
|
+
"metricsCount": 6
|
|
176
|
+
},
|
|
177
|
+
"timestamp": "2026-03-03T10:30:00Z"
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Validation Rules**
|
|
182
|
+
- `clientId`: Required, string
|
|
183
|
+
- `cohortId`: Required, string (alphanumeric, hyphens, underscores only)
|
|
184
|
+
- `cohortName`: Required, 3-100 characters
|
|
185
|
+
- `cohortDescription`: Required, 10-500 characters
|
|
186
|
+
- `metrics`: Required, array with 1-50 items
|
|
187
|
+
- Each metric must have: `metricId`, `metricName`, `metricDescription`, `hasContext`, `isNumneric`
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
### 2. Get Cohort by ID
|
|
192
|
+
Retrieve a specific cohort by its cohort ID.
|
|
193
|
+
|
|
194
|
+
**Endpoint**
|
|
195
|
+
```
|
|
196
|
+
GET /cohorts/:cohortId
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Example**
|
|
200
|
+
```
|
|
201
|
+
GET /cohorts/cohort_photochromatic_001
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Response (200 OK)**
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"status": "success",
|
|
208
|
+
"data": {
|
|
209
|
+
"documentId": "550e8400-e29b-41d4-a716-446655440000",
|
|
210
|
+
"clientId": "11",
|
|
211
|
+
"cohortId": "cohort_photochromatic_001",
|
|
212
|
+
"cohortName": "Photochromatic",
|
|
213
|
+
"cohortDescription": "A strict evaluation...",
|
|
214
|
+
"metrics": [
|
|
215
|
+
{
|
|
216
|
+
"metricId": "pitch_quality_001",
|
|
217
|
+
"metricName": "Standard Pitch Quality",
|
|
218
|
+
"contexts": [...]
|
|
219
|
+
}
|
|
220
|
+
],
|
|
221
|
+
"createdAt": "2026-03-03T10:30:00Z",
|
|
222
|
+
"updatedAt": "2026-03-03T10:30:00Z"
|
|
223
|
+
},
|
|
224
|
+
"timestamp": "2026-03-03T10:30:00Z"
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### 3. Get Cohorts by Client
|
|
231
|
+
Retrieve all cohorts for a specific client with pagination.
|
|
232
|
+
|
|
233
|
+
**Endpoint**
|
|
234
|
+
```
|
|
235
|
+
GET /cohorts/client/:clientId?limit=10&offset=0
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Query Parameters**
|
|
239
|
+
- `limit` (optional): Results per page (default: 10, max: 100)
|
|
240
|
+
- `offset` (optional): Pagination offset (default: 0)
|
|
241
|
+
|
|
242
|
+
**Example**
|
|
243
|
+
```
|
|
244
|
+
GET /cohorts/client/11?limit=5&offset=0
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Response (200 OK)**
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"status": "success",
|
|
251
|
+
"data": [
|
|
252
|
+
{
|
|
253
|
+
"documentId": "550e8400-e29b-41d4-a716-446655440000",
|
|
254
|
+
"clientId": "11",
|
|
255
|
+
"cohortId": "cohort_photochromatic_001",
|
|
256
|
+
"cohortName": "Photochromatic",
|
|
257
|
+
"createdAt": "2026-03-03T10:30:00Z"
|
|
258
|
+
}
|
|
259
|
+
],
|
|
260
|
+
"pagination": {
|
|
261
|
+
"total": 15,
|
|
262
|
+
"limit": 5,
|
|
263
|
+
"offset": 0,
|
|
264
|
+
"hasMore": true
|
|
265
|
+
},
|
|
266
|
+
"timestamp": "2026-03-03T10:30:00Z"
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### 4. Search Cohorts
|
|
273
|
+
Search for cohorts by multiple criteria (clientId, cohortId, or cohortName).
|
|
274
|
+
|
|
275
|
+
**Endpoint**
|
|
276
|
+
```
|
|
277
|
+
POST /cohorts/search
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Request Body**
|
|
281
|
+
```json
|
|
282
|
+
{
|
|
283
|
+
"clientId": "11",
|
|
284
|
+
"cohortName": "Photochrom",
|
|
285
|
+
"limit": 10,
|
|
286
|
+
"offset": 0
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Response (200 OK)**
|
|
291
|
+
```json
|
|
292
|
+
{
|
|
293
|
+
"status": "success",
|
|
294
|
+
"data": [
|
|
295
|
+
{
|
|
296
|
+
"documentId": "550e8400-e29b-41d4-a716-446655440000",
|
|
297
|
+
"cohortId": "cohort_photochromatic_001",
|
|
298
|
+
"cohortName": "Photochromatic",
|
|
299
|
+
"clientId": "11",
|
|
300
|
+
"createdAt": "2026-03-03T10:30:00Z"
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
"pagination": {
|
|
304
|
+
"total": 1,
|
|
305
|
+
"limit": 10,
|
|
306
|
+
"offset": 0,
|
|
307
|
+
"hasMore": false
|
|
308
|
+
},
|
|
309
|
+
"timestamp": "2026-03-03T10:30:00Z"
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
### 5. Update Cohort
|
|
316
|
+
Update cohort name, description, or metrics.
|
|
317
|
+
|
|
318
|
+
**Endpoint**
|
|
319
|
+
```
|
|
320
|
+
PUT /cohorts/:documentId
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Request Body** (Partial update)
|
|
324
|
+
```json
|
|
325
|
+
{
|
|
326
|
+
"cohortName": "Updated Photochromatic",
|
|
327
|
+
"cohortDescription": "Updated description",
|
|
328
|
+
"metrics": [...]
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Response (200 OK)**
|
|
333
|
+
```json
|
|
334
|
+
{
|
|
335
|
+
"status": "success",
|
|
336
|
+
"message": "Cohort updated successfully",
|
|
337
|
+
"data": {
|
|
338
|
+
"documentId": "550e8400-e29b-41d4-a716-446655440000",
|
|
339
|
+
"cohortName": "Updated Photochromatic",
|
|
340
|
+
"updatedAt": "2026-03-03T11:00:00Z"
|
|
341
|
+
},
|
|
342
|
+
"timestamp": "2026-03-03T11:00:00Z"
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
### 6. Delete Cohort
|
|
349
|
+
Delete a cohort from OpenSearch.
|
|
350
|
+
|
|
351
|
+
**Endpoint**
|
|
352
|
+
```
|
|
353
|
+
DELETE /cohorts/:documentId
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Example**
|
|
357
|
+
```
|
|
358
|
+
DELETE /cohorts/550e8400-e29b-41d4-a716-446655440000
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Response (200 OK)**
|
|
362
|
+
```json
|
|
363
|
+
{
|
|
364
|
+
"status": "success",
|
|
365
|
+
"message": "Cohort deleted successfully",
|
|
366
|
+
"timestamp": "2026-03-03T11:00:00Z"
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
### 7. Get Cohort Analytics
|
|
373
|
+
Retrieve cohort analytics and metrics summary.
|
|
374
|
+
|
|
375
|
+
**Endpoint**
|
|
376
|
+
```
|
|
377
|
+
GET /cohorts/:cohortId/analytics
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**Example**
|
|
381
|
+
```
|
|
382
|
+
GET /cohorts/cohort_photochromatic_001/analytics
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Response (200 OK)**
|
|
386
|
+
```json
|
|
387
|
+
{
|
|
388
|
+
"status": "success",
|
|
389
|
+
"data": {
|
|
390
|
+
"cohortId": "cohort_photochromatic_001",
|
|
391
|
+
"cohortName": "Photochromatic",
|
|
392
|
+
"clientId": "11",
|
|
393
|
+
"totalMetrics": 6,
|
|
394
|
+
"metrics": [
|
|
395
|
+
{
|
|
396
|
+
"metricId": "pitch_quality_001",
|
|
397
|
+
"metricName": "Standard Pitch Quality",
|
|
398
|
+
"isNumneric": false,
|
|
399
|
+
"contextCount": 4,
|
|
400
|
+
"contexts": [...]
|
|
401
|
+
}
|
|
402
|
+
],
|
|
403
|
+
"createdAt": "2026-03-03T10:30:00Z",
|
|
404
|
+
"updatedAt": "2026-03-03T10:30:00Z"
|
|
405
|
+
},
|
|
406
|
+
"timestamp": "2026-03-03T11:00:00Z"
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## Error Responses
|
|
413
|
+
|
|
414
|
+
### 400 Bad Request - Validation Error
|
|
415
|
+
```json
|
|
416
|
+
{
|
|
417
|
+
"status": "error",
|
|
418
|
+
"message": "Validation failed",
|
|
419
|
+
"code": "VALIDATION_ERROR",
|
|
420
|
+
"details": [
|
|
421
|
+
{
|
|
422
|
+
"field": "cohortName",
|
|
423
|
+
"message": "\"cohortName\" length must be at least 3 characters long",
|
|
424
|
+
"type": "string.min"
|
|
425
|
+
}
|
|
426
|
+
],
|
|
427
|
+
"timestamp": "2026-03-03T10:30:00Z"
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 404 Not Found
|
|
432
|
+
```json
|
|
433
|
+
{
|
|
434
|
+
"status": "error",
|
|
435
|
+
"message": "Cohort not found",
|
|
436
|
+
"code": "COHORT_NOT_FOUND",
|
|
437
|
+
"timestamp": "2026-03-03T10:30:00Z"
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### 409 Conflict - Cohort Already Exists
|
|
442
|
+
```json
|
|
443
|
+
{
|
|
444
|
+
"status": "error",
|
|
445
|
+
"message": "Cohort with this ID already exists",
|
|
446
|
+
"code": "COHORT_EXISTS",
|
|
447
|
+
"timestamp": "2026-03-03T10:30:00Z"
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### 500 Internal Server Error
|
|
452
|
+
```json
|
|
453
|
+
{
|
|
454
|
+
"status": "error",
|
|
455
|
+
"message": "Internal server error",
|
|
456
|
+
"code": "INTERNAL_ERROR",
|
|
457
|
+
"timestamp": "2026-03-03T10:30:00Z"
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Data Structure
|
|
464
|
+
|
|
465
|
+
### Cohort Document
|
|
466
|
+
```json
|
|
467
|
+
{
|
|
468
|
+
"documentId": "UUID",
|
|
469
|
+
"clientId": "string",
|
|
470
|
+
"cohortId": "string",
|
|
471
|
+
"cohortName": "string",
|
|
472
|
+
"cohortDescription": "string",
|
|
473
|
+
"metrics": [
|
|
474
|
+
{
|
|
475
|
+
"metricId": "string",
|
|
476
|
+
"metricName": "string",
|
|
477
|
+
"metricDescription": "string",
|
|
478
|
+
"hasContext": boolean,
|
|
479
|
+
"isNumneric": boolean,
|
|
480
|
+
"contexts": [...]
|
|
481
|
+
}
|
|
482
|
+
],
|
|
483
|
+
"isActive": true,
|
|
484
|
+
"version": "1.0",
|
|
485
|
+
"createdAt": "ISO-8601 timestamp",
|
|
486
|
+
"updatedAt": "ISO-8601 timestamp"
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Implementation Notes
|
|
493
|
+
|
|
494
|
+
### OpenSearch Integration
|
|
495
|
+
- Uses `insert(index, documentId, document)` function from `tango-app-api-middleware`
|
|
496
|
+
- Auto-generates UUID for each document
|
|
497
|
+
- Stores metadata (createdAt, updatedAt, isActive, version)
|
|
498
|
+
- Search operations use Elasticsearch Query DSL
|
|
499
|
+
|
|
500
|
+
### Validation
|
|
501
|
+
- All requests validated using Joi schemas
|
|
502
|
+
- Custom validation schemas in `src/validations/cohort.validation.js`
|
|
503
|
+
- Validation middleware in `src/middlewares/validation.middleware.js`
|
|
504
|
+
|
|
505
|
+
### Service Layer
|
|
506
|
+
- Cohort operations in `src/services/cohort.service.js`
|
|
507
|
+
- Handles all OpenSearch CRUD operations
|
|
508
|
+
- Error handling with specific error codes
|
|
509
|
+
|
|
510
|
+
### Controller Layer
|
|
511
|
+
- API handlers in `src/controllers/cohort.controller.js`
|
|
512
|
+
- Request validation and response formatting
|
|
513
|
+
- Comprehensive logging via `tango-app-api-middleware`
|