fix(representation): pay commission on online bookings from city sites
Three things kept a city-site booking from ever reaching its representative. The domain never resolved. City sites carry their own domain on cities.domain while a representative's coverage is a set of cities, and representations.domain is normally only filled for a global agent. The resolver looked at that column alone, so bookingRepId was always null and the commission guard rejected every booking made through a city site. It now falls back to the active representative covering that city, and stays null when two of them cover it — an ambiguous money assignment has to be resolved in the data, not guessed. Commission waited for confirmation. The money has already arrived when the gateway callback succeeds; confirming the appointment is the doctor's or secretary's job and may happen days later or never. It is now recorded on payment, with the appointment still pending. Recording is idempotent, so the confirmation path stays and creates nothing twice. The dashboard counted every appointment of the representative's doctors, including the ones a secretary typed into the panel. It now counts only bookings that came from the representative's own site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -328,6 +328,17 @@ final class PaymentManager
|
||||
$this->em->persist($appointment);
|
||||
|
||||
$doctor = $appointment->getDoctor();
|
||||
|
||||
// تقسیم مالی همینجا انجام میشود، نه در لحظهٔ تأیید نوبت: پول از سایت آمده و
|
||||
// سهم نماینده باید همان لحظه ثبت شود. تأیید نوبت کارِ پزشک/منشی است و ممکن
|
||||
// است روزها بعد یا هرگز انجام نشود. ثبت idempotent است، پس مسیر تأیید
|
||||
// (`AppointmentConfirmationService`) دوباره چیزی نمیسازد.
|
||||
$this->commissionService->processAppointment(
|
||||
$payment,
|
||||
$doctor->getRepresentationId(),
|
||||
$this->bookingRepresentationIdFor($payment),
|
||||
$doctor->getId(),
|
||||
);
|
||||
$mobile = $appointment->getPatientMobile();
|
||||
if ($mobile) {
|
||||
$this->smsService->dispatchTemplate(SmsLog::TAG_PAYMENT, $mobile, [
|
||||
|
||||
@@ -493,11 +493,15 @@ class RepresentationController extends BaseController
|
||||
{
|
||||
$repId = $rep->getId();
|
||||
|
||||
// نوبتهای پزشکانِ همین نماینده در بازه.
|
||||
// فقط نوبتهای **آنلاینِ** همین نماینده: نوبتی که منشی در پنل ثبت میکند از
|
||||
// سایتِ نماینده نیامده و سهمی هم نمیسازد، پس در آمار نماینده هم نباید بیاید.
|
||||
// `bookingRepresentationId` تنها در مسیر رزرو سایت عمومی پر میشود.
|
||||
$totalAppointments = (int) $this->em->createQuery(
|
||||
'SELECT COUNT(a.id) FROM App\Appointment\Entity\Appointment a
|
||||
JOIN a.doctor d
|
||||
WHERE d.representationId = :repId AND a.createdAt BETWEEN :start AND :end'
|
||||
WHERE d.representationId = :repId
|
||||
AND a.bookingRepresentationId = :repId
|
||||
AND a.createdAt BETWEEN :start AND :end'
|
||||
)->setParameters(['repId' => $repId, 'start' => $startTs, 'end' => $endTs])
|
||||
->getSingleScalarResult();
|
||||
|
||||
|
||||
@@ -29,6 +29,30 @@ class RepresentationRepository extends ServiceEntityRepository
|
||||
return $this->findOneBy(['domain' => $domain, 'active' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* نمایندهٔ فعالِ یک شهر، از نگاشت چندشهریِ `representation_cities`.
|
||||
*
|
||||
* سایتهای شهری دامنهٔ خودشان را دارند (`cities.domain`) و نماینده معمولاً ستون
|
||||
* `domain` ندارد؛ بدون این، دامنهٔ شهری به هیچ نمایندهای نمیرسید و پورسانتِ
|
||||
* نوبتهای آن سایت هرگز حساب نمیشد.
|
||||
*
|
||||
* فقط وقتی نتیجه یکتاست برمیگردد: اگر دو نماینده یک شهر را پوشش دهند، انتساب
|
||||
* پول مبهم است و باید صریح حل شود، نه با حدس.
|
||||
*/
|
||||
public function findActiveByCityId(int $cityId): ?Representation
|
||||
{
|
||||
$rows = $this->createQueryBuilder('r')
|
||||
->join('r.cities', 'c')
|
||||
->where('r.active = true')
|
||||
->andWhere('c.id = :cityId')
|
||||
->setParameter('cityId', $cityId)
|
||||
->setMaxResults(2)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
return count($rows) === 1 ? $rows[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* تطبیق بر اساس اولین label دامنه (TLD-agnostic) — همراستا با تطبیق subdomainِ
|
||||
* شهرها؛ لازم برای dev (مثلاً host=`x-nobat.localhost` باید نمایندهی `x-nobat.ir`
|
||||
|
||||
@@ -50,9 +50,12 @@ class DomainContextResolver
|
||||
|
||||
$city = $this->cityRepo->findByDomain($domain);
|
||||
if ($city !== null) {
|
||||
// دامنهی شهری؛ نمایندهای که دامنهی اختصاصیاش این باشد وجود ندارد،
|
||||
// ولی ممکن است نمایندهای دامنهی شهر را بهعنوان دامنهی خودش ثبت کرده باشد.
|
||||
$rep = $this->representationRepo->findActiveByDomain($domain);
|
||||
// دامنهی شهری. اول نمایندهای که همین دامنه را بهعنوان دامنهی خودش ثبت
|
||||
// کرده، بعد نمایندهی خودِ شهر: مدل پوشش نماینده شهری است، نه دامنهای، و
|
||||
// ستون `domain` معمولاً فقط برای نمایندهی سراسری پر میشود.
|
||||
$rep = $this->representationRepo->findActiveByDomain($domain)
|
||||
?? $this->representationRepo->findActiveByCityId((int) $city->getId());
|
||||
|
||||
return new DomainContext($rep, $city, false);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user