fix(insurance): let a clinic owner save the visit price of a member doctor
The pricing rows are keyed by the *target* tenant, but the tenant filter pins every read to the environment of whoever is asking. A clinic owner setting the free-visit price for one of their doctors was therefore blind to the row that already existed: each save inserted another one — the unique key does not stop it, because insurance_id is NULL for the free-visit row and MySQL does not treat NULLs as equal — and the following read was blind in the same way, so the panel kept showing the old value. From the outside it simply looked like the field would not save. Reads now run outside the filter, the same exception the tenant-insurance repository already makes for the same reason, with authorization still coming from resolveTargetEntity(). findOneForInsurance() takes the newest row so a tenant that already accumulated duplicates converges on the last value the user entered, and a migration collapses those leftovers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,29 +3,61 @@
|
||||
namespace App\Insurance\Repository;
|
||||
|
||||
use App\Insurance\Entity\EntityInsurancePricing;
|
||||
use App\Shared\Tenant\TenantFilterScope;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
class EntityInsurancePricingRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
public function __construct(ManagerRegistry $registry, private readonly TenantFilterScope $tenantScope)
|
||||
{
|
||||
parent::__construct($registry, EntityInsurancePricing::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* قیمت ویزیت به محیطِ *مقصد* تعلق دارد، نه محیطِ کاربرِ درخواستدهنده.
|
||||
*
|
||||
* مالک کلینیک قیمت ویزیت پزشکِ زیرمجموعه را ثبت میکند؛ با TenantFilter روشن این
|
||||
* کوئریها به محیط خودِ او محدود میشدند و ردیفِ موجود را نمیدیدند. نتیجهاش این
|
||||
* بود که هر ذخیره ردیفِ تازهای میساخت — و چون `insurance_id` برای «ویزیت آزاد»
|
||||
* NULL است، قیدِ یکتا هم جلویش را نمیگرفت (MySQL چند NULL را تکراری نمیشمارد) —
|
||||
* و خواندنِ بعدی هم همانطور کور بود، پس کاربر میدید «ثبت نمیشود».
|
||||
*
|
||||
* مجوزِ دیدنِ آن محیط قبلاً در `InsuranceController::resolveTargetEntity()` سنجیده
|
||||
* شده است.
|
||||
*
|
||||
* @template T
|
||||
* @param callable():T $query
|
||||
* @return T
|
||||
*/
|
||||
private function unscoped(callable $query): mixed
|
||||
{
|
||||
return $this->tenantScope->withoutFilter($query);
|
||||
}
|
||||
|
||||
/** @return EntityInsurancePricing[] */
|
||||
public function findByEntity(string $entityType, int $entityId): array
|
||||
{
|
||||
return $this->findBy(['entityType' => $entityType, 'entityId' => $entityId]);
|
||||
// ترتیب صریح روی id: خوانندهها روی حلقه «آخری برنده» حساب میکنند و بدون
|
||||
// ترتیب، ردیفهای تکراریِ بهجامانده میتوانستند قیمت قدیمی را برگردانند.
|
||||
return $this->unscoped(fn (): array => $this->findBy(
|
||||
['entityType' => $entityType, 'entityId' => $entityId],
|
||||
['id' => 'ASC'],
|
||||
));
|
||||
}
|
||||
|
||||
public function findOneForInsurance(string $entityType, int $entityId, ?int $insuranceId): ?EntityInsurancePricing
|
||||
{
|
||||
return $this->findOneBy([
|
||||
'entityType' => $entityType,
|
||||
'entityId' => $entityId,
|
||||
'insuranceId' => $insuranceId,
|
||||
]);
|
||||
return $this->unscoped(fn (): ?EntityInsurancePricing => $this->findOneBy(
|
||||
[
|
||||
'entityType' => $entityType,
|
||||
'entityId' => $entityId,
|
||||
'insuranceId' => $insuranceId,
|
||||
],
|
||||
// ردیفهای تکراریِ بهجامانده از دورهٔ باگ: تازهترین معتبر است، وگرنه
|
||||
// findOneBy میتوانست به قیمتِ قدیمی برگردد و اصلاح دوباره گم شود.
|
||||
['id' => 'DESC'],
|
||||
));
|
||||
}
|
||||
|
||||
public function save(EntityInsurancePricing $entity, bool $flush = true): void
|
||||
|
||||
Reference in New Issue
Block a user