feat: Add online share functionality for secretaries

- Introduced `online_share_enabled` and `online_share_percent` fields in the `doctor_secretaries` table to manage secretary shares from online appointments.
- Added `bank_account` field in the `profiles` table to store user-level IBANs for settlements.
- Created `secretary_earnings` table to track earnings per secretary from online appointments, including a foreign key relationship with `financial_breakdowns`.
- Implemented `SecretaryEarning` entity and repository for managing secretary earnings.
- Developed `SecretaryShareResolver` service to determine which secretaries earn from online payments.
- Added `UserIbanResolver` service to handle user IBAN retrieval and management.
- Created `HasIbansTrait` for entities to manage IBANs in a JSON format.
- Implemented tests for secretary earnings and API endpoints for managing secretary shares and IBANs.
This commit is contained in:
hamed
2026-07-25 18:34:18 +03:30
parent 73a608c3dd
commit 8d2b0d908a
33 changed files with 2564 additions and 76 deletions
+22
View File
@@ -73,6 +73,17 @@ class DoctorSecretary
#[ORM\Column(type: 'boolean')]
private bool $active = true;
/**
* سهم منشی از نوبت‌های آنلاینِ همین پزشک/کلینیک فعال است؟ تنظیم per-relation است:
* یک منشی می‌تواند برای یک پزشک سهم داشته باشد و برای دیگری نه.
*/
#[ORM\Column(name: 'online_share_enabled', type: 'boolean', options: ['default' => false])]
private bool $onlineShareEnabled = false;
/** درصد سهم منشی از «خالصِ پس از مالیات» نوبت آنلاین. */
#[ORM\Column(name: 'online_share_percent', type: 'decimal', precision: 5, scale: 2, options: ['default' => '0.00'])]
private string $onlineSharePercent = '0.00';
#[ORM\Column(name: 'created_at', type: 'integer')]
private int $createdAt;
@@ -101,6 +112,13 @@ class DoctorSecretary
public function getNationalCode(): ?string { return $this->nationalCode; }
public function getAddress(): ?string { return $this->address; }
public function isActive(): bool { return $this->active; }
public function isOnlineShareEnabled(): bool { return $this->onlineShareEnabled; }
public function getOnlineSharePercent(): float { return (float) $this->onlineSharePercent; }
/** سهم مؤثر: درصد فقط وقتی معنا دارد که رابطه فعال و سهم روشن باشد. */
public function effectiveOnlineSharePercent(): float
{
return ($this->active && $this->onlineShareEnabled) ? (float) $this->onlineSharePercent : 0.0;
}
public function getCreatedAt(): int { return $this->createdAt; }
public function getUpdatedAt(): int { return $this->updatedAt; }
@@ -108,6 +126,8 @@ class DoctorSecretary
public function setPermissions(array $v): self { $this->permissions = $v; $this->touch(); return $this; }
public function setNationalCode(?string $v): self { $this->nationalCode = $v; $this->touch(); return $this; }
public function setAddress(?string $v): self { $this->address = $v; $this->touch(); return $this; }
public function setOnlineShareEnabled(bool $v): self { $this->onlineShareEnabled = $v; $this->touch(); return $this; }
public function setOnlineSharePercent(float $v): self { $this->onlineSharePercent = (string) $v; $this->touch(); return $this; }
/** Deep merge: only provided resources/actions are updated */
public function mergePermissions(array $patch): void
@@ -144,6 +164,8 @@ class DoctorSecretary
'owner_type' => $this->ownerType,
'clinic_uuid' => $this->clinic?->getUuid(),
'is_active' => $this->active,
'online_share_enabled' => $this->onlineShareEnabled,
'online_share_percent' => (float) $this->onlineSharePercent,
'national_code' => $this->nationalCode,
'address' => $this->address,
'permissions' => $this->getPermissions()['resources'] ?? $this->getPermissions(),
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace App\Secretary\Entity;
use App\Auth\Entity\User;
use App\Secretary\Repository\SecretaryEarningRepository;
use App\Settlement\Entity\FinancialBreakdown;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
/**
* سهم یک منشی از یک نوبت آنلاین.
*
* جدول جداست (نه JSON روی FinancialBreakdown) چون گزارش پنل منشی باید per-user
* فیلتر و جمع‌بندی شود؛ یک پرداخت می‌تواند چند منشیِ سهم‌بر داشته باشد.
*/
#[ORM\Entity(repositoryClass: SecretaryEarningRepository::class)]
#[ORM\Table(name: 'secretary_earnings')]
#[ORM\Index(columns: ['secretary_user_id', 'created_at'], name: 'idx_secretary_earnings_user_time')]
class SecretaryEarning
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 36, unique: true)]
private string $uuid;
#[ORM\ManyToOne(targetEntity: FinancialBreakdown::class)]
#[ORM\JoinColumn(name: 'breakdown_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
private FinancialBreakdown $breakdown;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: 'secretary_user_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
private User $secretary;
/** رابطهٔ منشی–پزشک/کلینیکی که سهم از آن آمده (snapshot، برای ردگیری). */
#[ORM\Column(name: 'relation_uuid', type: 'string', length: 36)]
private string $relationUuid;
#[ORM\Column(name: 'share_percent', type: 'decimal', precision: 5, scale: 2)]
private string $sharePercent;
#[ORM\Column(name: 'share_rials', type: 'integer')]
private int $shareRials;
#[ORM\Column(name: 'created_at', type: 'integer')]
private int $createdAt;
public function __construct(
FinancialBreakdown $breakdown,
User $secretary,
string $relationUuid,
float $sharePercent,
int $shareRials,
) {
$this->uuid = Uuid::v4()->toRfc4122();
$this->breakdown = $breakdown;
$this->secretary = $secretary;
$this->relationUuid = $relationUuid;
$this->sharePercent = number_format($sharePercent, 2, '.', '');
$this->shareRials = $shareRials;
$this->createdAt = time();
}
public function getId(): ?int { return $this->id; }
public function getUuid(): string { return $this->uuid; }
public function getBreakdown(): FinancialBreakdown { return $this->breakdown; }
public function getSecretary(): User { return $this->secretary; }
public function getRelationUuid(): string { return $this->relationUuid; }
public function getSharePercent(): float { return (float) $this->sharePercent; }
public function getShareRials(): int { return $this->shareRials; }
public function getCreatedAt(): int { return $this->createdAt; }
public function toArray(): array
{
return [
'uuid' => $this->uuid,
'relation_uuid' => $this->relationUuid,
'share_percent' => (float) $this->sharePercent,
'share_rials' => $this->shareRials,
'created_at' => $this->createdAt,
];
}
}