Files
hamedandClaude Fable 5 18d9cc9812 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>
2026-07-17 12:02:03 +03:30

4.1 KiB

Discount API

Generic per-tenant discount rules and their evaluation for a patient session. Owner is resolved from the authenticated user (ROLE_DOCTOR → doctor, ROLE_CLINIC → clinic). All responses use the shared envelope; single/object payloads are double-nested (data.data).

Rule types (type)

type target field(s) meaning
patient_tag target_tag_uuid (TenantTag uuid) patient carries the tag
invoice_amount min_amount_rials session final_price_rials ≥ threshold
specific_patient target_record_uuid (PatientRecord uuid) a specific patient's record
occasion valid_from/valid_to, optional occasion_kind: birthday date window; birthday also requires today == patient birthday (month/day)
service target_service_item_uuid (ServiceItem uuid) session contains that service (discount base = that service's line total)
visit_count min_visit_count patient's session count ≥ threshold

Shared fields: discount_type (percent|fixed), value (percent 0..100 or rials), priority (int, higher first), combinable (bool), active (bool), valid_from/valid_to (unix, nullable).


GET /api/v1/admin/discount-rules

List the owner's rules (array hydration, priority desc). Auth: doctor/clinic.

Response 200

{ "success": true, "data": { "data": [ { "uuid": "…", "name": "بالای ۱۰۰ هزار → ۱۰٪", "type": "invoice_amount", "discount_type": "percent", "value": 10, "priority": 5, "combinable": false, "active": true, "min_amount_rials": 1000000 } ] } }

POST /api/v1/admin/discount-rules

Create a rule. Auth: doctor/clinic.

Request Body

Field Type Required Notes
name string
type string one of the rule types above
discount_type string percent (default) or fixed
value int percent or rials
priority int default 0
combinable bool default false
active bool default true
valid_from / valid_to int (unix) validity window
target_tag_uuid / target_record_uuid / target_service_item_uuid string (uuid) per-type target
min_amount_rials / min_visit_count int per-type threshold
occasion_kind string birthday or null

Response 201

Created rule object (double-nested data.data).

Errors

Code HTTP Description
ERR_VALIDATION_001 422 missing name / invalid type
ERR_AUTH_006 403 user is not a doctor/clinic owner

PATCH /api/v1/admin/discount-rules/{uuid}

Update a rule (owner-scoped). Same body fields (all optional). Auth: doctor/clinic. 404 ERR_VALIDATION_002 if the rule is not found for this owner.

DELETE /api/v1/admin/discount-rules/{uuid}

Delete a rule (owner-scoped). Auth: doctor/clinic. Response 200{ data: { data: { deleted: true } } }.


GET /api/v1/session/{uuid}/discount-suggestions

Evaluate all active owner rules against a session and return applicable discounts. Auth: doctor/clinic; the session's record must belong to the caller's owner.

Response 200

{
  "success": true,
  "data": { "data": [
    {
      "rule_uuid": "740bcfc4-…",
      "rule_name": "بالای ۱۰۰ هزار → ۱۰٪",
      "type": "invoice_amount",
      "discount_type": "percent",
      "value": 10,
      "discount_rials": 700000,
      "combinable": false,
      "priority": 5
    }
  ] }
}

Ordered by priority desc. Each discount_rials is capped at the session's unpaid remainder. Rules that don't apply (or compute to 0) are omitted.

Errors

Code HTTP Description
ERR_VALIDATION_002 404 session not found
ERR_AUTH_006 403 not the owner / not a doctor-clinic user

Applying a suggested discount goes through PATCH /api/v1/session/{uuid} with discount_rule_uuid (see patient.md); the applied rule is recorded on the session as applied_discount_rule_id / applied_discount_rule_label for audit and reporting.