35 KiB
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
- Core Concepts
- Key Models and Relationships
- ModelManifest — Replacing Models
- Filament Panel Integration
- Extending Resources and Pages
- Activity Logging
- Pricing
- Tax
- Channels and Currencies
- Cart and Checkout
- Orders
- Customers and Addresses
- Discounts
- Products and Variants
- Collections and URLs
- Payments
- Shipping
- Search Indexing
- 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 inconfig/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\Priceobjects 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 aredatabase.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:
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
// 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:
\Lunar\Facades\ModelManifest::addDirectory(__DIR__.'/../Models');
Dynamic relationships (preferred for simple extensions)
For adding relationships without replacing models:
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
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:
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
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 — notgetDefaultLogExcept()on a subclass.
Extending Resources and Pages
ResourceExtension
Extend a Lunar resource's form, table, relations, pages, and sub-navigation:
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
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
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:
public function extendForm(Form $form): Form
{
$form->schema([
...$form->getComponents(true),
TextInput::make('my_field'),
]);
return $form;
}
Activity Logging
Lunar uses Spatie 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
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:
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
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:
$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.
$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
$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:
\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.
$product->scheduleChannel($channel);
$product->scheduleChannel($channel, now()->addDays(14), now()->addDays(28));
$products = Product::channel($channel)->get();
Currencies
$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:
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
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
$cart->add($variant, quantity: 2, meta: ['gift_wrap' => true]);
$cart->addLines([
['purchasable' => $variant, 'quantity' => 2],
]);
Updating and removing
$cart->updateLine($cartLineId, quantity: 3, meta: [...]);
$cart->remove($cartLineId);
$cart->clear();
Addresses
$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
$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:
class MyPipeline
{
public function handle(Cart $cart, Closure $next): mixed
{
// modify cart...
return $next($cart);
}
}
Cart auth policy (login/logout behavior)
// 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
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
$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
$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 productdigital— digital productshipping— 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:
'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
$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.
$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.
$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 offLunar\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.
$cart->update(['coupon_code' => '20OFF']);
$cart->recalculate();
Discounts::validateCoupon('20OFF'); // bool
Discounts::resetDiscounts(); // clear cached discounts for current request
Custom discount types
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 whenstock > 0
$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
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
$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.
$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)
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
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:
'types' => [
'card' => [
'driver' => 'stripe',
'released' => 'payment-received',
],
'cash-in-hand' => [
'driver' => 'offline',
'authorized' => 'payment-offline',
],
],
Custom payment driver
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
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
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:
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:
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
// config/lunar/search.php
'indexers' => [
Lunar\Models\Product::class => App\Search\CustomProductIndexer::class,
],
Custom indexer extends Lunar\Search\ScoutIndexer:
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):
// In a migration
\Spatie\Permission\Models\Permission::create(['name' => 'manage-contracts', 'guard_name' => 'staff']);
Two-factor auth
LunarPanel::forceTwoFactorAuth()->register(); // enforce 2FA for all staff
LunarPanel::disableTwoFactorAuth()->register(); // disable 2FA entirely (this project uses OTP instead)
Authorization checks
// 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
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