# 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` ```json { "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` ```json { "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.