, price?: array} $options */ public function __construct(private readonly int $addonId, private readonly array $options = []) {} public function handle(PaddleAddonCatalogService $catalog): void { $addon = PackageAddon::query()->find($this->addonId); if (! $addon) { return; } $dryRun = (bool) ($this->options['dry_run'] ?? false); $productOverrides = Arr::get($this->options, 'product', []); $priceOverrides = Arr::get($this->options, 'price', []); if ($dryRun) { $this->storeDryRunSnapshot($catalog, $addon, $productOverrides, $priceOverrides); return; } // Mark syncing (metadata) $addon->forceFill([ 'metadata' => array_merge($addon->metadata ?? [], [ 'paddle_sync_status' => 'syncing', 'paddle_synced_at' => now()->toIso8601String(), ]), ])->save(); try { $payloadOverrides = $this->buildPayloadOverrides($addon, $productOverrides, $priceOverrides); $productResponse = $addon->metadata['paddle_product_id'] ?? null ? $catalog->updateProduct($addon->metadata['paddle_product_id'], $addon, $payloadOverrides['product']) : $catalog->createProduct($addon, $payloadOverrides['product']); $productId = (string) ($productResponse['id'] ?? $addon->metadata['paddle_product_id'] ?? null); if (! $productId) { throw new PaddleException('Paddle product ID missing after addon sync.'); } $priceResponse = $addon->price_id ? $catalog->updatePrice($addon->price_id, $addon, array_merge($payloadOverrides['price'], ['product_id' => $productId])) : $catalog->createPrice($addon, $productId, $payloadOverrides['price']); $priceId = (string) ($priceResponse['id'] ?? $addon->price_id); if (! $priceId) { throw new PaddleException('Paddle price ID missing after addon sync.'); } $addon->forceFill([ 'price_id' => $priceId, 'metadata' => array_merge($addon->metadata ?? [], [ 'paddle_sync_status' => 'synced', 'paddle_synced_at' => now()->toIso8601String(), 'paddle_product_id' => $productId, 'paddle_snapshot' => [ 'product' => $productResponse, 'price' => $priceResponse, 'payload' => $payloadOverrides, ], ]), ])->save(); } catch (Throwable $exception) { Log::error('Paddle addon sync failed', [ 'addon_id' => $addon->id, 'message' => $exception->getMessage(), 'exception' => $exception, ]); $addon->forceFill([ 'metadata' => array_merge($addon->metadata ?? [], [ 'paddle_sync_status' => 'failed', 'paddle_synced_at' => now()->toIso8601String(), 'paddle_error' => [ 'message' => $exception->getMessage(), 'class' => $exception::class, ], ]), ])->save(); throw $exception; } } /** * @param array $productOverrides * @param array $priceOverrides * @return array{product: array, price: array} */ protected function buildPayloadOverrides(PackageAddon $addon, array $productOverrides, array $priceOverrides): array { // Reuse Package model payload builder shape via duck typing $baseProduct = [ 'name' => $addon->label, 'description' => sprintf('Addon %s', $addon->key), 'type' => 'standard', 'custom_data' => [ 'addon_key' => $addon->key, 'increments' => $addon->increments, ], ]; $basePrice = [ 'description' => $addon->label, 'custom_data' => [ 'addon_key' => $addon->key, ], ]; return [ 'product' => array_merge($baseProduct, $productOverrides), 'price' => array_merge($basePrice, $priceOverrides), ]; } /** * @param array $productOverrides * @param array $priceOverrides */ protected function storeDryRunSnapshot(PaddleCatalogService $catalog, PackageAddon $addon, array $productOverrides, array $priceOverrides): void { $payloadOverrides = $this->buildPayloadOverrides($addon, $productOverrides, $priceOverrides); $addon->forceFill([ 'metadata' => array_merge($addon->metadata ?? [], [ 'paddle_sync_status' => 'dry-run', 'paddle_synced_at' => now()->toIso8601String(), 'paddle_snapshot' => [ 'dry_run' => true, 'payload' => $payloadOverrides, ], ]), ])->save(); Log::info('Paddle addon dry-run snapshot generated', [ 'addon_id' => $addon->id, ]); } }