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
@@ -46,6 +46,8 @@ class AdminApiController extends BaseController
private readonly \App\Patient\Service\PatientResolver $patientResolver,
private readonly \App\Insurance\Service\VisitPriceRequirementResolver $visitPriceResolver,
private readonly \App\Appointment\Service\AppointmentConfirmationService $appointmentConfirmation,
private readonly \App\Secretary\Repository\DoctorSecretaryRepository $doctorSecretaryRepo,
private readonly \App\Secretary\Repository\SecretaryEarningRepository $secretaryEarningRepo,
) {}
// ── Users ─────────────────────────────────────────────────────────────────
@@ -1500,6 +1502,7 @@ class AdminApiController extends BaseController
$qb = $this->em->createQueryBuilder()
->select(
'ds.uuid, ds.permissions, ds.active, ds.createdAt',
'ds.onlineShareEnabled, ds.onlineSharePercent',
'u.mobileNumber as mobile, u.realName as user_name',
'd.name as doctor_name, d.uuid as doctor_uuid',
)
@@ -1525,6 +1528,8 @@ class AdminApiController extends BaseController
'doctor_name' => $ds['doctor_name'],
'doctor_uuid' => $ds['doctor_uuid'],
'is_active' => (bool) $ds['active'],
'online_share_enabled' => (bool) $ds['onlineShareEnabled'],
'online_share_percent' => (float) $ds['onlineSharePercent'],
'permissions' => $ds['permissions'] ?? DoctorSecretary::DEFAULT_PERMISSIONS,
'created_at' => date('c', (int) $ds['createdAt']),
], $rows);
@@ -1532,6 +1537,75 @@ class AdminApiController extends BaseController
return $this->paginated($items, (int) $total, $page, $limit);
}
// ── Secretary detail + online-appointment share ───────────────────────────
#[OA\Get(
path: '/api/v1/admin/secretary/{uuid}',
summary: 'Secretary relation detail with its online-appointment share settings and earnings',
security: [['bearerAuth' => []]],
responses: [
new OA\Response(response: 200, description: 'Secretary detail'),
new OA\Response(response: 404, description: 'Secretary not found'),
]
)]
#[Route('/api/v1/admin/secretary/{uuid}', methods: ['GET'])]
public function secretaryDetail(string $uuid): JsonResponse
{
$relation = $this->doctorSecretaryRepo->findByUuid($uuid);
if ($relation === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'منشی یافت نشد', 404);
}
$secretary = $relation->getSecretary();
$monthStart = time() - 30 * 86_400;
return $this->success([
'data' => $relation->toArray() + [
'clinic_name' => $relation->getClinic()?->getName(),
'earnings' => [
'total_rials' => $this->secretaryEarningRepo->sumFor($secretary),
'this_month_rials' => $this->secretaryEarningRepo->sumFor($secretary, $monthStart),
'appointments_count' => $this->secretaryEarningRepo->countFor($secretary),
],
],
]);
}
#[OA\Put(
path: '/api/v1/admin/secretary/{uuid}/online-share',
summary: 'Enable/disable the secretary share of online appointments and set its percent',
security: [['bearerAuth' => []]],
responses: [
new OA\Response(response: 200, description: 'Saved'),
new OA\Response(response: 404, description: 'Secretary not found'),
new OA\Response(response: 422, description: 'Invalid percent'),
]
)]
#[Route('/api/v1/admin/secretary/{uuid}/online-share', methods: ['PUT'])]
public function saveSecretaryOnlineShare(string $uuid, Request $request): JsonResponse
{
$relation = $this->doctorSecretaryRepo->findByUuid($uuid);
if ($relation === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'منشی یافت نشد', 404);
}
$data = json_decode($request->getContent(), true) ?? [];
$enabled = (bool) ($data['enabled'] ?? false);
$percent = (float) ($data['percent'] ?? 0);
if ($percent < 0 || $percent > 100) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'درصد سهم باید بین ۰ تا ۱۰۰ باشد', 422, 'percent');
}
if ($enabled && $percent <= 0) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'برای فعال‌سازی، درصد سهم باید بیشتر از صفر باشد', 422, 'percent');
}
$relation->setOnlineShareEnabled($enabled)->setOnlineSharePercent($percent);
$this->doctorSecretaryRepo->save($relation);
return $this->success(['data' => $relation->toArray()]);
}
// ── Ratings ───────────────────────────────────────────────────────────────
#[OA\Get(