Add Shopify pull-from feature and fix product creation payload
Deploy / deploy (push) Successful in 14s
Deploy / deploy (push) Successful in 14s
- Add "Aus Shopify laden" button on product edit page that pulls description and variant prices/SKUs/GTINs from Shopify back into SMS - Add ShopifyService::getProduct() for fetching a single product - Add 'options' to createProduct() payload so Shopify properly creates variants (without it Shopify ignores variant option values) - Add request/response logging to createProduct() for diagnosing transfer issues - Remove dead code from ProductResource (mutateFormData*, getRecord, getActions) - Clean up imports in ProductResource Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
9ef24fc45f
commit
811f5a015e
3 files changed
+99
-58
No files matched your search
@@ -4,26 +4,25 @@ namespace App\Filament\User\Resources;
|
|||||||
|
|
||||||
use App\Filament\User\Resources\ProductResource\Pages;
|
use App\Filament\User\Resources\ProductResource\Pages;
|
||||||
use App\Filament\User\Resources\ProductResource\RelationManagers;
|
use App\Filament\User\Resources\ProductResource\RelationManagers;
|
||||||
|
use App\Models\Packing;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
|
use App\Models\ProductSize;
|
||||||
|
use App\Models\Spice;
|
||||||
|
use App\Rules\SumEquals100;
|
||||||
use Filament\Forms;
|
use Filament\Forms;
|
||||||
use Filament\Forms\Form;
|
use Filament\Forms\Components\Actions\Action as FormAction;
|
||||||
use Filament\Forms\Components\FileUpload;
|
use Filament\Forms\Components\FileUpload;
|
||||||
|
use Filament\Forms\Components\Hidden;
|
||||||
use Filament\Forms\Components\Placeholder;
|
use Filament\Forms\Components\Placeholder;
|
||||||
use Filament\Forms\Components\Repeater;
|
use Filament\Forms\Components\Repeater;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Form;
|
||||||
use Filament\Forms\Get;
|
use Filament\Forms\Get;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Support\HtmlString;
|
use Illuminate\Support\HtmlString;
|
||||||
use App\Models\Packing;
|
|
||||||
use App\Models\ProductSize;
|
|
||||||
use App\Models\Recipe;
|
|
||||||
use App\Models\Spice;
|
|
||||||
use App\Rules\SumEquals100;
|
|
||||||
use Filament\Forms\Components\Actions\Action as FormAction;
|
|
||||||
use Filament\Forms\Components\Hidden;
|
|
||||||
use Picqer\Barcode\BarcodeGeneratorSVG;
|
use Picqer\Barcode\BarcodeGeneratorSVG;
|
||||||
|
|
||||||
class ProductResource extends Resource {
|
class ProductResource extends Resource {
|
||||||
@@ -235,52 +234,4 @@ class ProductResource extends Resource {
|
|||||||
public static function getPluralLabel(): string {
|
public static function getPluralLabel(): string {
|
||||||
return 'Produkte';
|
return 'Produkte';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected function mutateFormDataBeforeCreate(array $data): array {
|
|
||||||
if (isset($data['images'])) {
|
|
||||||
$data['images'] = json_encode($data['images']);
|
|
||||||
}
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function mutateFormDataBeforeUpdate(array $data): array {
|
|
||||||
if (isset($data['images'])) {
|
|
||||||
$data['images'] = json_encode($data['images']);
|
|
||||||
}
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getRecord($id) {
|
|
||||||
return Product::with('sizes')->findOrFail($id); // Eager load spices relationship
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getTableRecordTitle() {
|
|
||||||
return function ($record) {
|
|
||||||
$images = json_decode($record->images, true);
|
|
||||||
return implode(', ', $images);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getActions(): array {
|
|
||||||
return [
|
|
||||||
Actions\DeleteAction::make(),
|
|
||||||
Action::make('deleteImage')
|
|
||||||
->action(function ($record, $data) {
|
|
||||||
$images = json_decode($record->images, true);
|
|
||||||
$index = array_search($data['image'], $images);
|
|
||||||
if ($index !== false) {
|
|
||||||
unset($images[$index]);
|
|
||||||
$record->update(['images' => array_values($images)]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
->form([
|
|
||||||
Forms\Components\Select::make('image')
|
|
||||||
->options(function ($record) {
|
|
||||||
return array_combine($record->images, $record->images);
|
|
||||||
})
|
|
||||||
->required(),
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,15 @@ class EditProduct extends EditRecord
|
|||||||
->exists()
|
->exists()
|
||||||
)
|
)
|
||||||
->action(fn () => $this->createProductInShopify()),
|
->action(fn () => $this->createProductInShopify()),
|
||||||
|
Actions\Action::make('pull-from-shopify')
|
||||||
|
->label('Aus Shopify laden')
|
||||||
|
->icon('heroicon-o-arrow-down-tray')
|
||||||
|
->color('gray')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->modalHeading('Aus Shopify aktualisieren')
|
||||||
|
->modalDescription('Überschreibt Beschreibung und Variantenpreise/-SKUs mit den aktuellen Shopify-Daten.')
|
||||||
|
->visible(fn () => (bool) $this->record->shopify_product_id)
|
||||||
|
->action(fn () => $this->pullFromShopify()),
|
||||||
Actions\Action::make('rezept-versionen')
|
Actions\Action::make('rezept-versionen')
|
||||||
->label('Rezept-Versionen')
|
->label('Rezept-Versionen')
|
||||||
->icon('heroicon-o-clock')
|
->icon('heroicon-o-clock')
|
||||||
@@ -181,6 +190,50 @@ class EditProduct extends EditRecord
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function pullFromShopify(): void
|
||||||
|
{
|
||||||
|
$product = $this->record;
|
||||||
|
|
||||||
|
if (! $product->shopify_product_id) {
|
||||||
|
Notification::make()->title('Kein Shopify-Produkt verknüpft')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$data = app(ShopifyService::class)->getProduct($product->shopify_product_id);
|
||||||
|
|
||||||
|
$updates = [];
|
||||||
|
if (! empty($data['description'])) {
|
||||||
|
$updates['description'] = $data['description'];
|
||||||
|
}
|
||||||
|
if ($updates) {
|
||||||
|
$product->update($updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
$variantMap = collect($data['variants'])->keyBy('shopify_variant_id');
|
||||||
|
|
||||||
|
ProductSize::where('product_id', $product->id)
|
||||||
|
->whereNotNull('shopify_variant_id')
|
||||||
|
->get()
|
||||||
|
->each(function (ProductSize $size) use ($variantMap) {
|
||||||
|
$shopifyVariant = $variantMap->get($size->shopify_variant_id);
|
||||||
|
if (! $shopifyVariant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$size->update([
|
||||||
|
'price' => $shopifyVariant['price'],
|
||||||
|
'sku' => $shopifyVariant['sku'] ?: $size->sku,
|
||||||
|
'gtin' => $shopifyVariant['gtin'] ?: $size->gtin,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Notification::make()->title('Aus Shopify aktualisiert')->success()->send();
|
||||||
|
$this->redirect(ProductResource::getUrl('edit', ['record' => $product]));
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Notification::make()->title('Shopify-Fehler')->body($e->getMessage())->danger()->send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function syncVariantWithShopify(int $sizeId): void
|
public function syncVariantWithShopify(int $sizeId): void
|
||||||
{
|
{
|
||||||
$size = ProductSize::find($sizeId);
|
$size = ProductSize::find($sizeId);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Product;
|
||||||
use App\Models\ProductSize;
|
use App\Models\ProductSize;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
@@ -76,14 +77,38 @@ class ShopifyService
|
|||||||
return $variants;
|
return $variants;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createProduct(\App\Models\Product $product, \Illuminate\Support\Collection $sizes): array
|
public function getProduct(string $shopifyProductId): array
|
||||||
|
{
|
||||||
|
$response = Http::withHeader('X-Shopify-Access-Token', $this->token)
|
||||||
|
->get("{$this->baseUrl}/products/{$shopifyProductId}.json");
|
||||||
|
|
||||||
|
if ($response->failed()) {
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Shopify-Fehler ' . $response->status() . ': ' . $response->body()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$p = $response->json('product');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'description' => strip_tags($p['body_html'] ?? ''),
|
||||||
|
'variants' => collect($p['variants'] ?? [])->map(fn ($v) => [
|
||||||
|
'shopify_variant_id' => (string) $v['id'],
|
||||||
|
'price' => (float) ($v['price'] ?? 0),
|
||||||
|
'sku' => $v['sku'] ?? null,
|
||||||
|
'gtin' => $v['barcode'] ?? null,
|
||||||
|
])->all(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createProduct(Product $product, \Illuminate\Support\Collection $sizes): array
|
||||||
{
|
{
|
||||||
$variants = $sizes->map(fn (ProductSize $s) => array_filter([
|
$variants = $sizes->map(fn (ProductSize $s) => array_filter([
|
||||||
'option1' => $s->packing->name . ' ' . $s->packing->size . 'g',
|
'option1' => $s->packing->name . ' ' . $s->packing->size . 'g',
|
||||||
'price' => number_format((float) $s->price, 2, '.', ''),
|
'price' => number_format((float) $s->price, 2, '.', ''),
|
||||||
'sku' => $s->sku ?: null,
|
'sku' => $s->sku ?: null,
|
||||||
'barcode' => $s->gtin ?: null,
|
'barcode' => $s->gtin ?: null,
|
||||||
]))->values()->toArray();
|
], fn ($v) => $v !== null))->values()->toArray();
|
||||||
|
|
||||||
$body = [
|
$body = [
|
||||||
'product' => [
|
'product' => [
|
||||||
@@ -91,6 +116,7 @@ class ShopifyService
|
|||||||
'body_html' => $product->description
|
'body_html' => $product->description
|
||||||
? '<p>' . nl2br(e($product->description)) . '</p>'
|
? '<p>' . nl2br(e($product->description)) . '</p>'
|
||||||
: '',
|
: '',
|
||||||
|
'options' => [['name' => 'Größe']],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -98,9 +124,20 @@ class ShopifyService
|
|||||||
$body['product']['variants'] = $variants;
|
$body['product']['variants'] = $variants;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log::info('Shopify createProduct request', [
|
||||||
|
'product_id' => $product->id,
|
||||||
|
'sizes' => $sizes->count(),
|
||||||
|
'body' => $body,
|
||||||
|
]);
|
||||||
|
|
||||||
$response = Http::withHeader('X-Shopify-Access-Token', $this->token)
|
$response = Http::withHeader('X-Shopify-Access-Token', $this->token)
|
||||||
->post("{$this->baseUrl}/products.json", $body);
|
->post("{$this->baseUrl}/products.json", $body);
|
||||||
|
|
||||||
|
Log::info('Shopify createProduct response', [
|
||||||
|
'status' => $response->status(),
|
||||||
|
'body' => $response->json(),
|
||||||
|
]);
|
||||||
|
|
||||||
if ($response->failed()) {
|
if ($response->failed()) {
|
||||||
throw new \RuntimeException(
|
throw new \RuntimeException(
|
||||||
'Shopify-Fehler ' . $response->status() . ': ' . $response->body()
|
'Shopify-Fehler ' . $response->status() . ': ' . $response->body()
|
||||||
|
|||||||
Reference in new issue
Block a user