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>
97 lines
4.1 KiB
Markdown
97 lines
4.1 KiB
Markdown
# 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.
|