feat(discount): admin Discount Management tab + uuid-based rule targets
Switch rule target references from int ids to uuids (frontend-friendly; the engine compares uuids directly) via a migration. Add a Discount Management tab to the subscription page with a full CRUD UI (DiscountTab): table + modal form with per-type dynamic target fields (tenant tag / service cascade / amount / visit count / specific patient / occasion + validity window), priority and combinable/active toggles. Verified TSC + build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -161,9 +161,9 @@ class DiscountController extends BaseController
|
||||
if (array_key_exists('active', $data)) { $rule->setActive((bool) $data['active']); }
|
||||
if (array_key_exists('valid_from', $data)) { $rule->setValidFrom($data['valid_from'] !== null ? (int) $data['valid_from'] : null); }
|
||||
if (array_key_exists('valid_to', $data)) { $rule->setValidTo($data['valid_to'] !== null ? (int) $data['valid_to'] : null); }
|
||||
if (array_key_exists('target_tag_id', $data)) { $rule->setTargetTagId($data['target_tag_id'] !== null ? (int) $data['target_tag_id'] : null); }
|
||||
if (array_key_exists('target_record_id', $data)) { $rule->setTargetRecordId($data['target_record_id'] !== null ? (int) $data['target_record_id'] : null); }
|
||||
if (array_key_exists('target_service_item_id', $data)) { $rule->setTargetServiceItemId($data['target_service_item_id'] !== null ? (int) $data['target_service_item_id'] : null); }
|
||||
if (array_key_exists('target_tag_uuid', $data)) { $rule->setTargetTagUuid($data['target_tag_uuid'] !== null ? (string) $data['target_tag_uuid'] : null); }
|
||||
if (array_key_exists('target_record_uuid', $data)) { $rule->setTargetRecordUuid($data['target_record_uuid'] !== null ? (string) $data['target_record_uuid'] : null); }
|
||||
if (array_key_exists('target_service_item_uuid', $data)) { $rule->setTargetServiceItemUuid($data['target_service_item_uuid'] !== null ? (string) $data['target_service_item_uuid'] : null); }
|
||||
if (array_key_exists('min_amount_rials', $data)) { $rule->setMinAmountRials($data['min_amount_rials'] !== null ? (int) $data['min_amount_rials'] : null); }
|
||||
if (array_key_exists('min_visit_count', $data)) { $rule->setMinVisitCount($data['min_visit_count'] !== null ? (int) $data['min_visit_count'] : null); }
|
||||
if (array_key_exists('occasion_kind', $data)) { $rule->setOccasionKind($data['occasion_kind'] !== null ? (string) $data['occasion_kind'] : null); }
|
||||
|
||||
@@ -79,15 +79,15 @@ class DiscountRule
|
||||
#[ORM\Column(name: 'valid_to', type: 'integer', nullable: true)]
|
||||
private ?int $validTo = null;
|
||||
|
||||
// ── target fields (بسته به type فقط یکی معنیدار است) ──────────────────────
|
||||
#[ORM\Column(name: 'target_tag_id', type: 'integer', nullable: true)]
|
||||
private ?int $targetTagId = null;
|
||||
// ── target fields (بسته به type فقط یکی معنیدار است؛ uuid برای سازگاری با UI) ──
|
||||
#[ORM\Column(name: 'target_tag_uuid', type: 'string', length: 36, nullable: true)]
|
||||
private ?string $targetTagUuid = null;
|
||||
|
||||
#[ORM\Column(name: 'target_record_id', type: 'integer', nullable: true)]
|
||||
private ?int $targetRecordId = null;
|
||||
#[ORM\Column(name: 'target_record_uuid', type: 'string', length: 36, nullable: true)]
|
||||
private ?string $targetRecordUuid = null;
|
||||
|
||||
#[ORM\Column(name: 'target_service_item_id', type: 'integer', nullable: true)]
|
||||
private ?int $targetServiceItemId = null;
|
||||
#[ORM\Column(name: 'target_service_item_uuid', type: 'string', length: 36, nullable: true)]
|
||||
private ?string $targetServiceItemUuid = null;
|
||||
|
||||
#[ORM\Column(name: 'min_amount_rials', type: 'integer', nullable: true)]
|
||||
private ?int $minAmountRials = null;
|
||||
@@ -128,9 +128,9 @@ class DiscountRule
|
||||
public function isActive(): bool { return $this->active; }
|
||||
public function getValidFrom(): ?int { return $this->validFrom; }
|
||||
public function getValidTo(): ?int { return $this->validTo; }
|
||||
public function getTargetTagId(): ?int { return $this->targetTagId; }
|
||||
public function getTargetRecordId(): ?int { return $this->targetRecordId; }
|
||||
public function getTargetServiceItemId(): ?int { return $this->targetServiceItemId; }
|
||||
public function getTargetTagUuid(): ?string { return $this->targetTagUuid; }
|
||||
public function getTargetRecordUuid(): ?string { return $this->targetRecordUuid; }
|
||||
public function getTargetServiceItemUuid(): ?string { return $this->targetServiceItemUuid; }
|
||||
public function getMinAmountRials(): ?int { return $this->minAmountRials; }
|
||||
public function getMinVisitCount(): ?int { return $this->minVisitCount; }
|
||||
public function getOccasionKind(): ?string { return $this->occasionKind; }
|
||||
@@ -144,9 +144,9 @@ class DiscountRule
|
||||
public function setActive(bool $v): self { $this->active = $v; $this->touch(); return $this; }
|
||||
public function setValidFrom(?int $v): self { $this->validFrom = $v; $this->touch(); return $this; }
|
||||
public function setValidTo(?int $v): self { $this->validTo = $v; $this->touch(); return $this; }
|
||||
public function setTargetTagId(?int $v): self { $this->targetTagId = $v; $this->touch(); return $this; }
|
||||
public function setTargetRecordId(?int $v): self { $this->targetRecordId = $v; $this->touch(); return $this; }
|
||||
public function setTargetServiceItemId(?int $v): self { $this->targetServiceItemId = $v; $this->touch(); return $this; }
|
||||
public function setTargetTagUuid(?string $v): self { $this->targetTagUuid = $v; $this->touch(); return $this; }
|
||||
public function setTargetRecordUuid(?string $v): self { $this->targetRecordUuid = $v; $this->touch(); return $this; }
|
||||
public function setTargetServiceItemUuid(?string $v): self { $this->targetServiceItemUuid = $v; $this->touch(); return $this; }
|
||||
public function setMinAmountRials(?int $v): self { $this->minAmountRials = $v; $this->touch(); return $this; }
|
||||
public function setMinVisitCount(?int $v): self { $this->minVisitCount = $v; $this->touch(); return $this; }
|
||||
public function setOccasionKind(?string $v): self { $this->occasionKind = $v; $this->touch(); return $this; }
|
||||
@@ -166,10 +166,10 @@ class DiscountRule
|
||||
'active' => $this->active,
|
||||
'valid_from' => $this->validFrom,
|
||||
'valid_to' => $this->validTo,
|
||||
'target_tag_id' => $this->targetTagId,
|
||||
'target_record_id' => $this->targetRecordId,
|
||||
'target_service_item_id' => $this->targetServiceItemId,
|
||||
'min_amount_rials' => $this->minAmountRials,
|
||||
'target_tag_uuid' => $this->targetTagUuid,
|
||||
'target_record_uuid' => $this->targetRecordUuid,
|
||||
'target_service_item_uuid' => $this->targetServiceItemUuid,
|
||||
'min_amount_rials' => $this->minAmountRials,
|
||||
'min_visit_count' => $this->minVisitCount,
|
||||
'occasion_kind' => $this->occasionKind,
|
||||
'created_at' => $this->createdAt,
|
||||
|
||||
@@ -91,15 +91,15 @@ class DiscountEngine
|
||||
$record = $session->getRecord();
|
||||
|
||||
return match ($rule->getType()) {
|
||||
DiscountRule::TYPE_PATIENT_TAG => $this->hasTag($session, $rule->getTargetTagId()) ? $final : null,
|
||||
DiscountRule::TYPE_PATIENT_TAG => $this->hasTag($session, $rule->getTargetTagUuid()) ? $final : null,
|
||||
|
||||
DiscountRule::TYPE_INVOICE_AMOUNT => ($rule->getMinAmountRials() !== null && $final >= $rule->getMinAmountRials()) ? $final : null,
|
||||
|
||||
DiscountRule::TYPE_SPECIFIC_PATIENT => ($rule->getTargetRecordId() !== null && $record->getId() === $rule->getTargetRecordId()) ? $final : null,
|
||||
DiscountRule::TYPE_SPECIFIC_PATIENT => ($rule->getTargetRecordUuid() !== null && $record->getUuid() === $rule->getTargetRecordUuid()) ? $final : null,
|
||||
|
||||
DiscountRule::TYPE_OCCASION => $this->occasionMatches($rule, $session, $now) ? $final : null,
|
||||
|
||||
DiscountRule::TYPE_SERVICE => $this->serviceBase($session, $rule->getTargetServiceItemId()),
|
||||
DiscountRule::TYPE_SERVICE => $this->serviceBase($session, $rule->getTargetServiceItemUuid()),
|
||||
|
||||
DiscountRule::TYPE_VISIT_COUNT => ($rule->getMinVisitCount() !== null
|
||||
&& $this->sessionRepo->countByRecord($record) >= $rule->getMinVisitCount()) ? $final : null,
|
||||
@@ -108,13 +108,13 @@ class DiscountEngine
|
||||
};
|
||||
}
|
||||
|
||||
private function hasTag(PatientSession $session, ?int $tagId): bool
|
||||
private function hasTag(PatientSession $session, ?string $tagUuid): bool
|
||||
{
|
||||
if ($tagId === null) {
|
||||
if ($tagUuid === null) {
|
||||
return false;
|
||||
}
|
||||
foreach ($session->getRecord()->getTags() as $tag) {
|
||||
if ($tag->getId() === $tagId) {
|
||||
if ($tag->getUuid() === $tagUuid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -122,15 +122,15 @@ class DiscountEngine
|
||||
}
|
||||
|
||||
/** مبنای تخفیف سرویس = جمع خطوطِ همان سرویس؛ null اگر سرویس در پرونده نباشد. */
|
||||
private function serviceBase(PatientSession $session, ?int $serviceItemId): ?int
|
||||
private function serviceBase(PatientSession $session, ?string $serviceItemUuid): ?int
|
||||
{
|
||||
if ($serviceItemId === null) {
|
||||
if ($serviceItemUuid === null) {
|
||||
return null;
|
||||
}
|
||||
$sum = 0;
|
||||
foreach ($session->getServices() as $line) {
|
||||
/** @var SessionService $line */
|
||||
if ($line->getServiceItem()->getId() === $serviceItemId) {
|
||||
if ($line->getServiceItem()->getUuid() === $serviceItemUuid) {
|
||||
$sum += $line->getLineTotalRials();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user