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>
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}withdiscount_rule_uuid(seepatient.md); the applied rule is recorded on the session asapplied_discount_rule_id/applied_discount_rule_labelfor audit and reporting.