Skip to content

Commit 0ab26c3

Browse files
Alexey PortnovAlexey Portnov
authored andcommitted
fix(cdr): implement GET /cdr/{id} retrieval and accept linkedid (#997)
GetRecordAction was a TODO stub returning empty data for any valid CDR ID. Implements lookup by numeric primary key or linkedid (mikopbx-*), formats response via DataStructure::createFromModel(), and returns 404 when not found. Also aligns the getRecord id pattern with delete (^([0-9]+|mikopbx-.+)$) so the same identifier works for both endpoints — the prior numeric-only pattern was inconsistent with DeleteRecordAction's behavior.
1 parent b6ff441 commit 0ab26c3

2 files changed

Lines changed: 59 additions & 9 deletions

File tree

src/PBXCoreREST/Controllers/Cdr/RestController.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@
6666
// 'mikopbx-' generates mikopbx-[^/:]+ (matches linkedid like "mikopbx-1760784793.4627")
6767
//
6868
// IMPORTANT: Individual methods further restrict ID format via ApiParameterRef pattern:
69-
// - getRecord, playback, download: numeric only (pattern: '^[0-9]+$')
70-
// - delete: numeric OR linkedid (pattern: '^([0-9]+|mikopbx-.+)$')
69+
// - getRecord, delete: numeric OR linkedid (pattern: '^([0-9]+|mikopbx-.+)$')
70+
// - playback, download: numeric only (pattern: '^[0-9]+$')
7171
//
7272
// SECURITY NOTE: ResourceSecurity removed from class level because this resource has mixed security:
7373
// - getList/getRecord/delete: require Bearer token (added at method level)
@@ -129,7 +129,7 @@ public function getList(): void
129129
description: 'rest_cdr_GetRecordDesc',
130130
operationId: 'getCdrById'
131131
)]
132-
#[ApiParameterRef('id', dataStructure: CommonDataStructure::class, pattern: '^[0-9]+$', example: '12345')]
132+
#[ApiParameterRef('id', dataStructure: CommonDataStructure::class, pattern: '^([0-9]+|mikopbx-.+)$', example: '12345')]
133133
#[ApiResponse(200, 'rest_response_200_get')]
134134
#[ApiResponse(401, 'rest_response_401_unauthorized', 'PBXApiResult')]
135135
#[ApiResponse(403, 'rest_response_403_forbidden', 'PBXApiResult')]

src/PBXCoreREST/Lib/Cdr/GetRecordAction.php

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,86 @@
1919

2020
namespace MikoPBX\PBXCoreREST\Lib\Cdr;
2121

22+
use MikoPBX\Common\Models\CallDetailRecords;
2223
use MikoPBX\PBXCoreREST\Lib\PBXApiResult;
24+
use Throwable;
2325

2426
/**
2527
* Get single CDR record action
2628
*
29+
* Supports two lookup modes based on ID format (mirrors DeleteRecordAction):
30+
* - numeric ID: lookup by primary key
31+
* - linkedid (mikopbx-*): returns the originating record of the conversation
32+
* (earliest by start time). Use GET /cdr?linkedid=... to fetch every segment
33+
* of a transferred call.
34+
*
2735
* @package MikoPBX\PBXCoreREST\Lib\Cdr
2836
*/
2937
class GetRecordAction
3038
{
3139
/**
32-
* Get single CDR record
40+
* Get single CDR record by numeric ID or linkedid.
3341
*
34-
* @param string|null $id Record ID
42+
* @param string|null $id Record ID (numeric) or linkedid (mikopbx-*)
3543
* @return PBXApiResult
3644
*/
3745
public static function main(?string $id = null): PBXApiResult
3846
{
3947
$res = new PBXApiResult();
4048
$res->processor = __METHOD__;
4149

50+
// ============ PHASE 1: VALIDATION ============
4251
if (empty($id)) {
4352
$res->messages['error'][] = 'Record ID is required';
53+
$res->httpCode = 400;
54+
return $res;
55+
}
56+
57+
// ============ PHASE 2: DETERMINE ID TYPE ============
58+
$isLinkedId = str_starts_with($id, 'mikopbx-');
59+
// Reject decimal numbers (12.34) — only accept integers.
60+
$isNumericId = is_numeric($id) && (string)(int)$id === $id;
61+
62+
if (!$isLinkedId && !$isNumericId) {
63+
$res->messages['error'][] = 'Invalid ID format. Expected: integer ID or linkedid (mikopbx-*)';
64+
$res->httpCode = 400;
65+
return $res;
66+
}
67+
68+
// ============ PHASE 3: FETCH RECORD ============
69+
try {
70+
if ($isLinkedId) {
71+
// Originating record of the conversation — earliest segment.
72+
// Full conversation is available via GET /cdr?linkedid=...
73+
$record = CallDetailRecords::findFirst([
74+
'conditions' => 'linkedid = :linkedid:',
75+
'bind' => ['linkedid' => $id],
76+
'order' => 'start ASC',
77+
]);
78+
} else {
79+
$record = CallDetailRecords::findFirst([
80+
'conditions' => 'id = :id:',
81+
'bind' => ['id' => (int)$id],
82+
]);
83+
}
84+
} catch (Throwable $e) {
85+
$res->messages['error'][] = 'Failed to query CDR record: ' . $e->getMessage();
86+
$res->httpCode = 500;
87+
return $res;
88+
}
89+
90+
if ($record === null) {
91+
$res->messages['error'][] = 'CDR record not found';
92+
$res->httpCode = 404;
4493
return $res;
4594
}
4695

47-
// TODO: Implement single CDR record retrieval
48-
// This is a placeholder for future implementation
49-
$res->data = [];
96+
// ============ PHASE 4: FORMAT RESPONSE ============
97+
// createFromModel() adds playback_url/download_url and applies schema typing.
98+
$res->data = DataStructure::createFromModel($record);
5099
$res->success = true;
100+
$res->httpCode = 200;
51101

52102
return $res;
53103
}
54-
}
104+
}

0 commit comments

Comments
 (0)