1194 lines
35 KiB
Markdown
1194 lines
35 KiB
Markdown
# 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
|
|
```
|