refactor(pricing): make the service the only price source
Price lists, annual tariffs and per-branch price overrides each answered
"what does this service cost?" differently, so a single date could carry
several answers and nobody could say which one was right. Price now lives
only on ServiceItem.price_rials, edited from the services page.
- drop PriceList/PriceListItem, their repositories and the seven
/api/v1/price-list(s) endpoints; PricingController keeps only quote and
the appointment price snapshot
- drop Tariff, TariffRepository, TariffService and the two
/service-items/{uuid}/tariffs endpoints; creating or repricing a service
no longer upserts a current-year tariff
- drop price_rials from ServiceBranchOverride; the entity stays for its
duration columns, which DurationCalculator and ServiceSelectionValidator
still read
- InvoiceService reads the item price directly
- PricingEngine collapses to a single source; breakdown.sources always
reports service_item, keeping the response contract intact
- remove the price-lists admin page, its route and settings-menu entry, the
tariff modal and the service detail tariffs tab; useAppointmentInvoice
moves to its own hook file
Migration drops price_lists, price_list_items, service_tariffs and the
override price column.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,6 @@ use App\ClinicService\Entity\ServiceItemAuditLog;
|
||||
use App\ClinicService\Entity\ServiceSection;
|
||||
use App\Insurance\Entity\TenantServiceCoverage;
|
||||
use App\Insurance\Enum\ServiceCategory;
|
||||
use App\ClinicService\Entity\Tariff;
|
||||
use App\Clinic\Security\ClinicDoctorAccessChecker;
|
||||
use App\Secretary\Security\SecretaryAccessChecker;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -16,9 +15,7 @@ use App\ClinicService\Repository\CatalogCategoryRepository;
|
||||
use App\ClinicService\Repository\ServiceItemAuditLogRepository;
|
||||
use App\ClinicService\Repository\ServiceItemRepository;
|
||||
use App\ClinicService\Repository\ServiceSectionRepository;
|
||||
use App\ClinicService\Repository\TariffRepository;
|
||||
use App\ClinicService\Service\ServiceItemAuditService;
|
||||
use App\ClinicService\Service\TariffService;
|
||||
use App\Inventory\Repository\InventoryItemRepository;
|
||||
use App\Inventory\Repository\InventoryPackageRepository;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
@@ -44,8 +41,6 @@ class ClinicServiceController extends BaseController
|
||||
private readonly ServiceItemRepository $itemRepo,
|
||||
private readonly ClinicStaffRepository $staffRepo,
|
||||
private readonly SubscriptionService $subscriptionService,
|
||||
private readonly TariffRepository $tariffRepo,
|
||||
private readonly TariffService $tariffService,
|
||||
private readonly InventoryPackageRepository $packageRepo,
|
||||
private readonly \App\Appointment\Plan\Repository\SegmentTemplateRepository $segmentRepo,
|
||||
private readonly InventoryItemRepository $inventoryItemRepo,
|
||||
@@ -426,8 +421,6 @@ class ClinicServiceController extends BaseController
|
||||
|
||||
$this->itemRepo->save($item);
|
||||
|
||||
// قیمت سرویس همان تعرفهی سال جاری است؛ هنگام ساخت، تعرفهی سال جاری ثبت میشود.
|
||||
$this->tariffService->upsert($item->getId(), $this->tariffService->currentJalaliYear(), $item->getPriceRials());
|
||||
$this->auditService->logCreate($item, $user);
|
||||
|
||||
return $this->success($this->serializeItems([$item])[0], 201);
|
||||
@@ -447,9 +440,8 @@ class ClinicServiceController extends BaseController
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$before = $this->auditService->snapshot($item);
|
||||
|
||||
$priceChanged = false;
|
||||
if (isset($data['name']) && trim($data['name']) !== '') { $item->setName(trim($data['name'])); }
|
||||
if (isset($data['price_rials'])) { $item->setPriceRials((int) $data['price_rials']); $priceChanged = true; }
|
||||
if (isset($data['price_rials'])) { $item->setPriceRials((int) $data['price_rials']); }
|
||||
if (isset($data['active'])) { $item->setActive((bool) $data['active']); }
|
||||
if (array_key_exists('staff_uuids', $data) || array_key_exists('staff_uuid', $data)) {
|
||||
$staffError = $this->applyStaffMembers($item, $data, $entityType, $entityId);
|
||||
@@ -484,11 +476,6 @@ class ClinicServiceController extends BaseController
|
||||
|
||||
$this->itemRepo->save($item);
|
||||
|
||||
// اگر قیمت پایه تغییر کرد، تعرفهی سال جاری هم همگام میشود (قیمت واحد).
|
||||
if ($priceChanged) {
|
||||
$this->tariffService->upsert($item->getId(), $this->tariffService->currentJalaliYear(), $item->getPriceRials());
|
||||
}
|
||||
|
||||
$this->auditService->logChanges($item, $before, $this->auditService->snapshot($item), $user);
|
||||
|
||||
return $this->success($this->serializeItems([$item])[0]);
|
||||
@@ -508,59 +495,6 @@ class ClinicServiceController extends BaseController
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tariffs (تعرفهی نسخهدار سالانه) ──────────────────────────────────────
|
||||
|
||||
#[Route('/api/v1/service-items/{uuid}/tariffs', methods: ['GET'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function listTariffs(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->denyServices($user, 'view');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
|
||||
$item = $this->itemRepo->findByUuid($uuid);
|
||||
if ($item === null || !$this->ownsSection($item->getSection(), $entityType, $entityId)) {
|
||||
return $this->error(ErrorCodes::ERR_SERVICE_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_SERVICE_NOT_FOUND), 404);
|
||||
}
|
||||
|
||||
$tariffs = $this->tariffRepo->findByService($item->getId());
|
||||
|
||||
return $this->success([
|
||||
'current_year' => $this->tariffService->currentJalaliYear(),
|
||||
'default_price_rials' => $item->getPriceRials(),
|
||||
'data' => array_map(fn($t) => $t->toArray(), $tariffs),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/service-items/{uuid}/tariffs/{year}', methods: ['PUT'], requirements: ['year' => '\d+'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function setTariff(string $uuid, int $year, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->denyServices($user, 'update');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
|
||||
$item = $this->itemRepo->findByUuid($uuid);
|
||||
if ($item === null || !$this->ownsSection($item->getSection(), $entityType, $entityId)) {
|
||||
return $this->error(ErrorCodes::ERR_SERVICE_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_SERVICE_NOT_FOUND), 404);
|
||||
}
|
||||
|
||||
if ($year < 1390 || $year > 1500) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'سال نامعتبر است', 422);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$price = (int) ($data['price_rials'] ?? 0);
|
||||
|
||||
$tariff = $this->tariffService->upsert($item->getId(), $year, $price);
|
||||
|
||||
// تعرفهی سال جاری = قیمت پایهی سرویس (قیمت واحد در همهجا).
|
||||
if ($year === $this->tariffService->currentJalaliYear()) {
|
||||
$item->setPriceRials($price);
|
||||
$this->itemRepo->save($item);
|
||||
}
|
||||
|
||||
return $this->success(['data' => $tariff->toArray()]);
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -446,7 +446,7 @@ class ServiceCatalogController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد address_uuid الزامی است', 422, 'address_uuid');
|
||||
}
|
||||
|
||||
foreach (['price_rials', 'solo_duration_minutes', 'additional_duration_minutes'] as $field) {
|
||||
foreach (['solo_duration_minutes', 'additional_duration_minutes'] as $field) {
|
||||
$value = $row[$field] ?? null;
|
||||
|
||||
if ($value !== null && (!is_numeric($value) || (int) $value < 0)) {
|
||||
@@ -464,7 +464,6 @@ class ServiceCatalogController extends BaseController
|
||||
|
||||
// `isset()` خودش null را رد میکند، پس مقایسهٔ اضافه لازم نیست.
|
||||
// `null` یعنی «همان مقدار خودِ سرویس» — صفر نیست.
|
||||
$override->setPriceRials(isset($row['price_rials']) ? (int) $row['price_rials'] : null);
|
||||
$override->setSoloDurationMinutes(isset($row['solo_duration_minutes']) ? (int) $row['solo_duration_minutes'] : null);
|
||||
$override->setAdditionalDurationMinutes(isset($row['additional_duration_minutes']) ? (int) $row['additional_duration_minutes'] : null);
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
/**
|
||||
* قیمت و مدتِ اختصاصیِ یک سرویس در یک شعبه.
|
||||
* مدتِ اختصاصیِ یک سرویس در یک شعبه.
|
||||
*
|
||||
* قیمت اینجا نیست: تنها منبع قیمت `ServiceItem::priceRials` است.
|
||||
*
|
||||
* «شعبه» همان `doctor_addresses` است ({@see docs/new_feture/taskes/_shared/branch-is-doctor-address.md}).
|
||||
* هر ستون تهیپذیر است و `null` یعنی «همان مقدار خودِ سرویس» — نه صفر.
|
||||
@@ -38,9 +40,6 @@ class ServiceBranchOverride
|
||||
#[ORM\JoinColumn(name: 'address_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private DoctorAddress $address;
|
||||
|
||||
#[ORM\Column(name: 'price_rials', type: 'bigint', nullable: true)]
|
||||
private ?int $priceRials = null;
|
||||
|
||||
#[ORM\Column(name: 'solo_duration_minutes', type: 'smallint', nullable: true)]
|
||||
private ?int $soloDurationMinutes = null;
|
||||
|
||||
@@ -68,11 +67,9 @@ class ServiceBranchOverride
|
||||
public function getUuid(): string { return $this->uuid; }
|
||||
public function getItem(): ServiceItem { return $this->item; }
|
||||
public function getAddress(): DoctorAddress { return $this->address; }
|
||||
public function getPriceRials(): ?int { return $this->priceRials === null ? null : (int) $this->priceRials; }
|
||||
public function getSoloDurationMinutes(): ?int { return $this->soloDurationMinutes; }
|
||||
public function getAdditionalDurationMinutes(): ?int { return $this->additionalDurationMinutes; }
|
||||
|
||||
public function setPriceRials(?int $v): self { $this->priceRials = $v; $this->touch(); return $this; }
|
||||
public function setSoloDurationMinutes(?int $v): self { $this->soloDurationMinutes = $v; $this->touch(); return $this; }
|
||||
public function setAdditionalDurationMinutes(?int $v): self { $this->additionalDurationMinutes = $v; $this->touch(); return $this; }
|
||||
|
||||
@@ -85,7 +82,6 @@ class ServiceBranchOverride
|
||||
'item_uuid' => $this->item->getUuid(),
|
||||
'address_uuid' => $this->address->getUuid(),
|
||||
'address_name' => $this->address->getName(),
|
||||
'price_rials' => $this->getPriceRials(),
|
||||
'solo_duration_minutes' => $this->soloDurationMinutes,
|
||||
'additional_duration_minutes' => $this->additionalDurationMinutes,
|
||||
];
|
||||
|
||||
@@ -104,7 +104,7 @@ class ServiceItem
|
||||
|
||||
/**
|
||||
* پکیج کالای مصرفی این خدمت ({@see \App\Inventory\Entity\InventoryPackage}).
|
||||
* ارجاع خام int بدون FK — همان الگوی Tariff و TenantServiceCoverage — تا دامنهٔ
|
||||
* ارجاع خام int بدون FK — همان الگوی TenantServiceCoverage — تا دامنهٔ
|
||||
* ClinicService به Inventory وابسته نشود.
|
||||
*/
|
||||
#[ORM\Column(name: 'inventory_package_id', type: 'integer', nullable: true)]
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\ClinicService\Entity;
|
||||
|
||||
use App\ClinicService\Repository\TariffRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TariffRepository::class)]
|
||||
#[ORM\Table(name: 'service_tariffs')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_service_tariff_year', columns: ['service_item_id', 'year'])]
|
||||
#[ORM\Index(columns: ['service_item_id', 'is_active'], name: 'idx_tariff_service_active')]
|
||||
class Tariff
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\Column(name: 'service_item_id', type: 'integer')]
|
||||
private int $serviceItemId;
|
||||
|
||||
#[ORM\Column(type: 'smallint')]
|
||||
private int $year;
|
||||
|
||||
#[ORM\Column(name: 'price_rials', type: 'integer')]
|
||||
private int $priceRials = 0;
|
||||
|
||||
#[ORM\Column(name: 'is_active', type: 'boolean')]
|
||||
private bool $isActive = true;
|
||||
|
||||
#[ORM\Column(name: 'created_at', type: 'integer')]
|
||||
private int $createdAt;
|
||||
|
||||
#[ORM\Column(name: 'updated_at', type: 'integer')]
|
||||
private int $updatedAt;
|
||||
|
||||
public function __construct(int $serviceItemId, int $year, int $priceRials = 0)
|
||||
{
|
||||
$this->uuid = Uuid::v4()->toRfc4122();
|
||||
$this->serviceItemId = $serviceItemId;
|
||||
$this->year = $year;
|
||||
$this->priceRials = $priceRials;
|
||||
$this->createdAt = time();
|
||||
$this->updatedAt = time();
|
||||
}
|
||||
|
||||
public function getId(): ?int { return $this->id; }
|
||||
public function getUuid(): string { return $this->uuid; }
|
||||
public function getServiceItemId(): int { return $this->serviceItemId; }
|
||||
public function getYear(): int { return $this->year; }
|
||||
public function getPriceRials(): int { return $this->priceRials; }
|
||||
public function isActive(): bool { return $this->isActive; }
|
||||
|
||||
public function setPriceRials(int $v): self { $this->priceRials = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setActive(bool $v): self { $this->isActive = $v; $this->updatedAt = time(); return $this; }
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'uuid' => $this->uuid,
|
||||
'service_item_id' => $this->serviceItemId,
|
||||
'year' => $this->year,
|
||||
'price_rials' => $this->priceRials,
|
||||
'is_active' => $this->isActive,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\ClinicService\Repository;
|
||||
|
||||
use App\ClinicService\Entity\Tariff;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
class TariffRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Tariff::class);
|
||||
}
|
||||
|
||||
public function findForServiceYear(int $serviceItemId, int $year): ?Tariff
|
||||
{
|
||||
return $this->findOneBy([
|
||||
'serviceItemId' => $serviceItemId,
|
||||
'year' => $year,
|
||||
'isActive' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return Tariff[] */
|
||||
public function findByService(int $serviceItemId): array
|
||||
{
|
||||
return $this->createQueryBuilder('t')
|
||||
->where('t.serviceItemId = :sid')
|
||||
->setParameter('sid', $serviceItemId)
|
||||
->orderBy('t.year', 'DESC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function findByUuid(string $uuid): ?Tariff
|
||||
{
|
||||
return $this->findOneBy(['uuid' => $uuid]);
|
||||
}
|
||||
|
||||
public function save(Tariff $entity, bool $flush = true): void
|
||||
{
|
||||
$this->getEntityManager()->persist($entity);
|
||||
if ($flush) {
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,16 +77,16 @@ final class DurationCalculator
|
||||
}
|
||||
|
||||
/**
|
||||
* قیمت فقط از خودِ سرویس میآید؛ شعبه دیگر قیمت اختصاصی ندارد.
|
||||
*
|
||||
* @param ServiceItem[] $items
|
||||
* @param array<int, ServiceBranchOverride> $overrides
|
||||
*/
|
||||
public function totalPriceRials(array $items, array $overrides = []): int
|
||||
public function totalPriceRials(array $items): int
|
||||
{
|
||||
$total = 0;
|
||||
|
||||
foreach ($items as $item) {
|
||||
$override = $overrides[(int) $item->getId()] ?? null;
|
||||
$total += $override?->getPriceRials() ?? $item->getPriceRials();
|
||||
$total += $item->getPriceRials();
|
||||
}
|
||||
|
||||
return $total;
|
||||
@@ -128,7 +128,7 @@ final class DurationCalculator
|
||||
'minutes' => $isAnchor
|
||||
? $solos[$index]
|
||||
: ($override?->getAdditionalDurationMinutes() ?? $item->effectiveAdditionalMinutes() ?? $solos[$index]),
|
||||
'price_rials' => $override?->getPriceRials() ?? $item->getPriceRials(),
|
||||
'price_rials' => $item->getPriceRials(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ use App\Resource\Repository\ResourceServiceOfferingRepository;
|
||||
*
|
||||
* ۱. منبع + گزینه → `ResourceServiceOffering(resource, option)`
|
||||
* ۲. منبع + سرویس → `ResourceServiceOffering(resource, parent)`
|
||||
* ۳. شعبه + آیتم → `ServiceBranchOverride(item, address)`
|
||||
* ۳. شعبه + آیتم → `ServiceBranchOverride(item, address)` — فقط مدت
|
||||
* ۴. پیشفرض آیتم → `ServiceItem`
|
||||
*
|
||||
* **مدت و قیمت جدا حل میشوند.** منبعی که فقط مدتش فرق دارد نباید قیمتش هم از همان
|
||||
* سطح بیاید؛ اگر با هم حل شوند، اولین override باعث میشود تعرفهٔ شعبه بیصدا نادیده گرفته شود.
|
||||
* سطح بیاید. شعبه دیگر قیمت اختصاصی ندارد: قیمت روی خودِ سرویس مدیریت میشود.
|
||||
*
|
||||
* سطحِ والد را **صدازننده** میدهد، نه یک کوئری معکوس روی گروهها: جریان رزرو هر دو را
|
||||
* از قبل در دست دارد (سرویس انتخابشده و گزینهاش)، و کوئری معکوس فقط یک راه اضافه برای
|
||||
@@ -66,7 +66,6 @@ final class ResourceServiceResolver
|
||||
[$price, $priceSource] = $this->first([
|
||||
[$optionOffering?->getPriceRials(), ResolvedServiceSpec::SOURCE_RESOURCE_OPTION],
|
||||
[$parentOffering?->getPriceRials(), ResolvedServiceSpec::SOURCE_RESOURCE_SERVICE],
|
||||
[$branch?->getPriceRials(), ResolvedServiceSpec::SOURCE_BRANCH],
|
||||
[$item->getPriceRials(), ResolvedServiceSpec::SOURCE_SERVICE_DEFAULT],
|
||||
]);
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ final class ServiceSelectionValidator
|
||||
'valid' => $errors === [],
|
||||
'errors' => $errors,
|
||||
'total_duration_minutes' => $this->durations->totalMinutes($selected, $overrides),
|
||||
'total_price_rials' => $this->durations->totalPriceRials($selected, $overrides),
|
||||
'total_price_rials' => $this->durations->totalPriceRials($selected),
|
||||
'breakdown' => $this->durations->breakdown($selected, $overrides),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\ClinicService\Service;
|
||||
|
||||
use App\ClinicService\Entity\ServiceItem;
|
||||
use App\ClinicService\Entity\Tariff;
|
||||
use App\ClinicService\Repository\TariffRepository;
|
||||
|
||||
class TariffService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TariffRepository $tariffRepo,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* تعرفهی یک خدمت برای یک سال شمسی.
|
||||
* اگر تعرفهی آن سال ثبت نشده باشد، به priceRials خود خدمت fallback میشود.
|
||||
*/
|
||||
public function resolvePrice(ServiceItem $service, ?int $year = null): int
|
||||
{
|
||||
$year ??= $this->currentJalaliYear();
|
||||
|
||||
$tariff = $service->getId() !== null
|
||||
? $this->tariffRepo->findForServiceYear($service->getId(), $year)
|
||||
: null;
|
||||
|
||||
return $tariff?->getPriceRials() ?? $service->getPriceRials();
|
||||
}
|
||||
|
||||
public function upsert(int $serviceItemId, int $year, int $priceRials): Tariff
|
||||
{
|
||||
$tariff = $this->tariffRepo->findForServiceYear($serviceItemId, $year);
|
||||
if ($tariff === null) {
|
||||
$tariff = new Tariff($serviceItemId, $year, $priceRials);
|
||||
} else {
|
||||
$tariff->setPriceRials($priceRials)->setActive(true);
|
||||
}
|
||||
$this->tariffRepo->save($tariff);
|
||||
return $tariff;
|
||||
}
|
||||
|
||||
public function currentJalaliYear(): int
|
||||
{
|
||||
$fmt = new \IntlDateFormatter(
|
||||
'en_US@calendar=persian',
|
||||
\IntlDateFormatter::FULL,
|
||||
\IntlDateFormatter::NONE,
|
||||
'Asia/Tehran',
|
||||
\IntlDateFormatter::TRADITIONAL,
|
||||
'yyyy'
|
||||
);
|
||||
return (int) $fmt->format(time());
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ final readonly class ResolvedServiceSpec
|
||||
/** ردیف همان منبع ولی روی سرویسِ والد — وقتی گزینه مقدار خودش را ندارد. */
|
||||
public const SOURCE_RESOURCE_SERVICE = 'resource_service';
|
||||
|
||||
/** `ServiceBranchOverride` — تنظیم این شعبه، مستقل از اینکه کدام منبع کار را میکند. */
|
||||
/** `ServiceBranchOverride` — مدتِ این شعبه، مستقل از اینکه کدام منبع کار را میکند. */
|
||||
public const SOURCE_BRANCH = 'branch';
|
||||
|
||||
/** مقدار خودِ `ServiceItem`. */
|
||||
|
||||
Reference in New Issue
Block a user