# Lunar - Laravel E-Commerce Platform
Lunar is a comprehensive set of Laravel packages that provide full e-commerce functionality similar to Shopify. It offers a modular architecture with packages for core commerce functionality, admin panel (built on Filament), payment integrations (Stripe, PayPal, Opayo), search capabilities, and table-rate shipping. The platform handles products, variants, pricing, carts, orders, customers, discounts, taxes, and shipping while giving developers complete freedom to build custom storefronts.
The monorepo structure includes the core package (models, actions, facades), admin panel, search integration (Meilisearch, Typesense), and payment gateways. Lunar uses Laravel's service provider pattern for extensibility, allowing customization of pricing pipelines, cart calculations, tax handling, and shipping options. The admin panel leverages Filament for a modern, extensible back-office interface with full product, order, and customer management.
## Core Models
### Product Model
The Product model represents items for sale with support for variants, pricing, media, collections, brands, and attributes.
```php
use Lunar\Models\Product;
use Lunar\Models\ProductType;
use Lunar\Models\Brand;
// Create a product
$product = Product::create([
'product_type_id' => ProductType::first()->id,
'status' => 'published',
'brand_id' => Brand::first()->id,
'attribute_data' => [
'name' => new \Lunar\FieldTypes\TranslatedText([
'en' => 'Blue T-Shirt',
'fr' => 'T-Shirt Bleu'
]),
'description' => new \Lunar\FieldTypes\TranslatedText([
'en' => 'A comfortable cotton t-shirt'
])
]
]);
// Query products with relationships
$products = Product::with(['variants', 'brand', 'collections', 'images'])
->status('published')
->get();
// Get product prices through variants
$prices = $product->prices;
// Associate products
$product->associate($relatedProduct, 'cross-sell');
$product->dissociate($relatedProduct, 'cross-sell');
// Get translated attribute
$name = $product->translateAttribute('name'); // Returns "Blue T-Shirt" for English locale
```
### ProductVariant Model
Product variants represent purchasable items with SKU, pricing, inventory, and option values.
```php
use Lunar\Models\ProductVariant;
use Lunar\Models\Price;
use Lunar\Models\Currency;
// Create a variant with pricing
$variant = ProductVariant::create([
'product_id' => $product->id,
'tax_class_id' => $taxClass->id,
'sku' => 'TSHIRT-BLUE-L',
'stock' => 100,
'backorder' => 50,
'purchasable' => 'in_stock_or_backorder', // 'always', 'in_stock', 'in_stock_or_backorder'
'shippable' => true,
'unit_quantity' => 1,
'min_quantity' => 1,
'quantity_increment' => 1,
'weight_value' => 200,
'weight_unit' => 'g'
]);
// Add pricing
Price::create([
'priceable_type' => $variant->getMorphClass(),
'priceable_id' => $variant->id,
'currency_id' => Currency::getDefault()->id,
'price' => 2999, // Stored in smallest unit (cents)
'min_quantity' => 1
]);
// Check inventory
$canFulfill = $variant->canBeFulfilledAtQuantity(5); // Returns true/false
$totalInventory = $variant->getTotalInventory(); // stock + backorder
```
### Cart Model
The Cart model handles shopping cart functionality with line items, addresses, shipping, and order creation.
```php
use Lunar\Models\Cart;
use Lunar\Models\Currency;
use Lunar\Models\Channel;
use Lunar\Facades\CartSession;
// Create a cart
$cart = Cart::create([
'currency_id' => Currency::getDefault()->id,
'channel_id' => Channel::getDefault()->id,
]);
// Add items to cart
$cart->add($productVariant, quantity: 2, meta: ['gift_wrap' => true]);
// Add multiple items at once
$cart->addLines([
['purchasable' => $variant1, 'quantity' => 1],
['purchasable' => $variant2, 'quantity' => 3, 'meta' => ['color' => 'blue']]
]);
// Update cart line quantity
$cart->updateLine(cartLineId: $lineId, quantity: 5);
// Remove item from cart
$cart->remove($lineId);
// Clear entire cart
$cart->clear();
// Calculate totals (automatically cached)
$cart = $cart->calculate();
echo $cart->subTotal->formatted(); // "£59.98"
echo $cart->taxTotal->formatted(); // "£11.99"
echo $cart->discountTotal->formatted(); // "£5.00"
echo $cart->total->formatted(); // "£66.97"
// Add addresses
$cart->setShippingAddress([
'first_name' => 'John',
'last_name' => 'Doe',
'line_one' => '123 Main St',
'city' => 'London',
'postcode' => 'SW1A 1AA',
'country_id' => $country->id
]);
$cart->setBillingAddress($addressArray);
// Set shipping option
$shippingOption = $cart->getEstimatedShipping(['postcode' => 'SW1A 1AA']);
$cart->setShippingOption($shippingOption);
// Associate user
$cart->associate($user, policy: 'merge'); // 'merge' or 'override'
// Create order from cart
$order = $cart->createOrder();
```
### Order Model
The Order model represents completed purchases with line items, transactions, and status management.
```php
use Lunar\Models\Order;
// Orders are typically created from carts
$order = $cart->createOrder();
// Query orders
$orders = Order::with(['lines', 'transactions', 'customer', 'addresses'])
->whereNotNull('placed_at')
->orderBy('created_at', 'desc')
->get();
// Access order data
echo $order->reference; // "ORD-2024-0001"
echo $order->status; // "pending"
echo $order->statusLabel; // "Pending Payment"
echo $order->total->formatted(); // "£129.99"
// Order relationships
$shippingAddress = $order->shippingAddress;
$billingAddress = $order->billingAddress;
$productLines = $order->productLines;
$shippingLines = $order->shippingLines;
$captures = $order->captures;
$refunds = $order->refunds;
// Check order state
$isDraft = $order->isDraft(); // No placed_at date
$isPlaced = $order->isPlaced(); // Has placed_at date
```
### Customer Model
The Customer model manages customer data with addresses, orders, and customer groups.
```php
use Lunar\Models\Customer;
use Lunar\Models\CustomerGroup;
// Create customer
$customer = Customer::create([
'first_name' => 'John',
'last_name' => 'Doe',
'company_name' => 'Acme Inc',
'tax_identifier' => 'GB123456789',
'attribute_data' => [
'loyalty_points' => new \Lunar\FieldTypes\Number(500)
]
]);
// Associate user with customer
$customer->users()->attach($user);
// Add to customer groups
$customer->customerGroups()->attach(CustomerGroup::where('handle', 'wholesale')->first());
// Add addresses
$customer->addresses()->create([
'first_name' => 'John',
'last_name' => 'Doe',
'line_one' => '123 Main St',
'city' => 'London',
'postcode' => 'SW1A 1AA',
'country_id' => $country->id,
'shipping_default' => true,
'billing_default' => true
]);
// Get customer orders
$orders = $customer->orders()->with('lines')->get();
```
### Discount Model
The Discount model handles promotional pricing, coupons, and discount rules.
```php
use Lunar\Models\Discount;
use Lunar\Facades\Discounts;
// Create a percentage discount
$discount = Discount::create([
'name' => 'Summer Sale',
'handle' => 'summer-sale',
'coupon' => 'SUMMER20',
'type' => \Lunar\DiscountTypes\AmountOff::class,
'starts_at' => now(),
'ends_at' => now()->addMonth(),
'max_uses' => 1000,
'priority' => 1,
'stop' => false, // Continue applying other discounts
'data' => [
'fixed_value' => false,
'percentage' => 20
]
]);
// Associate with customer groups, collections, products
$discount->customerGroups()->attach($customerGroup, [
'enabled' => true,
'visible' => true
]);
$discount->collections()->attach($collection, ['type' => 'condition']);
$discount->brands()->attach($brand, ['type' => 'limitation']);
// Query active discounts
$activeDiscounts = Discount::active()->usable()->get();
// Apply discount to cart (happens automatically in cart calculation)
$cart->coupon_code = 'SUMMER20';
$cart = $cart->calculate();
// Validate coupon
$isValid = Discounts::validateCoupon('SUMMER20');
```
### Collection Model
Collections organize products into hierarchical groups with nested set support.
```php
use Lunar\Models\Collection;
use Lunar\Models\CollectionGroup;
// Create a collection group
$group = CollectionGroup::create(['name' => 'Categories', 'handle' => 'categories']);
// Create root collection
$root = Collection::create([
'collection_group_id' => $group->id,
'attribute_data' => [
'name' => new \Lunar\FieldTypes\TranslatedText(['en' => 'Clothing'])
]
]);
// Create child collection
$child = Collection::create([
'collection_group_id' => $group->id,
'attribute_data' => [
'name' => new \Lunar\FieldTypes\TranslatedText(['en' => 'T-Shirts'])
]
]);
$child->appendToNode($root)->save();
// Query nested collections
$ancestors = $collection->ancestors;
$descendants = $collection->descendants;
$breadcrumb = $collection->breadcrumb; // ['Clothing', 'T-Shirts']
// Attach products to collection
$collection->products()->attach($product, ['position' => 1]);
// Get products in collection
$products = $collection->products()->with('variants')->get();
```
## Facades
### CartSession Facade
Manages cart session state across requests with user association and shipping estimation.
```php
use Lunar\Facades\CartSession;
// Get current cart (creates one if none exists)
$cart = CartSession::current();
// Use a specific cart
CartSession::use($existingCart);
// Associate cart with authenticated user
CartSession::associate($cart, $user, policy: 'merge');
// Set channel and currency
CartSession::setChannel($channel);
CartSession::setCurrency($currency);
// Get shipping options for current cart
$shippingOptions = CartSession::getShippingOptions();
// Estimate shipping before address is set
CartSession::estimateShippingUsing(['country' => 'GB', 'postcode' => 'SW1A']);
$cart = CartSession::current(estimateShipping: true);
// Create order and forget cart
$order = CartSession::createOrder(forget: true);
// Clear cart from session
CartSession::forget(delete: true);
```
### Pricing Facade
Calculates product pricing based on customer groups, currencies, and quantities.
```php
use Lunar\Facades\Pricing;
use Lunar\Models\CustomerGroup;
use Lunar\Models\Currency;
// Get pricing for a purchasable
$pricing = Pricing::for($productVariant)->get();
echo $pricing->base->price->formatted(); // Base price: "£29.99"
echo $pricing->matched->price->formatted(); // Best matching price: "£24.99"
echo $pricing->priceBreaks; // Collection of price breaks
// Get pricing for specific customer group
$pricing = Pricing::for($variant)
->customerGroup(CustomerGroup::where('handle', 'wholesale')->first())
->get();
// Get pricing for specific quantity
$pricing = Pricing::for($variant)
->qty(100) // Bulk quantity
->get();
// Get pricing for specific currency
$pricing = Pricing::for($variant)
->currency(Currency::where('code', 'EUR')->first())
->get();
// Combine all options
$pricing = Pricing::for($variant)
->user($authenticatedUser)
->currency($currency)
->customerGroups($customerGroups)
->qty(50)
->get();
```
### Discounts Facade
Manages discount application, validation, and type registration.
```php
use Lunar\Facades\Discounts;
use Lunar\Models\Cart;
// Apply discounts to a cart
$cart = Discounts::apply($cart);
// Get available discounts for cart
$discounts = Discounts::getDiscounts($cart);
// Validate a coupon code
$isValid = Discounts::validateCoupon('SAVE10');
// Filter discounts by channel and customer group
$discounts = Discounts::channel($channel)
->customerGroup($customerGroup)
->getDiscounts();
// Register custom discount type
Discounts::addType(\App\DiscountTypes\BuyXGetY::class);
// Get registered discount types
$types = Discounts::getTypes();
// Get applied discounts after cart calculation
$appliedDiscounts = Discounts::getApplied();
```
### ShippingManifest Facade
Manages shipping options and retrieval for cart shipping calculations.
```php
use Lunar\Facades\ShippingManifest;
use Lunar\DataTypes\ShippingOption;
use Lunar\DataTypes\Price;
use Lunar\Models\TaxClass;
// Add a shipping option
ShippingManifest::addOption(new ShippingOption(
name: 'Standard Delivery',
description: '3-5 business days',
identifier: 'standard',
price: new Price(499, Currency::getDefault(), 1),
taxClass: TaxClass::getDefault(),
collect: false,
meta: ['carrier' => 'Royal Mail']
));
// Add multiple options
ShippingManifest::addOptions(collect([
new ShippingOption(
name: 'Express Delivery',
description: 'Next business day',
identifier: 'express',
price: new Price(999, Currency::getDefault(), 1),
taxClass: TaxClass::getDefault()
),
new ShippingOption(
name: 'Click & Collect',
description: 'Collect from store',
identifier: 'collect',
price: new Price(0, Currency::getDefault(), 1),
taxClass: TaxClass::getDefault(),
collect: true
)
]));
// Get available options for cart
$options = ShippingManifest::getOptions($cart);
// Get specific option
$option = ShippingManifest::getOption($cart, 'express');
// Get currently selected shipping option
$selected = ShippingManifest::getShippingOption($cart);
// Clear all options
ShippingManifest::clearOptions();
```
### Taxes Facade
Manages tax calculation drivers and tax zone resolution.
```php
use Lunar\Facades\Taxes;
use Lunar\Models\TaxClass;
use Lunar\Models\TaxZone;
use Lunar\Models\TaxRate;
// Get the current tax driver
$taxDriver = Taxes::driver();
// Register a custom tax driver
Taxes::extend('custom', function ($app) {
return new \App\TaxDrivers\CustomTaxDriver();
});
// Use specific driver
$taxDriver = Taxes::driver('custom');
// Configure tax zones
$taxZone = TaxZone::create([
'name' => 'UK',
'zone_type' => 'country',
'price_display' => 'tax_inclusive',
'default' => true
]);
// Add countries to zone
$taxZone->countries()->create(['country_id' => $ukCountry->id]);
// Create tax class and rates
$taxClass = TaxClass::create(['name' => 'Standard Rate', 'default' => true]);
$taxRate = TaxRate::create(['tax_zone_id' => $taxZone->id, 'name' => 'VAT']);
$taxRate->amounts()->create([
'tax_class_id' => $taxClass->id,
'percentage' => 20.00
]);
```
## Admin Panel
### Setting Up Admin Panel
Configure and extend the Lunar admin panel built on Filament.
```php
// config/lunar/panel.php or AppServiceProvider
use Lunar\Admin\Support\Facades\LunarPanel;
use Lunar\Admin\Filament\Resources\ProductResource;
// Register the panel in a service provider
public function boot()
{
// Extend product resource with custom pages
ProductResource::extendDefaultPages(function (array $pages) {
return array_merge($pages, [
'custom' => \App\Filament\Pages\CustomProductPage::route('/{record}/custom'),
]);
});
// Add custom navigation items
LunarPanel::registerNavigationItems([
\Filament\Navigation\NavigationItem::make('Reports')
->url('/admin/reports')
->icon('heroicon-o-chart-bar')
]);
}
// Create admin staff user
use Lunar\Admin\Models\Staff;
$staff = Staff::create([
'first_name' => 'Admin',
'last_name' => 'User',
'email' => 'admin@example.com',
'password' => bcrypt('password'),
'admin' => true // Super admin access
]);
// Assign roles and permissions
$staff->assignRole('admin');
$staff->givePermissionTo('catalog:manage-products');
```
### Custom Filament Resources
Extend or create custom admin resources for the Lunar panel.
```php
// app/Filament/Resources/CustomProductResource.php
namespace App\Filament\Resources;
use Lunar\Admin\Filament\Resources\ProductResource;
use Filament\Tables\Table;
use Filament\Schemas\Schema;
class CustomProductResource extends ProductResource
{
// Add custom table columns
public static function getTableColumns(): array
{
return array_merge(parent::getTableColumns(), [
\Filament\Tables\Columns\TextColumn::make('custom_field')
->label('Custom Field')
->sortable()
]);
}
// Extend the form
public static function getDefaultForm(Schema $schema): Schema
{
return parent::getDefaultForm($schema)->components([
...parent::getMainFormComponents(),
\Filament\Forms\Components\TextInput::make('custom_field')
->label('Custom Field')
]);
}
}
```
## Stripe Payment Integration
### Stripe Setup and Payment Intent
Configure Stripe and create payment intents for cart checkout.
```php
// config/services.php
'stripe' => [
'key' => env('STRIPE_SECRET'),
'public_key' => env('STRIPE_PUBLIC_KEY'),
],
// config/lunar/stripe.php
return [
'policy' => 'automatic', // 'automatic' or 'manual' capture
];
// Usage in controller/Livewire component
use Lunar\Stripe\Facades\Stripe;
use Lunar\Facades\CartSession;
class CheckoutController
{
public function getPaymentIntent()
{
$cart = CartSession::current()->calculate();
// Create or fetch existing payment intent
$paymentIntent = Stripe::fetchOrCreateIntent($cart, [
'metadata' => [
'cart_id' => $cart->id,
'customer_email' => $cart->billingAddress->contact_email
]
]);
return response()->json([
'clientSecret' => $paymentIntent->client_secret,
'amount' => $cart->total->value,
'currency' => $cart->currency->code
]);
}
public function updateShippingOnIntent()
{
$cart = CartSession::current();
Stripe::updateShippingAddress($cart);
}
public function syncPaymentAmount()
{
$cart = CartSession::current();
Stripe::syncIntent($cart); // Updates amount if cart changed
}
}
```
### Stripe Payment Authorization
Process payment authorization and create orders.
```php
use Lunar\Stripe\StripePaymentType;
use Lunar\Facades\Payments;
use Lunar\Facades\CartSession;
class PaymentController
{
public function processPayment(Request $request)
{
$cart = CartSession::current()->calculate();
// Authorize payment using payment intent
$payment = Payments::driver('stripe')
->cart($cart)
->withData([
'payment_intent' => $request->payment_intent_id
])
->authorize();
if ($payment->success) {
// Payment successful, order created
return redirect()->route('order.confirmation', [
'order' => $payment->orderId
]);
}
// Payment failed
return back()->withErrors(['payment' => $payment->message]);
}
public function capturePayment(Transaction $transaction, int $amount = 0)
{
$capture = Payments::driver('stripe')
->capture($transaction, $amount);
return $capture->success;
}
public function refundPayment(Transaction $transaction, int $amount, string $notes = null)
{
$refund = Payments::driver('stripe')
->refund($transaction, $amount, $notes);
return $refund->success;
}
}
```
### Stripe Livewire Component
Use the built-in Livewire payment component for Stripe Elements.
```blade
{{-- In your checkout blade view --}}
@stripeScripts