feat: enhance payment status handling and summary in MyPaymentsPage
- Added support for 'partial' payment status in MyPaymentsPage and related components. - Updated API responses to include 'paid_rials' and 'summary' for invoices. - Introduced InvoicePaymentStatus service to derive payment status based on actual payments. - Enhanced tests to cover new payment scenarios including partial payments and payment methods. - Updated documentation to reflect changes in payment status and API responses.
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
namespace App\Billing\Repository;
|
||||
|
||||
use App\Billing\Entity\Invoice;
|
||||
use App\Billing\Service\InvoicePaymentStatus;
|
||||
use App\Patient\Entity\PatientRecord;
|
||||
use App\Patient\Entity\SessionPayment;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@@ -40,7 +42,8 @@ class InvoiceRepository extends ServiceEntityRepository
|
||||
->select(
|
||||
'i.uuid AS invoice_uuid', 'r.uuid AS patient_uuid', 'u.realName AS patient_name',
|
||||
'u.nationalCode AS national_code', 'i.issuedAt AS issued_at',
|
||||
'i.patientRials AS amount_rials', 'i.status AS status',
|
||||
'i.patientRials AS amount_rials',
|
||||
sprintf('%s AS paid_rials', self::paidSumDql('sp_row')),
|
||||
)
|
||||
->orderBy('i.issuedAt', 'DESC')
|
||||
->setFirstResult(($page - 1) * $limit)
|
||||
@@ -55,10 +58,78 @@ class InvoiceRepository extends ServiceEntityRepository
|
||||
'national_code' => $r['national_code'],
|
||||
'issued_at' => (int) $r['issued_at'],
|
||||
'amount_rials' => (int) $r['amount_rials'],
|
||||
'status' => $r['status'] === Invoice::STATUS_PAID ? 'paid' : 'unsettled',
|
||||
'paid_rials' => (int) $r['paid_rials'],
|
||||
'status' => InvoicePaymentStatus::resolve((int) $r['amount_rials'], (int) $r['paid_rials']),
|
||||
], $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* زیرپرسوجوی مجموع پرداختهای مراجعهی همان صورتحساب. صورتحساب بدون مراجعه
|
||||
* هیچ پرداختی ندارد، پس `COALESCE` صفر میدهد و «تسویهنشده» میماند.
|
||||
*
|
||||
* @param string $alias نام یکتا؛ استفادهی دوبار از یک alias در یک query خطای semantical میدهد.
|
||||
*/
|
||||
private static function paidSumDql(string $alias): string
|
||||
{
|
||||
return sprintf(
|
||||
'(SELECT COALESCE(SUM(%1$s.amountRials), 0) FROM %2$s %1$s WHERE IDENTITY(%1$s.session) = i.patientSessionId)',
|
||||
$alias,
|
||||
SessionPayment::class,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* پرداختهای ثبتشدهی هر صورتحساب (از راه مراجعهاش)، در یک کوئری برای کل صفحه.
|
||||
* مبلغ وصولشده هم از همین ردیفها جمع میشود تا کوئری دوم لازم نباشد.
|
||||
*
|
||||
* @param list<Invoice> $invoices
|
||||
* @return array<int, list<array{method:string,amount_rials:int,paid_at:int,created_by_name:?string}>>
|
||||
* کلید = شناسهی صورتحساب؛ قدیمیترین پرداخت اول.
|
||||
*/
|
||||
public function paymentsForInvoices(array $invoices): array
|
||||
{
|
||||
$sessionIds = [];
|
||||
foreach ($invoices as $invoice) {
|
||||
$sessionId = $invoice->getPatientSessionId();
|
||||
if ($sessionId !== null) {
|
||||
$sessionIds[$invoice->getId()] = $sessionId;
|
||||
}
|
||||
}
|
||||
if ($sessionIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->getEntityManager()->createQueryBuilder()
|
||||
->select(
|
||||
'IDENTITY(sp.session) AS session_id', 'sp.method AS method',
|
||||
'sp.amountRials AS amount_rials', 'sp.paidAt AS paid_at',
|
||||
'sp.createdByName AS created_by_name',
|
||||
)
|
||||
->from(SessionPayment::class, 'sp')
|
||||
->where('IDENTITY(sp.session) IN (:sessions)')
|
||||
->setParameter('sessions', array_values($sessionIds))
|
||||
->orderBy('sp.paidAt', 'ASC')
|
||||
->getQuery()
|
||||
->getArrayResult();
|
||||
|
||||
$bySession = [];
|
||||
foreach ($rows as $row) {
|
||||
$bySession[(int) $row['session_id']][] = [
|
||||
'method' => $row['method'],
|
||||
'amount_rials' => (int) $row['amount_rials'],
|
||||
'paid_at' => (int) $row['paid_at'],
|
||||
'created_by_name' => $row['created_by_name'],
|
||||
];
|
||||
}
|
||||
|
||||
$byInvoice = [];
|
||||
foreach ($sessionIds as $invoiceId => $sessionId) {
|
||||
$byInvoice[$invoiceId] = $bySession[$sessionId] ?? [];
|
||||
}
|
||||
|
||||
return $byInvoice;
|
||||
}
|
||||
|
||||
public function countTenantInvoices(string $entityType, int $entityId, array $filters): int
|
||||
{
|
||||
return (int) $this->tenantInvoicesQuery($entityType, $entityId, $filters)
|
||||
@@ -76,18 +147,43 @@ class InvoiceRepository extends ServiceEntityRepository
|
||||
*/
|
||||
public function tenantInvoiceSummary(string $entityType, int $entityId, array $filters): array
|
||||
{
|
||||
$row = $this->tenantInvoicesQuery($entityType, $entityId, $filters)
|
||||
return $this->summarize($this->tenantInvoicesQuery($entityType, $entityId, $filters), 'patientRials');
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate totals over one patient's invoices — the same set {@see invoicesForPatient}
|
||||
* pages through, so the detail page's cards match its table.
|
||||
*
|
||||
* @return array{total_rials:int,paid_rials:int,unsettled_rials:int,invoices_count:int}
|
||||
*/
|
||||
public function patientInvoiceSummary(string $entityType, int $entityId, int $recordId): array
|
||||
{
|
||||
return $this->summarize($this->patientInvoicesQuery($entityType, $entityId, $recordId), 'totalRials');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum `$field` over a prepared invoice query, split by paid vs. still unsettled.
|
||||
*
|
||||
* @return array{total_rials:int,paid_rials:int,unsettled_rials:int,invoices_count:int}
|
||||
*/
|
||||
private function summarize(QueryBuilder $qb, string $field): array
|
||||
{
|
||||
$row = (clone $qb)
|
||||
->select(
|
||||
'COALESCE(SUM(i.patientRials), 0) AS total_rials',
|
||||
'COALESCE(SUM(CASE WHEN i.status = :paidStatus THEN i.patientRials ELSE 0 END), 0) AS paid_rials',
|
||||
sprintf('COALESCE(SUM(i.%s), 0) AS total_rials', $field),
|
||||
'COUNT(i.id) AS invoices_count',
|
||||
)
|
||||
->setParameter('paidStatus', Invoice::STATUS_PAID)
|
||||
->getQuery()
|
||||
->getSingleResult();
|
||||
|
||||
// «پرداختشده» = مجموع صورتحسابهایی که سهم بیمارشان کامل وصول شده
|
||||
$paid = (int) (clone $qb)
|
||||
->select(sprintf('COALESCE(SUM(i.%s), 0)', $field))
|
||||
->andWhere(sprintf('%s >= i.patientRials', self::paidSumDql('sp_sum')))
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
|
||||
$total = (int) $row['total_rials'];
|
||||
$paid = (int) $row['paid_rials'];
|
||||
|
||||
return [
|
||||
'total_rials' => $total,
|
||||
@@ -118,11 +214,17 @@ class InvoiceRepository extends ServiceEntityRepository
|
||||
if (!empty($filters['to'])) {
|
||||
$qb->andWhere('i.issuedAt <= :to')->setParameter('to', (int) $filters['to']);
|
||||
}
|
||||
if (($filters['status'] ?? null) === 'paid') {
|
||||
$qb->andWhere('i.status = :st')->setParameter('st', Invoice::STATUS_PAID);
|
||||
} elseif (($filters['status'] ?? null) === 'unsettled') {
|
||||
$qb->andWhere('i.status = :st')->setParameter('st', Invoice::STATUS_FINALIZED);
|
||||
}
|
||||
// وضعیت پرداخت مشتق است (پرداختهای مراجعه در برابر سهم بیمار)، نه ستون status
|
||||
match ($filters['status'] ?? null) {
|
||||
InvoicePaymentStatus::PAID => $qb->andWhere(sprintf('%s >= i.patientRials', self::paidSumDql('sp_f1'))),
|
||||
InvoicePaymentStatus::PARTIAL => $qb->andWhere(sprintf(
|
||||
'%s > 0 AND %s < i.patientRials',
|
||||
self::paidSumDql('sp_f1'),
|
||||
self::paidSumDql('sp_f2'),
|
||||
)),
|
||||
InvoicePaymentStatus::UNSETTLED => $qb->andWhere(sprintf('%s <= 0 AND i.patientRials > 0', self::paidSumDql('sp_f1'))),
|
||||
default => null,
|
||||
};
|
||||
|
||||
return $qb;
|
||||
}
|
||||
@@ -141,14 +243,6 @@ class InvoiceRepository extends ServiceEntityRepository
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function countInvoicesForPatient(string $entityType, int $entityId, int $recordId): int
|
||||
{
|
||||
return (int) $this->patientInvoicesQuery($entityType, $entityId, $recordId)
|
||||
->select('COUNT(i.id)')
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
private function patientInvoicesQuery(string $entityType, int $entityId, int $recordId): QueryBuilder
|
||||
{
|
||||
return $this->createQueryBuilder('i')
|
||||
|
||||
Reference in New Issue
Block a user