Skip to content

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

FieldTypeRequiredDescription
idintegerautoPrimary key
codestringyesUnique coupon code (max 50 chars)
typeenumyespercentage, fixed, free_product
valuedecimal(12,3)yesDiscount value (percentage or fixed amount)
max_discountdecimal(12,3)noMaximum discount cap for percentage coupons
min_order_amountdecimal(12,3)noMinimum cart total required to use the coupon
free_product_idFKnoProduct given free (when type = free_product)
usage_limitintegernoMaximum number of times the coupon can be used (null = unlimited)
used_countintegerautoCurrent usage count
valid_fromdateyesStart of validity period
valid_todateyesEnd of validity period (must be >= valid_from)
is_activebooleannoWhether the coupon is active (default: true)
applicable_productsJSON arraynoProduct IDs this coupon applies to (null = all)
applicable_categoriesJSON arraynoCategory IDs this coupon applies to (null = all)
created_atdatetimeautoCreation timestamp
updated_atdatetimeautoLast update timestamp

CouponType Enum

ValueLabelDescription
percentagePercentageDiscount as % of cart total, optionally capped by max_discount
fixedFixed AmountFixed monetary discount, capped at cart total
free_productFree ProductAwards a free product (discount_amount = 0)

ER Diagram

API Endpoints

MethodEndpointDescriptionPermission
GET/api/pos/couponsList coupons (paginated)pos.coupons.view
POST/api/pos/couponsCreate a couponpos.coupons.create
GET/api/pos/coupons/{id}Get coupon detailspos.coupons.view
PUT/api/pos/coupons/{id}Update a couponpos.coupons.update
DELETE/api/pos/coupons/{id}Delete a couponpos.coupons.delete
POST/api/pos/coupons/validateValidate a coupon code against a cartpos.coupons.validate
POST/api/pos/coupons/{id}/applyApply coupon (increment used_count)pos.coupons.validate

Query Parameters (List)

ParameterTypeDescription
is_activebooleanFilter by active status
searchstringSearch 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:

  1. Active check -- is_active must be true.
  2. Date range check -- Current date must be between valid_from and valid_to.
  3. Usage limit check -- used_count must be less than usage_limit (if set).
  4. Minimum order check -- cart_total must be >= min_order_amount (if set).
  5. Product eligibility -- If applicable_products is set, at least one product in the cart must match.
  6. Discount calculation:
    • Percentage: cart_total * value / 100, capped at max_discount if set.
    • Fixed: min(value, cart_total) (cannot exceed cart total).
    • Free Product: discount_amount = 0 (product is awarded separately).

Business Rules

  1. Percentage cap -- When type = "percentage" and max_discount is set, the calculated discount is capped at that amount. For example, 25% off a 300.000 cart = 75.000, but if max_discount = 50.000, the discount is 50.000.
  2. Usage tracking -- The validate endpoint checks validity but does not increment used_count. Call apply separately after the sale to track usage.
  3. Product scope -- If applicable_products is set and no products in the cart match, the coupon is rejected.
  4. Category scope -- applicable_categories limits which product categories the coupon applies to.
  5. Soft delete -- Coupons are soft-deleted, preserving historical data for usage tracking.

Moon ERP API Documentation