2026-07-10 12:31:59 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Modules\Core\Command;
|
|
|
|
|
|
|
|
|
|
use Illuminate\Console\Command;
|
|
|
|
|
use Lunar\Facades\DB;
|
|
|
|
|
use Lunar\FieldTypes\TranslatedText;
|
|
|
|
|
use Lunar\Models\Attribute;
|
|
|
|
|
use Lunar\Models\AttributeGroup;
|
|
|
|
|
use Lunar\Models\Channel;
|
|
|
|
|
use Lunar\Models\Collection;
|
|
|
|
|
use Lunar\Models\CollectionGroup;
|
|
|
|
|
use Lunar\Models\Country;
|
|
|
|
|
use Lunar\Models\Currency;
|
|
|
|
|
use Lunar\Models\CustomerGroup;
|
|
|
|
|
use Lunar\Models\Language;
|
|
|
|
|
use Lunar\Models\Product;
|
|
|
|
|
use Lunar\Models\ProductType;
|
|
|
|
|
use Lunar\Models\TaxClass;
|
|
|
|
|
use Lunar\Models\TaxZone;
|
2026-08-27 12:11:01 +03:00
|
|
|
use Modules\Core\Localization\Models\LanguageLine;
|
|
|
|
|
use Modules\Core\Localization\Services\StorefrontLabels;
|
|
|
|
|
use Modules\Core\Localization\Services\TranslationService;
|
2026-08-31 13:54:20 +03:00
|
|
|
use Modules\Core\Payment\Models\PaymentMethod;
|
2026-07-10 12:31:59 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Overrides Lunar's own lunar:install to skip the interactive prompts (migrate
|
|
|
|
|
* confirmation, admin creation, GitHub star), so it's safe to run unattended from
|
2026-07-10 12:38:48 +03:00
|
|
|
* the entrypoint on every boot. Migrations already run as a separate, earlier
|
|
|
|
|
* entrypoint step; the admin user is created manually.
|
2026-07-10 12:31:59 +03:00
|
|
|
*/
|
|
|
|
|
class InstallLunarCommand extends Command
|
|
|
|
|
{
|
|
|
|
|
protected $signature = 'lunar:install';
|
|
|
|
|
|
2026-07-10 12:38:48 +03:00
|
|
|
protected $description = 'Seed the default Lunar store data (countries, channel, currency, tax zone, attributes, product type)';
|
2026-07-10 12:31:59 +03:00
|
|
|
|
2026-08-27 12:11:01 +03:00
|
|
|
public function handle(TranslationService $translations): void
|
2026-07-10 12:31:59 +03:00
|
|
|
{
|
|
|
|
|
$this->components->info('Seeding default Lunar store data...');
|
|
|
|
|
|
2026-07-10 12:38:48 +03:00
|
|
|
if (! Country::count()) {
|
|
|
|
|
$this->components->info('Importing countries');
|
|
|
|
|
$this->call('lunar:import:address-data');
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 12:31:59 +03:00
|
|
|
DB::transaction(function () {
|
|
|
|
|
if (! Channel::whereDefault(true)->exists()) {
|
|
|
|
|
$this->components->info('Setting up default channel');
|
|
|
|
|
|
|
|
|
|
Channel::create([
|
|
|
|
|
'name' => 'Webstore',
|
|
|
|
|
'handle' => 'webstore',
|
|
|
|
|
'default' => true,
|
|
|
|
|
'url' => 'http://localhost',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! Language::count()) {
|
|
|
|
|
$this->components->info('Adding default language');
|
|
|
|
|
|
|
|
|
|
Language::create([
|
|
|
|
|
'code' => 'en',
|
|
|
|
|
'name' => 'English',
|
|
|
|
|
'default' => true,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! Currency::whereDefault(true)->exists()) {
|
|
|
|
|
$this->components->info('Adding a default currency (USD)');
|
|
|
|
|
|
|
|
|
|
Currency::create([
|
|
|
|
|
'code' => 'USD',
|
|
|
|
|
'name' => 'US Dollar',
|
|
|
|
|
'exchange_rate' => 1,
|
|
|
|
|
'decimal_places' => 2,
|
|
|
|
|
'default' => true,
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! CustomerGroup::whereDefault(true)->exists()) {
|
|
|
|
|
$this->components->info('Adding a default customer group.');
|
|
|
|
|
|
|
|
|
|
CustomerGroup::create([
|
|
|
|
|
'name' => 'Retail',
|
|
|
|
|
'handle' => 'retail',
|
|
|
|
|
'default' => true,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! CollectionGroup::count()) {
|
|
|
|
|
$this->components->info('Adding an initial collection group');
|
|
|
|
|
|
|
|
|
|
CollectionGroup::create([
|
|
|
|
|
'name' => 'Main',
|
|
|
|
|
'handle' => 'main',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! TaxClass::count()) {
|
|
|
|
|
$this->components->info('Adding a default tax class.');
|
|
|
|
|
|
|
|
|
|
TaxClass::create([
|
|
|
|
|
'name' => 'Default Tax Class',
|
|
|
|
|
'default' => true,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! TaxZone::count()) {
|
|
|
|
|
$this->components->info('Adding a default tax zone.');
|
|
|
|
|
|
|
|
|
|
$taxZone = TaxZone::create([
|
|
|
|
|
'name' => 'Default Tax Zone',
|
|
|
|
|
'zone_type' => 'country',
|
|
|
|
|
'price_display' => 'tax_exclusive',
|
|
|
|
|
'default' => true,
|
|
|
|
|
'active' => true,
|
|
|
|
|
]);
|
|
|
|
|
$taxZone->countries()->createMany(
|
|
|
|
|
Country::get()->map(fn ($country) => [
|
|
|
|
|
'country_id' => $country->id,
|
|
|
|
|
])
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! Attribute::count()) {
|
|
|
|
|
$this->components->info('Setting up initial attributes');
|
|
|
|
|
|
|
|
|
|
$group = AttributeGroup::create([
|
|
|
|
|
'attributable_type' => Product::morphName(),
|
|
|
|
|
'name' => collect([
|
|
|
|
|
'en' => 'Details',
|
|
|
|
|
]),
|
|
|
|
|
'handle' => 'details',
|
|
|
|
|
'position' => 1,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$collectionGroup = AttributeGroup::create([
|
|
|
|
|
'attributable_type' => Collection::morphName(),
|
|
|
|
|
'name' => collect([
|
|
|
|
|
'en' => 'Details',
|
|
|
|
|
]),
|
|
|
|
|
'handle' => 'collection_details',
|
|
|
|
|
'position' => 1,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
Attribute::create([
|
|
|
|
|
'attribute_type' => 'product',
|
|
|
|
|
'attribute_group_id' => $group->id,
|
|
|
|
|
'position' => 1,
|
|
|
|
|
'name' => [
|
|
|
|
|
'en' => 'Name',
|
|
|
|
|
],
|
|
|
|
|
'handle' => 'name',
|
|
|
|
|
'section' => 'main',
|
|
|
|
|
'type' => TranslatedText::class,
|
|
|
|
|
'required' => true,
|
|
|
|
|
'default_value' => null,
|
|
|
|
|
'configuration' => [
|
|
|
|
|
'richtext' => false,
|
|
|
|
|
],
|
|
|
|
|
'system' => true,
|
|
|
|
|
'description' => [
|
|
|
|
|
'en' => '',
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
Attribute::create([
|
|
|
|
|
'attribute_type' => 'collection',
|
|
|
|
|
'attribute_group_id' => $collectionGroup->id,
|
|
|
|
|
'position' => 1,
|
|
|
|
|
'name' => [
|
|
|
|
|
'en' => 'Name',
|
|
|
|
|
],
|
|
|
|
|
'handle' => 'name',
|
|
|
|
|
'section' => 'main',
|
|
|
|
|
'type' => TranslatedText::class,
|
|
|
|
|
'required' => true,
|
|
|
|
|
'default_value' => null,
|
|
|
|
|
'configuration' => [
|
|
|
|
|
'richtext' => false,
|
|
|
|
|
],
|
|
|
|
|
'system' => true,
|
|
|
|
|
'description' => [
|
|
|
|
|
'en' => '',
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
Attribute::create([
|
|
|
|
|
'attribute_type' => 'product',
|
|
|
|
|
'attribute_group_id' => $group->id,
|
|
|
|
|
'position' => 2,
|
|
|
|
|
'name' => [
|
|
|
|
|
'en' => 'Description',
|
|
|
|
|
],
|
|
|
|
|
'handle' => 'description',
|
|
|
|
|
'section' => 'main',
|
|
|
|
|
'type' => TranslatedText::class,
|
|
|
|
|
'required' => false,
|
|
|
|
|
'default_value' => null,
|
|
|
|
|
'configuration' => [
|
|
|
|
|
'richtext' => true,
|
|
|
|
|
],
|
|
|
|
|
'system' => false,
|
|
|
|
|
'description' => [
|
|
|
|
|
'en' => '',
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
Attribute::create([
|
|
|
|
|
'attribute_type' => 'collection',
|
|
|
|
|
'attribute_group_id' => $collectionGroup->id,
|
|
|
|
|
'position' => 2,
|
|
|
|
|
'name' => [
|
|
|
|
|
'en' => 'Description',
|
|
|
|
|
],
|
|
|
|
|
'handle' => 'description',
|
|
|
|
|
'section' => 'main',
|
|
|
|
|
'type' => TranslatedText::class,
|
|
|
|
|
'required' => false,
|
|
|
|
|
'default_value' => null,
|
|
|
|
|
'configuration' => [
|
|
|
|
|
'richtext' => true,
|
|
|
|
|
],
|
|
|
|
|
'system' => false,
|
|
|
|
|
'description' => [
|
|
|
|
|
'en' => '',
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! ProductType::count()) {
|
|
|
|
|
$this->components->info('Adding a product type.');
|
|
|
|
|
|
|
|
|
|
$type = ProductType::create([
|
|
|
|
|
'name' => 'Stock',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$type->mappedAttributes()->attach(
|
|
|
|
|
Attribute::whereAttributeType(
|
|
|
|
|
Product::morphName()
|
|
|
|
|
)->get()->pluck('id')
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-27 12:11:01 +03:00
|
|
|
$this->components->info('Seeding storefront label translations');
|
|
|
|
|
$this->seedStorefrontLabels($translations);
|
2026-08-06 00:13:13 +03:00
|
|
|
|
2026-08-31 13:54:20 +03:00
|
|
|
$this->components->info('Seeding payment method settings');
|
|
|
|
|
$this->seedPaymentMethods();
|
|
|
|
|
|
2026-07-10 12:31:59 +03:00
|
|
|
$this->components->info('Publishing Filament assets');
|
|
|
|
|
$this->call('filament:assets');
|
|
|
|
|
|
|
|
|
|
$this->components->info('Lunar default data seeded.');
|
|
|
|
|
}
|
2026-08-06 00:13:13 +03:00
|
|
|
|
2026-08-27 12:11:01 +03:00
|
|
|
/**
|
|
|
|
|
* Per-key upsert, not an all-or-nothing "only seed if the group is empty" guard —
|
|
|
|
|
* a key already present in the database (including one an admin has since edited
|
|
|
|
|
* via the Filament Languages resource) is left untouched; only keys missing
|
|
|
|
|
* entirely are created. This is what makes it safe to add new keys to
|
|
|
|
|
* StorefrontLabels later and re-run this on an already-installed store without
|
|
|
|
|
* either skipping the new keys (the old all-or-nothing guard) or reverting an
|
|
|
|
|
* admin's edits back to the hardcoded default (a naive updateOrCreate would).
|
|
|
|
|
*/
|
|
|
|
|
private function seedStorefrontLabels(TranslationService $translations): void
|
2026-08-06 00:13:13 +03:00
|
|
|
{
|
2026-08-27 12:11:01 +03:00
|
|
|
$labels = StorefrontLabels::all();
|
|
|
|
|
|
|
|
|
|
$existingKeys = LanguageLine::where('group', 'storefront')
|
|
|
|
|
->whereIn('key', array_keys($labels))
|
|
|
|
|
->pluck('key');
|
2026-08-06 00:13:13 +03:00
|
|
|
|
|
|
|
|
foreach ($labels as $key => $text) {
|
2026-08-27 12:11:01 +03:00
|
|
|
if ($existingKeys->contains($key)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$translations->create('storefront', $key, $text);
|
2026-08-06 00:13:13 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-08-31 13:54:20 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Per-type skip-if-exists, same idempotent convention as
|
|
|
|
|
* seedStorefrontLabels() — a type already present (including one an
|
|
|
|
|
* admin has since edited via the Filament Payment Methods resource) is
|
|
|
|
|
* left untouched. Safe to re-run after a new payment type is added to
|
|
|
|
|
* config('lunar.payments.types') (e.g. installing a Stripe/Nexi
|
|
|
|
|
* package), which is the whole reason this isn't a one-time-only seed.
|
|
|
|
|
*/
|
|
|
|
|
private function seedPaymentMethods(): void
|
|
|
|
|
{
|
|
|
|
|
$existingTypes = PaymentMethod::pluck('type');
|
|
|
|
|
|
|
|
|
|
foreach (array_keys(config('lunar.payments.types', [])) as $type) {
|
|
|
|
|
if ($existingTypes->contains($type)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PaymentMethod::create([
|
|
|
|
|
'type' => $type,
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
'data' => [],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-10 12:31:59 +03:00
|
|
|
}
|