2026-07-06 03:42:24 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Modules\Core\Command;
|
|
|
|
|
|
|
|
|
|
use Illuminate\Console\Command;
|
2026-09-24 21:41:11 +03:00
|
|
|
use Lunar\Models\Language;
|
2026-09-24 22:50:41 +03:00
|
|
|
use Modules\Core\MigrateImport\DTOs\ImportSpec;
|
|
|
|
|
use Modules\Core\MigrateImport\Jobs\RunMigrateImportJob;
|
2026-07-06 03:42:24 +03:00
|
|
|
|
|
|
|
|
class MigrateImportCommand extends Command
|
|
|
|
|
{
|
2026-07-10 15:19:48 +03:00
|
|
|
protected $signature = 'boboko:migrate:import
|
2026-07-06 03:42:24 +03:00
|
|
|
{--source= : The source platform to import from}
|
|
|
|
|
{--type= : The import mechanism for the chosen source}
|
2026-07-10 15:19:48 +03:00
|
|
|
{--file= : Path to the export file (when type is export)}';
|
2026-07-06 03:42:24 +03:00
|
|
|
|
2026-07-10 15:19:48 +03:00
|
|
|
protected $description = 'Migrate data (products, customers, etc.) from an external source';
|
2026-07-06 03:42:24 +03:00
|
|
|
|
2026-07-10 15:19:48 +03:00
|
|
|
// Supported sources. Only "shopify" and "judgeme" are wired up for now.
|
2026-07-06 03:42:24 +03:00
|
|
|
private const SOURCES = [
|
2026-07-10 15:19:48 +03:00
|
|
|
'shopify',
|
|
|
|
|
'judgeme',
|
2026-07-06 03:42:24 +03:00
|
|
|
// "woocommerce",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Supported import types per source. Only "export" is wired up for now.
|
|
|
|
|
private const TYPES = [
|
2026-07-10 15:19:48 +03:00
|
|
|
'export',
|
2026-07-06 03:42:24 +03:00
|
|
|
// "api",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public function handle(): void
|
|
|
|
|
{
|
2026-07-10 15:19:48 +03:00
|
|
|
$source = $this->option('source')
|
|
|
|
|
?? $this->choice('Select a source:', self::SOURCES, 0);
|
2026-07-06 03:42:24 +03:00
|
|
|
|
2026-07-10 15:19:48 +03:00
|
|
|
$type = $this->option('type')
|
|
|
|
|
?? $this->choice('Select an import type:', self::TYPES, 0);
|
2026-07-06 03:42:24 +03:00
|
|
|
|
2026-07-10 15:19:48 +03:00
|
|
|
if ($type === 'api') {
|
2026-07-06 03:42:24 +03:00
|
|
|
$credentials = [
|
2026-07-10 15:19:48 +03:00
|
|
|
'api_key' => $this->secret('API key:'),
|
|
|
|
|
'api_secret' => $this->secret('API secret:'),
|
2026-07-06 03:42:24 +03:00
|
|
|
];
|
|
|
|
|
$filePath = null;
|
|
|
|
|
} else {
|
2026-07-10 15:19:48 +03:00
|
|
|
$importsPath = storage_path('app/private/imports');
|
2026-07-09 00:37:14 +03:00
|
|
|
|
2026-07-10 15:19:48 +03:00
|
|
|
$filePath = $this->option('file')
|
2026-07-09 00:37:14 +03:00
|
|
|
?? $this->resolveImportPath($importsPath, $this->ask("Path to the export file, relative to {$importsPath}:"));
|
|
|
|
|
|
|
|
|
|
while (blank($filePath) || ! is_file($filePath)) {
|
|
|
|
|
if (filled($filePath)) {
|
|
|
|
|
$this->error("File not found: {$filePath}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$filePath = $this->resolveImportPath(
|
|
|
|
|
$importsPath,
|
|
|
|
|
$this->ask("Path to the export file, relative to {$importsPath}:"),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 03:42:24 +03:00
|
|
|
$credentials = null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-24 21:41:11 +03:00
|
|
|
// Shopify's own product export is a flat CSV — one Title/Body
|
|
|
|
|
// (HTML)/etc. column per row, no per-locale columns at all — so
|
|
|
|
|
// its text is necessarily written in exactly one language, and
|
|
|
|
|
// there is no reliable way to detect which one from the file
|
2026-09-24 22:50:41 +03:00
|
|
|
// itself. Modules\Core\MigrateImport\Services\ImportLocale::code() used to
|
2026-09-24 21:41:11 +03:00
|
|
|
// (as its former name, DefaultLocale, admits) assume it always
|
|
|
|
|
// matched this store's own Lunar\Models\
|
|
|
|
|
// Language::getDefault(), which is often wrong (a store's default
|
|
|
|
|
// admin/storefront language and the language a given export
|
|
|
|
|
// happens to be written in are two independent facts) — every
|
|
|
|
|
// imported product's name/description then saved silently under
|
|
|
|
|
// the wrong language, invisible unless that language happened to
|
|
|
|
|
// also be selected when viewing/editing the product afterward.
|
|
|
|
|
$locale = $source === 'shopify' && $type === 'export'
|
|
|
|
|
? $this->askImportLocale()
|
|
|
|
|
: null;
|
|
|
|
|
|
2026-07-06 03:42:24 +03:00
|
|
|
$spec = new ImportSpec(
|
|
|
|
|
source: $source,
|
|
|
|
|
type: $type,
|
|
|
|
|
filePath: $filePath,
|
|
|
|
|
credentials: $credentials,
|
2026-09-24 21:41:11 +03:00
|
|
|
locale: $locale,
|
2026-07-06 03:42:24 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
RunMigrateImportJob::dispatch($spec);
|
|
|
|
|
|
2026-07-10 15:19:48 +03:00
|
|
|
$this->info('Import queued.');
|
2026-07-06 03:42:24 +03:00
|
|
|
}
|
2026-07-09 00:37:14 +03:00
|
|
|
|
2026-09-24 21:41:11 +03:00
|
|
|
/**
|
|
|
|
|
* Choices come from Language::all() — the same list an admin manages
|
|
|
|
|
* from the Filament panel (Settings > Languages) — not a hardcoded
|
|
|
|
|
* set, so a language this store doesn't have yet simply isn't
|
|
|
|
|
* offered here; the hint below says where to add it instead of this
|
|
|
|
|
* command silently accepting an arbitrary code Lunar has no row for.
|
|
|
|
|
*/
|
|
|
|
|
private function askImportLocale(): string
|
|
|
|
|
{
|
|
|
|
|
$languages = Language::orderBy('default', 'desc')->get(['code', 'name']);
|
|
|
|
|
|
|
|
|
|
return $this->choice(
|
|
|
|
|
"Which language is the export file's own text (product titles, descriptions, etc.) written in?\n".
|
|
|
|
|
' (Not necessarily this store\'s default language — the two are independent. '.
|
|
|
|
|
"If the language you need isn't listed, add it first from the admin panel under Languages.)",
|
|
|
|
|
$languages->mapWithKeys(fn (Language $language) => [$language->code => "{$language->name} ({$language->code})"])->all(),
|
|
|
|
|
$languages->first()?->code,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 00:37:14 +03:00
|
|
|
// Answers are relative to storage/app/private/imports (e.g. "shopify" or
|
|
|
|
|
// "shopify/products_export.csv"); absolute paths are used as-is. A
|
|
|
|
|
// directory answer picks the first CSV file found inside it.
|
|
|
|
|
private function resolveImportPath(string $importsPath, ?string $answer): ?string
|
|
|
|
|
{
|
|
|
|
|
if (blank($answer)) {
|
|
|
|
|
return $answer;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 15:19:48 +03:00
|
|
|
$path = str_starts_with($answer, '/') ? $answer : "{$importsPath}/{$answer}";
|
2026-07-09 00:37:14 +03:00
|
|
|
|
|
|
|
|
if (is_dir($path)) {
|
|
|
|
|
$csv = collect(glob("{$path}/*.csv"))->first();
|
|
|
|
|
|
|
|
|
|
return $csv ?? $path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $path;
|
|
|
|
|
}
|
2026-07-06 03:42:24 +03:00
|
|
|
}
|