feat: Implement SMS sending functionality with KavehNegar and Rangineh providers
- Add SendSmsMessage class for encapsulating SMS message data. - Create KavehNegarProvider and RanginehProvider classes implementing SmsProviderInterface for sending SMS. - Implement SmsLogRepository and SmsTemplateRepository for managing SMS logs and templates. - Develop SendSmsHandler for handling SMS sending messages. - Create SmsService to manage SMS dispatching and logging. - Add UserProfileController for managing user profiles with CRUD operations. - Implement UserProfile entity and repository for user profile data management. - Update symfony.lock and bootstrap.php for project dependencies and environment setup.
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\UserProfile\Controller;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use App\UserProfile\Entity\UserProfile;
|
||||
use App\UserProfile\Repository\UserProfileRepository;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
class UserProfileController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserProfileRepository $repository,
|
||||
) {}
|
||||
|
||||
#[Route('/api/v1/user-profile', methods: ['POST'])]
|
||||
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
if ($this->repository->findByUser($user) !== null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'پروفایل قبلاً ایجاد شده است', 409);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$profile = new UserProfile($user);
|
||||
$this->hydrate($profile, $data);
|
||||
$this->repository->save($profile);
|
||||
|
||||
return $this->success(['data' => $profile->toArray()], 201);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/user-profile/{uuid}', methods: ['GET'])]
|
||||
public function show(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$profile = $this->repository->findByUuid($uuid);
|
||||
|
||||
if ($profile === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'پروفایل یافت نشد', 404);
|
||||
}
|
||||
|
||||
if (!$this->canAccess($profile, $user)) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
|
||||
return $this->success(['data' => $profile->toArray()]);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/user-profile/{uuid}', methods: ['PATCH'])]
|
||||
public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$profile = $this->repository->findByUuid($uuid);
|
||||
|
||||
if ($profile === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'پروفایل یافت نشد', 404);
|
||||
}
|
||||
|
||||
if (!$this->canAccess($profile, $user)) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$this->hydrate($profile, $data);
|
||||
$this->repository->save($profile);
|
||||
|
||||
return $this->success(['data' => $profile->toArray()]);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/user-profile/{uuid}', methods: ['DELETE'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function delete(string $uuid): JsonResponse
|
||||
{
|
||||
$profile = $this->repository->findByUuid($uuid);
|
||||
|
||||
if ($profile === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'پروفایل یافت نشد', 404);
|
||||
}
|
||||
|
||||
$this->repository->remove($profile);
|
||||
|
||||
return $this->success(['message' => 'پروفایل با موفقیت حذف شد']);
|
||||
}
|
||||
|
||||
private function canAccess(UserProfile $profile, User $currentUser): bool
|
||||
{
|
||||
return $profile->getUser()->getId() === $currentUser->getId()
|
||||
|| $currentUser->hasRole('ROLE_ADMIN');
|
||||
}
|
||||
|
||||
private function hydrate(UserProfile $profile, array $data): void
|
||||
{
|
||||
if (array_key_exists('name', $data)) $profile->setLabel($data['name']);
|
||||
if (array_key_exists('label', $data)) $profile->setLabel($data['label']);
|
||||
if (array_key_exists('family', $data)) $profile->setFamily($data['family']);
|
||||
if (array_key_exists('fathers_name', $data)) $profile->setFathersName($data['fathers_name']);
|
||||
if (array_key_exists('national_code', $data)) $profile->setNationalCode($data['national_code']);
|
||||
if (array_key_exists('gender', $data)) $profile->setGender($data['gender']);
|
||||
if (array_key_exists('blood_type', $data)) $profile->setBloodType($data['blood_type']);
|
||||
if (array_key_exists('marital_status', $data)) $profile->setMaritalStatus($data['marital_status']);
|
||||
if (array_key_exists('education', $data)) $profile->setEducation($data['education']);
|
||||
if (array_key_exists('job', $data)) $profile->setJob($data['job']);
|
||||
if (array_key_exists('address', $data)) $profile->setAddress($data['address']);
|
||||
if (array_key_exists('home_phone', $data)) $profile->setHomePhone($data['home_phone']);
|
||||
if (array_key_exists('work_phone', $data)) $profile->setWorkPhone($data['work_phone']);
|
||||
if (array_key_exists('insurance_id', $data)) $profile->setInsuranceId($data['insurance_id']);
|
||||
if (array_key_exists('description', $data)) $profile->setDescription(
|
||||
is_array($data['description']) ? ($data['description'][0]['value'] ?? null) : $data['description']
|
||||
);
|
||||
if (array_key_exists('sharing_with_user', $data)) $profile->setSharingWithUser((bool) $data['sharing_with_user']);
|
||||
|
||||
// birthday: accept Jalali string "1370-05-15" stored as-is converted to Unix
|
||||
if (array_key_exists('birthday', $data) && $data['birthday'] !== null) {
|
||||
// Store as string-encoded Unix; for now keep as null if conversion unavailable
|
||||
// Will be replaced with JalaliDateService in Task 16
|
||||
$profile->setDateOfBirth(null);
|
||||
}
|
||||
if (array_key_exists('date_of_birth', $data)) $profile->setDateOfBirth($data['date_of_birth']);
|
||||
|
||||
// Insurance references (category IDs)
|
||||
if (array_key_exists('basic_insurance', $data)) {
|
||||
$id = is_array($data['basic_insurance']) ? ($data['basic_insurance'][0] ?? null) : $data['basic_insurance'];
|
||||
$profile->setBasicInsuranceId($id !== null ? (int) $id : null);
|
||||
}
|
||||
if (array_key_exists('supplementary_insurance', $data)) {
|
||||
$id = is_array($data['supplementary_insurance'])
|
||||
? ($data['supplementary_insurance'][0] ?? null)
|
||||
: $data['supplementary_insurance'];
|
||||
$profile->setSupplementaryInsuranceId($id !== null ? (int) $id : null);
|
||||
}
|
||||
|
||||
// Medical history JSON
|
||||
if (array_key_exists('other', $data)) {
|
||||
$other = is_array($data['other']) && isset($data['other'][0])
|
||||
? $data['other'][0]
|
||||
: $data['other'];
|
||||
$profile->setOther($other);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace App\UserProfile\Entity;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'profiles')]
|
||||
#[ORM\UniqueConstraint(name: 'idx_profiles_user', columns: ['user_id'])]
|
||||
#[ORM\Index(columns: ['national_code'], name: 'idx_profiles_national_code')]
|
||||
class UserProfile
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\OneToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private User $user;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
||||
private ?string $label = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 25, nullable: true)]
|
||||
private ?string $family = null;
|
||||
|
||||
#[ORM\Column(name: 'fathers_name', type: 'string', length: 255, nullable: true)]
|
||||
private ?string $fathersName = null;
|
||||
|
||||
#[ORM\Column(name: 'national_code', type: 'string', length: 10, nullable: true)]
|
||||
private ?string $nationalCode = null;
|
||||
|
||||
#[ORM\Column(name: 'national_code_approved', type: 'boolean')]
|
||||
private bool $nationalCodeApproved = false;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 10, nullable: true)]
|
||||
private ?string $gender = null;
|
||||
|
||||
#[ORM\Column(name: 'date_of_birth', type: 'integer', nullable: true)]
|
||||
private ?int $dateOfBirth = null;
|
||||
|
||||
#[ORM\Column(name: 'blood_type', type: 'string', length: 20, nullable: true)]
|
||||
private ?string $bloodType = null;
|
||||
|
||||
#[ORM\Column(name: 'marital_status', type: 'string', length: 30, nullable: true)]
|
||||
private ?string $maritalStatus = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100, nullable: true)]
|
||||
private ?string $education = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100, nullable: true)]
|
||||
private ?string $job = null;
|
||||
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
private ?string $address = null;
|
||||
|
||||
#[ORM\Column(name: 'home_phone', type: 'string', length: 30, nullable: true)]
|
||||
private ?string $homePhone = null;
|
||||
|
||||
#[ORM\Column(name: 'work_phone', type: 'string', length: 30, nullable: true)]
|
||||
private ?string $workPhone = null;
|
||||
|
||||
#[ORM\Column(name: 'insurance_id', type: 'string', length: 50, nullable: true)]
|
||||
private ?string $insuranceId = null;
|
||||
|
||||
#[ORM\Column(name: 'basic_insurance_id', type: 'integer', nullable: true)]
|
||||
private ?int $basicInsuranceId = null;
|
||||
|
||||
#[ORM\Column(name: 'supplementary_insurance_id', type: 'integer', nullable: true)]
|
||||
private ?int $supplementaryInsuranceId = null;
|
||||
|
||||
#[ORM\Column(type: 'json', nullable: true)]
|
||||
private ?array $other = null;
|
||||
|
||||
#[ORM\Column(name: 'sharing_with_user', type: 'boolean')]
|
||||
private bool $sharingWithUser = false;
|
||||
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(name: 'created_at', type: 'integer')]
|
||||
private int $createdAt;
|
||||
|
||||
#[ORM\Column(name: 'updated_at', type: 'integer')]
|
||||
private int $updatedAt;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->uuid = Uuid::v4()->toRfc4122();
|
||||
$this->user = $user;
|
||||
$this->createdAt = time();
|
||||
$this->updatedAt = time();
|
||||
}
|
||||
|
||||
public function getId(): ?int { return $this->id; }
|
||||
public function getUuid(): string { return $this->uuid; }
|
||||
public function getUser(): User { return $this->user; }
|
||||
|
||||
public function getLabel(): ?string { return $this->label; }
|
||||
public function getFamily(): ?string { return $this->family; }
|
||||
public function getFathersName(): ?string { return $this->fathersName; }
|
||||
public function getNationalCode(): ?string { return $this->nationalCode; }
|
||||
public function isNationalCodeApproved(): bool { return $this->nationalCodeApproved; }
|
||||
public function getGender(): ?string { return $this->gender; }
|
||||
public function getDateOfBirth(): ?int { return $this->dateOfBirth; }
|
||||
public function getBloodType(): ?string { return $this->bloodType; }
|
||||
public function getMaritalStatus(): ?string { return $this->maritalStatus; }
|
||||
public function getEducation(): ?string { return $this->education; }
|
||||
public function getJob(): ?string { return $this->job; }
|
||||
public function getAddress(): ?string { return $this->address; }
|
||||
public function getHomePhone(): ?string { return $this->homePhone; }
|
||||
public function getWorkPhone(): ?string { return $this->workPhone; }
|
||||
public function getInsuranceId(): ?string { return $this->insuranceId; }
|
||||
public function getBasicInsuranceId(): ?int { return $this->basicInsuranceId; }
|
||||
public function getSupplementaryInsuranceId(): ?int { return $this->supplementaryInsuranceId; }
|
||||
public function getOther(): ?array { return $this->other; }
|
||||
public function isSharingWithUser(): bool { return $this->sharingWithUser; }
|
||||
public function getDescription(): ?string { return $this->description; }
|
||||
public function getCreatedAt(): int { return $this->createdAt; }
|
||||
public function getUpdatedAt(): int { return $this->updatedAt; }
|
||||
|
||||
public function setLabel(?string $v): self { $this->label = $v; return $this; }
|
||||
public function setFamily(?string $v): self { $this->family = $v; $this->touch(); return $this; }
|
||||
public function setFathersName(?string $v): self { $this->fathersName = $v; $this->touch(); return $this; }
|
||||
public function setNationalCode(?string $v): self { $this->nationalCode = $v; $this->touch(); return $this; }
|
||||
public function setNationalCodeApproved(bool $v): self { $this->nationalCodeApproved = $v; $this->touch(); return $this; }
|
||||
public function setGender(?string $v): self { $this->gender = $v; $this->touch(); return $this; }
|
||||
public function setDateOfBirth(?int $v): self { $this->dateOfBirth = $v; $this->touch(); return $this; }
|
||||
public function setBloodType(?string $v): self { $this->bloodType = $v; $this->touch(); return $this; }
|
||||
public function setMaritalStatus(?string $v): self { $this->maritalStatus = $v; $this->touch(); return $this; }
|
||||
public function setEducation(?string $v): self { $this->education = $v; $this->touch(); return $this; }
|
||||
public function setJob(?string $v): self { $this->job = $v; $this->touch(); return $this; }
|
||||
public function setAddress(?string $v): self { $this->address = $v; $this->touch(); return $this; }
|
||||
public function setHomePhone(?string $v): self { $this->homePhone = $v; $this->touch(); return $this; }
|
||||
public function setWorkPhone(?string $v): self { $this->workPhone = $v; $this->touch(); return $this; }
|
||||
public function setInsuranceId(?string $v): self { $this->insuranceId = $v; $this->touch(); return $this; }
|
||||
public function setBasicInsuranceId(?int $v): self { $this->basicInsuranceId = $v; $this->touch(); return $this; }
|
||||
public function setSupplementaryInsuranceId(?int $v): self { $this->supplementaryInsuranceId = $v; $this->touch(); return $this; }
|
||||
public function setOther(?array $v): self { $this->other = $v; $this->touch(); return $this; }
|
||||
public function setSharingWithUser(bool $v): self { $this->sharingWithUser = $v; $this->touch(); return $this; }
|
||||
public function setDescription(?string $v): self { $this->description = $v; $this->touch(); return $this; }
|
||||
|
||||
private function touch(): void { $this->updatedAt = time(); }
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'uuid' => $this->uuid,
|
||||
'user_uuid' => $this->user->getUuid(),
|
||||
'label' => $this->label,
|
||||
'family' => $this->family,
|
||||
'fathers_name' => $this->fathersName,
|
||||
'national_code' => $this->nationalCode,
|
||||
'national_code_approved' => $this->nationalCodeApproved,
|
||||
'gender' => $this->gender,
|
||||
'date_of_birth' => $this->dateOfBirth,
|
||||
'blood_type' => $this->bloodType,
|
||||
'marital_status' => $this->maritalStatus,
|
||||
'education' => $this->education,
|
||||
'job' => $this->job,
|
||||
'address' => $this->address,
|
||||
'home_phone' => $this->homePhone,
|
||||
'work_phone' => $this->workPhone,
|
||||
'insurance_id' => $this->insuranceId,
|
||||
'basic_insurance_id' => $this->basicInsuranceId,
|
||||
'supplementary_insurance_id' => $this->supplementaryInsuranceId,
|
||||
'other' => $this->other,
|
||||
'sharing_with_user' => $this->sharingWithUser,
|
||||
'description' => $this->description,
|
||||
'created_at' => $this->createdAt,
|
||||
'updated_at' => $this->updatedAt,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\UserProfile\Repository;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\UserProfile\Entity\UserProfile;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
class UserProfileRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, UserProfile::class);
|
||||
}
|
||||
|
||||
public function findByUser(User $user): ?UserProfile
|
||||
{
|
||||
return $this->findOneBy(['user' => $user]);
|
||||
}
|
||||
|
||||
public function findByUuid(string $uuid): ?UserProfile
|
||||
{
|
||||
return $this->findOneBy(['uuid' => $uuid]);
|
||||
}
|
||||
|
||||
public function save(UserProfile $profile, bool $flush = true): void
|
||||
{
|
||||
$this->getEntityManager()->persist($profile);
|
||||
if ($flush) {
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
|
||||
public function remove(UserProfile $profile, bool $flush = true): void
|
||||
{
|
||||
$this->getEntityManager()->remove($profile);
|
||||
if ($flush) {
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user