docs: publish contact detail op payloads
This commit is contained in:
parent
5a7b9b257e
commit
577ceb3d54
@ -12,6 +12,8 @@ class ContactDetailModel extends BaseModel {
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'ContactStartDate';
|
||||
protected $updatedField = '';
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = 'ContactEndDate';
|
||||
|
||||
public function syncDetails(int $ContactID, array $contactDetails) {
|
||||
try {
|
||||
@ -60,4 +62,99 @@ class ContactDetailModel extends BaseModel {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function applyDetailOperations(int $contactID, array $operations): array
|
||||
{
|
||||
try {
|
||||
if (!empty($operations['edited']) && !$this->updateDetails($contactID, $operations['edited'])) {
|
||||
return ['status' => 'error', 'message' => 'Failed to update contact details'];
|
||||
}
|
||||
|
||||
if (!empty($operations['deleted']) && !$this->softDeleteDetails($contactID, $operations['deleted'])) {
|
||||
return ['status' => 'error', 'message' => 'Failed to delete contact details'];
|
||||
}
|
||||
|
||||
if (!empty($operations['created']) && !$this->insertDetailRows($contactID, $operations['created'])) {
|
||||
return ['status' => 'error', 'message' => 'Failed to create contact details'];
|
||||
}
|
||||
|
||||
return ['status' => 'success'];
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'applyDetailOperations error: ' . $e->getMessage());
|
||||
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function insertDetailRows(int $contactID, array $items): bool
|
||||
{
|
||||
foreach ($items as $item) {
|
||||
if (!is_array($item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$item['ContactID'] = $contactID;
|
||||
$this->insert($item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function updateDetails(int $contactID, array $items): bool
|
||||
{
|
||||
foreach ($items as $detail) {
|
||||
$detailID = $detail['ContactDetID'] ?? null;
|
||||
if (!$detailID || !ctype_digit((string) $detailID)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$existing = $this->where('ContactDetID', (int) $detailID)
|
||||
->where('ContactID', $contactID)
|
||||
->first();
|
||||
|
||||
if (empty($existing)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$updateData = array_intersect_key($detail, array_flip($this->allowedFields));
|
||||
unset($updateData['ContactID']);
|
||||
|
||||
if ($updateData !== []) {
|
||||
$db = \Config\Database::connect();
|
||||
$db->table($this->table)
|
||||
->where('ContactDetID', (int) $detailID)
|
||||
->where('ContactID', $contactID)
|
||||
->where('ContactEndDate', null)
|
||||
->update($updateData);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function softDeleteDetails(int $contactID, array $ids): bool
|
||||
{
|
||||
$ids = array_values(array_unique(array_map('intval', $ids)));
|
||||
if ($ids === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$existing = $this->select('ContactDetID')
|
||||
->whereIn('ContactDetID', $ids)
|
||||
->where('ContactID', $contactID)
|
||||
->findAll();
|
||||
|
||||
if (count($existing) !== count($ids)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->whereIn('ContactDetID', $ids)
|
||||
->where('ContactID', $contactID)
|
||||
->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +57,10 @@ class ContactModel extends BaseModel {
|
||||
];
|
||||
}
|
||||
if (!empty($row['ContactDetID'])) {
|
||||
if (!empty($row['ContactEndDate'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$contact['Details'][] = [
|
||||
'SiteID' => $row['SiteID'] ?? null,
|
||||
'ContactDetID' => $row['ContactDetID'],
|
||||
@ -95,7 +99,11 @@ class ContactModel extends BaseModel {
|
||||
|
||||
if (!empty($details)) {
|
||||
$modelDetail = new \App\Models\Contact\ContactDetailModel();
|
||||
$result = $modelDetail->syncDetails($contactId, $details);
|
||||
if ($this->isDetailOperationsPayload($details)) {
|
||||
$result = $modelDetail->applyDetailOperations($contactId, $details);
|
||||
} else {
|
||||
$result = $modelDetail->syncDetails($contactId, $details);
|
||||
}
|
||||
|
||||
if ($result['status'] !== 'success') {
|
||||
throw new \RuntimeException('SyncDetails failed: ' . $result['message']);
|
||||
@ -109,14 +117,18 @@ class ContactModel extends BaseModel {
|
||||
'ContactID' => $contactId,
|
||||
'DetailsCount' => count($details),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
} catch (\Throwable $e) {
|
||||
$db->transRollback();
|
||||
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function isDetailOperationsPayload(array $details): bool {
|
||||
return (bool) array_intersect(array_keys($details), ['created', 'edited', 'deleted']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -633,6 +633,16 @@ paths:
|
||||
SubSpecialty:
|
||||
type: string
|
||||
description: Sub-specialty code
|
||||
Details:
|
||||
description: |
|
||||
Detail payload supports either a flat array of new rows (legacy format)
|
||||
or an operations object with `created`, `edited`, and `deleted` arrays.
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/ContactDetailOperations'
|
||||
- type: array
|
||||
description: Legacy format for replacing details with new rows only
|
||||
items:
|
||||
$ref: '#/components/schemas/ContactDetail'
|
||||
responses:
|
||||
'201':
|
||||
description: Contact updated successfully
|
||||
@ -8351,6 +8361,68 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Occupation display text
|
||||
ContactDetail:
|
||||
type: object
|
||||
properties:
|
||||
ContactDetID:
|
||||
type: integer
|
||||
description: Primary key
|
||||
ContactID:
|
||||
type: integer
|
||||
description: Parent contact ID
|
||||
SiteID:
|
||||
type: integer
|
||||
nullable: true
|
||||
description: Site identifier
|
||||
ContactCode:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Contact code at site
|
||||
ContactEmail:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Contact email address
|
||||
OccupationID:
|
||||
type: integer
|
||||
nullable: true
|
||||
description: Occupation reference
|
||||
JobTitle:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Job title
|
||||
Department:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Department name
|
||||
ContactStartDate:
|
||||
type: string
|
||||
format: date-time
|
||||
ContactEndDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
ContactDetailOperations:
|
||||
type: object
|
||||
properties:
|
||||
created:
|
||||
type: array
|
||||
description: New contact details to create
|
||||
items:
|
||||
$ref: '#/components/schemas/ContactDetail'
|
||||
edited:
|
||||
type: array
|
||||
description: Existing contact details to update
|
||||
items:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ContactDetail'
|
||||
- type: object
|
||||
required:
|
||||
- ContactDetID
|
||||
deleted:
|
||||
type: array
|
||||
description: Contact detail IDs to soft delete
|
||||
items:
|
||||
type: integer
|
||||
OrderSpecimen:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@ -87,6 +87,70 @@ Contact:
|
||||
format: date-time
|
||||
description: Occupation display text
|
||||
|
||||
ContactDetail:
|
||||
type: object
|
||||
properties:
|
||||
ContactDetID:
|
||||
type: integer
|
||||
description: Primary key
|
||||
ContactID:
|
||||
type: integer
|
||||
description: Parent contact ID
|
||||
SiteID:
|
||||
type: integer
|
||||
nullable: true
|
||||
description: Site identifier
|
||||
ContactCode:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Contact code at site
|
||||
ContactEmail:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Contact email address
|
||||
OccupationID:
|
||||
type: integer
|
||||
nullable: true
|
||||
description: Occupation reference
|
||||
JobTitle:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Job title
|
||||
Department:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Department name
|
||||
ContactStartDate:
|
||||
type: string
|
||||
format: date-time
|
||||
ContactEndDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
|
||||
ContactDetailOperations:
|
||||
type: object
|
||||
properties:
|
||||
created:
|
||||
type: array
|
||||
description: New contact details to create
|
||||
items:
|
||||
$ref: '#/ContactDetail'
|
||||
edited:
|
||||
type: array
|
||||
description: Existing contact details to update
|
||||
items:
|
||||
allOf:
|
||||
- $ref: '#/ContactDetail'
|
||||
- type: object
|
||||
required:
|
||||
- ContactDetID
|
||||
deleted:
|
||||
type: array
|
||||
description: Contact detail IDs to soft delete
|
||||
items:
|
||||
type: integer
|
||||
|
||||
Occupation:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@ -213,6 +213,16 @@
|
||||
SubSpecialty:
|
||||
type: string
|
||||
description: Sub-specialty code
|
||||
Details:
|
||||
description: |
|
||||
Detail payload supports either a flat array of new rows (legacy format)
|
||||
or an operations object with `created`, `edited`, and `deleted` arrays.
|
||||
oneOf:
|
||||
- $ref: '../components/schemas/master-data.yaml#/ContactDetailOperations'
|
||||
- type: array
|
||||
description: Legacy format for replacing details with new rows only
|
||||
items:
|
||||
$ref: '../components/schemas/master-data.yaml#/ContactDetail'
|
||||
responses:
|
||||
'201':
|
||||
description: Contact updated successfully
|
||||
|
||||
@ -37,9 +37,9 @@ class ContactPatchTest extends CIUnitTestCase
|
||||
private function createContact(array $data = []): array
|
||||
{
|
||||
$payload = array_merge([
|
||||
'ContactCode' => 'CON_' . uniqid(),
|
||||
'ContactName' => 'Test Contact ' . uniqid(),
|
||||
'ContactType' => 'PERSON',
|
||||
'NameFirst' => 'Test',
|
||||
'NameLast' => 'Contact ' . uniqid(),
|
||||
'Specialty' => 'GP',
|
||||
], $data);
|
||||
|
||||
$response = $this->withHeaders($this->authHeaders())
|
||||
@ -58,7 +58,7 @@ class ContactPatchTest extends CIUnitTestCase
|
||||
|
||||
$patch = $this->withHeaders($this->authHeaders())
|
||||
->withBodyFormat('json')
|
||||
->call('patch', "{$this->endpoint}/{$id}", ['ContactName' => 'Updated Contact']);
|
||||
->call('patch', "{$this->endpoint}/{$id}", ['NameFirst' => 'Updated']);
|
||||
|
||||
$patch->assertStatus(200);
|
||||
$patchData = json_decode($patch->getJSON(), true);
|
||||
@ -68,15 +68,14 @@ class ContactPatchTest extends CIUnitTestCase
|
||||
$show->assertStatus(200);
|
||||
$showData = json_decode($show->getJSON(), true)['data'];
|
||||
|
||||
$this->assertEquals('Updated Contact', $showData['ContactName']);
|
||||
$this->assertEquals($contact['ContactCode'], $showData['ContactCode']);
|
||||
$this->assertEquals('Updated', $showData['NameFirst']);
|
||||
}
|
||||
|
||||
public function testPartialUpdateContactNotFound()
|
||||
{
|
||||
$patch = $this->withHeaders($this->authHeaders())
|
||||
->withBodyFormat('json')
|
||||
->call('patch', "{$this->endpoint}/999999", ['ContactName' => 'Updated']);
|
||||
->call('patch', "{$this->endpoint}/999999", ['NameFirst' => 'Updated']);
|
||||
|
||||
$patch->assertStatus(404);
|
||||
}
|
||||
@ -85,7 +84,7 @@ class ContactPatchTest extends CIUnitTestCase
|
||||
{
|
||||
$patch = $this->withHeaders($this->authHeaders())
|
||||
->withBodyFormat('json')
|
||||
->call('patch', "{$this->endpoint}/invalid", ['ContactName' => 'Updated']);
|
||||
->call('patch', "{$this->endpoint}/invalid", ['NameFirst' => 'Updated']);
|
||||
|
||||
$patch->assertStatus(400);
|
||||
}
|
||||
@ -109,14 +108,82 @@ class ContactPatchTest extends CIUnitTestCase
|
||||
|
||||
$patch = $this->withHeaders($this->authHeaders())
|
||||
->withBodyFormat('json')
|
||||
->call('patch', "{$this->endpoint}/{$id}", ['ContactCode' => 'NEW_' . uniqid()]);
|
||||
->call('patch', "{$this->endpoint}/{$id}", ['Specialty' => 'Pathology']);
|
||||
|
||||
$patch->assertStatus(200);
|
||||
$showData = json_decode($this->withHeaders($this->authHeaders())
|
||||
->call('get', "{$this->endpoint}/{$id}")
|
||||
->getJSON(), true)['data'];
|
||||
|
||||
$this->assertNotEquals($contact['ContactCode'], $showData['ContactCode']);
|
||||
$this->assertEquals($contact['ContactName'], $showData['ContactName']);
|
||||
$this->assertEquals('Pathology', $showData['Specialty']);
|
||||
}
|
||||
|
||||
public function testPartialUpdateContactDetailOperations()
|
||||
{
|
||||
$contact = $this->createContact([
|
||||
'Details' => [
|
||||
[
|
||||
'SiteID' => '1',
|
||||
'ContactCode' => 'CODE1',
|
||||
'ContactEmail' => 'code1@example.com',
|
||||
'OccupationID' => '1',
|
||||
'JobTitle' => 'Doctor',
|
||||
'Department' => 'General',
|
||||
],
|
||||
[
|
||||
'SiteID' => '2',
|
||||
'ContactCode' => 'CODE2',
|
||||
'ContactEmail' => 'code2@example.com',
|
||||
'OccupationID' => '2',
|
||||
'JobTitle' => 'Specialist',
|
||||
'Department' => 'Laboratory',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$showBefore = $this->withHeaders($this->authHeaders())
|
||||
->call('get', "{$this->endpoint}/{$contact['ContactID']}");
|
||||
$showBefore->assertStatus(200);
|
||||
$beforeData = json_decode($showBefore->getJSON(), true)['data'];
|
||||
$keepDetail = $beforeData['Details'][0];
|
||||
$deleteDetail = $beforeData['Details'][1];
|
||||
|
||||
$patch = $this->withHeaders($this->authHeaders())
|
||||
->withBodyFormat('json')
|
||||
->call('patch', "{$this->endpoint}/{$contact['ContactID']}", [
|
||||
'Details' => [
|
||||
'edited' => [
|
||||
[
|
||||
'ContactDetID' => $keepDetail['ContactDetID'],
|
||||
'JobTitle' => 'Senior Doctor',
|
||||
],
|
||||
],
|
||||
'created' => [
|
||||
[
|
||||
'SiteID' => '3',
|
||||
'ContactCode' => 'CODE3',
|
||||
'ContactEmail' => 'code3@example.com',
|
||||
'OccupationID' => '3',
|
||||
'JobTitle' => 'Consultant',
|
||||
'Department' => 'Pathology',
|
||||
],
|
||||
],
|
||||
'deleted' => [$deleteDetail['ContactDetID']],
|
||||
],
|
||||
]);
|
||||
|
||||
$patch->assertStatus(200);
|
||||
|
||||
$showAfter = $this->withHeaders($this->authHeaders())
|
||||
->call('get', "{$this->endpoint}/{$contact['ContactID']}");
|
||||
$showAfter->assertStatus(200);
|
||||
$afterData = json_decode($showAfter->getJSON(), true)['data'];
|
||||
|
||||
$this->assertCount(2, $afterData['Details']);
|
||||
$detailIds = array_column($afterData['Details'], 'ContactDetID');
|
||||
$this->assertContains($keepDetail['ContactDetID'], $detailIds);
|
||||
|
||||
$updatedDetails = array_values(array_filter($afterData['Details'], static fn ($row) => $row['ContactDetID'] === $keepDetail['ContactDetID']));
|
||||
$this->assertNotEmpty($updatedDetails);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user