charlielangridge / lunar-marketplace
Marketplace integration scaffolding for Lunar stores.
Package info
github.com/charlielangridge/lunar-marketplace
pkg:composer/charlielangridge/lunar-marketplace
Requires
- php: ^8.4
- illuminate/bus: ^12.0
- illuminate/contracts: ^12.0
- illuminate/database: ^12.0
- illuminate/http: ^12.0
- illuminate/support: ^12.0
Requires (Dev)
- laravel/pint: ^1.27
- orchestra/testbench: ^10.0
- pestphp/pest: ^4.0
Suggests
- filament/filament: Required to use LunarMarketplacePlugin for conditional admin nav items.
README
Reusable marketplace integration scaffolding for Lunar stores.
Included channels
- Amazon UK
- eBay UK
- Etsy
Requirements
- PHP 8.4+
- Laravel 12+
- A Lunar store (migrations assume a
productsor similar ownable entity)
Installation
1. Require the package
composer require charlielangridge/lunar-marketplace
2. Publish the config
php artisan vendor:publish --tag=lunar-marketplace-config
3. Run migrations
php artisan migrate
This creates the channel account, listing, sync event, and order tables for each marketplace.
Configuration
After publishing, edit config/lunar-marketplace.php. All values can be overridden via environment variables.
.env variables
# Amazon UK AMAZON_UK_MARKETPLACE_ID=A1F83G8C2ARO7P AMAZON_UK_REGION=eu-west-1 AMAZON_UK_BASE_URL=https://sellingpartnerapi-eu.amazon.com AMAZON_UK_CURRENCY=GBP # eBay UK EBAY_UK_MARKETPLACE_ID=EBAY_GB EBAY_UK_BASE_URL=https://api.ebay.com EBAY_UK_CURRENCY=GBP # Etsy ETSY_BASE_URL=https://api.etsy.com ETSY_CURRENCY=GBP # HTTP client (optional — defaults shown) LUNAR_MARKETPLACE_TIMEOUT=20 LUNAR_MARKETPLACE_CONNECT_TIMEOUT=10 LUNAR_MARKETPLACE_RETRY_TIMES=3
Connecting a marketplace account
Each marketplace has a ChannelAccount model that stores credentials. Create one record per connected account.
Amazon
use Charlielangridge\LunarMarketplace\Models\AmazonChannelAccount; AmazonChannelAccount::create([ 'name' => 'My Amazon UK Store', 'marketplace_code' => 'amazon.co.uk', 'marketplace_id' => config('lunar-marketplace.marketplaces.amazon.co.uk.marketplace_id'), 'region' => config('lunar-marketplace.marketplaces.amazon.co.uk.region'), 'base_url' => config('lunar-marketplace.marketplaces.amazon.co.uk.base_url'), 'seller_id' => 'AXXXXXXXXXX', 'refresh_token' => 'Atzr|...', 'lwa_client_id' => 'amzn1.application-oa2-client...', 'lwa_client_secret' => '...', 'aws_access_key_id' => 'AKIAIOSFODNN7EXAMPLE', 'aws_secret_access_key' => 'wJalrXUtnFEMI/...', 'currency' => 'GBP', 'active' => true, ]);
eBay
use Charlielangridge\LunarMarketplace\Models\Ebay\EbayChannelAccount; EbayChannelAccount::create([ 'name' => 'My eBay UK Store', 'marketplace_code' => 'ebay.co.uk', 'marketplace_id' => config('lunar-marketplace.marketplaces.ebay.co.uk.marketplace_id'), 'base_url' => config('lunar-marketplace.marketplaces.ebay.co.uk.base_url'), 'client_id' => '...', 'client_secret' => '...', 'refresh_token' => '...', 'merchant_location_key' => 'default', 'currency' => 'GBP', 'active' => true, ]);
Etsy
use Charlielangridge\LunarMarketplace\Models\Etsy\EtsyChannelAccount; EtsyChannelAccount::create([ 'name' => 'My Etsy Shop', 'marketplace_code' => 'etsy.com/uk', 'base_url' => config('lunar-marketplace.marketplaces.etsy.com/uk.base_url'), 'shop_id' => '12345678', 'client_id' => '...', 'client_secret' => '...', 'refresh_token' => '...', 'api_key' => '...', 'currency' => 'GBP', 'active' => true, ]);
Implementing the contracts
The package ships with three extensibility contracts per marketplace. Your app must implement and bind these.
Contracts
| Contract | Purpose |
|---|---|
{Channel}SellableResolverInterface |
Resolves a listing to sellable data (title, description, attributes, images, inventory) |
{Channel}PriceCalculatorInterface |
Calculates the channel price from your website price |
{Channel}OrderTransformerInterface |
Transforms a raw marketplace order payload into your app's order format |
Example: Amazon sellable resolver
namespace App\Marketplace\Amazon; use Charlielangridge\LunarMarketplace\Contracts\AmazonSellableResolverInterface; use Charlielangridge\LunarMarketplace\Data\AmazonChannelConfig; use Charlielangridge\LunarMarketplace\Data\AmazonSellableData; use Charlielangridge\LunarMarketplace\Models\AmazonListing; class AmazonSellableResolver implements AmazonSellableResolverInterface { public function resolve(AmazonListing $listing): AmazonSellableData { $product = $listing->owner; // your Lunar product return new AmazonSellableData( sellerSku: $listing->seller_sku, title: $product->translateAttribute('name'), description: $product->translateAttribute('description'), productType: 'HOME_BED_AND_BATH', brand: $product->brand, imageUrls: $product->images->map->original_url->all(), attributes: [], listingAttributes: [], inventory: $product->stock->value, ); } }
Example: Amazon price calculator
namespace App\Marketplace\Amazon; use Charlielangridge\LunarMarketplace\Contracts\AmazonPriceCalculatorInterface; use Charlielangridge\LunarMarketplace\Data\AmazonChannelConfig; use Charlielangridge\LunarMarketplace\Data\AmazonPriceBreakdown; use Charlielangridge\LunarMarketplace\Models\AmazonListing; class AmazonPriceCalculator implements AmazonPriceCalculatorInterface { public function calculate(AmazonListing $listing, AmazonChannelConfig $config): AmazonPriceBreakdown { $websitePrice = $listing->owner->price->value; // minor units (pence) $markup = (int) round($websitePrice * 0.10); // 10% markup return new AmazonPriceBreakdown( websiteMinorAmount: $websitePrice, amazonMinorAmount: $websitePrice + $markup, markupMinorAmount: $markup, components: [], currency: $config->currency, ); } }
Binding interfaces
Register your implementations in a service provider:
use Charlielangridge\LunarMarketplace\Contracts\AmazonSellableResolverInterface; use Charlielangridge\LunarMarketplace\Contracts\AmazonPriceCalculatorInterface; use Charlielangridge\LunarMarketplace\Contracts\AmazonOrderTransformerInterface; use App\Marketplace\Amazon\AmazonSellableResolver; use App\Marketplace\Amazon\AmazonPriceCalculator; use App\Marketplace\Amazon\AmazonOrderTransformer; public function register(): void { $this->app->bind(AmazonSellableResolverInterface::class, AmazonSellableResolver::class); $this->app->bind(AmazonPriceCalculatorInterface::class, AmazonPriceCalculator::class); $this->app->bind(AmazonOrderTransformerInterface::class, AmazonOrderTransformer::class); // repeat for eBay and Etsy }
Syncing listings
Dispatch queue jobs to sync a listing to a marketplace:
use Charlielangridge\LunarMarketplace\Jobs\SyncAmazonListingJob; use Charlielangridge\LunarMarketplace\Jobs\SyncEbayListingJob; use Charlielangridge\LunarMarketplace\Jobs\SyncEtsyListingJob; SyncAmazonListingJob::dispatch($amazonListing); SyncEbayListingJob::dispatch($ebayListing); SyncEtsyListingJob::dispatch($etsyListing);
Each job resolves the sellable and price via the bound contracts, calls the appropriate API, and updates the listing's sync_status and last_synced_at.
Importing orders
use Charlielangridge\LunarMarketplace\Jobs\ImportAmazonOrdersJob; use Charlielangridge\LunarMarketplace\Jobs\ImportEbayOrdersJob; use Charlielangridge\LunarMarketplace\Jobs\ImportEtsyOrdersJob; ImportAmazonOrdersJob::dispatch($amazonChannelAccount); ImportEbayOrdersJob::dispatch($ebayChannelAccount); ImportEtsyOrdersJob::dispatch($etsyChannelAccount);
Imported orders are stored in *_external_orders tables. Your app's order transformer is called to produce the transformed_payload, which you then use to create Lunar orders.
Lunar admin panel integration
If your store uses a Filament-based admin panel, register LunarMarketplacePlugin to add marketplace navigation groups. Groups are only shown when at least one active channel account exists for that marketplace — disconnected marketplaces are hidden automatically.
Install Filament if you haven't already:
composer require filament/filament
Register the plugin on your panel provider:
use Charlielangridge\LunarMarketplace\LunarMarketplacePlugin; public function panel(Panel $panel): Panel { return $panel ->plugins([ LunarMarketplacePlugin::make(), ]); }
In your Filament resources, set the $navigationGroup to match the marketplace name so they appear under the correct group:
// app/Filament/Resources/AmazonListingResource.php protected static ?string $navigationGroup = 'Amazon'; // app/Filament/Resources/EbayListingResource.php protected static ?string $navigationGroup = 'eBay'; // app/Filament/Resources/EtsyListingResource.php protected static ?string $navigationGroup = 'Etsy';
When no active account exists for a marketplace, that navigation group is not registered, so any resources in that group will be hidden from the sidebar.
Store-side responsibilities
The consuming app is expected to provide:
- Channel-specific sellable resolvers (maps listings to marketplace product data)
- Pricing parity calculators (applies markup/fee logic)
- Order transformers (converts marketplace orders to Lunar orders)
- Admin UI and Filament resources
- Product metadata models
Development
composer install
composer test
composer format