POS Coupons (كوبونات الخصم)
The Coupons system allows creating discount codes that cashiers can validate and apply during POS checkout. Coupons support percentage discounts (with optional caps), fixed-amount discounts, and free product giveaways.
Entity Attributes
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | auto | Primary key |
code | string | yes | Unique coupon code (max 50 chars) |
type | enum | yes | percentage, fixed, free_product |
value | decimal(12,3) | yes | Discount value (percentage or fixed amount) |
max_discount | decimal(12,3) | no | Maximum discount cap for percentage coupons |
min_order_amount | decimal(12,3) | no | Minimum cart total required to use the coupon |
free_product_id | FK | no | Product given free (when type = free_product) |
usage_limit | integer | no | Maximum number of times the coupon can be used (null = unlimited) |
used_count | integer | auto | Current usage count |
valid_from | date | yes | Start of validity period |
valid_to | date | yes | End of validity period (must be >= valid_from) |
is_active | boolean | no | Whether the coupon is active (default: true) |
applicable_products | JSON array | no | Product IDs this coupon applies to (null = all) |
applicable_categories | JSON array | no | Category IDs this coupon applies to (null = all) |
created_at | datetime | auto | Creation timestamp |
updated_at | datetime | auto | Last update timestamp |
CouponType Enum
| Value | Label | Description |
|---|---|---|
percentage | Percentage | Discount as % of cart total, optionally capped by max_discount |
fixed | Fixed Amount | Fixed monetary discount, capped at cart total |
free_product | Free Product | Awards a free product (discount_amount = 0) |
ER Diagram
API Endpoints
| Method | Endpoint | Description | Permission |
|---|---|---|---|
GET | /api/pos/coupons | List coupons (paginated) | pos.coupons.view |
POST | /api/pos/coupons | Create a coupon | pos.coupons.create |
GET | /api/pos/coupons/{id} | Get coupon details | pos.coupons.view |
PUT | /api/pos/coupons/{id} | Update a coupon | pos.coupons.update |
DELETE | /api/pos/coupons/{id} | Delete a coupon | pos.coupons.delete |
POST | /api/pos/coupons/validate | Validate a coupon code against a cart | pos.coupons.validate |
POST | /api/pos/coupons/{id}/apply | Apply coupon (increment used_count) | pos.coupons.validate |
Query Parameters (List)
| Parameter | Type | Description |
|---|---|---|
is_active | boolean | Filter by active status |
search | string | Search by coupon code |
Request / Response Examples
Create Coupon
bash
curl -X POST /api/pos/coupons \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"code": "SUMMER25",
"type": "percentage",
"value": 25,
"max_discount": 50.000,
"min_order_amount": 20.000,
"usage_limit": 100,
"valid_from": "2026-03-01",
"valid_to": "2026-06-30",
"is_active": true,
"applicable_categories": [1, 3]
}'dart
final response = await dio.post('/api/pos/coupons', data: {
'code': 'SUMMER25',
'type': 'percentage',
'value': 25,
'max_discount': 50.000,
'min_order_amount': 20.000,
'usage_limit': 100,
'valid_from': '2026-03-01',
'valid_to': '2026-06-30',
'is_active': true,
'applicable_categories': [1, 3],
});Response 201 Created
json
{
"data": {
"id": 1,
"code": "SUMMER25",
"type": "percentage",
"value": "25.000",
"max_discount": "50.000",
"min_order_amount": "20.000",
"free_product_id": null,
"usage_limit": 100,
"used_count": 0,
"valid_from": "2026-03-01",
"valid_to": "2026-06-30",
"is_active": true,
"applicable_products": null,
"applicable_categories": [1, 3],
"created_at": "2026-02-28T10:00:00.000000Z"
}
}Validate Coupon
bash
curl -X POST /api/pos/coupons/validate \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"code": "SUMMER25",
"cart_total": 100.000,
"product_ids": [10, 22, 35]
}'dart
final response = await dio.post('/api/pos/coupons/validate', data: {
'code': 'SUMMER25',
'cart_total': 100.000,
'product_ids': [10, 22, 35],
});Response 200 OK (valid coupon)
json
{
"valid": true,
"discount_type": "percentage",
"discount_value": 25.000,
"discount_amount": 25.000,
"message": "Coupon applied successfully."
}Response 200 OK (invalid coupon)
json
{
"valid": false,
"discount_amount": 0,
"message": "Minimum order amount not met."
}Apply Coupon
bash
curl -X POST /api/pos/coupons/1/apply \
-H "Authorization: Bearer {token}"dart
final response = await dio.post('/api/pos/coupons/1/apply');Response 200 OK
json
{
"message": "Coupon applied.",
"used_count": 5
}Validation Logic
The calculateDiscount() method on POSCoupon performs the following checks in order:
- Active check --
is_activemust betrue. - Date range check -- Current date must be between
valid_fromandvalid_to. - Usage limit check --
used_countmust be less thanusage_limit(if set). - Minimum order check --
cart_totalmust be >=min_order_amount(if set). - Product eligibility -- If
applicable_productsis set, at least one product in the cart must match. - Discount calculation:
- Percentage:
cart_total * value / 100, capped atmax_discountif set. - Fixed:
min(value, cart_total)(cannot exceed cart total). - Free Product: discount_amount = 0 (product is awarded separately).
- Percentage:
Business Rules
- Percentage cap -- When
type = "percentage"andmax_discountis set, the calculated discount is capped at that amount. For example, 25% off a 300.000 cart = 75.000, but ifmax_discount = 50.000, the discount is 50.000. - Usage tracking -- The
validateendpoint checks validity but does not incrementused_count. Callapplyseparately after the sale to track usage. - Product scope -- If
applicable_productsis set and no products in the cart match, the coupon is rejected. - Category scope --
applicable_categorieslimits which product categories the coupon applies to. - Soft delete -- Coupons are soft-deleted, preserving historical data for usage tracking.