# 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 {{-- Or build custom Stripe Elements integration --}}
``` ## Table Rate Shipping ### Configure Shipping Zones and Rates Set up table rate shipping with zones, methods, and pricing tiers. ```php use Lunar\Shipping\Models\ShippingZone; use Lunar\Shipping\Models\ShippingMethod; use Lunar\Shipping\Models\ShippingRate; // Create a shipping zone $zone = ShippingZone::create([ 'name' => 'United Kingdom', 'type' => 'countries' // 'countries', 'states', 'postcodes' ]); // Add countries to zone $zone->countries()->create(['country_id' => $uk->id]); // Or add postcodes $zone->postcodes()->create(['postcode' => 'SW*']); // Wildcard support // Create shipping method $method = ShippingMethod::create([ 'name' => 'Standard Delivery', 'description' => '3-5 business days', 'driver' => 'ship-by', // 'ship-by' for table rates 'code' => 'STD', 'enabled' => true, 'data' => [ 'minimum_spend' => 0 ] ]); // Add shipping rates with tiers ShippingRate::create([ 'shipping_method_id' => $method->id, 'shipping_zone_id' => $zone->id, 'currency_id' => Currency::getDefault()->id, 'price' => 499, // £4.99 in pence 'min_quantity' => 1, 'max_quantity' => 10 ]); ShippingRate::create([ 'shipping_method_id' => $method->id, 'shipping_zone_id' => $zone->id, 'currency_id' => Currency::getDefault()->id, 'price' => 0, // Free shipping 'min_quantity' => 11, 'max_quantity' => null // No upper limit ]); ``` ### Custom Shipping Modifier Create custom shipping logic with shipping modifiers. ```php // app/Shipping/CustomShippingModifier.php namespace App\Shipping; use Lunar\Base\ShippingModifier; use Lunar\DataTypes\ShippingOption; use Lunar\DataTypes\Price; use Lunar\Models\Cart; use Lunar\Models\TaxClass; class CustomShippingModifier extends ShippingModifier { public function handle(Cart $cart, \Closure $next) { // Add custom shipping options based on cart contents $hasHeavyItems = $cart->lines->some(function ($line) { return $line->purchasable->weight_value > 5000; // 5kg }); if ($hasHeavyItems) { $this->addOption(new ShippingOption( name: 'Heavy Goods Delivery', description: 'Specialized delivery for heavy items', identifier: 'heavy-goods', price: new Price(1499, $cart->currency, 1), taxClass: TaxClass::getDefault() )); } // Add free shipping for orders over £100 if ($cart->subTotal->value >= 10000) { $this->addOption(new ShippingOption( name: 'Free Standard Delivery', description: 'Free shipping on orders over £100', identifier: 'free-standard', price: new Price(0, $cart->currency, 1), taxClass: TaxClass::getDefault() )); } return $next($cart); } } // Register in AppServiceProvider public function boot(\Lunar\Base\ShippingModifiers $shippingModifiers) { $shippingModifiers->add(\App\Shipping\CustomShippingModifier::class); } ``` ## Search Integration ### Configure Search with Meilisearch Set up product search using Meilisearch or Typesense. ```php // config/scout.php 'driver' => 'meilisearch', 'meilisearch' => [ 'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'), 'key' => env('MEILISEARCH_KEY'), ], // Sync products to search index use Lunar\Models\Product; // Import all products Product::all()->searchable(); // Configure searchable attributes on Product model // The Searchable trait from Lunar handles this automatically // Products are indexed with: name, sku, brand, collections, tags // Search products $results = Product::search('blue t-shirt') ->where('status', 'published') ->get(); // Search with filters $results = Product::search($query) ->where('brand_id', $brandId) ->whereIn('collection_ids', $collectionIds) ->orderBy('price', 'asc') ->paginate(20); // Use Lunar search facade for advanced queries use Lunar\Search\Facades\Search; $results = Search::for(Product::class) ->query($searchTerm) ->filters([ 'brand_id' => $brandId, 'price_range' => [1000, 5000] ]) ->get(); ``` ## Event System ### Core Events Listen to Lunar events for custom business logic. ```php // app/Providers/EventServiceProvider.php use Lunar\Events\PaymentAttemptEvent; use Lunar\Events\CartCreated; protected $listen = [ \Lunar\Events\CartCreated::class => [ \App\Listeners\NotifyCartCreated::class, ], \Lunar\Events\PaymentAttemptEvent::class => [ \App\Listeners\LogPaymentAttempt::class, ], ]; // app/Listeners/LogPaymentAttempt.php namespace App\Listeners; use Lunar\Events\PaymentAttemptEvent; use Illuminate\Support\Facades\Log; class LogPaymentAttempt { public function handle(PaymentAttemptEvent $event) { $result = $event->paymentResult; Log::info('Payment attempt', [ 'success' => $result->success, 'order_id' => $result->orderId, 'payment_type' => $result->paymentType, 'message' => $result->message ]); if (!$result->success) { // Send alert for failed payment \App\Jobs\AlertPaymentFailure::dispatch($result); } } } // Admin panel events use Lunar\Admin\Events\ProductPricingUpdated; use Lunar\Admin\Events\ModelChannelsUpdated; // These events trigger automatic search index sync ``` ## Main Use Cases and Integration Patterns Lunar excels at building custom e-commerce storefronts where full control over the frontend is required while leveraging a robust, tested backend. Common use cases include headless commerce APIs (using Laravel Sanctum for API authentication with Lunar's cart and order models), multi-channel retail (managing products across web, mobile apps, and marketplaces), B2B wholesale platforms (with customer group pricing and tiered discounts), and subscription box services (combining Lunar's product management with custom recurring billing logic). Integration typically follows a service-oriented pattern: use CartSession facade for stateful cart management in web contexts, inject the Cart model directly for API endpoints, extend the pricing pipeline for custom pricing rules, and implement shipping modifiers for dynamic delivery options. The admin panel can be extended with custom Filament resources for business-specific workflows like supplier management or inventory forecasting. For payments, the abstract payment type system supports multiple gateways simultaneously, allowing customers to choose between Stripe, PayPal, or custom payment processors at checkout.