Backend:
- Add profile columns field_of_study, province_id, city_id, postal_code,
referral_source (UserProfile + migration).
- Extend PATCH /api/v1/patient/{uuid} to persist all demographic fields
and return them in the patient profile payload.
- Support editable mobile (login identifier): validation, uniqueness,
User.setMobileNumber, new ERR_PROFILE_002.
- Update docs/api/patient.md.
Frontend:
- New reusable Input, Field, and PatientRecordInfoForm (RHF + Zod).
- usePatient/useUpdatePatient hooks and patientForm mapping helpers.
- Extend the existing "info" tab in MyPatientsPage to the full field set
via the shared form (province/city/insurance options, Jalali date).
Tests: Patient entity + PATCH integration (PHPUnit); form, hooks, and
mapping helpers (Vitest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
124 lines
4.7 KiB
PHP
124 lines
4.7 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Patient;
|
|
|
|
use App\Auth\Entity\User;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Patient\Entity\PatientRecord;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* Integration coverage for PATCH /api/v1/patient/{uuid}:
|
|
* the extended demographic fields persist, and mobile edits validate,
|
|
* enforce uniqueness, and rewrite the patient's login identifier.
|
|
*
|
|
* Gate: a doctor caller falls back to the seeded `free` plan, which grants
|
|
* `patient_records` in db_test — so no explicit subscription is needed.
|
|
*/
|
|
class PatientUpdateProfileTest extends ApiTestCase
|
|
{
|
|
private User $doctorUser;
|
|
private int $doctorId;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->doctorUser = $this->createUser(['ROLE_DOCTOR']);
|
|
$doctor = new Doctor($this->doctorUser, 'دکتر تست');
|
|
$this->em->persist($doctor);
|
|
$this->em->flush();
|
|
$this->doctorId = $doctor->getId();
|
|
}
|
|
|
|
/** Create a patient User + PatientRecord owned by the test doctor; return the record uuid. */
|
|
private function makeRecord(?string $mobile = null): array
|
|
{
|
|
$patient = $this->createUser(['ROLE_USER'], $mobile);
|
|
$patient->setRealName('ساغر صابری نژاد');
|
|
$this->em->persist($patient);
|
|
|
|
$record = new PatientRecord('doctor', $this->doctorId, $patient, 'doctor', $this->doctorId);
|
|
$this->em->persist($record);
|
|
$this->em->flush();
|
|
|
|
return [$record->getUuid(), $patient];
|
|
}
|
|
|
|
public function testUpdatePersistsExtendedDemographicFields(): void
|
|
{
|
|
[$uuid] = $this->makeRecord();
|
|
|
|
$res = $this->authJson('PATCH', "/api/v1/patient/{$uuid}", $this->doctorUser, [
|
|
'fathers_name' => 'رضا',
|
|
'education' => 'کارشناسی',
|
|
'field_of_study' => 'نرمافزار',
|
|
'job' => 'مهندس',
|
|
'province_id' => 8,
|
|
'city_id' => 42,
|
|
'postal_code' => '8913746351',
|
|
'referral_source' => 'اینستاگرام',
|
|
'description' => 'یادداشت آزمایشی',
|
|
]);
|
|
|
|
self::assertSame(200, $this->responseCode());
|
|
$profile = $res['data']['profile'] ?? [];
|
|
self::assertSame('رضا', $profile['fathers_name']);
|
|
self::assertSame('کارشناسی', $profile['education']);
|
|
self::assertSame('نرمافزار', $profile['field_of_study']);
|
|
self::assertSame('مهندس', $profile['job']);
|
|
self::assertSame(8, $profile['province_id']);
|
|
self::assertSame(42, $profile['city_id']);
|
|
self::assertSame('8913746351', $profile['postal_code']);
|
|
self::assertSame('اینستاگرام', $profile['referral_source']);
|
|
self::assertSame('یادداشت آزمایشی', $profile['description']);
|
|
}
|
|
|
|
public function testEmptyStringClearsFieldToNull(): void
|
|
{
|
|
[$uuid] = $this->makeRecord();
|
|
|
|
$this->authJson('PATCH', "/api/v1/patient/{$uuid}", $this->doctorUser, ['job' => 'مهندس']);
|
|
$res = $this->authJson('PATCH', "/api/v1/patient/{$uuid}", $this->doctorUser, ['job' => '']);
|
|
|
|
self::assertSame(200, $this->responseCode());
|
|
self::assertNull($res['data']['profile']['job']);
|
|
}
|
|
|
|
public function testMobileEditRewritesLoginIdentifier(): void
|
|
{
|
|
[$uuid, $patient] = $this->makeRecord();
|
|
$newMobile = '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT);
|
|
|
|
$res = $this->authJson('PATCH', "/api/v1/patient/{$uuid}", $this->doctorUser, ['mobile' => $newMobile]);
|
|
|
|
self::assertSame(200, $this->responseCode());
|
|
self::assertSame($newMobile, $res['data']['profile']['mobile']);
|
|
|
|
$this->em->refresh($patient);
|
|
self::assertSame($newMobile, $patient->getMobileNumber());
|
|
self::assertSame($newMobile, $patient->getUserIdentifier());
|
|
}
|
|
|
|
public function testMobileTakenByAnotherUserReturns409(): void
|
|
{
|
|
$taken = '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT);
|
|
$this->createUser(['ROLE_USER'], $taken); // occupy the number
|
|
|
|
[$uuid] = $this->makeRecord();
|
|
$res = $this->authJson('PATCH', "/api/v1/patient/{$uuid}", $this->doctorUser, ['mobile' => $taken]);
|
|
|
|
self::assertSame(409, $this->responseCode());
|
|
self::assertSame('ERR_PROFILE_002', $res['errors'][0]['code']);
|
|
}
|
|
|
|
public function testInvalidMobileReturns422(): void
|
|
{
|
|
[$uuid] = $this->makeRecord();
|
|
$res = $this->authJson('PATCH', "/api/v1/patient/{$uuid}", $this->doctorUser, ['mobile' => '12345']);
|
|
|
|
self::assertSame(422, $this->responseCode());
|
|
self::assertSame('ERR_VALIDATION_001', $res['errors'][0]['code']);
|
|
}
|
|
}
|