Portable Laravel package: a small rule-based pricing engine with config toggles, injectable coupon validation, and defaults for coupon (percent), tax, and flat shipping.
php artisan boost:add-skill banulakwin/laravel-pricing
Save this content to: AGENTS.md
# Agent guide: `banulakwin/laravel-pricing`
Portable Laravel package: a small rule-based pricing engine with config toggles, injectable coupon validation, and defaults for coupon (percent), tax, and flat shipping.
## Mental model
- **`PricingEngine`**: Central class that runs `PriceRule` instances in sequence. It builds a `PriceContext` containing the running subtotal, discount, tax, shipping, and total.
- **`PriceContext`**: State object passed through the engine pipeline. Contains line items, metadata (e.g., `user_id`), and running totals.
- **`PriceRule`**: Interface for rules (e.g., `CouponRule`, `TaxRule`, `ShippingRule`) that mutate the `PriceContext`.
## Multi-Item/Multi-User Coupons
- **Pivot Tables**: `coupon_items` (for targeted item matching) and `coupon_users` (for user redemption restrictions).
- **Global vs. Targeted**:
- A coupon is **Global** if the `items` relation is empty. It applies its discount to the full subtotal.
- A coupon is **Targeted** if `items` has entries. It calculates the eligible subtotal based on line items matching the `couponable_type` and optionally `couponable_id` and `item_subtype`.
- A coupon is restricted to specific users if the `users` relation has entries. Unrestricted if empty.
- **`CouponRule` mechanics**:
- The constructor expects the `$coupon` array to contain eagerly loaded `items` and `users`.
- The `$mapping` constructor parameter maps the database columns to the structure of the incoming cart line items.
- Validates `first_time_only` coupons dynamically by querying the `user_orders_relation` configured in `pricing.php`.
- Validates `once_per_user` coupons by checking if the user has already redeemed the same coupon code.
- Guest users are allowed through for `first_time_only` and `once_per_user` checks — server-side checkout re-validates once authenticated.
- An optional `user_orders_scope` config can be set to apply a scope (e.g., `captured`) to order queries.
## Do
- Use `pricing()->engine()` or `Pricing::engine()` for a fresh mutable engine per calculation.
- Pass user context in `$meta` so user restrictions can be evaluated.
- Compose rules explicitly in your app — rule order matters.
- Define your `orders` relation to return only the orders you want counted (e.g., captured/paid orders) instead of using `user_orders_scope`.
## Do not
- Import `App\*` from the package.
- Cache a mutable engine — always get a new one per calculation.
- Hardcode API keys — use config/env.
## Testing & Quality
```bash
composer test # PHPUnit
composer pint # Laravel Pint code style fix
composer pint:check # Pint check only (no fix)
composer phpstan # PHPStan level max on src/
composer quality # All: pint + phpstan + test
```
## CI
GitHub Actions runs tests, Pint, and PHPStan on push/PR (`.github/workflows/tests.yml`).
banulakwin/laravel-pricingPortable Laravel package: a small rule-based pricing engine with config toggles, injectable coupon validation, and defaults for coupon (percent), tax, and flat shipping.
PricingEngine: Central class that runs PriceRule instances in sequence. It builds a PriceContext containing the running subtotal, discount, tax, shipping, and total.PriceContext: State object passed through the engine pipeline. Contains line items, metadata (e.g., user_id), and running totals.PriceRule: Interface for rules (e.g., CouponRule, TaxRule, ShippingRule) that mutate the PriceContext.coupon_items (for targeted item matching) and coupon_users (for user redemption restrictions).items relation is empty. It applies its discount to the full subtotal.items has entries. It calculates the eligible subtotal based on line items matching the couponable_type and optionally couponable_id and item_subtype.users relation has entries. Unrestricted if empty.CouponRule mechanics:
$coupon array to contain eagerly loaded items and users.$mapping constructor parameter maps the database columns to the structure of the incoming cart line items.first_time_only coupons dynamically by querying the user_orders_relation configured in pricing.php.once_per_user coupons by checking if the user has already redeemed the same coupon code.first_time_only and once_per_user checks — server-side checkout re-validates once authenticated.user_orders_scope config can be set to apply a scope (e.g., captured) to order queries.pricing()->engine() or Pricing::engine() for a fresh mutable engine per calculation.$meta so user restrictions can be evaluated.orders relation to return only the orders you want counted (e.g., captured/paid orders) instead of using user_orders_scope.App\* from the package.composer test # PHPUnit
composer pint # Laravel Pint code style fix
composer pint:check # Pint check only (no fix)
composer phpstan # PHPStan level max on src/
composer quality # All: pint + phpstan + test
GitHub Actions runs tests, Pint, and PHPStan on push/PR (.github/workflows/tests.yml).
How can I help you explore Laravel packages today?