# Lunar Reference Lunar is a headless e-commerce package for Laravel (`lunarphp/core` + `lunarphp/lunar`). It handles products, carts, orders, payments, discounts, taxation, and more while leaving full control over the storefront UI. The admin panel is built on Filament. --- ## Table of Contents 1. [Core Concepts](#core-concepts) 2. [Key Models and Relationships](#key-models-and-relationships) 3. [ModelManifest — Replacing Models](#modelmanifest--replacing-models) 4. [Filament Panel Integration](#filament-panel-integration) 5. [Extending Resources and Pages](#extending-resources-and-pages) 6. [Activity Logging](#activity-logging) 7. [Pricing](#pricing) 8. [Tax](#tax) 9. [Channels and Currencies](#channels-and-currencies) 10. [Cart and Checkout](#cart-and-checkout) 11. [Orders](#orders) 12. [Customers and Addresses](#customers-and-addresses) 13. [Discounts](#discounts) 14. [Products and Variants](#products-and-variants) 15. [Collections and URLs](#collections-and-urls) 16. [Payments](#payments) 17. [Shipping](#shipping) 18. [Search Indexing](#search-indexing) 19. [Access Control](#access-control) --- ## Core Concepts - **Headless**: Lunar is a Laravel package, not a standalone app. It lives inside your Laravel application. - **No storefront UI prescribed**: The admin panel uses Filament/Livewire; the storefront can be Blade, Livewire, Inertia/Vue, or a pure API. - **Table prefix**: All Lunar database tables use the `lunar_` prefix by default (configurable in `config/lunar/database.php`). - **Prices stored as integers**: All monetary values are stored in the currency's smallest unit (e.g. cents). They are cast to `Lunar\DataTypes\Price` objects on retrieval. - **Polymorphic morph map**: Lunar uses string aliases (e.g. `product`, `customer`) for polymorphic relations — not fully-qualified class names. - **Config files**: Published to `config/lunar/` — key files are `database.php`, `cart.php`, `orders.php`, `taxes.php`, `payments.php`, `pricing.php`, `urls.php`, `media.php`. ### LunarUser trait Add to your `User` model to gain relationships to carts, orders, and customers: ```php use Lunar\Base\Traits\LunarUser; use Lunar\Base\LunarUser as LunarUserInterface; class User extends Authenticatable implements LunarUserInterface { use LunarUser; } ``` This adds: `customers` (BelongsToMany), `carts` (HasMany), `orders` (HasMany), and `latestCustomer()`. --- ## Key Models and Relationships ### Quick map | Model | Namespace | Notes | |---|---|---| | Product | `Lunar\Models\Product` | Has many variants; belongs to ProductType and optionally Brand | | ProductVariant | `Lunar\Models\ProductVariant` | The actual purchasable item; has prices, stock, SKU | | ProductType | `Lunar\Models\ProductType` | Defines which attributes a product has | | ProductOption | `Lunar\Models\ProductOption` | e.g. "Size", "Color" | | ProductOptionValue | `Lunar\Models\ProductOptionValue` | e.g. "Red", "Large" | | Collection | `Lunar\Models\Collection` | Nested-set tree; belongs to CollectionGroup | | CollectionGroup | `Lunar\Models\CollectionGroup` | Top-level grouping of collections | | Cart | `Lunar\Models\Cart` | Has many CartLines; belongs to Currency and Channel | | CartLine | `Lunar\Models\CartLine` | Polymorphic purchasable (usually ProductVariant) + quantity | | CartAddress | `Lunar\Models\CartAddress` | Shipping or billing address on a cart | | Order | `Lunar\Models\Order` | Created from a cart; has OrderLines and OrderAddresses | | OrderLine | `Lunar\Models\OrderLine` | Snapshot of a purchased item | | OrderAddress | `Lunar\Models\OrderAddress` | Immutable snapshot of shipping/billing address | | Transaction | `Lunar\Models\Transaction` | Payment record (intent, capture, refund) | | Customer | `Lunar\Models\Customer` | Buyer; linked to User via `customer_user` pivot | | Address | `Lunar\Models\Address` | Saved address belonging to a Customer | | Price | `Lunar\Models\Price` | Polymorphic; supports customer groups and quantity tiers | | Currency | `Lunar\Models\Currency` | ISO 4217 code; exchange rate relative to default | | Channel | `Lunar\Models\Channel` | Sales outlet; products are published per channel | | Language | `Lunar\Models\Language` | ISO 639-1 code; controls translated attribute data | | TaxClass | `Lunar\Models\TaxClass` | Assigned to variants; determines which rate applies | | TaxZone | `Lunar\Models\TaxZone` | Geographic region; `tax_inclusive` or `tax_exclusive` | | TaxRate | `Lunar\Models\TaxRate` | Belongs to TaxZone; has many TaxRateAmounts | | Discount | `Lunar\Models\Discount` | Coupon/promo; AmountOff or BuyXGetY | | Tag | `Lunar\Models\Tag` | Polymorphic tagging | | Url | `Lunar\Models\Url` | SEO slug; polymorphic; one default per language | | Brand | `Lunar\Models\Brand` | Optional brand for products | ### Product relationships ``` Product ├── productType → ProductType ├── brand → Brand (optional) ├── variants → ProductVariant[] │ └── prices → Price[] ├── collections ↔ Collection[] ├── productOptions ↔ ProductOption[] │ └── values → ProductOptionValue[] ├── associations → ProductAssociation[] ├── urls → Url[] ├── tags ↔ Tag[] └── media (Spatie MediaLibrary) ``` ### Cart relationships ``` Cart ├── lines → CartLine[] │ └── purchasable → ProductVariant (polymorphic) ├── shippingAddress → CartAddress ├── billingAddress → CartAddress ├── currency → Currency ├── user → User ├── customer → Customer └── orders → Order[] ``` ### Order relationships ``` Order ├── lines → OrderLine[] │ ├── productLines (excludes shipping type) │ └── shippingLines (type = 'shipping') ├── shippingAddress → OrderAddress ├── billingAddress → OrderAddress ├── transactions → Transaction[] │ ├── captures (type = 'capture') │ ├── intents (type = 'intent') │ └── refunds (type = 'refund') ├── customer → Customer ├── cart → Cart └── currency → Currency (matched on currency_code) ``` ### Customer relationships ``` Customer ├── users ↔ User[] (pivot: customer_user) ├── customerGroups ↔ CustomerGroup[] ├── addresses → Address[] └── orders → Order[] ``` --- ## ModelManifest — Replacing Models `ModelManifest` is an interface-to-implementation registry. Registering a replacement does three things: binds the contract in the container, registers route model binding, and updates the morph map. **Only models with contracts can be replaced.** Contracts live in `vendor/lunarphp/core/src/Models/Contracts/`. `Lunar\Admin\Models\Staff` has no contract and cannot be replaced this way. ### Replace a single model ```php // In AppServiceProvider::boot() \Lunar\Facades\ModelManifest::replace( \Lunar\Models\Contracts\Customer::class, \App\Models\Customer::class, ); ``` Your `App\Models\Customer` should extend `Lunar\Models\Customer`. ### Replace models from a directory Scans the directory and replaces every model that extends a Lunar base model: ```php \Lunar\Facades\ModelManifest::addDirectory(__DIR__.'/../Models'); ``` ### Dynamic relationships (preferred for simple extensions) For adding relationships without replacing models: ```php use Lunar\Models\Order; Order::resolveRelationUsing('ticket', function ($order) { return $order->belongsTo(Ticket::class, 'ticket_id'); }); ``` Register in a service provider `boot()`. ### What is replaceable Check `vendor/lunarphp/core/src/Models/Contracts/` for available contracts: `Customer`, `Order`, `Product`, `ProductVariant`, `ProductType`, `Cart`, `CartLine`, `Channel`, `Currency`, `Language`, `Collection`, `CollectionGroup`, `Price`, `Discount`, `Tag`, `Brand`, `TaxClass`, `TaxZone`, `TaxRate`, and others. --- ## Filament Panel Integration ### Registering the panel ```php use Lunar\Admin\Support\Facades\LunarPanel; class AppServiceProvider extends ServiceProvider { public function register(): void { LunarPanel::panel(function ($panel) { return $panel ->path('admin') // default: 'lunar' ->pages([SalesReport::class]) ->resources([BannerResource::class]) ->plugin(new ShippingPlugin()) ->navigationGroups(['Catalog', 'Sales', 'Settings']); }) ->extensions([ CustomerResource::class => CustomerResourceExtension::class, ]) ->disableTwoFactorAuth() ->register(); } } ``` `LunarPanel::register()` must be called in `register()`, not `boot()`. ### Registering extensions (resources and pages) Extensions are keyed by the Lunar resource/page class: ```php LunarPanel::extensions([ \Lunar\Admin\Filament\Resources\ProductResource::class => MyProductExtension::class, \Lunar\Admin\Filament\Resources\CustomerResource::class => MyCustomerExtension::class, \Lunar\Admin\Filament\Resources\OrderResource\Pages\ManageOrder::class => MyOrderExtension::class, ]); ``` ### All extendable resources ```php use Lunar\Admin\Filament\Resources\{ ActivityResource, AttributeGroupResource, BrandResource, ChannelResource, CollectionGroupResource, CollectionResource, CurrencyResource, CustomerGroupResource, CustomerResource, DiscountResource, LanguageResource, OrderResource, ProductOptionResource, ProductResource, ProductTypeResource, ProductVariantResource, StaffResource, TagResource, TaxClassResource, TaxRateResource, TaxZoneResource }; ``` ### Staff model wiring `Lunar\Admin\Models\Staff` is the Filament auth model. It does **not** go through ModelManifest. To extend it: - Override it by extending the class and pointing Filament's auth config at your subclass. - Listen on `Lunar\Admin\Models\Staff::created` (not your subclass) for events fired by Lunar internals. - Use `LunarStaff::addActivitylogExcept([...])` on the base class to exclude fields from the activity log — not `getDefaultLogExcept()` on a subclass. --- ## Extending Resources and Pages ### ResourceExtension Extend a Lunar resource's form, table, relations, pages, and sub-navigation: ```php use Filament\Forms\Form; use Filament\Tables\Table; use Lunar\Admin\Support\Extending\ResourceExtension; class MyProductResourceExtension extends ResourceExtension { public function extendForm(Form $form): Form { return $form->schema([ ...$form->getComponents(withHidden: true), // always spread existing TextInput::make('custom_field'), ]); } public function extendTable(Table $table): Table { return $table->columns([ ...$table->getColumns(), TextColumn::make('custom_field'), ]); } public function getRelations(array $managers): array { return [...$managers, MyRelationManager::class]; } public function extendPages(array $pages): array { return [...$pages, 'my-page' => MyPage::route('/{record}/my-page')]; } public function extendSubNavigation(array $nav): array { return [...$nav, MyPage::class]; } public function headerActions(array $actions): array { return [...$actions, Actions\Action::make('custom')]; } } ``` ### RelationManagerExtension ```php use Lunar\Admin\Support\Extending\RelationManagerExtension; class MyExtension extends RelationManagerExtension { public function extendForm(Form $form): Form { /* ... */ } public function extendTable(Table $table): Table { /* ... */ } public function headerActions(array $actions): array { /* ... */ } } ``` ### Page extensions | Class | Use case | |---|---| | `CreatePageExtension` | `heading()`, `beforeCreate()`, `afterCreation()`, `formActions()`, `headerActions()`, `headerWidgets()`, `footerWidgets()` | | `EditPageExtension` | `heading()`, `beforeFill()`, `beforeSave()`, `beforeUpdate()`, `afterUpdate()` | | `ListPageExtension` | `heading()`, `getTabs()`, `headerActions()`, `headerWidgets()`, `footerWidgets()` | | `ViewPageExtension` | `heading()`, `headerActions()`, `headerWidgets()`, `footerWidgets()` | | `RelationPageExtension` | `heading()`, `headerActions()`, `extendTable()` | ### Order management extension ```php LunarPanel::extensions([ \Lunar\Admin\Filament\Resources\OrderResource\Pages\ManageOrder::class => MyOrderExtension::class, ]); ``` Overridable methods include `extendInfolistSchema()`, `extendOrderSummaryInfolist()`, `extendOrderTotalsInfolist()`, `extendTransactionsInfolist()`, and many targeted `extend*Entry()` methods. ### Extending in addons (defensive pattern) When modifying forms in an addon, always spread existing components to avoid conflicts with other extensions: ```php public function extendForm(Form $form): Form { $form->schema([ ...$form->getComponents(true), TextInput::make('my_field'), ]); return $form; } ``` --- ## Activity Logging Lunar uses [Spatie laravel-activitylog](https://spatie.be/docs/laravel-activitylog) with the trait `Lunar\Base\Traits\LogsActivity`. **Models logged by default**: AttributeGroup, Brand, Cart, CartAddress, CartLine, Channel, CollectionGroup, Currency, Customer, CustomerGroup, Discount, Order, OrderAddress, OrderLine, Product, ProductOption, ProductType, ProductVariant, Tag, TaxClass, TaxRate, TaxZone, Transaction. All entries use the `lunar` log name. Only dirty attributes are recorded; `updated_at` is excluded. ### Adding logging to custom models ```php use Lunar\Base\Traits\LogsActivity; class MyModel extends Model { use LogsActivity; public static function getDefaultLogExcept(): array { return ['internal_notes']; } } ``` ### Excluding fields on Lunar's own Staff model Because Filament instantiates `Lunar\Admin\Models\Staff` directly (not a subclass), use this pattern in a service provider boot: ```php use Lunar\Admin\Models\Staff as LunarStaff; LunarStaff::addActivitylogExcept(['otp_code', 'otp_expires_at', 'password']); ``` --- ## Pricing Prices are stored as integers in the smallest currency unit (e.g. cents). The `Price` model is polymorphic — any model implementing `HasPrices` can have prices. ### Price model fields | Field | Description | |---|---| | `price` | Integer; smallest currency unit | | `compare_price` | Optional; used for showing crossed-out "was" prices | | `min_quantity` | Minimum quantity for this tier (default 1) | | `currency_id` | Currency FK | | `customer_group_id` | Nullable; for group-specific pricing | | `priceable_type/id` | Polymorphic parent (e.g. ProductVariant) | ### Pricing facade ```php use Lunar\Facades\Pricing; $response = Pricing::for($variant) ->currency($currency) ->customerGroups($customerGroups) ->qty(5) ->get(); $response->matched; // Lunar\Models\Price — best match $response->base; // base price (qty=1, no group) $response->priceBreaks; // Collection of tier prices $response->customerGroupPrices; // Collection of group prices ``` ### Price data type Every price attribute returns `Lunar\DataTypes\Price`: ```php $price->value; // raw integer $price->decimal(); // float $price->unitDecimal(); // float, divided by unit_quantity $price->formatted(); // "£10.00" (uses PHP NumberFormatter) $price->unitFormatted(); // formatted per unit ``` ### Tax helpers on Price model Config `lunar.pricing.stored_inclusive_of_tax` controls whether stored prices include tax. ```php $priceModel->priceExTax(); // Lunar\DataTypes\Price $priceModel->priceIncTax(); // Lunar\DataTypes\Price $priceModel->comparePriceIncTax(); // Lunar\DataTypes\Price // Override tax zone (useful before a cart exists) $priceModel->priceIncTax($taxZone); ``` ### Custom price formatter Set `config/lunar/pricing.php` `formatter` to your class implementing `PriceFormatterInterface`. --- ## Tax ### Structure ``` TaxZone (geographic region) └── TaxRate[] (e.g. "VAT", "State Tax") └── TaxRateAmount[] (percentage per TaxClass) ProductVariant → TaxClass ``` ### TaxZone fields | Field | Values | Notes | |---|---|---| | `zone_type` | `country`, `states`, `postcodes` | Determines scope | | `price_display` | `tax_inclusive`, `tax_exclusive` | Controls how prices display | | `default` | boolean | Fallback zone | ### TaxRate and TaxRateAmount ```php $taxZone->taxRates()->create(['name' => 'VAT', 'priority' => 1]); $taxRate->taxRateAmounts()->create([ 'tax_class_id' => $defaultTaxClass->id, 'percentage' => 20.000, ]); ``` ### Custom tax driver Implement `Lunar\Base\TaxDriver` and register: ```php \Lunar\Facades\Taxes::extend('taxjar', fn ($app) => $app->make(TaxJar::class)); ``` Then set `config/lunar/taxes.php` `driver` to `'taxjar'`. --- ## Channels and Currencies ### Channels Channels represent sales outlets (webstore, wholesale portal, etc.). Products and collections are published per channel with optional start/end dates. ```php $product->scheduleChannel($channel); $product->scheduleChannel($channel, now()->addDays(14), now()->addDays(28)); $products = Product::channel($channel)->get(); ``` ### Currencies ```php $currency->code; // e.g. 'GBP' $currency->exchange_rate; // relative to default currency $currency->decimal_places; // e.g. 2 $currency->default; // boolean ``` Exchange rates do not auto-convert prices unless `sync_prices` is enabled on the non-default currency. ### Storefront session Manages the active channel, currency, customer, and customer groups for the session: ```php use Lunar\Facades\StorefrontSession; $channel = StorefrontSession::getChannel(); $currency = StorefrontSession::getCurrency(); $customer = StorefrontSession::getCustomer(); $groups = StorefrontSession::getCustomerGroups(); StorefrontSession::setChannel($channel); StorefrontSession::setCurrency($currency); StorefrontSession::setCustomer($customer); // validates customer belongs to user ``` Customer resolution order: session → `$user->latestCustomer()`. --- ## Cart and Checkout ### CartSession facade ```php use Lunar\Facades\CartSession; $cart = CartSession::current(); // calculates totals; returns null if no cart $cart->recalculate(); // force recalculation CartSession::createOrder(); // creates order, removes cart from session CartSession::createOrder(forget: false); // keeps cart in session CartSession::forget(); // clear session (soft deletes cart by default) CartSession::forget(delete: false); // clear session, keep cart in DB ``` ### Adding items ```php $cart->add($variant, quantity: 2, meta: ['gift_wrap' => true]); $cart->addLines([ ['purchasable' => $variant, 'quantity' => 2], ]); ``` ### Updating and removing ```php $cart->updateLine($cartLineId, quantity: 3, meta: [...]); $cart->remove($cartLineId); $cart->clear(); ``` ### Addresses ```php $cart->setShippingAddress([ 'country_id' => $id, 'first_name' => '...', 'line_one' => '...', 'city' => '...', 'postcode' => '...', ]); $cart->setBillingAddress([/* same fields */]); ``` Accepts an array or any model implementing `Lunar\Base\Addressable` (e.g. `Lunar\Models\Address`). ### Cart total properties (after calculate/recalculate) | Property | Description | |---|---| | `total` | Grand total including tax | | `subTotal` | Sum of lines before tax and discounts | | `subTotalDiscounted` | Subtotal after discounts | | `taxTotal` | Total tax | | `discountTotal` | Total discounts | | `shippingSubTotal` | Shipping before tax | | `shippingTaxTotal` | Tax on shipping | | `shippingTotal` | Shipping including tax | | `taxBreakdown` | TaxBreakdown object; iterate `.amounts` | | `discountBreakdown` | Collection of discount breakdowns | | `shippingBreakdown` | Collection of shipping line items | ### CartLine properties (after calculate) `unitPrice`, `unitPriceInclTax`, `subTotal`, `subTotalDiscounted`, `discountTotal`, `taxAmount`, `total` ### Cart exceptions | Exception | Cause | |---|---| | `Lunar\Exceptions\Carts\CartException` | Validation failure; access errors via `$e->errors()` | | `InvalidCartLineQuantityException` | Quantity <= 0 | | `NonPurchasableItemException` | Model does not implement `Purchasable` | | `CartLineIdMismatchException` | Cart line does not belong to cart | | `DisallowMultipleCartOrdersException` | Cart already has a completed order | | `FingerprintMismatchException` | Cart contents changed since fingerprint was generated | ### Cart fingerprint ```php $fingerprint = $cart->fingerprint(); // ... pass to frontend hidden input ... $cart->checkFingerprint($request->fingerprint); // throws FingerprintMismatchException ``` ### Cart pipelines Configurable in `config/lunar/cart.php`: ``` CalculateLines → ApplyShipping → ApplyDiscounts → CalculateTax → Calculate ``` Cart line pipeline: `GetUnitPrice` Custom pipeline class: ```php class MyPipeline { public function handle(Cart $cart, Closure $next): mixed { // modify cart... return $next($cart); } } ``` ### Cart auth policy (login/logout behavior) ```php // config/lunar/cart.php 'auth_policy' => 'merge', // or 'override' ``` `merge` — guest cart items combine with user's existing cart on login. `override` — guest cart replaces user's cart. ### Shipping options ```php use Lunar\Facades\ShippingManifest; $options = ShippingManifest::getOptions($cart); // Collection of ShippingOption $option = ShippingManifest::getOption($cart, $identifier); $cart->setShippingOption($option); // triggers recalculate ``` `ShippingOption` properties: `name`, `description`, `identifier`, `price`, `collect` (bool). --- ## Orders ### Order creation from cart ```php $order = $cart->createOrder(); // or via CartSession (also handles session cleanup): $order = CartSession::createOrder(); ``` Validation requirements before `createOrder()`: - Billing address with `country_id`, `first_name`, `line_one`, `city`, `postcode` - Shipping address (same fields) if cart contains shippable items - A shipping option must be selected if cart is shippable ### Draft vs placed ```php $order->isDraft(); // placed_at is null $order->isPlaced(); // placed_at is not null ``` Only placed orders should be shown in the customer's order history. ### Order fields (key ones) | Field | Type | Description | |---|---|---| | `status` | string | e.g. `payment-received`, `dispatched` | | `reference` | string | Auto-generated; format configurable | | `placed_at` | dateTime\|null | null = draft | | `sub_total` | Price | Subtotal excluding tax | | `tax_total` | Price | Total tax | | `shipping_total` | Price | Shipping including tax | | `discount_total` | Price | Total discounts | | `total` | Price | Grand total | | `currency_code` | string | ISO currency at time of order | | `meta` | json | Custom metadata | ### Order line types - `physical` — physical product - `digital` — digital product - `shipping` — shipping charge Access via `$order->productLines` (physical + digital), `$order->shippingLines`. ### Order pipelines Configurable in `config/lunar/orders.php`: ``` FillOrderFromCart → CreateOrderLines → CreateOrderAddresses → CreateShippingLine → CleanUpOrderLines → MapDiscountBreakdown ``` ### Order reference format Default: zero-padded order ID (8 digits). Configurable in `config/lunar/orders.php`: ```php 'reference_format' => [ 'prefix' => null, 'padding_direction' => STR_PAD_LEFT, 'padding_character' => '0', 'length' => 8, ], ``` ### Transactions | Field | Description | |---|---| | `type` | `intent`, `capture`, or `refund` | | `success` | boolean | | `amount` | integer (smallest unit) | | `driver` | e.g. `stripe` | | `reference` | provider reference | | `card_type` | e.g. `visa` | | `last_four` | last 4 digits | | `parent_transaction_id` | links capture to intent, refund to capture | --- ## Customers and Addresses ### Customer fields | Field | Description | |---|---| | `first_name`, `last_name` | Name | | `company_name` | Optional | | `tax_identifier` | VAT / tax ID | | `account_ref` | External reference | | `attribute_data` | Custom attributes | | `meta` | Flexible JSON | Accessor: `$customer->full_name` → `"Mr. Tony Stark"` ### Linking users to customers ```php $customer = Customer::create([...]); $customer->users()->attach($user); // For B2B: multiple users per customer $customer->users()->sync([$userA->id, $userB->id]); ``` ### Customer groups Customer groups (`retail`, `wholesale`, etc.) control pricing, product visibility, and discount eligibility. Default group: `retail`. ```php $customer->customerGroups()->attach($group); // Filter products/collections by group Product::customerGroup($group)->get(); // Schedule product availability for a group $product->scheduleCustomerGroup($group, starts: now()->addDays(7)); $product->unscheduleCustomerGroup($group); ``` ### Saved addresses `Address` belongs to `Customer`. Fields: `first_name`, `last_name`, `company_name`, `tax_identifier`, `line_one` through `line_three`, `city`, `state`, `postcode`, `country_id`, `contact_email`, `contact_phone`, `delivery_instructions`, `shipping_default`, `billing_default`, `meta`. When `shipping_default` is set to true, Lunar automatically unsets the previous default via an observer. ```php $customer->addresses()->create([...]); $customer->addresses()->where('shipping_default', true)->first(); ``` --- ## Discounts ### Discount model fields | Field | Description | |---|---| | `name` | Display name | | `coupon` | Optional coupon code | | `type` | Fully-qualified class name of the discount type | | `starts_at` / `ends_at` | Active window | | `max_uses` | Global usage limit | | `max_uses_per_user` | Per-user limit | | `priority` | Processing order | | `stop` | Stop processing further discounts if applied | | `data` | Type-specific config (JSON) | ### Built-in discount types - `Lunar\DiscountTypes\AmountOff` — percentage or fixed amount off - `Lunar\DiscountTypes\BuyXGetY` — buy X items, get Y free/discounted ### Discount status `$discount->status` returns: `active`, `pending`, `expired`, or `scheduled`. ### Applying discounts Discounts are applied automatically during cart calculation when `coupon_code` is set on the cart, or based on cart contents for automatic discounts. ```php $cart->update(['coupon_code' => '20OFF']); $cart->recalculate(); Discounts::validateCoupon('20OFF'); // bool Discounts::resetDiscounts(); // clear cached discounts for current request ``` ### Custom discount types ```php use Lunar\Facades\Discounts; Discounts::addType(MyCustomDiscountType::class); ``` Implement `Lunar\DiscountTypes\AbstractDiscountType`. For admin panel support, also implement `Lunar\Admin\Base\LunarPanelDiscountInterface` and provide `lunarPanelSchema()`, `lunarPanelOnFill()`, `lunarPanelOnSave()`. --- ## Products and Variants ### ProductVariant fields (key) | Field | Description | |---|---| | `sku` | Stock keeping unit | | `stock` | Stock quantity | | `purchasable` | `always` or `in_stock` | | `unit_quantity` | Units per price point (default 1) | | `weight_value`, `weight_unit` | For shipping weight calculations | | `tax_class_id` | Determines applicable tax rate | ### Purchasable values - `always` — can always be purchased (backorder) - `in_stock` — only when `stock > 0` ```php $variant->canBeFulfilledAtQuantity(3); // bool ``` ### Attributes All custom product/collection/brand attributes are stored in `attribute_data` as JSON. Access via `$model->attr('name')` which resolves translations based on current locale. Attribute field types: `Text`, `TranslatedText`, `Number`, `Toggle`, `Dropdown`, `ListField`, `File`, `YouTube`, `Vimeo`. ### Product associations ```php use Lunar\Base\Enums\ProductAssociation; $product->associate($target, ProductAssociation::CROSS_SELL); $product->associate($target, ProductAssociation::UP_SELL); $product->associate($target, ProductAssociation::ALTERNATE); $product->associate([$a, $b], ProductAssociation::CROSS_SELL); $product->associations()->crossSell()->with('target')->get(); $product->dissociate($target); ``` ### Pricing facade for products ```php $pricing = Pricing::for($variant) ->currency(StorefrontSession::getCurrency()) ->customerGroups(StorefrontSession::getCustomerGroups()) ->get(); $pricing->matched; // best price for this context $pricing->priceBreaks; // quantity tiers ``` --- ## Collections and URLs ### Collection hierarchy Collections use a nested set (via `HasNestedSets`). Each collection belongs to a `CollectionGroup`. ```php $rootCollections = Collection::where('collection_group_id', $group->id) ->whereIsRoot() ->defaultOrder() ->with(['defaultUrl', 'children' => fn ($q) => $q->defaultOrder(), 'children.defaultUrl']) ->get(); // Full tree $tree = Collection::where('collection_group_id', $group->id) ->defaultOrder() ->with('defaultUrl') ->get() ->toTree(); ``` ### URLs (slugs) ```php use Lunar\Models\Url; // Resolve product from slug $url = Url::where('slug', $slug) ->where('element_type', (new Product)->getMorphClass()) ->firstOrFail(); $product = Product::find($url->element_id); // Create a URL $product->urls()->create([ 'slug' => 'my-product', 'language_id' => $language->id, 'default' => true, ]); // Access URLs $product->defaultUrl; // default URL for current locale $product->localeUrl; // URL for current app locale $product->urls; // all URLs ``` Auto-generation from `attr('name')` is enabled by default via `Lunar\Generators\UrlGenerator`. Configure in `config/lunar/urls.php`. Add URL support to custom models with the `HasUrls` trait. --- ## Payments ### Payments facade ```php use Lunar\Facades\Payments; // Authorize $response = Payments::driver('card') ->cart($cart) ->withData(['payment_intent' => $intentId]) ->authorize(); $response->success; // bool $response->orderId; // int|null $response->message; // string|null $response->paymentType; // string|null // Capture (manual capture policy) Payments::driver('card')->order($order)->capture($transaction, $amount); // Refund Payments::driver('card')->order($order)->refund($transaction, $amount, 'notes'); ``` ### Payment types configuration In `config/lunar/payments.php`: ```php 'types' => [ 'card' => [ 'driver' => 'stripe', 'released' => 'payment-received', ], 'cash-in-hand' => [ 'driver' => 'offline', 'authorized' => 'payment-offline', ], ], ``` ### Custom payment driver ```php use Lunar\Facades\Payments; Payments::extend('custom', fn ($app) => $app->make(CustomPayment::class)); ``` Extend `Lunar\PaymentTypes\AbstractPayment`. Implement `authorize()`, `capture()`, `refund()`. Transaction types: `intent` (payment reserved, not yet captured), `capture` (payment captured), `refund`. ### Stripe integration ```php use Lunar\Stripe\Facades\Stripe; $intent = Stripe::fetchOrCreateIntent($cart); // create/get PaymentIntent Stripe::syncIntent($cart); // update amount after cart changes Stripe::cancelIntent($cart, CancellationReason::ABANDONED); $intentId = Stripe::getCartIntentId($cart); // get existing intent ID ``` Webhook endpoint auto-registered at `stripe/webhook` (configurable). Verify with `LUNAR_STRIPE_WEBHOOK_SECRET`. --- ## Shipping ### Adding a custom shipping modifier ```php namespace App\Modifiers; use Lunar\Base\ShippingModifier; use Lunar\DataTypes\Price; use Lunar\DataTypes\ShippingOption; use Lunar\Facades\ShippingManifest; class CustomShippingModifier extends ShippingModifier { public function handle(Cart $cart, \Closure $next) { ShippingManifest::addOption( new ShippingOption( name: 'Standard Delivery', description: 'Delivered in 3-5 days', identifier: 'STANDARD', price: new Price(500, $cart->currency, 1), taxClass: TaxClass::first(), ) ); return $next($cart); } } ``` Register in a service provider: ```php public function boot(\Lunar\Base\ShippingModifiers $shippingModifiers): void { $shippingModifiers->add(CustomShippingModifier::class); } ``` ### Table Rate Shipping add-on Install `lunarphp/table-rate-shipping`. Register the Filament plugin: ```php LunarPanel::panel(fn ($panel) => $panel->plugin(new ShippingPlugin()))->register(); ``` Concepts: **ShippingZone** (geographic region), **ShippingMethod** (delivery type), **ShippingRate** (zone + method + prices), **ShippingExclusionList** (products blocked from a zone). Drivers: `ship-by` (cart total or weight tiers), `flat-rate`, `free-shipping` (minimum spend), `collection` (in-store pickup). --- ## Search Indexing Lunar uses Laravel Scout for indexing. Models with the `Searchable` trait use indexers configured in `config/lunar/search.php`. ### ProductIndexer fields `id`, searchable attributes, `status`, `product_type`, `brand`, variant `skus`, `created_at`. ### Custom indexer ```php // config/lunar/search.php 'indexers' => [ Lunar\Models\Product::class => App\Search\CustomProductIndexer::class, ], ``` Custom indexer extends `Lunar\Search\ScoutIndexer`: ```php class CustomProductIndexer extends ScoutIndexer { public function toSearchableArray(Model $model): array { return array_merge([], $this->mapSearchableAttributes($model)); } public function getSortableFields(): array { return ['created_at']; } public function getFilterableFields(): array { return ['status']; } } ``` `mapSearchableAttributes()` automatically includes all attributes flagged as searchable in the admin panel. --- ## Access Control Lunar uses `spatie/laravel-permission` scoped to the `staff` guard. Built-in roles: `admin`, `staff`. ### Adding permissions Create via migration (not through the UI — authorization logic must accompany every permission): ```php // In a migration \Spatie\Permission\Models\Permission::create(['name' => 'manage-contracts', 'guard_name' => 'staff']); ``` ### Two-factor auth ```php LunarPanel::forceTwoFactorAuth()->register(); // enforce 2FA for all staff LunarPanel::disableTwoFactorAuth()->register(); // disable 2FA entirely (this project uses OTP instead) ``` ### Authorization checks ```php // Route middleware Route::get('/custom', Controller::class)->middleware('can:manage-contracts'); // In code Auth::user()->can('manage-contracts'); ``` --- ## Useful Facades Summary | Facade | Purpose | |---|---| | `Lunar\Facades\CartSession` | Manage the active cart session | | `Lunar\Facades\Payments` | Route payment calls to drivers | | `Lunar\Facades\Pricing` | Resolve the best price for a purchasable | | `Lunar\Facades\ShippingManifest` | Get/set shipping options on a cart | | `Lunar\Facades\Discounts` | Validate coupons, register custom types | | `Lunar\Facades\Taxes` | Register custom tax drivers | | `Lunar\Facades\ModelManifest` | Replace Lunar models with custom implementations | | `Lunar\Facades\StorefrontSession` | Manage channel, currency, customer for session | | `Lunar\Admin\Support\Facades\LunarPanel` | Configure and extend the Filament panel | --- ## Artisan Commands ```bash php artisan lunar:install # first-time setup php artisan vendor:publish --tag=lunar # publish all config php artisan vendor:publish --tag=lunar.migrations # publish migrations php artisan scout:import "Lunar\Models\Product" # index products php artisan scout:flush "Lunar\Models\Order" # flush order index ```