From 503036e145861d6a1f6df5df20a90f97556388b9 Mon Sep 17 00:00:00 2001 From: Dmytro Morozov Date: Sun, 24 May 2026 14:07:42 +0300 Subject: [PATCH 1/3] Add skill for laravel boost --- skills/lunarphp/SKILL.md | 1075 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1075 insertions(+) create mode 100644 skills/lunarphp/SKILL.md diff --git a/skills/lunarphp/SKILL.md b/skills/lunarphp/SKILL.md new file mode 100644 index 0000000000..d2123c5e93 --- /dev/null +++ b/skills/lunarphp/SKILL.md @@ -0,0 +1,1075 @@ +--- +name: lunarphp +description: "Develops e-commerce features with LunarPHP. Activates when working with products, variants, cart, checkout, orders, discounts, payments, pricing, search, collections, attributes, or any LunarPHP model/facade/config. Also activates when the user mentions Lunar, LunarPHP, product catalog, e-commerce cart, order management, or storefront checkout in a Laravel project. Make sure to use this skill whenever the user works with e-commerce functionality, even if they don't explicitly mention Lunar." +license: MIT +metadata: + author: lunarphp +--- + +# LunarPHP E-Commerce Development + +## Documentation + +LunarPHP docs are at `https://docs.lunarphp.com/1.x/`. The LLMS index is at `https://docs.lunarphp.com/llms.txt`. + +Use `search-docs` for pattern-specific queries — Lunar docs are indexed by the search tool. + +## When to Apply + +Activate this skill when: + +- Installing or configuring LunarPHP in a Laravel app +- Creating or modifying products, variants, product options, brands, or collections +- Building cart functionality (add/update/remove items, coupons, totals) +- Implementing checkout flow (addresses, shipping, payment, order creation) +- Managing orders, order lines, addresses, transactions, and refunds +- Setting up discounts (AmountOff, BuyXGetY, custom types) +- Integrating payment drivers (Stripe, PayPal, offline, custom) +- Configuring pricing pipelines, formatting, and customer group pricing +- Setting up search indexing with Scout for Lunar models +- Working with attributes, attribute groups, and custom field types +- Extending Lunar models with dynamic relationships or model replacement +- Configuring channels, currencies, languages, tax zones, and customer groups +- Building storefront features: catalog menu, PLP, PDP, cart, checkout + +## Key Concepts + +### Core Models & Namespaces + +All Lunar models live under the `Lunar\Models` namespace: + +| Model | Purpose | +|-------|---------| +| `Product` | Core product with variants, options, attributes | +| `ProductVariant` | Purchasable variant with pricing, stock, dimensions | +| `ProductOption` | Option definitions (e.g. "Color", "Size") | +| `ProductOptionValue` | Individual values for options | +| `ProductType` | Determines which attributes are available per product | +| `Brand` | Optional brand association | +| `Collection` | Hierarchical product groups via nested sets | +| `CollectionGroup` | Top-level container for collections | +| `Cart` | Shopping cart with lines, addresses, calculated totals | +| `CartLine` | Line item in a cart, linked to a purchasable | +| `CartAddress` | Shipping/billing address on a cart | +| `Order` | Completed or draft purchase from a cart | +| `OrderLine` | Line item in an order | +| `OrderAddress` | Address on an order | +| `Transaction` | Payment transaction (capture, intent, refund) | +| `Discount` | Discount/coupon with type-specific data | +| `Price` | Pricing per variant, currency, customer group, quantity break | +| `Currency` | Currency configuration with exchange rates | +| `Channel` | Sales outlet (webstore, mobile, wholesale) | +| `CustomerGroup` | Customer segment for pricing/visibility | +| `TaxClass` | Tax categorization for products | +| `TaxZone` | Geographic region for tax calculation | +| `TaxRate` | Tax rate within a zone | +| `Attribute` | Custom data field definition | +| `AttributeGroup` | Logical grouping of attributes | +| `Country` | Country reference data | +| `Url` | SEO-friendly URL slugs (polymorphic) | + +### Key Facades + +| Facade | Purpose | +|--------|---------| +| `Lunar\Facades\CartSession` | Session-based cart management | +| `Lunar\Facades\Pricing` | Fetch correct price for variant/criteria | +| `Lunar\Facades\Payments` | Payment driver management | +| `Lunar\Facades\ShippingManifest` | Shipping option retrieval | +| `Lunar\Facades\Discounts` | Discount application and coupon validation | +| `Lunar\Facades\PricingManager` | Pricing pipeline management | + +### Config Files + +All under `config/lunar/`. Key files: + +- `cart.php` — Cart behavior, pipelines, validators, fingerprint generator, pruning +- `cart_session.php` — Session key, auto-create, multiple orders per cart +- `payments.php` — Payment type definitions and driver mapping +- `pricing.php` — Pricing pipelines and formatter +- `orders.php` — Order pipelines, reference generator, statuses +- `search.php` — Searchable models, engine map, indexers +- `taxes.php` — Tax driver selection +- `database.php` — Table prefix, user ID field type +- `discounts.php` — Coupon validator class +- `shipping.php` — Shipping configuration and units of measure +- `media.php` — Media/image handling + +The database uses a `lunar_` table prefix (configurable in `config/lunar/database.php`). All prices are stored as integers (smallest currency unit, e.g. cents). + +## Installation & Setup + +### Requirements + +- PHP >= 8.3 +- Laravel 12 or 13 +- MySQL 8.0+ / PostgreSQL 9.4+ +- `bcmath`, `exif`, `intl` PHP extensions + +### Step-by-Step + +1. Install via Composer: + +```bash +vendor/bin/sail composer require lunarphp/lunar:"^1.0" -W +``` + +2. Add `LunarUser` trait and interface to the `User` model: + +```php +use Lunar\Base\Traits\LunarUser; +use Lunar\Base\LunarUser as LunarUserInterface; + +class User extends Authenticatable implements LunarUserInterface +{ + use LunarUser; +} +``` + +3. Register the admin panel in `AppServiceProvider`: + +```php +use Lunar\Admin\Support\Facades\LunarPanel; + +public function register(): void +{ + LunarPanel::register(); +} +``` + +4. Run the installer: + +```bash +vendor/bin/sail artisan lunar:install +``` + +The installer publishes configs, runs migrations, creates an admin user, and seeds default data (channel, language, currency, customer group, etc.). + +5. Access the admin panel at `/lunar`. + +### Post-Install Configuration + +Publish configs before installing to customize settings: + +```bash +vendor/bin/sail artisan vendor:publish --tag=lunar +``` + +Key config values to check: + +```php +// config/lunar/database.php +'table_prefix' => 'lunar_', +'users_id_type' => 'bigint', // or 'int', 'uuid' + +// config/lunar/cart_session.php +'session_key' => 'lunar_cart', +'auto_create' => false, + +// config/lunar/payments.php +'default' => env('PAYMENTS_TYPE', 'cash-in-hand'), +``` + +## Products & Catalog + +### Products + +Products always have at least one variant. Custom data is stored as `attribute_data` (JSON) using field types: + +```php +use Lunar\Models\Product; +use Lunar\FieldTypes\Text; +use Lunar\FieldTypes\TranslatedText; + +Product::create([ + 'product_type_id' => $productType->id, + 'status' => 'published', + 'attribute_data' => [ + 'name' => new TranslatedText(collect([ + 'en' => new Text('FooBar'), + ])), + 'description' => new Text('Description text.'), + ], +]); +``` + +The `name` attribute is required — Lunar expects it internally. + +Key scopes: `status($status)`, `channel($channel)`, `customerGroup($group)`. + +### Variants + +Variants hold pricing, stock, dimensions, and product identifiers: + +```php +use Lunar\Models\ProductVariant; +use Lunar\Models\TaxClass; +use Lunar\Models\Currency; + +$variant = ProductVariant::create([ + 'product_id' => $product->id, + 'tax_class_id' => $taxClass->id, + 'sku' => 'BLUE-001', + 'unit_quantity' => 1, + 'min_quantity' => 1, + 'quantity_increment' => 1, + 'stock' => 100, + 'purchasable' => 'always', // or 'in_stock' + 'shippable' => true, +]); + +$variant->prices()->create([ + 'price' => 1999, // $19.99 in cents + 'currency_id' => $currency->id, +]); +``` + +Key fields: `sku`, `gtin`, `mpn`, `ean`, `unit_quantity`, `stock`, `backorder`, `purchasable`, dimensions (`length_value`, `width_value`, `height_value`, `weight_value`, `volume_value`). + +### Product Options + +Define variations like color/size: + +```php +use Lunar\Models\ProductOption; + +$color = ProductOption::create([ + 'name' => ['en' => 'Color'], + 'label' => ['en' => 'Color'], +]); + +$blue = $color->values()->create([ + 'name' => ['en' => 'Blue'], +]); + +$variant->values()->attach($blue); +``` + +Both `ProductOption` and `ProductOptionValue` have a `meta` JSON field for custom data (e.g. hex color values). + +### Product Types + +Product types determine which attributes are available for editing: + +```php +use Lunar\Models\ProductType; + +$productType = ProductType::create(['name' => 'Boots']); +$productType->mappedAttributes()->attach($attributeIds); +``` + +### Brands + +```php +use Lunar\Models\Brand; + +$brand = Brand::create(['name' => 'Nike']); +$product->update(['brand_id' => $brand->id]); +``` + +### Associations + +Link products as cross-sells, up-sells, or alternates: + +```php +$product->associations()->create([ + 'association_type' => 'cross-sell', // or 'up-sell', 'alternate' + 'target_id' => $targetProduct->id, +]); +``` + +### Pricing + +Prices are stored as integers (smallest currency unit). Use the `Pricing` facade for retrieval: + +```php +use Lunar\Facades\Pricing; + +$pricing = Pricing::qty(5) + ->customerGroup($group) + ->currency($currency) + ->for($variant) + ->get(); + +$pricing->matched; // The matched Price model +$pricing->base; // The base Price model +$pricing->priceBreaks; // Collection of quantity break prices +``` + +Or via the model: + +```php +$pricing = $variant->pricing()->qty(5)->get(); +``` + +### Collections + +Collections use nested sets for hierarchy. Always create a root collection before children: + +```php +use Lunar\Models\Collection; +use Lunar\Models\CollectionGroup; + +$group = CollectionGroup::create([ + 'name' => 'Main Catalogue', + 'handle' => 'main-catalogue', +]); + +$parent = Collection::create([ + 'collection_group_id' => $group->id, + 'attribute_data' => ['name' => new \Lunar\FieldTypes\Text('Clothing')], +]); + +$child = Collection::create([ + 'collection_group_id' => $group->id, + 'attribute_data' => ['name' => new \Lunar\FieldTypes\Text('T-Shirts')], +]); + +$parent->appendNode($child); +``` + +Add products with sort position: + +```php +$collection->products()->sync([ + $productA->id => ['position' => 1], +]); +``` + +Sort options: `min_price:asc`, `min_price:desc`, `sku:asc`, `sku:desc`, `custom`. + +## Cart + +### CartSession Facade + +Use `CartSession` for session-based cart management (recommended for traditional storefronts): + +```php +use Lunar\Facades\CartSession; + +// Get current cart (returns null if none exists and auto_create is false) +$cart = CartSession::current(); + +// Add item +CartSession::add($purchasable, quantity: 2, meta: ['personalization' => 'Happy Birthday!']); + +// Add multiple +CartSession::addLines([ + ['purchasable' => $variant1, 'quantity' => 2], + ['purchasable' => $variant2, 'quantity' => 1], +]); + +// Update line +CartSession::updateLine($cartLineId, quantity: 3, meta: ['foo' => 'bar']); + +// Remove line +CartSession::remove($cartLineId); + +// Clear cart +CartSession::clear(); + +// Create order +$order = CartSession::createOrder(); + +// Forget cart (removes from session and soft-deletes) +CartSession::forget(); + +// Associate user +CartSession::associate($cart, $user, policy: 'merge'); +``` + +### Cart Model Directly + +```php +use Lunar\Models\Cart; + +// Create cart +$cart = Cart::create([ + 'currency_id' => $currency->id, + 'channel_id' => $channel->id, +]); + +// Add lines +$cart->add($purchasable, quantity: 2, meta: ['key' => 'value']); + +$cart->addLines([ + ['purchasable' => $variant1, 'quantity' => 2], + ['purchasable' => $variant2, 'quantity' => 1], +]); + +// Update/remove +$cart->updateLine($lineId, quantity: 5); +$cart->remove($lineId); +$cart->clear(); + +// Addresses +$cart->setShippingAddress([/* address fields */]); +$cart->setBillingAddress([/* address fields */]); + +// Shipping option +$cart->setShippingOption($shippingOption); + +// Coupon +$cart->update(['coupon_code' => '20OFF']); +$cart->recalculate(); + +// Calculate totals +$cart->calculate(); +$cart->recalculate(); // Force +$cart->isCalculated(); + +// Check order readiness +$cart->canCreateOrder(); +$cart->hasCompletedOrders(); + +// Create order +$order = $cart->createOrder(); +$order = $cart->createOrder(allowMultipleOrders: true); +$order = $cart->createOrder(orderIdToUpdate: $existingOrderId); + +// Stock validation +$cart->validateStock(); + +// Fingerprint (detect cart changes) +$fingerprint = $cart->fingerprint(); +$cart->checkFingerprint($fingerprint); // Throws FingerprintMismatchException +``` + +### Cart Line Properties (after calculation) + +| Property | Description | +|----------|-------------| +| `unitPrice` | Single unit price (excl. tax) | +| `unitPriceInclTax` | Single unit price (incl. tax) | +| `subTotal` | Line total before discounts and tax | +| `subTotalDiscounted` | Line total after discounts, before tax | +| `discountTotal` | Discount applied to this line | +| `taxAmount` | Tax for this line | +| `total` | Final line total (incl. tax and discounts) | + +### Cart Properties (after calculation) + +| Property | Description | +|----------|-------------| +| `subTotal` | Sum of line subtotals, before tax/discounts | +| `subTotalDiscounted` | Subtotal after line-level discounts | +| `discountTotal` | Total discounts applied | +| `discountBreakdown` | Collection of discount breakdowns | +| `taxTotal` | Total tax across all lines and shipping | +| `taxBreakdown` | Tax breakdown by rate | +| `shippingSubTotal` | Shipping cost before tax | +| `shippingTaxTotal` | Tax on shipping | +| `shippingTotal` | Shipping cost including tax | +| `total` | Final cart total | + +### Cart Scopes + +```php +Cart::unmerged()->get(); // Carts not merged into another +Cart::active()->get(); // Carts with no completed orders +``` + +### User Login Handling + +Lunar listens to auth events and handles cart association automatically. Config `auth_policy` in `config/lunar/cart.php` controls `merge` vs `override` behavior when a user logs in. + +## Checkout + +### Flow + +1. Set addresses on cart +2. Select shipping option +3. Review order +4. Create order from cart +5. Process payment + +### Setting Addresses + +```php +$cart->setBillingAddress([ + 'country_id' => $country->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'line_one' => '123 Main St', + 'city' => 'London', + 'postcode' => 'NW1 1WN', + 'contact_email' => 'john@example.com', +]); + +$cart->setShippingAddress([ + // same structure +]); + +// Reuse billing for shipping +$cart->setShippingAddress($cart->shippingAddress); +``` + +Required fields for order creation: `country_id`, `first_name`, `line_one`, `city`, `postcode`. + +### Shipping Options + +```php +use Lunar\Facades\ShippingManifest; + +// Get available options (requires shipping address on cart) +$options = ShippingManifest::getOptions($cart); + +// Set selected option +$option = ShippingManifest::getOption($cart, 'standard-delivery'); +$cart->setShippingOption($option); +``` + +### Creating Order + +```php +use Lunar\Exceptions\Carts\CartException; + +try { + $order = $cart->createOrder(); +} catch (CartException $e) { + $e->errors(); // MessageBag with validation failures +} +``` + +### Estimated Shipping (before full address) + +```php +$shippingOption = $cart->getEstimatedShipping([ + 'postcode' => '123456', + 'state' => 'Essex', + 'country' => Country::first(), +], setOverride: true); +``` + +## Orders + +### Order Model + +```php +use Lunar\Models\Order; + +$order->isDraft(); // placed_at is null +$order->isPlaced(); // placed_at is set + +// Relationships +$order->lines; +$order->productLines; // Excluding shipping +$order->shippingLines; +$order->physicalLines; +$order->digitalLines; +$order->addresses; +$order->shippingAddress; +$order->billingAddress; +$order->transactions; +$order->captures; +$order->intents; +$order->refunds; +$order->currency; // Matched on currency_code +$order->cart; +$order->customer; +$order->user; +``` + +Order fields all use `Lunar\DataTypes\Price` casting: `sub_total`, `discount_total`, `shipping_total`, `tax_total`, `total`. + +### Order Reference Generation + +Default format: `{prefix}{order_id_padded_to_8_digits}`. Customize via `config/lunar/orders.php`: + +```php +'reference_generator' => App\Generators\MyCustomGenerator::class, +``` + +### Order Status Notifications + +Configure mailers per status in `config/lunar/orders.php`: + +```php +'statuses' => [ + 'awaiting-payment' => [ + 'label' => 'Awaiting Payment', + 'color' => '#848a8c', + 'mailers' => [App\Mail\MyMailer::class], + 'notifications' => [], + ], +], +``` + +### Transactions + +```php +use Lunar\Models\Transaction; + +$order->transactions()->create([ + 'success' => true, + 'type' => 'capture', // capture, intent, refund + 'driver' => 'stripe', + 'amount' => 1999, + 'reference' => 'ch_123456', + 'status' => 'settled', + 'card_type' => 'visa', + 'last_four' => '4242', +]); +``` + +## Payments + +### Payment Drivers + +Lunar uses a driver-based system. Built-in: `offline`. First-party: Stripe. + +```php +use Lunar\Facades\Payments; + +$payment = Payments::driver('card') + ->cart($cart) + ->withData(['payment_token' => $token]) + ->authorize(); + +$payment->success; // bool +$payment->message; // ?string +$payment->orderId; // ?int +``` + +Configuration in `config/lunar/payments.php`: + +```php +'types' => [ + 'cash-in-hand' => [ + 'driver' => 'offline', + 'authorized' => 'payment-offline', + ], + 'card' => [ + 'driver' => 'stripe', + 'authorized' => 'payment-received', + ], +], +``` + +### Payment Lifecycle + +1. Authorize: `Payments::driver('card')->cart($cart)->authorize()` +2. Capture: The driver typically handles this during authorization +3. Refund: Via the payment provider or manually in admin + +The `PaymentAttemptEvent` is dispatched on every payment attempt. + +## Discounts + +### Built-in Types + +1. **AmountOff** — Percentage or fixed amount off +2. **BuyXGetY** — Buy X, get Y free promotions + +### Creating a Discount + +```php +use Lunar\Models\Discount; + +$discount = Discount::create([ + 'name' => '20% Off', + 'handle' => '20_percent_off', + 'coupon' => '20OFF', + 'type' => 'Lunar\DiscountTypes\AmountOff', + 'data' => [ + 'fixed_value' => false, + 'percentage' => 20, + 'min_prices' => ['USD' => 2000], + ], + 'starts_at' => now(), + 'ends_at' => null, + 'max_uses' => null, + 'max_uses_per_user' => 1, + 'priority' => 1, + 'stop' => false, +]); +``` + +### Discountable Types + +| Type | Purpose | +|------|---------| +| `condition` | Product must be in cart for discount to activate | +| `exclusion` | Product is excluded from discount | +| `limitation` | Discount only applies to these products | +| `reward` | Reward products (for BuyXGetY) | + +```php +$discount->discountableConditions()->create([ + 'discountable_type' => 'product_variant', + 'discountable_id' => $variant->id, +]); +``` + +### Discount Statuses + +```php +$discount->status; // 'active', 'pending', 'expired', 'scheduled' +``` + +### Coupon Validation + +```php +use Lunar\Facades\Discounts; + +Discounts::validateCoupon('20OFF'); // bool +Discounts::resetDiscounts(); // Clear cached discounts +``` + +### Custom Discount Types + +```php +use Lunar\DiscountTypes\AbstractDiscountType; +use Lunar\Facades\Discounts; + +class CustomDiscount extends AbstractDiscountType +{ + public function getName(): string + { + return 'Custom Discount'; + } + + public function apply(Cart $cart): Cart + { + return $cart; + } +} + +Discounts::addType(CustomDiscount::class); +``` + +## Pricing + +### Price Data Type + +All monetary values return `Lunar\DataTypes\Price` objects with: + +```php +$price->value; // Raw integer (e.g. 1999) +$price->decimal(); // Float (e.g. 19.99) +$price->unitDecimal(); // Per unit (factors unit_quantity) +$price->formatted('en-gb'); // Formatted string (e.g. "£19.99") +$price->unitFormatted('en-gb'); // Per-unit formatted string +``` + +### Price Model + +```php +use Lunar\Models\Price; + +Price::create([ + 'price' => 1999, + 'compare_price' => 2999, + 'currency_id' => $currency->id, + 'min_quantity' => 1, + 'customer_group_id' => null, // null = all groups + 'priceable_type' => $variant->getMorphClass(), + 'priceable_id' => $variant->id, +]); +``` + +### Tax Helpers + +```php +$price->priceExTax(); +$price->priceIncTax(); +$price->comparePriceIncTax(); + +// With specific tax zone override (added in 1.5) +$price->priceIncTax($taxZone); +``` + +### Pricing Pipelines + +Configured in `config/lunar/pricing.php`. Custom pipelines can modify resolved pricing: + +```php +'pipelines' => [ + App\Pipelines\Pricing\CustomPricingPipeline::class, +], +``` + +### Custom Formatter + +```php +use Lunar\Pricing\PriceFormatterInterface; + +class CustomFormatter implements PriceFormatterInterface +{ + public function __construct( + public int $value, + public ?Currency $currency = null, + public int $unitQty = 1 + ) {} + // Implement decimal(), unitDecimal(), formatted(), unitFormatted() +} +``` + +Register in `config/lunar/pricing.php`: + +```php +'formatter' => App\Pricing\CustomFormatter::class, +``` + +## Search + +Lunar search is built on Laravel Scout with pre-configured indexers per model. + +### Configuration + +```php +// config/lunar/search.php +'models' => [ + Lunar\Models\Product::class, + Lunar\Models\Collection::class, + // ... +], +'engine_map' => [ + Lunar\Models\Product::class => 'typesense', + Lunar\Models\Order::class => 'meilisearch', +], +'indexers' => [ + Lunar\Models\Product::class => Lunar\Search\ProductIndexer::class, + // ... +], +``` + +Requires `soft_delete: true` in `config/scout.php`. + +### Indexing + +```bash +vendor/bin/sail artisan lunar:search:index +vendor/bin/sail artisan lunar:search:index "Lunar\Models\Product" --refresh +vendor/bin/sail artisan lunar:search:index "Lunar\Models\Order" --flush +``` + +For Meilisearch, run setup to configure filterable/sortable attributes: + +```bash +vendor/bin/sail artisan lunar:meilisearch:setup +``` + +### Storefront Search Add-on + +For faceted search with consistent API across engines: + +```bash +vendor/bin/sail composer require lunarphp/search +``` + +```php +use Lunar\Search\Facades\Search; + +$results = Search::search('boots') + ->paginate(20) + ->get(); +``` + +## Attributes + +### Attribute System + +Custom data stored as JSON (`attribute_data`) with typed field values. + +### Field Types + +| Type | Description | +|------|-------------| +| `Lunar\FieldTypes\Text` | Plain or rich text | +| `Lunar\FieldTypes\TranslatedText` | Translatable text (one value per locale) | +| `Lunar\FieldTypes\Number` | Integer or decimal | +| `Lunar\FieldTypes\Toggle` | Boolean | +| `Lunar\FieldTypes\Dropdown` | Single select from options | +| `Lunar\FieldTypes\ListField` | Reorderable list of text values | +| `Lunar\FieldTypes\File` | File references | +| `Lunar\FieldTypes\YouTube` | YouTube video ID/URL | +| `Lunar\FieldTypes\Vimeo` | Vimeo video ID/URL | + +### Saving & Reading + +```php +use Lunar\FieldTypes\Text; +use Lunar\FieldTypes\TranslatedText; + +$product->attribute_data = collect([ + 'meta_title' => new Text('Best Screwdriver'), + 'description' => new TranslatedText(collect([ + 'en' => new Text('Great tool'), + 'fr' => new Text('Super outil'), + ])), +]); +$product->save(); + +// Read +$product->translateAttribute('name'); // Current locale +$product->translateAttribute('name', 'fr'); // Specific locale +$product->attr('name'); // Shorthand +``` + +### Custom Attributable Models + +```php +use Lunar\Base\Casts\AsAttributeData; +use Lunar\Base\Traits\HasAttributes; +use Lunar\Facades\AttributeManifest; + +class MyModel extends Model +{ + use HasAttributes; + + protected $casts = [ + 'attribute_data' => AsAttributeData::class, + ]; +} + +// Register in service provider +AttributeManifest::addType(MyModel::class); +``` + +## System Settings + +### Channels + +```php +use Lunar\Models\Channel; + +// Schedule product for a channel +$product->scheduleChannel($channel); +$product->scheduleChannel($channel, now()->addDays(7)); // With start date + +// Query +Product::channel($channel)->get(); +``` + +### Customer Groups + +```php +use Lunar\Models\CustomerGroup; + +$product->scheduleCustomerGroup($group); +$product->scheduleCustomerGroup($group, now()->addDays(7)); + +Product::customerGroup($group)->get(); +``` + +### Currencies + +```php +use Lunar\Models\Currency; + +Currency::default()->first(); // Get default currency + +// Sync pricing: Lunar auto-calculates prices from default currency +// using the exchange rate when enabled +``` + +### Tax Setup + +```php +use Lunar\Models\TaxZone; +use Lunar\Models\TaxClass; + +// Tax zone with country, states, or postcodes +$ukZone = TaxZone::create([ + 'name' => 'UK VAT', + 'zone_type' => 'country', + 'price_display' => 'tax_inclusive', // or 'tax_exclusive' +]); + +$rate = $ukZone->taxRates()->create(['name' => 'VAT', 'priority' => 1]); +$rate->taxRateAmounts()->create([ + 'tax_class_id' => TaxClass::default()->first()->id, + 'percentage' => 20.000, +]); +``` + +## Extending Lunar + +### Dynamic Relationships (Recommended) + +```php +use Lunar\Models\Order; +use App\Models\Ticket; + +Order::resolveRelationUsing('ticket', function ($orderModel) { + return $orderModel->belongsTo(Ticket::class, 'ticket_id'); +}); +``` + +Register in a service provider's `boot` method. + +### Model Replacement + +For deeper customization (overriding methods, adding scopes): + +```php +use Lunar\Facades\ModelManifest; + +// Single model +ModelManifest::replace( + Lunar\Models\Contracts\Product::class, + App\Model\Product::class, +); + +// Scan directory +ModelManifest::addDirectory(__DIR__.'/../Models'); +``` + +Custom model must extend the Lunar model. Contract-based type hints work with route binding and relationships. + +### Extending Cart Calculations + +Define custom pipelines in `config/lunar/cart.php`: + +```php +'pipelines' => [ + App\Pipelines\Cart\CustomCartPipeline::class, +], +``` + +Override order creation action: + +```php +'actions' => [ + 'order_create' => App\Actions\Carts\CustomCreateOrder::class, +], +``` + +Add custom validators: + +```php +'validators' => [ + 'order_create' => [App\Validation\Cart\CustomValidator::class], +], +``` + +## Key Artisan Commands + +| Command | Purpose | +|---------|---------| +| `lunar:install` | Run the Lunar installer | +| `lunar:search:index` | Import/refresh search indexes | +| `lunar:meilisearch:setup` | Configure Meilisearch filterable/sortable fields | +| `lunar:update` | Handle Lunar updates and migrations | +| `vendor:publish --tag=lunar` | Publish Lunar configs | +| `vendor:publish --tag=lunarpanel.pdf` | Publish order PDF template | + +## Common Pitfalls + +- **`name` attribute required**: Internally expects `name` in product/collection attribute data. Missing it causes admin panel errors. +- **Prices stored as integers**: Always store prices in smallest currency unit (cents/pence). A $19.99 price is `1999`. +- **Cart prices are dynamic**: Not stored in DB until order is created. Always call `calculate()` or `recalculate()`. +- **`MissingCurrencyPriceException`**: Thrown when fetching price for a currency with no pricing defined for the variant. +- **Cart validation exceptions**: All throw `CartException`. Catch and display `$e->errors()` for detailed messages. +- **Single order per cart by default**: Pass `allowMultipleOrders: true` to `createOrder()` for split shipments. +- **`canCreateOrder()` before `createOrder()`**: Check readiness without throwing exceptions. +- **Fingerprint mismatch**: Use `checkFingerprint()` before payment to detect cart changes between checkout steps. +- **Model replacement conflicts**: Add-ons must never replace core models. Use dynamic relationships instead. +- **Scout `soft_delete`**: Must be `true` in `config/scout.php` or soft-deleted models appear in search results. +- **Engine map fallback**: Models not in `engine_map` use the default `SCOUT_DRIVER`. Configure per model to control indexing costs. +- **`scheduleCustomerGroup` / `scheduleChannel` with no dates**: Enables immediately. Pass `null` for `ends_at` to never expire. +- **Root collection required**: At least one root collection must exist before child collections can be created. +- **Tax zone clearance on address set**: Setting shipping address clears explicit `tax_zone_id`. Pass `clearTaxZone: false` to keep it. +- **BuyXGetY collection conditions**: Match against entire collections (added in 1.5) instead of listing individual products. +- **`resetDiscounts()` after coupon application**: Discounts are cached per request. Call this after applying a coupon to refresh. From ebb85520080fc8193abbee55e1c845a47fdf9b40 Mon Sep 17 00:00:00 2001 From: Dmytro Morozov Date: Sat, 30 May 2026 12:32:48 +0300 Subject: [PATCH 2/3] Remove laravel sail mentions --- skills/lunarphp/SKILL.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/skills/lunarphp/SKILL.md b/skills/lunarphp/SKILL.md index d2123c5e93..fe288638d7 100644 --- a/skills/lunarphp/SKILL.md +++ b/skills/lunarphp/SKILL.md @@ -111,7 +111,7 @@ The database uses a `lunar_` table prefix (configurable in `config/lunar/databas 1. Install via Composer: ```bash -vendor/bin/sail composer require lunarphp/lunar:"^1.0" -W +composer require lunarphp/lunar:"^1.0" -W ``` 2. Add `LunarUser` trait and interface to the `User` model: @@ -140,7 +140,7 @@ public function register(): void 4. Run the installer: ```bash -vendor/bin/sail artisan lunar:install +php artisan lunar:install ``` The installer publishes configs, runs migrations, creates an admin user, and seeds default data (channel, language, currency, customer group, etc.). @@ -152,7 +152,7 @@ The installer publishes configs, runs migrations, creates an admin user, and see Publish configs before installing to customize settings: ```bash -vendor/bin/sail artisan vendor:publish --tag=lunar +php artisan vendor:publish --tag=lunar ``` Key config values to check: @@ -839,15 +839,15 @@ Requires `soft_delete: true` in `config/scout.php`. ### Indexing ```bash -vendor/bin/sail artisan lunar:search:index -vendor/bin/sail artisan lunar:search:index "Lunar\Models\Product" --refresh -vendor/bin/sail artisan lunar:search:index "Lunar\Models\Order" --flush +php artisan lunar:search:index +php artisan lunar:search:index "Lunar\Models\Product" --refresh +php artisan lunar:search:index "Lunar\Models\Order" --flush ``` For Meilisearch, run setup to configure filterable/sortable attributes: ```bash -vendor/bin/sail artisan lunar:meilisearch:setup +php artisan lunar:meilisearch:setup ``` ### Storefront Search Add-on @@ -855,7 +855,7 @@ vendor/bin/sail artisan lunar:meilisearch:setup For faceted search with consistent API across engines: ```bash -vendor/bin/sail composer require lunarphp/search +composer require lunarphp/search ``` ```php From 2b458abae8b814488f4974c6edcea5ac781829a5 Mon Sep 17 00:00:00 2001 From: Dmytro Morozov Date: Wed, 10 Jun 2026 15:42:37 +0300 Subject: [PATCH 3/3] Split into references --- skills/lunarphp/SKILL.md | 998 +----------------- skills/lunarphp/references/cart-checkout.md | 284 +++++ skills/lunarphp/references/extending.md | 83 ++ skills/lunarphp/references/installation.md | 52 + .../lunarphp/references/payments-discounts.md | 132 +++ skills/lunarphp/references/pitfalls.md | 18 + .../lunarphp/references/products-catalog.md | 249 +++++ .../lunarphp/references/search-attributes.md | 126 +++ skills/lunarphp/references/system-settings.md | 69 ++ 9 files changed, 1047 insertions(+), 964 deletions(-) create mode 100644 skills/lunarphp/references/cart-checkout.md create mode 100644 skills/lunarphp/references/extending.md create mode 100644 skills/lunarphp/references/installation.md create mode 100644 skills/lunarphp/references/payments-discounts.md create mode 100644 skills/lunarphp/references/pitfalls.md create mode 100644 skills/lunarphp/references/products-catalog.md create mode 100644 skills/lunarphp/references/search-attributes.md create mode 100644 skills/lunarphp/references/system-settings.md diff --git a/skills/lunarphp/SKILL.md b/skills/lunarphp/SKILL.md index fe288638d7..98baedfb9e 100644 --- a/skills/lunarphp/SKILL.md +++ b/skills/lunarphp/SKILL.md @@ -18,7 +18,6 @@ Use `search-docs` for pattern-specific queries — Lunar docs are indexed by the Activate this skill when: -- Installing or configuring LunarPHP in a Laravel app - Creating or modifying products, variants, product options, brands, or collections - Building cart functionality (add/update/remove items, coupons, totals) - Implementing checkout flow (addresses, shipping, payment, order creation) @@ -97,963 +96,38 @@ All under `config/lunar/`. Key files: The database uses a `lunar_` table prefix (configurable in `config/lunar/database.php`). All prices are stored as integers (smallest currency unit, e.g. cents). -## Installation & Setup - -### Requirements - -- PHP >= 8.3 -- Laravel 12 or 13 -- MySQL 8.0+ / PostgreSQL 9.4+ -- `bcmath`, `exif`, `intl` PHP extensions - -### Step-by-Step - -1. Install via Composer: - -```bash -composer require lunarphp/lunar:"^1.0" -W -``` - -2. Add `LunarUser` trait and interface to the `User` model: - -```php -use Lunar\Base\Traits\LunarUser; -use Lunar\Base\LunarUser as LunarUserInterface; - -class User extends Authenticatable implements LunarUserInterface -{ - use LunarUser; -} -``` - -3. Register the admin panel in `AppServiceProvider`: - -```php -use Lunar\Admin\Support\Facades\LunarPanel; - -public function register(): void -{ - LunarPanel::register(); -} -``` - -4. Run the installer: - -```bash -php artisan lunar:install -``` - -The installer publishes configs, runs migrations, creates an admin user, and seeds default data (channel, language, currency, customer group, etc.). - -5. Access the admin panel at `/lunar`. - -### Post-Install Configuration - -Publish configs before installing to customize settings: - -```bash -php artisan vendor:publish --tag=lunar -``` - -Key config values to check: - -```php -// config/lunar/database.php -'table_prefix' => 'lunar_', -'users_id_type' => 'bigint', // or 'int', 'uuid' - -// config/lunar/cart_session.php -'session_key' => 'lunar_cart', -'auto_create' => false, - -// config/lunar/payments.php -'default' => env('PAYMENTS_TYPE', 'cash-in-hand'), -``` - -## Products & Catalog - -### Products - -Products always have at least one variant. Custom data is stored as `attribute_data` (JSON) using field types: - -```php -use Lunar\Models\Product; -use Lunar\FieldTypes\Text; -use Lunar\FieldTypes\TranslatedText; - -Product::create([ - 'product_type_id' => $productType->id, - 'status' => 'published', - 'attribute_data' => [ - 'name' => new TranslatedText(collect([ - 'en' => new Text('FooBar'), - ])), - 'description' => new Text('Description text.'), - ], -]); -``` - -The `name` attribute is required — Lunar expects it internally. - -Key scopes: `status($status)`, `channel($channel)`, `customerGroup($group)`. - -### Variants - -Variants hold pricing, stock, dimensions, and product identifiers: - -```php -use Lunar\Models\ProductVariant; -use Lunar\Models\TaxClass; -use Lunar\Models\Currency; - -$variant = ProductVariant::create([ - 'product_id' => $product->id, - 'tax_class_id' => $taxClass->id, - 'sku' => 'BLUE-001', - 'unit_quantity' => 1, - 'min_quantity' => 1, - 'quantity_increment' => 1, - 'stock' => 100, - 'purchasable' => 'always', // or 'in_stock' - 'shippable' => true, -]); - -$variant->prices()->create([ - 'price' => 1999, // $19.99 in cents - 'currency_id' => $currency->id, -]); -``` - -Key fields: `sku`, `gtin`, `mpn`, `ean`, `unit_quantity`, `stock`, `backorder`, `purchasable`, dimensions (`length_value`, `width_value`, `height_value`, `weight_value`, `volume_value`). - -### Product Options - -Define variations like color/size: - -```php -use Lunar\Models\ProductOption; - -$color = ProductOption::create([ - 'name' => ['en' => 'Color'], - 'label' => ['en' => 'Color'], -]); - -$blue = $color->values()->create([ - 'name' => ['en' => 'Blue'], -]); - -$variant->values()->attach($blue); -``` - -Both `ProductOption` and `ProductOptionValue` have a `meta` JSON field for custom data (e.g. hex color values). - -### Product Types - -Product types determine which attributes are available for editing: - -```php -use Lunar\Models\ProductType; - -$productType = ProductType::create(['name' => 'Boots']); -$productType->mappedAttributes()->attach($attributeIds); -``` - -### Brands - -```php -use Lunar\Models\Brand; - -$brand = Brand::create(['name' => 'Nike']); -$product->update(['brand_id' => $brand->id]); -``` - -### Associations - -Link products as cross-sells, up-sells, or alternates: - -```php -$product->associations()->create([ - 'association_type' => 'cross-sell', // or 'up-sell', 'alternate' - 'target_id' => $targetProduct->id, -]); -``` - -### Pricing - -Prices are stored as integers (smallest currency unit). Use the `Pricing` facade for retrieval: - -```php -use Lunar\Facades\Pricing; - -$pricing = Pricing::qty(5) - ->customerGroup($group) - ->currency($currency) - ->for($variant) - ->get(); - -$pricing->matched; // The matched Price model -$pricing->base; // The base Price model -$pricing->priceBreaks; // Collection of quantity break prices -``` - -Or via the model: - -```php -$pricing = $variant->pricing()->qty(5)->get(); -``` - -### Collections - -Collections use nested sets for hierarchy. Always create a root collection before children: - -```php -use Lunar\Models\Collection; -use Lunar\Models\CollectionGroup; - -$group = CollectionGroup::create([ - 'name' => 'Main Catalogue', - 'handle' => 'main-catalogue', -]); - -$parent = Collection::create([ - 'collection_group_id' => $group->id, - 'attribute_data' => ['name' => new \Lunar\FieldTypes\Text('Clothing')], -]); - -$child = Collection::create([ - 'collection_group_id' => $group->id, - 'attribute_data' => ['name' => new \Lunar\FieldTypes\Text('T-Shirts')], -]); - -$parent->appendNode($child); -``` - -Add products with sort position: - -```php -$collection->products()->sync([ - $productA->id => ['position' => 1], -]); -``` - -Sort options: `min_price:asc`, `min_price:desc`, `sku:asc`, `sku:desc`, `custom`. - -## Cart - -### CartSession Facade - -Use `CartSession` for session-based cart management (recommended for traditional storefronts): - -```php -use Lunar\Facades\CartSession; - -// Get current cart (returns null if none exists and auto_create is false) -$cart = CartSession::current(); - -// Add item -CartSession::add($purchasable, quantity: 2, meta: ['personalization' => 'Happy Birthday!']); - -// Add multiple -CartSession::addLines([ - ['purchasable' => $variant1, 'quantity' => 2], - ['purchasable' => $variant2, 'quantity' => 1], -]); - -// Update line -CartSession::updateLine($cartLineId, quantity: 3, meta: ['foo' => 'bar']); - -// Remove line -CartSession::remove($cartLineId); - -// Clear cart -CartSession::clear(); - -// Create order -$order = CartSession::createOrder(); - -// Forget cart (removes from session and soft-deletes) -CartSession::forget(); - -// Associate user -CartSession::associate($cart, $user, policy: 'merge'); -``` - -### Cart Model Directly - -```php -use Lunar\Models\Cart; - -// Create cart -$cart = Cart::create([ - 'currency_id' => $currency->id, - 'channel_id' => $channel->id, -]); - -// Add lines -$cart->add($purchasable, quantity: 2, meta: ['key' => 'value']); - -$cart->addLines([ - ['purchasable' => $variant1, 'quantity' => 2], - ['purchasable' => $variant2, 'quantity' => 1], -]); - -// Update/remove -$cart->updateLine($lineId, quantity: 5); -$cart->remove($lineId); -$cart->clear(); - -// Addresses -$cart->setShippingAddress([/* address fields */]); -$cart->setBillingAddress([/* address fields */]); - -// Shipping option -$cart->setShippingOption($shippingOption); - -// Coupon -$cart->update(['coupon_code' => '20OFF']); -$cart->recalculate(); - -// Calculate totals -$cart->calculate(); -$cart->recalculate(); // Force -$cart->isCalculated(); - -// Check order readiness -$cart->canCreateOrder(); -$cart->hasCompletedOrders(); - -// Create order -$order = $cart->createOrder(); -$order = $cart->createOrder(allowMultipleOrders: true); -$order = $cart->createOrder(orderIdToUpdate: $existingOrderId); - -// Stock validation -$cart->validateStock(); - -// Fingerprint (detect cart changes) -$fingerprint = $cart->fingerprint(); -$cart->checkFingerprint($fingerprint); // Throws FingerprintMismatchException -``` - -### Cart Line Properties (after calculation) - -| Property | Description | -|----------|-------------| -| `unitPrice` | Single unit price (excl. tax) | -| `unitPriceInclTax` | Single unit price (incl. tax) | -| `subTotal` | Line total before discounts and tax | -| `subTotalDiscounted` | Line total after discounts, before tax | -| `discountTotal` | Discount applied to this line | -| `taxAmount` | Tax for this line | -| `total` | Final line total (incl. tax and discounts) | - -### Cart Properties (after calculation) - -| Property | Description | -|----------|-------------| -| `subTotal` | Sum of line subtotals, before tax/discounts | -| `subTotalDiscounted` | Subtotal after line-level discounts | -| `discountTotal` | Total discounts applied | -| `discountBreakdown` | Collection of discount breakdowns | -| `taxTotal` | Total tax across all lines and shipping | -| `taxBreakdown` | Tax breakdown by rate | -| `shippingSubTotal` | Shipping cost before tax | -| `shippingTaxTotal` | Tax on shipping | -| `shippingTotal` | Shipping cost including tax | -| `total` | Final cart total | - -### Cart Scopes - -```php -Cart::unmerged()->get(); // Carts not merged into another -Cart::active()->get(); // Carts with no completed orders -``` - -### User Login Handling - -Lunar listens to auth events and handles cart association automatically. Config `auth_policy` in `config/lunar/cart.php` controls `merge` vs `override` behavior when a user logs in. - -## Checkout - -### Flow - -1. Set addresses on cart -2. Select shipping option -3. Review order -4. Create order from cart -5. Process payment - -### Setting Addresses - -```php -$cart->setBillingAddress([ - 'country_id' => $country->id, - 'first_name' => 'John', - 'last_name' => 'Doe', - 'line_one' => '123 Main St', - 'city' => 'London', - 'postcode' => 'NW1 1WN', - 'contact_email' => 'john@example.com', -]); - -$cart->setShippingAddress([ - // same structure -]); - -// Reuse billing for shipping -$cart->setShippingAddress($cart->shippingAddress); -``` - -Required fields for order creation: `country_id`, `first_name`, `line_one`, `city`, `postcode`. - -### Shipping Options - -```php -use Lunar\Facades\ShippingManifest; - -// Get available options (requires shipping address on cart) -$options = ShippingManifest::getOptions($cart); - -// Set selected option -$option = ShippingManifest::getOption($cart, 'standard-delivery'); -$cart->setShippingOption($option); -``` - -### Creating Order - -```php -use Lunar\Exceptions\Carts\CartException; - -try { - $order = $cart->createOrder(); -} catch (CartException $e) { - $e->errors(); // MessageBag with validation failures -} -``` - -### Estimated Shipping (before full address) - -```php -$shippingOption = $cart->getEstimatedShipping([ - 'postcode' => '123456', - 'state' => 'Essex', - 'country' => Country::first(), -], setOverride: true); -``` - -## Orders - -### Order Model - -```php -use Lunar\Models\Order; - -$order->isDraft(); // placed_at is null -$order->isPlaced(); // placed_at is set - -// Relationships -$order->lines; -$order->productLines; // Excluding shipping -$order->shippingLines; -$order->physicalLines; -$order->digitalLines; -$order->addresses; -$order->shippingAddress; -$order->billingAddress; -$order->transactions; -$order->captures; -$order->intents; -$order->refunds; -$order->currency; // Matched on currency_code -$order->cart; -$order->customer; -$order->user; -``` - -Order fields all use `Lunar\DataTypes\Price` casting: `sub_total`, `discount_total`, `shipping_total`, `tax_total`, `total`. - -### Order Reference Generation - -Default format: `{prefix}{order_id_padded_to_8_digits}`. Customize via `config/lunar/orders.php`: - -```php -'reference_generator' => App\Generators\MyCustomGenerator::class, -``` - -### Order Status Notifications - -Configure mailers per status in `config/lunar/orders.php`: - -```php -'statuses' => [ - 'awaiting-payment' => [ - 'label' => 'Awaiting Payment', - 'color' => '#848a8c', - 'mailers' => [App\Mail\MyMailer::class], - 'notifications' => [], - ], -], -``` - -### Transactions - -```php -use Lunar\Models\Transaction; - -$order->transactions()->create([ - 'success' => true, - 'type' => 'capture', // capture, intent, refund - 'driver' => 'stripe', - 'amount' => 1999, - 'reference' => 'ch_123456', - 'status' => 'settled', - 'card_type' => 'visa', - 'last_four' => '4242', -]); -``` - -## Payments - -### Payment Drivers - -Lunar uses a driver-based system. Built-in: `offline`. First-party: Stripe. - -```php -use Lunar\Facades\Payments; - -$payment = Payments::driver('card') - ->cart($cart) - ->withData(['payment_token' => $token]) - ->authorize(); - -$payment->success; // bool -$payment->message; // ?string -$payment->orderId; // ?int -``` - -Configuration in `config/lunar/payments.php`: - -```php -'types' => [ - 'cash-in-hand' => [ - 'driver' => 'offline', - 'authorized' => 'payment-offline', - ], - 'card' => [ - 'driver' => 'stripe', - 'authorized' => 'payment-received', - ], -], -``` - -### Payment Lifecycle - -1. Authorize: `Payments::driver('card')->cart($cart)->authorize()` -2. Capture: The driver typically handles this during authorization -3. Refund: Via the payment provider or manually in admin - -The `PaymentAttemptEvent` is dispatched on every payment attempt. - -## Discounts - -### Built-in Types - -1. **AmountOff** — Percentage or fixed amount off -2. **BuyXGetY** — Buy X, get Y free promotions - -### Creating a Discount - -```php -use Lunar\Models\Discount; - -$discount = Discount::create([ - 'name' => '20% Off', - 'handle' => '20_percent_off', - 'coupon' => '20OFF', - 'type' => 'Lunar\DiscountTypes\AmountOff', - 'data' => [ - 'fixed_value' => false, - 'percentage' => 20, - 'min_prices' => ['USD' => 2000], - ], - 'starts_at' => now(), - 'ends_at' => null, - 'max_uses' => null, - 'max_uses_per_user' => 1, - 'priority' => 1, - 'stop' => false, -]); -``` - -### Discountable Types - -| Type | Purpose | -|------|---------| -| `condition` | Product must be in cart for discount to activate | -| `exclusion` | Product is excluded from discount | -| `limitation` | Discount only applies to these products | -| `reward` | Reward products (for BuyXGetY) | - -```php -$discount->discountableConditions()->create([ - 'discountable_type' => 'product_variant', - 'discountable_id' => $variant->id, -]); -``` - -### Discount Statuses - -```php -$discount->status; // 'active', 'pending', 'expired', 'scheduled' -``` - -### Coupon Validation - -```php -use Lunar\Facades\Discounts; - -Discounts::validateCoupon('20OFF'); // bool -Discounts::resetDiscounts(); // Clear cached discounts -``` - -### Custom Discount Types - -```php -use Lunar\DiscountTypes\AbstractDiscountType; -use Lunar\Facades\Discounts; - -class CustomDiscount extends AbstractDiscountType -{ - public function getName(): string - { - return 'Custom Discount'; - } - - public function apply(Cart $cart): Cart - { - return $cart; - } -} - -Discounts::addType(CustomDiscount::class); -``` - -## Pricing - -### Price Data Type - -All monetary values return `Lunar\DataTypes\Price` objects with: - -```php -$price->value; // Raw integer (e.g. 1999) -$price->decimal(); // Float (e.g. 19.99) -$price->unitDecimal(); // Per unit (factors unit_quantity) -$price->formatted('en-gb'); // Formatted string (e.g. "£19.99") -$price->unitFormatted('en-gb'); // Per-unit formatted string -``` - -### Price Model - -```php -use Lunar\Models\Price; - -Price::create([ - 'price' => 1999, - 'compare_price' => 2999, - 'currency_id' => $currency->id, - 'min_quantity' => 1, - 'customer_group_id' => null, // null = all groups - 'priceable_type' => $variant->getMorphClass(), - 'priceable_id' => $variant->id, -]); -``` - -### Tax Helpers - -```php -$price->priceExTax(); -$price->priceIncTax(); -$price->comparePriceIncTax(); - -// With specific tax zone override (added in 1.5) -$price->priceIncTax($taxZone); -``` - -### Pricing Pipelines - -Configured in `config/lunar/pricing.php`. Custom pipelines can modify resolved pricing: - -```php -'pipelines' => [ - App\Pipelines\Pricing\CustomPricingPipeline::class, -], -``` - -### Custom Formatter - -```php -use Lunar\Pricing\PriceFormatterInterface; - -class CustomFormatter implements PriceFormatterInterface -{ - public function __construct( - public int $value, - public ?Currency $currency = null, - public int $unitQty = 1 - ) {} - // Implement decimal(), unitDecimal(), formatted(), unitFormatted() -} -``` - -Register in `config/lunar/pricing.php`: - -```php -'formatter' => App\Pricing\CustomFormatter::class, -``` - -## Search - -Lunar search is built on Laravel Scout with pre-configured indexers per model. - -### Configuration - -```php -// config/lunar/search.php -'models' => [ - Lunar\Models\Product::class, - Lunar\Models\Collection::class, - // ... -], -'engine_map' => [ - Lunar\Models\Product::class => 'typesense', - Lunar\Models\Order::class => 'meilisearch', -], -'indexers' => [ - Lunar\Models\Product::class => Lunar\Search\ProductIndexer::class, - // ... -], -``` - -Requires `soft_delete: true` in `config/scout.php`. - -### Indexing - -```bash -php artisan lunar:search:index -php artisan lunar:search:index "Lunar\Models\Product" --refresh -php artisan lunar:search:index "Lunar\Models\Order" --flush -``` - -For Meilisearch, run setup to configure filterable/sortable attributes: - -```bash -php artisan lunar:meilisearch:setup -``` - -### Storefront Search Add-on - -For faceted search with consistent API across engines: - -```bash -composer require lunarphp/search -``` - -```php -use Lunar\Search\Facades\Search; - -$results = Search::search('boots') - ->paginate(20) - ->get(); -``` - -## Attributes - -### Attribute System - -Custom data stored as JSON (`attribute_data`) with typed field values. - -### Field Types - -| Type | Description | -|------|-------------| -| `Lunar\FieldTypes\Text` | Plain or rich text | -| `Lunar\FieldTypes\TranslatedText` | Translatable text (one value per locale) | -| `Lunar\FieldTypes\Number` | Integer or decimal | -| `Lunar\FieldTypes\Toggle` | Boolean | -| `Lunar\FieldTypes\Dropdown` | Single select from options | -| `Lunar\FieldTypes\ListField` | Reorderable list of text values | -| `Lunar\FieldTypes\File` | File references | -| `Lunar\FieldTypes\YouTube` | YouTube video ID/URL | -| `Lunar\FieldTypes\Vimeo` | Vimeo video ID/URL | - -### Saving & Reading - -```php -use Lunar\FieldTypes\Text; -use Lunar\FieldTypes\TranslatedText; - -$product->attribute_data = collect([ - 'meta_title' => new Text('Best Screwdriver'), - 'description' => new TranslatedText(collect([ - 'en' => new Text('Great tool'), - 'fr' => new Text('Super outil'), - ])), -]); -$product->save(); - -// Read -$product->translateAttribute('name'); // Current locale -$product->translateAttribute('name', 'fr'); // Specific locale -$product->attr('name'); // Shorthand -``` - -### Custom Attributable Models - -```php -use Lunar\Base\Casts\AsAttributeData; -use Lunar\Base\Traits\HasAttributes; -use Lunar\Facades\AttributeManifest; - -class MyModel extends Model -{ - use HasAttributes; - - protected $casts = [ - 'attribute_data' => AsAttributeData::class, - ]; -} - -// Register in service provider -AttributeManifest::addType(MyModel::class); -``` - -## System Settings - -### Channels - -```php -use Lunar\Models\Channel; - -// Schedule product for a channel -$product->scheduleChannel($channel); -$product->scheduleChannel($channel, now()->addDays(7)); // With start date - -// Query -Product::channel($channel)->get(); -``` - -### Customer Groups - -```php -use Lunar\Models\CustomerGroup; - -$product->scheduleCustomerGroup($group); -$product->scheduleCustomerGroup($group, now()->addDays(7)); - -Product::customerGroup($group)->get(); -``` - -### Currencies - -```php -use Lunar\Models\Currency; - -Currency::default()->first(); // Get default currency - -// Sync pricing: Lunar auto-calculates prices from default currency -// using the exchange rate when enabled -``` - -### Tax Setup - -```php -use Lunar\Models\TaxZone; -use Lunar\Models\TaxClass; - -// Tax zone with country, states, or postcodes -$ukZone = TaxZone::create([ - 'name' => 'UK VAT', - 'zone_type' => 'country', - 'price_display' => 'tax_inclusive', // or 'tax_exclusive' -]); - -$rate = $ukZone->taxRates()->create(['name' => 'VAT', 'priority' => 1]); -$rate->taxRateAmounts()->create([ - 'tax_class_id' => TaxClass::default()->first()->id, - 'percentage' => 20.000, -]); -``` - -## Extending Lunar - -### Dynamic Relationships (Recommended) - -```php -use Lunar\Models\Order; -use App\Models\Ticket; - -Order::resolveRelationUsing('ticket', function ($orderModel) { - return $orderModel->belongsTo(Ticket::class, 'ticket_id'); -}); -``` - -Register in a service provider's `boot` method. - -### Model Replacement - -For deeper customization (overriding methods, adding scopes): - -```php -use Lunar\Facades\ModelManifest; - -// Single model -ModelManifest::replace( - Lunar\Models\Contracts\Product::class, - App\Model\Product::class, -); - -// Scan directory -ModelManifest::addDirectory(__DIR__.'/../Models'); -``` - -Custom model must extend the Lunar model. Contract-based type hints work with route binding and relationships. - -### Extending Cart Calculations - -Define custom pipelines in `config/lunar/cart.php`: - -```php -'pipelines' => [ - App\Pipelines\Cart\CustomCartPipeline::class, -], -``` - -Override order creation action: - -```php -'actions' => [ - 'order_create' => App\Actions\Carts\CustomCreateOrder::class, -], -``` - -Add custom validators: - -```php -'validators' => [ - 'order_create' => [App\Validation\Cart\CustomValidator::class], -], -``` - -## Key Artisan Commands - -| Command | Purpose | -|---------|---------| -| `lunar:install` | Run the Lunar installer | -| `lunar:search:index` | Import/refresh search indexes | -| `lunar:meilisearch:setup` | Configure Meilisearch filterable/sortable fields | -| `lunar:update` | Handle Lunar updates and migrations | -| `vendor:publish --tag=lunar` | Publish Lunar configs | -| `vendor:publish --tag=lunarpanel.pdf` | Publish order PDF template | +## Storefront Guides + +These official guides provide step-by-step walkthroughs for common storefront features. +Use them to avoid duplicating effort between the skill and the docs: + +| Guide | Description | +|-------|-------------| +| [Catalog Menu](https://docs.lunarphp.com/1.x/guides/catalog-menu.md) | Build a catalog navigation menu with collection groups, nested collections, active state tracking, and caching | +| [Product Listing Page](https://docs.lunarphp.com/1.x/guides/product-listing-page.md) | Collection resolution, product querying, filtering, sorting, pagination, and display | +| [Product Display Page](https://docs.lunarphp.com/1.x/guides/product-display-page.md) | Product resolution, variant selection, pricing, images, and add-to-cart | +| [Cart](https://docs.lunarphp.com/1.x/guides/cart.md) | Cart page with line items, quantity updates, coupon codes, and order totals | +| [Checkout](https://docs.lunarphp.com/1.x/guides/checkout.md) | Address collection, shipping options, order creation, and payment processing | +| [Customer Authentication](https://docs.lunarphp.com/1.x/guides/customer-authentication.md) | Link Laravel users to Lunar customers, manage sessions, and cart association | +| [Customer Addresses](https://docs.lunarphp.com/1.x/guides/customer-addresses.md) | Address listing, creation, editing, deletion, and default address selection | +| [Order History](https://docs.lunarphp.com/1.x/guides/order-history.md) | Customer-facing order listing, pagination, and order detail views | +| [Payment Integration](https://docs.lunarphp.com/1.x/guides/payment-integration.md) | Stripe payment intents, frontend elements, webhooks, and the complete payment lifecycle | +| [Search & Product Discovery](https://docs.lunarphp.com/1.x/guides/search.md) | Search setup, querying, faceted filtering, and URL-based product resolution | + +## Quick Reference + +Load the relevant reference file for detailed code examples: + +| Topic | Reference | +|-------|-----------| +| Post-install config, artisan commands, starter kits | [Installation](./references/installation.md) | +| Products, variants, options, brands, pricing, collections | [Products & Catalog](./references/products-catalog.md) | +| Cart, CartSession, checkout, orders, transactions | [Cart, Checkout & Orders](./references/cart-checkout.md) | +| Payment drivers, discounts, custom discount types | [Payments & Discounts](./references/payments-discounts.md) | +| Search indexing, attributes, field types | [Search & Attributes](./references/search-attributes.md) | +| Channels, customer groups, currencies, tax | [System Settings](./references/system-settings.md) | +| Model replacement, dynamic relationships, pipelines, admin panel | [Extending Lunar](./references/extending.md) | +| Common mistakes and gotchas | [Common Pitfalls](./references/pitfalls.md) | ## Common Pitfalls @@ -1063,13 +137,9 @@ Add custom validators: - **`MissingCurrencyPriceException`**: Thrown when fetching price for a currency with no pricing defined for the variant. - **Cart validation exceptions**: All throw `CartException`. Catch and display `$e->errors()` for detailed messages. - **Single order per cart by default**: Pass `allowMultipleOrders: true` to `createOrder()` for split shipments. -- **`canCreateOrder()` before `createOrder()`**: Check readiness without throwing exceptions. - **Fingerprint mismatch**: Use `checkFingerprint()` before payment to detect cart changes between checkout steps. - **Model replacement conflicts**: Add-ons must never replace core models. Use dynamic relationships instead. - **Scout `soft_delete`**: Must be `true` in `config/scout.php` or soft-deleted models appear in search results. -- **Engine map fallback**: Models not in `engine_map` use the default `SCOUT_DRIVER`. Configure per model to control indexing costs. -- **`scheduleCustomerGroup` / `scheduleChannel` with no dates**: Enables immediately. Pass `null` for `ends_at` to never expire. - **Root collection required**: At least one root collection must exist before child collections can be created. -- **Tax zone clearance on address set**: Setting shipping address clears explicit `tax_zone_id`. Pass `clearTaxZone: false` to keep it. -- **BuyXGetY collection conditions**: Match against entire collections (added in 1.5) instead of listing individual products. -- **`resetDiscounts()` after coupon application**: Discounts are cached per request. Call this after applying a coupon to refresh. + +> Full list with details in [Common Pitfalls](./references/pitfalls.md). diff --git a/skills/lunarphp/references/cart-checkout.md b/skills/lunarphp/references/cart-checkout.md new file mode 100644 index 0000000000..440c470408 --- /dev/null +++ b/skills/lunarphp/references/cart-checkout.md @@ -0,0 +1,284 @@ +# Cart, Checkout & Orders + +## CartSession Facade + +Use `CartSession` for session-based cart management (recommended for traditional storefronts): + +```php +use Lunar\Facades\CartSession; + +// Get current cart (returns null if none exists and auto_create is false) +$cart = CartSession::current(); + +// Add item +CartSession::add($purchasable, quantity: 2, meta: ['personalization' => 'Happy Birthday!']); + +// Add multiple +CartSession::addLines([ + ['purchasable' => $variant1, 'quantity' => 2], + ['purchasable' => $variant2, 'quantity' => 1], +]); + +// Update line +CartSession::updateLine($cartLineId, quantity: 3, meta: ['foo' => 'bar']); + +// Remove line +CartSession::remove($cartLineId); + +// Clear cart +CartSession::clear(); + +// Create order +$order = CartSession::createOrder(); + +// Forget cart (removes from session and soft-deletes) +CartSession::forget(); + +// Associate user +CartSession::associate($cart, $user, policy: 'merge'); +``` + +## Cart Model Directly + +```php +use Lunar\Models\Cart; + +// Create cart +$cart = Cart::create([ + 'currency_id' => $currency->id, + 'channel_id' => $channel->id, +]); + +// Add lines +$cart->add($purchasable, quantity: 2, meta: ['key' => 'value']); + +$cart->addLines([ + ['purchasable' => $variant1, 'quantity' => 2], + ['purchasable' => $variant2, 'quantity' => 1], +]); + +// Update/remove +$cart->updateLine($lineId, quantity: 5); +$cart->remove($lineId); +$cart->clear(); + +// Addresses +$cart->setShippingAddress([/* address fields */]); +$cart->setBillingAddress([/* address fields */]); + +// Shipping option +$cart->setShippingOption($shippingOption); + +// Coupon +$cart->update(['coupon_code' => '20OFF']); +$cart->recalculate(); + +// Calculate totals +$cart->calculate(); +$cart->recalculate(); // Force +$cart->isCalculated(); + +// Check order readiness +$cart->canCreateOrder(); +$cart->hasCompletedOrders(); + +// Create order +$order = $cart->createOrder(); +$order = $cart->createOrder(allowMultipleOrders: true); +$order = $cart->createOrder(orderIdToUpdate: $existingOrderId); + +// Stock validation +$cart->validateStock(); + +// Fingerprint (detect cart changes) +$fingerprint = $cart->fingerprint(); +$cart->checkFingerprint($fingerprint); // Throws FingerprintMismatchException +``` + +### Cart Line Properties (after calculation) + +| Property | Description | +|----------|-------------| +| `unitPrice` | Single unit price (excl. tax) | +| `unitPriceInclTax` | Single unit price (incl. tax) | +| `subTotal` | Line total before discounts and tax | +| `subTotalDiscounted` | Line total after discounts, before tax | +| `discountTotal` | Discount applied to this line | +| `taxAmount` | Tax for this line | +| `total` | Final line total (incl. tax and discounts) | + +### Cart Properties (after calculation) + +| Property | Description | +|----------|-------------| +| `subTotal` | Sum of line subtotals, before tax/discounts | +| `subTotalDiscounted` | Subtotal after line-level discounts | +| `discountTotal` | Total discounts applied | +| `discountBreakdown` | Collection of discount breakdowns | +| `taxTotal` | Total tax across all lines and shipping | +| `taxBreakdown` | Tax breakdown by rate | +| `shippingSubTotal` | Shipping cost before tax | +| `shippingTaxTotal` | Tax on shipping | +| `shippingTotal` | Shipping cost including tax | +| `total` | Final cart total | + +### Cart Scopes + +```php +Cart::unmerged()->get(); // Carts not merged into another +Cart::active()->get(); // Carts with no completed orders +``` + +### User Login Handling + +Lunar listens to auth events and handles cart association automatically. Config `auth_policy` in `config/lunar/cart.php` controls `merge` vs `override` behavior when a user logs in. + +## Checkout + +### Flow + +1. Set addresses on cart +2. Select shipping option +3. Review order +4. Create order from cart +5. Process payment + +### Setting Addresses + +```php +$cart->setBillingAddress([ + 'country_id' => $country->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'line_one' => '123 Main St', + 'city' => 'London', + 'postcode' => 'NW1 1WN', + 'contact_email' => 'john@example.com', +]); + +$cart->setShippingAddress([ + // same structure +]); + +// Reuse billing for shipping +$cart->setShippingAddress($cart->shippingAddress); +``` + +Required fields for order creation: `country_id`, `first_name`, `line_one`, `city`, `postcode`. + +### Shipping Options + +```php +use Lunar\Facades\ShippingManifest; + +// Get available options (requires shipping address on cart) +$options = ShippingManifest::getOptions($cart); + +// Set selected option +$option = ShippingManifest::getOption($cart, 'standard-delivery'); +$cart->setShippingOption($option); +``` + +### Creating Order + +```php +use Lunar\Exceptions\Carts\CartException; + +try { + $order = $cart->createOrder(); +} catch (CartException $e) { + $e->errors(); // MessageBag with validation failures +} +``` + +### Estimated Shipping (before full address) + +```php +$shippingOption = $cart->getEstimatedShipping([ + 'postcode' => '123456', + 'state' => 'Essex', + 'country' => Country::first(), +], setOverride: true); +``` + +> For a complete checkout flow walkthrough, see the [Checkout guide](https://docs.lunarphp.com/1.x/guides/checkout.md). +> For building a cart page with line items, quantities, coupons, and totals, see the [Cart guide](https://docs.lunarphp.com/1.x/guides/cart.md). + +## Orders + +### Order Model + +```php +use Lunar\Models\Order; + +$order->isDraft(); // placed_at is null +$order->isPlaced(); // placed_at is set + +// Relationships +$order->lines; +$order->productLines; // Excluding shipping +$order->shippingLines; +$order->physicalLines; +$order->digitalLines; +$order->addresses; +$order->shippingAddress; +$order->billingAddress; +$order->transactions; +$order->captures; +$order->intents; +$order->refunds; +$order->currency; // Matched on currency_code +$order->cart; +$order->customer; +$order->user; +``` + +Order fields all use `Lunar\DataTypes\Price` casting: `sub_total`, `discount_total`, `shipping_total`, `tax_total`, `total`. + +### Order Reference Generation + +Default format: `{prefix}{order_id_padded_to_8_digits}`. Customize via `config/lunar/orders.php`: + +```php +'reference_generator' => App\Generators\MyCustomGenerator::class, +``` + +### Order Status Notifications + +Configure mailers per status in `config/lunar/orders.php`: + +```php +'statuses' => [ + 'awaiting-payment' => [ + 'label' => 'Awaiting Payment', + 'color' => '#848a8c', + 'mailers' => [App\Mail\MyMailer::class], + 'notifications' => [], + ], +], +``` + +### Transactions + +```php +use Lunar\Models\Transaction; + +$order->transactions()->create([ + 'success' => true, + 'type' => 'capture', // capture, intent, refund + 'driver' => 'stripe', + 'amount' => 1999, + 'reference' => 'ch_123456', + 'status' => 'settled', + 'card_type' => 'visa', + 'last_four' => '4242', +]); +``` + +> See the [Order History guide](https://docs.lunarphp.com/1.x/guides/order-history.md) for building customer-facing order history pages. + +## References + +- [Carts Reference](https://docs.lunarphp.com/1.x/reference/carts.md) +- [Orders Reference](https://docs.lunarphp.com/1.x/reference/orders.md) +- [Addresses Reference](https://docs.lunarphp.com/1.x/reference/addresses.md) diff --git a/skills/lunarphp/references/extending.md b/skills/lunarphp/references/extending.md new file mode 100644 index 0000000000..afcb9bd0b9 --- /dev/null +++ b/skills/lunarphp/references/extending.md @@ -0,0 +1,83 @@ +# Extending Lunar + +## Dynamic Relationships (Recommended) + +```php +use Lunar\Models\Order; +use App\Models\Ticket; + +Order::resolveRelationUsing('ticket', function ($orderModel) { + return $orderModel->belongsTo(Ticket::class, 'ticket_id'); +}); +``` + +Register in a service provider's `boot` method. + +## Model Replacement + +For deeper customization (overriding methods, adding scopes): + +```php +use Lunar\Facades\ModelManifest; + +// Single model +ModelManifest::replace( + Lunar\Models\Contracts\Product::class, + App\Model\Product::class, +); + +// Scan directory +ModelManifest::addDirectory(__DIR__.'/../Models'); +``` + +Custom model must extend the Lunar model. Contract-based type hints work with route binding and relationships. + +## Extending Cart Calculations + +Define custom pipelines in `config/lunar/cart.php`: + +```php +'pipelines' => [ + App\Pipelines\Cart\CustomCartPipeline::class, +], +``` + +Override order creation action: + +```php +'actions' => [ + 'order_create' => App\Actions\Carts\CustomCreateOrder::class, +], +``` + +Add custom validators: + +```php +'validators' => [ + 'order_create' => [App\Validation\Cart\CustomValidator::class], +], +``` + +## Extending Other Systems + +- [Extending Carts](https://docs.lunarphp.com/1.x/extending/carts.md) +- [Extending Discounts](https://docs.lunarphp.com/1.x/extending/discounts.md) +- [Extending Models](https://docs.lunarphp.com/1.x/extending/models.md) +- [Extending Orders](https://docs.lunarphp.com/1.x/extending/orders.md) +- [Extending Payments](https://docs.lunarphp.com/1.x/extending/payments.md) +- [Extending Search](https://docs.lunarphp.com/1.x/extending/search.md) +- [Extending Shipping](https://docs.lunarphp.com/1.x/extending/shipping.md) +- [Extending Taxation](https://docs.lunarphp.com/1.x/extending/taxation.md) + +## Admin Panel Extension + +- [Extending the Admin Panel (Overview)](https://docs.lunarphp.com/1.x/admin/extending/overview.md) +- [Extending Pages](https://docs.lunarphp.com/1.x/admin/extending/pages.md) +- [Extending Resources](https://docs.lunarphp.com/1.x/admin/extending/resources.md) +- [Extending Relation Managers](https://docs.lunarphp.com/1.x/admin/extending/relation-managers.md) +- [Extending Order Management](https://docs.lunarphp.com/1.x/admin/extending/order-management.md) +- [Extending Attributes](https://docs.lunarphp.com/1.x/admin/extending/attributes.md) +- [Access Control](https://docs.lunarphp.com/1.x/admin/extending/access-control.md) +- [Configuration](https://docs.lunarphp.com/1.x/admin/extending/configuration.md) +- [Extending the Panel](https://docs.lunarphp.com/1.x/admin/extending/panel.md) +- [Developing Addons](https://docs.lunarphp.com/1.x/admin/extending/addons.md) diff --git a/skills/lunarphp/references/installation.md b/skills/lunarphp/references/installation.md new file mode 100644 index 0000000000..5d8c9679d9 --- /dev/null +++ b/skills/lunarphp/references/installation.md @@ -0,0 +1,52 @@ +# Installation + +> This reference covers post-install configuration and common setup tasks. +> For first-time installation, follow the [official installation guide](https://docs.lunarphp.com/1.x/getting-started/setup/installation.md). + +## Requirements + +- PHP >= 8.3 +- Laravel 12 or 13 +- MySQL 8.0+ / PostgreSQL 9.4+ +- `bcmath`, `exif`, `intl` PHP extensions + +## Post-Install Configuration + +Publish configs before installing to customize settings: + +```bash +php artisan vendor:publish --tag=lunar +``` + +Key config values to check: + +```php +// config/lunar/database.php +'table_prefix' => 'lunar_', +'users_id_type' => 'bigint', // or 'int', 'uuid' + +// config/lunar/cart_session.php +'session_key' => 'lunar_cart', +'auto_create' => false, + +// config/lunar/payments.php +'default' => env('PAYMENTS_TYPE', 'cash-in-hand'), +``` + +## Key Artisan Commands + +| Command | Purpose | +|---------|---------| +| `lunar:install` | Run the Lunar installer | +| `lunar:search:index` | Import/refresh search indexes | +| `lunar:meilisearch:setup` | Configure Meilisearch filterable/sortable fields | +| `lunar:update` | Handle Lunar updates and migrations | +| `vendor:publish --tag=lunar` | Publish Lunar configs | +| `vendor:publish --tag=lunarpanel.pdf` | Publish order PDF template | + +## Starter Kits + +For quick storefront setup, use one of the official starter kits: + +- [Livewire Starter Kit](https://docs.lunarphp.com/1.x/getting-started/starter-kits/livewire.md) +- [Inertia + Vue Starter Kit](https://docs.lunarphp.com/1.x/getting-starter-kits/inertia-vue.md) diff --git a/skills/lunarphp/references/payments-discounts.md b/skills/lunarphp/references/payments-discounts.md new file mode 100644 index 0000000000..c7b459f314 --- /dev/null +++ b/skills/lunarphp/references/payments-discounts.md @@ -0,0 +1,132 @@ +# Payments & Discounts + +## Payment Drivers + +Lunar uses a driver-based system. Built-in: `offline`. First-party: Stripe. + +```php +use Lunar\Facades\Payments; + +$payment = Payments::driver('card') + ->cart($cart) + ->withData(['payment_token' => $token]) + ->authorize(); + +$payment->success; // bool +$payment->message; // ?string +$payment->orderId; // ?int +``` + +Configuration in `config/lunar/payments.php`: + +```php +'types' => [ + 'cash-in-hand' => [ + 'driver' => 'offline', + 'authorized' => 'payment-offline', + ], + 'card' => [ + 'driver' => 'stripe', + 'authorized' => 'payment-received', + ], +], +``` + +### Payment Lifecycle + +1. Authorize: `Payments::driver('card')->cart($cart)->authorize()` +2. Capture: The driver typically handles this during authorization +3. Refund: Via the payment provider or manually in admin + +The `PaymentAttemptEvent` is dispatched on every payment attempt. + +> For a complete payment integration walkthrough with Stripe, see the [Payment Integration guide](https://docs.lunarphp.com/1.x/guides/payment-integration.md). + +## Discounts + +### Built-in Types + +1. **AmountOff** — Percentage or fixed amount off +2. **BuyXGetY** — Buy X, get Y free promotions + +### Creating a Discount + +```php +use Lunar\Models\Discount; + +$discount = Discount::create([ + 'name' => '20% Off', + 'handle' => '20_percent_off', + 'coupon' => '20OFF', + 'type' => 'Lunar\DiscountTypes\AmountOff', + 'data' => [ + 'fixed_value' => false, + 'percentage' => 20, + 'min_prices' => ['USD' => 2000], + ], + 'starts_at' => now(), + 'ends_at' => null, + 'max_uses' => null, + 'max_uses_per_user' => 1, + 'priority' => 1, + 'stop' => false, +]); +``` + +### Discountable Types + +| Type | Purpose | +|------|---------| +| `condition` | Product must be in cart for discount to activate | +| `exclusion` | Product is excluded from discount | +| `limitation` | Discount only applies to these products | +| `reward` | Reward products (for BuyXGetY) | + +```php +$discount->discountableConditions()->create([ + 'discountable_type' => 'product_variant', + 'discountable_id' => $variant->id, +]); +``` + +### Discount Statuses + +```php +$discount->status; // 'active', 'pending', 'expired', 'scheduled' +``` + +### Coupon Validation + +```php +use Lunar\Facades\Discounts; + +Discounts::validateCoupon('20OFF'); // bool +Discounts::resetDiscounts(); // Clear cached discounts +``` + +### Custom Discount Types + +```php +use Lunar\DiscountTypes\AbstractDiscountType; +use Lunar\Facades\Discounts; + +class CustomDiscount extends AbstractDiscountType +{ + public function getName(): string + { + return 'Custom Discount'; + } + + public function apply(Cart $cart): Cart + { + return $cart; + } +} + +Discounts::addType(CustomDiscount::class); +``` + +## References + +- [Payments Reference](https://docs.lunarphp.com/1.x/reference/payments.md) +- [Discounts Reference](https://docs.lunarphp.com/1.x/reference/discounts.md) diff --git a/skills/lunarphp/references/pitfalls.md b/skills/lunarphp/references/pitfalls.md new file mode 100644 index 0000000000..2d254f0946 --- /dev/null +++ b/skills/lunarphp/references/pitfalls.md @@ -0,0 +1,18 @@ +# Common Pitfalls + +- **`name` attribute required**: Internally expects `name` in product/collection attribute data. Missing it causes admin panel errors. +- **Prices stored as integers**: Always store prices in smallest currency unit (cents/pence). A $19.99 price is `1999`. +- **Cart prices are dynamic**: Not stored in DB until order is created. Always call `calculate()` or `recalculate()`. +- **`MissingCurrencyPriceException`**: Thrown when fetching price for a currency with no pricing defined for the variant. +- **Cart validation exceptions**: All throw `CartException`. Catch and display `$e->errors()` for detailed messages. +- **Single order per cart by default**: Pass `allowMultipleOrders: true` to `createOrder()` for split shipments. +- **`canCreateOrder()` before `createOrder()`**: Check readiness without throwing exceptions. +- **Fingerprint mismatch**: Use `checkFingerprint()` before payment to detect cart changes between checkout steps. +- **Model replacement conflicts**: Add-ons must never replace core models. Use dynamic relationships instead. +- **Scout `soft_delete`**: Must be `true` in `config/scout.php` or soft-deleted models appear in search results. +- **Engine map fallback**: Models not in `engine_map` use the default `SCOUT_DRIVER`. Configure per model to control indexing costs. +- **`scheduleCustomerGroup` / `scheduleChannel` with no dates**: Enables immediately. Pass `null` for `ends_at` to never expire. +- **Root collection required**: At least one root collection must exist before child collections can be created. +- **Tax zone clearance on address set**: Setting shipping address clears explicit `tax_zone_id`. Pass `clearTaxZone: false` to keep it. +- **BuyXGetY collection conditions**: Match against entire collections (added in 1.5) instead of listing individual products. +- **`resetDiscounts()` after coupon application**: Discounts are cached per request. Call this after applying a coupon to refresh. diff --git a/skills/lunarphp/references/products-catalog.md b/skills/lunarphp/references/products-catalog.md new file mode 100644 index 0000000000..318cc56b72 --- /dev/null +++ b/skills/lunarphp/references/products-catalog.md @@ -0,0 +1,249 @@ +# Products & Catalog + +## Products + +Products always have at least one variant. Custom data is stored as `attribute_data` (JSON) using field types: + +```php +use Lunar\Models\Product; +use Lunar\FieldTypes\Text; +use Lunar\FieldTypes\TranslatedText; + +Product::create([ + 'product_type_id' => $productType->id, + 'status' => 'published', + 'attribute_data' => [ + 'name' => new TranslatedText(collect([ + 'en' => new Text('FooBar'), + ])), + 'description' => new Text('Description text.'), + ], +]); +``` + +The `name` attribute is required — Lunar expects it internally. + +Key scopes: `status($status)`, `channel($channel)`, `customerGroup($group)`. + +## Variants + +Variants hold pricing, stock, dimensions, and product identifiers: + +```php +use Lunar\Models\ProductVariant; +use Lunar\Models\TaxClass; +use Lunar\Models\Currency; + +$variant = ProductVariant::create([ + 'product_id' => $product->id, + 'tax_class_id' => $taxClass->id, + 'sku' => 'BLUE-001', + 'unit_quantity' => 1, + 'min_quantity' => 1, + 'quantity_increment' => 1, + 'stock' => 100, + 'purchasable' => 'always', // or 'in_stock' + 'shippable' => true, +]); + +$variant->prices()->create([ + 'price' => 1999, // $19.99 in cents + 'currency_id' => $currency->id, +]); +``` + +Key fields: `sku`, `gtin`, `mpn`, `ean`, `unit_quantity`, `stock`, `backorder`, `purchasable`, dimensions (`length_value`, `width_value`, `height_value`, `weight_value`, `volume_value`). + +## Product Options + +Define variations like color/size: + +```php +use Lunar\Models\ProductOption; + +$color = ProductOption::create([ + 'name' => ['en' => 'Color'], + 'label' => ['en' => 'Color'], +]); + +$blue = $color->values()->create([ + 'name' => ['en' => 'Blue'], +]); + +$variant->values()->attach($blue); +``` + +Both `ProductOption` and `ProductOptionValue` have a `meta` JSON field for custom data (e.g. hex color values). + +## Product Types + +Product types determine which attributes are available for editing: + +```php +use Lunar\Models\ProductType; + +$productType = ProductType::create(['name' => 'Boots']); +$productType->mappedAttributes()->attach($attributeIds); +``` + +## Brands + +```php +use Lunar\Models\Brand; + +$brand = Brand::create(['name' => 'Nike']); +$product->update(['brand_id' => $brand->id]); +``` + +## Associations + +Link products as cross-sells, up-sells, or alternates: + +```php +$product->associations()->create([ + 'association_type' => 'cross-sell', // or 'up-sell', 'alternate' + 'target_id' => $targetProduct->id, +]); +``` + +## Pricing + +Prices are stored as integers (smallest currency unit). Use the `Pricing` facade for retrieval: + +```php +use Lunar\Facades\Pricing; + +$pricing = Pricing::qty(5) + ->customerGroup($group) + ->currency($currency) + ->for($variant) + ->get(); + +$pricing->matched; // The matched Price model +$pricing->base; // The base Price model +$pricing->priceBreaks; // Collection of quantity break prices +``` + +Or via the model: + +```php +$pricing = $variant->pricing()->qty(5)->get(); +``` + +### Price Data Type + +All monetary values return `Lunar\DataTypes\Price` objects with: + +```php +$price->value; // Raw integer (e.g. 1999) +$price->decimal(); // Float (e.g. 19.99) +$price->unitDecimal(); // Per unit (factors unit_quantity) +$price->formatted('en-gb'); // Formatted string (e.g. "£19.99") +$price->unitFormatted('en-gb'); // Per-unit formatted string +``` + +### Price Model + +```php +use Lunar\Models\Price; + +Price::create([ + 'price' => 1999, + 'compare_price' => 2999, + 'currency_id' => $currency->id, + 'min_quantity' => 1, + 'customer_group_id' => null, // null = all groups + 'priceable_type' => $variant->getMorphClass(), + 'priceable_id' => $variant->id, +]); +``` + +### Tax Helpers + +```php +$price->priceExTax(); +$price->priceIncTax(); +$price->comparePriceIncTax(); + +// With specific tax zone override (added in 1.5) +$price->priceIncTax($taxZone); +``` + +### Pricing Pipelines + +Configured in `config/lunar/pricing.php`: + +```php +'pipelines' => [ + App\Pipelines\Pricing\CustomPricingPipeline::class, +], +``` + +### Custom Formatter + +```php +use Lunar\Pricing\PriceFormatterInterface; + +class CustomFormatter implements PriceFormatterInterface +{ + public function __construct( + public int $value, + public ?Currency $currency = null, + public int $unitQty = 1 + ) {} + // Implement decimal(), unitDecimal(), formatted(), unitFormatted() +} +``` + +Register in `config/lunar/pricing.php`: + +```php +'formatter' => App\Pricing\CustomFormatter::class, +``` + +## Collections + +Collections use nested sets for hierarchy. Always create a root collection before children: + +```php +use Lunar\Models\Collection; +use Lunar\Models\CollectionGroup; + +$group = CollectionGroup::create([ + 'name' => 'Main Catalogue', + 'handle' => 'main-catalogue', +]); + +$parent = Collection::create([ + 'collection_group_id' => $group->id, + 'attribute_data' => ['name' => new \Lunar\FieldTypes\Text('Clothing')], +]); + +$child = Collection::create([ + 'collection_group_id' => $group->id, + 'attribute_data' => ['name' => new \Lunar\FieldTypes\Text('T-Shirts')], +]); + +$parent->appendNode($child); +``` + +Add products with sort position: + +```php +$collection->products()->sync([ + $productA->id => ['position' => 1], +]); +``` + +Sort options: `min_price:asc`, `min_price:desc`, `sku:asc`, `sku:desc`, `custom`. + +> For a complete walkthrough on building a catalog navigation menu, including active state tracking and caching, see the [Catalog Menu guide](https://docs.lunarphp.com/1.x/guides/catalog-menu.md). +> For product listing pages with filtering, sorting, and pagination, see the [Product Listing Page guide](https://docs.lunarphp.com/1.x/guides/product-listing-page.md). +> For product detail pages with variant selection and add-to-cart, see the [Product Display Page guide](https://docs.lunarphp.com/1.x/guides/product-display-page.md). + +## References + +- [Products Reference](https://docs.lunarphp.com/1.x/reference/products.md) +- [Collections Reference](https://docs.lunarphp.com/1.x/reference/collections.md) +- [Pricing Reference](https://docs.lunarphp.com/1.x/reference/pricing.md) +- [Associations Reference](https://docs.lunarphp.com/1.x/reference/associations.md) diff --git a/skills/lunarphp/references/search-attributes.md b/skills/lunarphp/references/search-attributes.md new file mode 100644 index 0000000000..a7d568a5db --- /dev/null +++ b/skills/lunarphp/references/search-attributes.md @@ -0,0 +1,126 @@ +# Search & Attributes + +## Search + +Lunar search is built on Laravel Scout with pre-configured indexers per model. + +### Configuration + +```php +// config/lunar/search.php +'models' => [ + Lunar\Models\Product::class, + Lunar\Models\Collection::class, + // ... +], +'engine_map' => [ + Lunar\Models\Product::class => 'typesense', + Lunar\Models\Order::class => 'meilisearch', +], +'indexers' => [ + Lunar\Models\Product::class => Lunar\Search\ProductIndexer::class, + // ... +], +``` + +Requires `soft_delete: true` in `config/scout.php`. + +### Indexing + +```bash +php artisan lunar:search:index +php artisan lunar:search:index "Lunar\Models\Product" --refresh +php artisan lunar:search:index "Lunar\Models\Order" --flush +``` + +For Meilisearch, run setup to configure filterable/sortable attributes: + +```bash +php artisan lunar:meilisearch:setup +``` + +### Storefront Search Add-on + +For faceted search with consistent API across engines: + +```bash +composer require lunarphp/search +``` + +```php +use Lunar\Search\Facades\Search; + +$results = Search::search('boots') + ->paginate(20) + ->get(); +``` + +> For a complete search walkthrough with faceted filtering, see the [Search & Product Discovery guide](https://docs.lunarphp.com/1.x/guides/search.md). + +## Attributes + +### Attribute System + +Custom data stored as JSON (`attribute_data`) with typed field values. + +### Field Types + +| Type | Description | +|------|-------------| +| `Lunar\FieldTypes\Text` | Plain or rich text | +| `Lunar\FieldTypes\TranslatedText` | Translatable text (one value per locale) | +| `Lunar\FieldTypes\Number` | Integer or decimal | +| `Lunar\FieldTypes\Toggle` | Boolean | +| `Lunar\FieldTypes\Dropdown` | Single select from options | +| `Lunar\FieldTypes\ListField` | Reorderable list of text values | +| `Lunar\FieldTypes\File` | File references | +| `Lunar\FieldTypes\YouTube` | YouTube video ID/URL | +| `Lunar\FieldTypes\Vimeo` | Vimeo video ID/URL | + +### Saving & Reading + +```php +use Lunar\FieldTypes\Text; +use Lunar\FieldTypes\TranslatedText; + +$product->attribute_data = collect([ + 'meta_title' => new Text('Best Screwdriver'), + 'description' => new TranslatedText(collect([ + 'en' => new Text('Great tool'), + 'fr' => new Text('Super outil'), + ])), +]); +$product->save(); + +// Read +$product->translateAttribute('name'); // Current locale +$product->translateAttribute('name', 'fr'); // Specific locale +$product->attr('name'); // Shorthand +``` + +### Custom Attributable Models + +```php +use Lunar\Base\Casts\AsAttributeData; +use Lunar\Base\Traits\HasAttributes; +use Lunar\Facades\AttributeManifest; + +class MyModel extends Model +{ + use HasAttributes; + + protected $casts = [ + 'attribute_data' => AsAttributeData::class, + ]; +} + +// Register in service provider +AttributeManifest::addType(MyModel::class); +``` + +## References + +- [Search Reference](https://docs.lunarphp.com/1.x/reference/search.md) +- [Attributes Reference](https://docs.lunarphp.com/1.x/reference/attributes.md) +- [Media Reference](https://docs.lunarphp.com/1.x/reference/media.md) +- [URLs Reference](https://docs.lunarphp.com/1.x/reference/urls.md) diff --git a/skills/lunarphp/references/system-settings.md b/skills/lunarphp/references/system-settings.md new file mode 100644 index 0000000000..f73eaa14bd --- /dev/null +++ b/skills/lunarphp/references/system-settings.md @@ -0,0 +1,69 @@ +# System Settings + +## Channels + +```php +use Lunar\Models\Channel; + +// Schedule product for a channel +$product->scheduleChannel($channel); +$product->scheduleChannel($channel, now()->addDays(7)); // With start date + +// Query +Product::channel($channel)->get(); +``` + +## Customer Groups + +```php +use Lunar\Models\CustomerGroup; + +$product->scheduleCustomerGroup($group); +$product->scheduleCustomerGroup($group, now()->addDays(7)); + +Product::customerGroup($group)->get(); +``` + +## Currencies + +```php +use Lunar\Models\Currency; + +Currency::default()->first(); // Get default currency + +// Sync pricing: Lunar auto-calculates prices from default currency +// using the exchange rate when enabled +``` + +## Tax Setup + +```php +use Lunar\Models\TaxZone; +use Lunar\Models\TaxClass; + +// Tax zone with country, states, or postcodes +$ukZone = TaxZone::create([ + 'name' => 'UK VAT', + 'zone_type' => 'country', + 'price_display' => 'tax_inclusive', // or 'tax_exclusive' +]); + +$rate = $ukZone->taxRates()->create(['name' => 'VAT', 'priority' => 1]); +$rate->taxRateAmounts()->create([ + 'tax_class_id' => TaxClass::default()->first()->id, + 'percentage' => 20.000, +]); +``` + +> For a detailed walkthrough on configuring system settings after installation, see the [System Settings guide](https://docs.lunarphp.com/1.x/getting-started/setup/system-settings.md). + +## References + +- [Channels Reference](https://docs.lunarphp.com/1.x/reference/channels.md) +- [Currencies Reference](https://docs.lunarphp.com/1.x/reference/currencies.md) +- [Languages Reference](https://docs.lunarphp.com/1.x/reference/languages.md) +- [Taxation Reference](https://docs.lunarphp.com/1.x/reference/taxation.md) +- [Countries & States Reference](https://docs.lunarphp.com/1.x/reference/countries-states.md) +- [Customers Reference](https://docs.lunarphp.com/1.x/reference/customers.md) +- [Tags Reference](https://docs.lunarphp.com/1.x/reference/tags.md) +- [Activity Log Reference](https://docs.lunarphp.com/1.x/reference/activity-log.md)