charlielangridge/lunar-marketplace

Marketplace integration scaffolding for Lunar stores.

Maintainers

Package info

github.com/charlielangridge/lunar-marketplace

pkg:composer/charlielangridge/lunar-marketplace

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.0.1 2026-04-13 12:43 UTC

This package is auto-updated.

Last update: 2026-04-13 12:47:53 UTC


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 products or 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