feat: eBay business policies + per-adapter admin navigation
Some checks are pending
CI / test (push) Waiting to run
Some checks are pending
CI / test (push) Waiting to run
- ArticleTypePlatformConfig: fulfillmentPolicyId, paymentPolicyId, returnPolicyId, merchantLocationKey (all nullable) - EbayAccountApiClient: fetches Fulfillment/Payment/Return policies from eBay Account API (/sell/account/v1) - EbayInventoryApiClient: adds getLocations() - EbayPolicyProvider: aggregates choices with 5 min cache; returns empty array on API failure so the form degrades to TextField - EbayAdapter: reads real ArticleTypePlatformConfig (category ID no longer hardcoded), passes listingPolicies + merchantLocationKey into createOffer() when set - EbayArticleTypePlatformConfigCrudController: live policy dropdowns from EbayPolicyProvider; fallback to TextField with help text - DashboardController: eBay subMenu with Kategorie-Konfigurationen - 7 new unit tests for EbayAdapter policy scenarios Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
31c5116a1b
commit
6bd8e0bec8
15 changed files with 562 additions and 46 deletions
|
|
@ -120,6 +120,8 @@ services:
|
||||||
$adapters: !tagged_iterator app.channel_adapter
|
$adapters: !tagged_iterator app.channel_adapter
|
||||||
|
|
||||||
App\Infrastructure\Channel\Ebay\EbayAdapter:
|
App\Infrastructure\Channel\Ebay\EbayAdapter:
|
||||||
|
arguments:
|
||||||
|
$marketplaceId: '%env(EBAY_MARKETPLACE_ID)%'
|
||||||
tags: ['app.channel_adapter']
|
tags: ['app.channel_adapter']
|
||||||
|
|
||||||
App\Infrastructure\Channel\Ebay\EbayOAuthClient:
|
App\Infrastructure\Channel\Ebay\EbayOAuthClient:
|
||||||
|
|
@ -134,6 +136,15 @@ services:
|
||||||
$apiBaseUrl: '%env(EBAY_API_BASE_URL)%'
|
$apiBaseUrl: '%env(EBAY_API_BASE_URL)%'
|
||||||
$marketplaceId: '%env(EBAY_MARKETPLACE_ID)%'
|
$marketplaceId: '%env(EBAY_MARKETPLACE_ID)%'
|
||||||
|
|
||||||
|
App\Infrastructure\Channel\Ebay\EbayAccountApiClient:
|
||||||
|
arguments:
|
||||||
|
$apiBaseUrl: '%env(EBAY_API_BASE_URL)%'
|
||||||
|
$marketplaceId: '%env(EBAY_MARKETPLACE_ID)%'
|
||||||
|
|
||||||
|
App\Infrastructure\Channel\Ebay\EbayPolicyProvider:
|
||||||
|
arguments:
|
||||||
|
$cache: '@cache.app'
|
||||||
|
|
||||||
App\Infrastructure\Channel\Ebay\EbayTaxonomyService:
|
App\Infrastructure\Channel\Ebay\EbayTaxonomyService:
|
||||||
arguments:
|
arguments:
|
||||||
$apiBaseUrl: '%env(EBAY_API_BASE_URL)%'
|
$apiBaseUrl: '%env(EBAY_API_BASE_URL)%'
|
||||||
|
|
|
||||||
42
migrations/Version20260519070206.php
Normal file
42
migrations/Version20260519070206.php
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260519070206 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE app.article_type_ebay_mappings ALTER required DROP DEFAULT');
|
||||||
|
$this->addSql('ALTER INDEX app.article_type_ebay_mappings_article_type_id_ebay_aspect_name_key RENAME TO uq_ebay_mapping');
|
||||||
|
$this->addSql('ALTER TABLE app.article_type_platform_configs ADD fulfillment_policy_id VARCHAR(100) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE app.article_type_platform_configs ADD payment_policy_id VARCHAR(100) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE app.article_type_platform_configs ADD return_policy_id VARCHAR(100) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE app.article_type_platform_configs ADD merchant_location_key VARCHAR(100) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA logs_archive');
|
||||||
|
$this->addSql('ALTER TABLE app.article_type_ebay_mappings ALTER required SET DEFAULT false');
|
||||||
|
$this->addSql('ALTER INDEX app.uq_ebay_mapping RENAME TO article_type_ebay_mappings_article_type_id_ebay_aspect_name_key');
|
||||||
|
$this->addSql('ALTER TABLE app.article_type_platform_configs DROP fulfillment_policy_id');
|
||||||
|
$this->addSql('ALTER TABLE app.article_type_platform_configs DROP payment_policy_id');
|
||||||
|
$this->addSql('ALTER TABLE app.article_type_platform_configs DROP return_policy_id');
|
||||||
|
$this->addSql('ALTER TABLE app.article_type_platform_configs DROP merchant_location_key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,18 @@ class ArticleTypePlatformConfig
|
||||||
#[ORM\Column(type: 'string', length: 255)]
|
#[ORM\Column(type: 'string', length: 255)]
|
||||||
private string $categoryId;
|
private string $categoryId;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 100, nullable: true)]
|
||||||
|
private ?string $fulfillmentPolicyId = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 100, nullable: true)]
|
||||||
|
private ?string $paymentPolicyId = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 100, nullable: true)]
|
||||||
|
private ?string $returnPolicyId = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 100, nullable: true)]
|
||||||
|
private ?string $merchantLocationKey = null;
|
||||||
|
|
||||||
/** @var Collection<int, AttributeMapping> */
|
/** @var Collection<int, AttributeMapping> */
|
||||||
#[ORM\OneToMany(mappedBy: 'platformConfig', targetEntity: AttributeMapping::class, cascade: ['persist', 'remove'])]
|
#[ORM\OneToMany(mappedBy: 'platformConfig', targetEntity: AttributeMapping::class, cascade: ['persist', 'remove'])]
|
||||||
private Collection $attributeMappings;
|
private Collection $attributeMappings;
|
||||||
|
|
@ -68,6 +80,46 @@ class ArticleTypePlatformConfig
|
||||||
$this->categoryId = $categoryId;
|
$this->categoryId = $categoryId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFulfillmentPolicyId(): ?string
|
||||||
|
{
|
||||||
|
return $this->fulfillmentPolicyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFulfillmentPolicyId(?string $fulfillmentPolicyId): void
|
||||||
|
{
|
||||||
|
$this->fulfillmentPolicyId = $fulfillmentPolicyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPaymentPolicyId(): ?string
|
||||||
|
{
|
||||||
|
return $this->paymentPolicyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPaymentPolicyId(?string $paymentPolicyId): void
|
||||||
|
{
|
||||||
|
$this->paymentPolicyId = $paymentPolicyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReturnPolicyId(): ?string
|
||||||
|
{
|
||||||
|
return $this->returnPolicyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setReturnPolicyId(?string $returnPolicyId): void
|
||||||
|
{
|
||||||
|
$this->returnPolicyId = $returnPolicyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMerchantLocationKey(): ?string
|
||||||
|
{
|
||||||
|
return $this->merchantLocationKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMerchantLocationKey(?string $merchantLocationKey): void
|
||||||
|
{
|
||||||
|
$this->merchantLocationKey = $merchantLocationKey;
|
||||||
|
}
|
||||||
|
|
||||||
/** @return Collection<int, AttributeMapping> */
|
/** @return Collection<int, AttributeMapping> */
|
||||||
public function getAttributeMappings(): Collection
|
public function getAttributeMappings(): Collection
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Domain\Channel\Repository;
|
namespace App\Domain\Channel\Repository;
|
||||||
|
|
||||||
|
use App\Domain\Article\ArticleType;
|
||||||
use App\Domain\Channel\ArticleTypePlatformConfig;
|
use App\Domain\Channel\ArticleTypePlatformConfig;
|
||||||
use Symfony\Component\Uid\Uuid;
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
|
|
@ -16,6 +17,8 @@ interface ArticleTypePlatformConfigRepositoryInterface
|
||||||
/** @return list<ArticleTypePlatformConfig> */
|
/** @return list<ArticleTypePlatformConfig> */
|
||||||
public function findByArticleType(Uuid $articleTypeId): array;
|
public function findByArticleType(Uuid $articleTypeId): array;
|
||||||
|
|
||||||
|
public function findByArticleTypeAndPlatformType(ArticleType $articleType, string $platformType): ?ArticleTypePlatformConfig;
|
||||||
|
|
||||||
public function save(ArticleTypePlatformConfig $config): void;
|
public function save(ArticleTypePlatformConfig $config): void;
|
||||||
|
|
||||||
public function remove(ArticleTypePlatformConfig $config): void;
|
public function remove(ArticleTypePlatformConfig $config): void;
|
||||||
|
|
|
||||||
80
src/Infrastructure/Channel/Ebay/EbayAccountApiClient.php
Normal file
80
src/Infrastructure/Channel/Ebay/EbayAccountApiClient.php
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Infrastructure\Channel\Ebay;
|
||||||
|
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
final class EbayAccountApiClient
|
||||||
|
{
|
||||||
|
private const ACCOUNT_BASE = '/sell/account/v1';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly HttpClientInterface $httpClient,
|
||||||
|
private readonly EbayOAuthClient $oauthClient,
|
||||||
|
private readonly string $apiBaseUrl,
|
||||||
|
private readonly string $marketplaceId,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{fulfillmentPolicyId: string, name: string}>
|
||||||
|
*/
|
||||||
|
public function getFulfillmentPolicies(): array
|
||||||
|
{
|
||||||
|
$data = $this->request('GET', self::ACCOUNT_BASE.'/fulfillment_policy?marketplace_id='.$this->marketplaceId);
|
||||||
|
|
||||||
|
/** @var list<array{fulfillmentPolicyId: string, name: string}> */
|
||||||
|
return $data['fulfillmentPolicies'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{paymentPolicyId: string, name: string}>
|
||||||
|
*/
|
||||||
|
public function getPaymentPolicies(): array
|
||||||
|
{
|
||||||
|
$data = $this->request('GET', self::ACCOUNT_BASE.'/payment_policy?marketplace_id='.$this->marketplaceId);
|
||||||
|
|
||||||
|
/** @var list<array{paymentPolicyId: string, name: string}> */
|
||||||
|
return $data['paymentPolicies'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{returnPolicyId: string, name: string}>
|
||||||
|
*/
|
||||||
|
public function getReturnPolicies(): array
|
||||||
|
{
|
||||||
|
$data = $this->request('GET', self::ACCOUNT_BASE.'/return_policy?marketplace_id='.$this->marketplaceId);
|
||||||
|
|
||||||
|
/** @var list<array{returnPolicyId: string, name: string}> */
|
||||||
|
return $data['returnPolicies'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function request(string $method, string $path): array
|
||||||
|
{
|
||||||
|
$token = $this->oauthClient->getAccessToken();
|
||||||
|
|
||||||
|
$response = $this->httpClient->request($method, $this->apiBaseUrl.$path, [
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Bearer '.$token,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'X-EBAY-C-MARKETPLACE-ID' => $this->marketplaceId,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$statusCode = $response->getStatusCode();
|
||||||
|
if ($statusCode >= 400) {
|
||||||
|
$content = $response->getContent(false);
|
||||||
|
throw new \RuntimeException("eBay Account API error {$statusCode}: {$content}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var array<string, mixed> $data */
|
||||||
|
$data = $response->toArray();
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,12 +7,16 @@ namespace App\Infrastructure\Channel\Ebay;
|
||||||
use App\Application\Channel\ChannelAdapterInterface;
|
use App\Application\Channel\ChannelAdapterInterface;
|
||||||
use App\Domain\Article\Article;
|
use App\Domain\Article\Article;
|
||||||
use App\Domain\Article\ArticleCondition;
|
use App\Domain\Article\ArticleCondition;
|
||||||
|
use App\Domain\Article\ArticleTypeEbayMapping;
|
||||||
|
use App\Domain\Channel\Repository\ArticleTypePlatformConfigRepositoryInterface;
|
||||||
use App\Domain\Order\Order;
|
use App\Domain\Order\Order;
|
||||||
|
|
||||||
final class EbayAdapter implements ChannelAdapterInterface
|
final class EbayAdapter implements ChannelAdapterInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly EbayInventoryApiClient $apiClient,
|
private readonly EbayInventoryApiClient $apiClient,
|
||||||
|
private readonly ArticleTypePlatformConfigRepositoryInterface $platformConfigRepository,
|
||||||
|
private readonly string $marketplaceId,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,6 +28,14 @@ final class EbayAdapter implements ChannelAdapterInterface
|
||||||
public function publishListing(Article $article): string
|
public function publishListing(Article $article): string
|
||||||
{
|
{
|
||||||
$sku = $article->getSku();
|
$sku = $article->getSku();
|
||||||
|
$config = $this->platformConfigRepository->findByArticleTypeAndPlatformType(
|
||||||
|
$article->getArticleType(),
|
||||||
|
'ebay'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (null === $config) {
|
||||||
|
throw new \RuntimeException(\sprintf('No eBay platform config found for ArticleType "%s"', $article->getArticleType()->getName()));
|
||||||
|
}
|
||||||
|
|
||||||
$this->apiClient->upsertInventoryItem($sku, [
|
$this->apiClient->upsertInventoryItem($sku, [
|
||||||
'availability' => [
|
'availability' => [
|
||||||
|
|
@ -40,11 +52,18 @@ final class EbayAdapter implements ChannelAdapterInterface
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$offerId = $this->apiClient->createOffer([
|
$listingPolicies = array_filter([
|
||||||
|
'fulfillmentPolicyId' => $config->getFulfillmentPolicyId(),
|
||||||
|
'paymentPolicyId' => $config->getPaymentPolicyId(),
|
||||||
|
'returnPolicyId' => $config->getReturnPolicyId(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$offerBody = [
|
||||||
'sku' => $sku,
|
'sku' => $sku,
|
||||||
'marketplaceId' => 'EBAY_DE',
|
'marketplaceId' => $this->marketplaceId,
|
||||||
'format' => 'FIXED_PRICE',
|
'format' => 'FIXED_PRICE',
|
||||||
'availableQuantity' => $article->getStock(),
|
'availableQuantity' => $article->getStock(),
|
||||||
|
'categoryId' => $config->getCategoryId(),
|
||||||
'pricingSummary' => [
|
'pricingSummary' => [
|
||||||
'price' => [
|
'price' => [
|
||||||
'currency' => 'EUR',
|
'currency' => 'EUR',
|
||||||
|
|
@ -52,8 +71,17 @@ final class EbayAdapter implements ChannelAdapterInterface
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'listingDescription' => $article->getEbayDescription() ?? '',
|
'listingDescription' => $article->getEbayDescription() ?? '',
|
||||||
'categoryId' => $this->getCategoryId(),
|
];
|
||||||
]);
|
|
||||||
|
if ([] !== $listingPolicies) {
|
||||||
|
$offerBody['listingPolicies'] = $listingPolicies;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $config->getMerchantLocationKey()) {
|
||||||
|
$offerBody['merchantLocationKey'] = $config->getMerchantLocationKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
$offerId = $this->apiClient->createOffer($offerBody);
|
||||||
|
|
||||||
return $this->apiClient->publishOffer($offerId);
|
return $this->apiClient->publishOffer($offerId);
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +140,6 @@ final class EbayAdapter implements ChannelAdapterInterface
|
||||||
{
|
{
|
||||||
$aspects = [];
|
$aspects = [];
|
||||||
|
|
||||||
// Index attribute values by definition ID for O(1) lookup
|
|
||||||
$valuesByDefId = [];
|
$valuesByDefId = [];
|
||||||
foreach ($article->getAttributeValues() as $value) {
|
foreach ($article->getAttributeValues() as $value) {
|
||||||
$valuesByDefId[$value->getAttributeDefinition()->getId()->toRfc4122()] = $value->getValue();
|
$valuesByDefId[$value->getAttributeDefinition()->getId()->toRfc4122()] = $value->getValue();
|
||||||
|
|
@ -121,7 +148,7 @@ final class EbayAdapter implements ChannelAdapterInterface
|
||||||
foreach ($article->getArticleType()->getEbayMappings() as $mapping) {
|
foreach ($article->getArticleType()->getEbayMappings() as $mapping) {
|
||||||
$ebayName = $mapping->getEbayAspectName();
|
$ebayName = $mapping->getEbayAspectName();
|
||||||
|
|
||||||
if ($mapping->getSourceType() === \App\Domain\Article\ArticleTypeEbayMapping::SOURCE_ARTICLE_FIELD) {
|
if (ArticleTypeEbayMapping::SOURCE_ARTICLE_FIELD === $mapping->getSourceType()) {
|
||||||
$getter = 'get'.ucfirst((string) $mapping->getArticleFieldKey());
|
$getter = 'get'.ucfirst((string) $mapping->getArticleFieldKey());
|
||||||
if (!method_exists($article, $getter)) {
|
if (!method_exists($article, $getter)) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -144,9 +171,4 @@ final class EbayAdapter implements ChannelAdapterInterface
|
||||||
|
|
||||||
return $aspects;
|
return $aspects;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getCategoryId(): string
|
|
||||||
{
|
|
||||||
return '177';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,17 @@ class EbayInventoryApiClient
|
||||||
$this->request('POST', self::INVENTORY_BASE.'/bulk_update_price_quantity', $quantityUpdate);
|
$this->request('POST', self::INVENTORY_BASE.'/bulk_update_price_quantity', $quantityUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{merchantLocationKey: string, name: string}>
|
||||||
|
*/
|
||||||
|
public function getLocations(): array
|
||||||
|
{
|
||||||
|
$data = $this->request('GET', self::INVENTORY_BASE.'/location', []);
|
||||||
|
|
||||||
|
/** @var list<array{merchantLocationKey: string, name: string}> */
|
||||||
|
return $data['locations'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds tracking info to an order.
|
* Adds tracking info to an order.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
104
src/Infrastructure/Channel/Ebay/EbayPolicyProvider.php
Normal file
104
src/Infrastructure/Channel/Ebay/EbayPolicyProvider.php
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Infrastructure\Channel\Ebay;
|
||||||
|
|
||||||
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
|
|
||||||
|
final class EbayPolicyProvider
|
||||||
|
{
|
||||||
|
private const TTL = 300;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly EbayAccountApiClient $accountClient,
|
||||||
|
private readonly EbayInventoryApiClient $inventoryClient,
|
||||||
|
private readonly CacheItemPoolInterface $cache,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string> "Name (ID)" => policyId
|
||||||
|
*/
|
||||||
|
public function getFulfillmentChoices(): array
|
||||||
|
{
|
||||||
|
return $this->cached('ebay_policy_fulfillment', function (): array {
|
||||||
|
$choices = [];
|
||||||
|
foreach ($this->accountClient->getFulfillmentPolicies() as $policy) {
|
||||||
|
$choices[$policy['name'].' ('.$policy['fulfillmentPolicyId'].')'] = $policy['fulfillmentPolicyId'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $choices;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string> "Name (ID)" => policyId
|
||||||
|
*/
|
||||||
|
public function getPaymentChoices(): array
|
||||||
|
{
|
||||||
|
return $this->cached('ebay_policy_payment', function (): array {
|
||||||
|
$choices = [];
|
||||||
|
foreach ($this->accountClient->getPaymentPolicies() as $policy) {
|
||||||
|
$choices[$policy['name'].' ('.$policy['paymentPolicyId'].')'] = $policy['paymentPolicyId'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $choices;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string> "Name (ID)" => policyId
|
||||||
|
*/
|
||||||
|
public function getReturnChoices(): array
|
||||||
|
{
|
||||||
|
return $this->cached('ebay_policy_return', function (): array {
|
||||||
|
$choices = [];
|
||||||
|
foreach ($this->accountClient->getReturnPolicies() as $policy) {
|
||||||
|
$choices[$policy['name'].' ('.$policy['returnPolicyId'].')'] = $policy['returnPolicyId'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $choices;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string> "Name (key)" => merchantLocationKey
|
||||||
|
*/
|
||||||
|
public function getLocationChoices(): array
|
||||||
|
{
|
||||||
|
return $this->cached('ebay_policy_location', function (): array {
|
||||||
|
$choices = [];
|
||||||
|
foreach ($this->inventoryClient->getLocations() as $location) {
|
||||||
|
$choices[$location['name'].' ('.$location['merchantLocationKey'].')'] = $location['merchantLocationKey'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $choices;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callable(): array<string, string> $loader
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private function cached(string $key, callable $loader): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$item = $this->cache->getItem($key);
|
||||||
|
if ($item->isHit()) {
|
||||||
|
/** @var array<string, string> */
|
||||||
|
return $item->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
$choices = $loader();
|
||||||
|
$item->set($choices);
|
||||||
|
$item->expiresAfter(self::TTL);
|
||||||
|
$this->cache->save($item);
|
||||||
|
|
||||||
|
return $choices;
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -71,6 +71,9 @@ final class DashboardController extends AbstractDashboardController
|
||||||
yield MenuItem::linkTo(TranslationCrudController::class, $t('menu.translations'), 'fa fa-language');
|
yield MenuItem::linkTo(TranslationCrudController::class, $t('menu.translations'), 'fa fa-language');
|
||||||
yield MenuItem::linkTo(UserCrudController::class, $t('menu.users'), 'fa fa-users');
|
yield MenuItem::linkTo(UserCrudController::class, $t('menu.users'), 'fa fa-users');
|
||||||
yield MenuItem::linkTo(LogEntryCrudController::class, $t('menu.logs'), 'fa fa-list');
|
yield MenuItem::linkTo(LogEntryCrudController::class, $t('menu.logs'), 'fa fa-list');
|
||||||
|
yield MenuItem::subMenu($t('menu.section_ebay'), 'fa fa-store')->setSubItems([
|
||||||
|
MenuItem::linkTo(EbayArticleTypePlatformConfigCrudController::class, $t('menu.ebay_platform_configs'), 'fa fa-sliders'),
|
||||||
|
]);
|
||||||
yield MenuItem::section($t('menu.section_sales'));
|
yield MenuItem::section($t('menu.section_sales'));
|
||||||
yield MenuItem::linkTo(OrderCrudController::class, $t('menu.orders'), 'fa fa-shopping-cart');
|
yield MenuItem::linkTo(OrderCrudController::class, $t('menu.orders'), 'fa fa-shopping-cart');
|
||||||
yield MenuItem::linkTo(CustomerCrudController::class, $t('menu.customers'), 'fa fa-users');
|
yield MenuItem::linkTo(CustomerCrudController::class, $t('menu.customers'), 'fa fa-users');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Infrastructure\Http\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Domain\Channel\ArticleTypePlatformConfig;
|
||||||
|
use App\Infrastructure\Channel\Ebay\EbayPolicyProvider;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Collection\FilterCollection;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository;
|
||||||
|
|
||||||
|
/** @extends AbstractCrudController<ArticleTypePlatformConfig> */
|
||||||
|
final class EbayArticleTypePlatformConfigCrudController extends AbstractCrudController
|
||||||
|
{
|
||||||
|
public function __construct(private readonly EbayPolicyProvider $policyProvider)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getEntityFqcn(): string
|
||||||
|
{
|
||||||
|
return ArticleTypePlatformConfig::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureCrud(Crud $crud): Crud
|
||||||
|
{
|
||||||
|
return $crud
|
||||||
|
->setEntityLabelInSingular('eBay Kategorie-Konfiguration')
|
||||||
|
->setEntityLabelInPlural('eBay Kategorie-Konfigurationen')
|
||||||
|
->setDefaultSort(['id' => 'ASC']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->container->get(EntityRepository::class)->createQueryBuilder($searchDto, $entityDto, $fields, $filters);
|
||||||
|
$qb->join(\sprintf('%s.platform', $qb->getRootAliases()[0]), 'ebay_platform')
|
||||||
|
->andWhere('ebay_platform.type = :platformType')
|
||||||
|
->setParameter('platformType', 'ebay');
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureFields(string $pageName): iterable
|
||||||
|
{
|
||||||
|
yield AssociationField::new('articleType', 'Artikel-Typ');
|
||||||
|
yield TextField::new('categoryId', 'eBay Kategorie-ID');
|
||||||
|
|
||||||
|
$fulfillmentChoices = $this->policyProvider->getFulfillmentChoices();
|
||||||
|
if ([] !== $fulfillmentChoices) {
|
||||||
|
yield ChoiceField::new('fulfillmentPolicyId', 'Versand-Policy')
|
||||||
|
->setChoices($fulfillmentChoices)
|
||||||
|
->setRequired(false);
|
||||||
|
} else {
|
||||||
|
yield TextField::new('fulfillmentPolicyId', 'Versand-Policy (ID)')
|
||||||
|
->setHelp('eBay-API nicht erreichbar — ID manuell eingeben')
|
||||||
|
->setRequired(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$paymentChoices = $this->policyProvider->getPaymentChoices();
|
||||||
|
if ([] !== $paymentChoices) {
|
||||||
|
yield ChoiceField::new('paymentPolicyId', 'Zahlungs-Policy')
|
||||||
|
->setChoices($paymentChoices)
|
||||||
|
->setRequired(false);
|
||||||
|
} else {
|
||||||
|
yield TextField::new('paymentPolicyId', 'Zahlungs-Policy (ID)')
|
||||||
|
->setHelp('eBay-API nicht erreichbar — ID manuell eingeben')
|
||||||
|
->setRequired(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$returnChoices = $this->policyProvider->getReturnChoices();
|
||||||
|
if ([] !== $returnChoices) {
|
||||||
|
yield ChoiceField::new('returnPolicyId', 'Rückgabe-Policy')
|
||||||
|
->setChoices($returnChoices)
|
||||||
|
->setRequired(false);
|
||||||
|
} else {
|
||||||
|
yield TextField::new('returnPolicyId', 'Rückgabe-Policy (ID)')
|
||||||
|
->setHelp('eBay-API nicht erreichbar — ID manuell eingeben')
|
||||||
|
->setRequired(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$locationChoices = $this->policyProvider->getLocationChoices();
|
||||||
|
if ([] !== $locationChoices) {
|
||||||
|
yield ChoiceField::new('merchantLocationKey', 'Lagerstandort')
|
||||||
|
->setChoices($locationChoices)
|
||||||
|
->setRequired(false);
|
||||||
|
} else {
|
||||||
|
yield TextField::new('merchantLocationKey', 'Lagerstandort (Key)')
|
||||||
|
->setHelp('eBay-API nicht erreichbar — Key manuell eingeben')
|
||||||
|
->setRequired(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Infrastructure\Persistence\Repository;
|
namespace App\Infrastructure\Persistence\Repository;
|
||||||
|
|
||||||
|
use App\Domain\Article\ArticleType;
|
||||||
use App\Domain\Channel\ArticleTypePlatformConfig;
|
use App\Domain\Channel\ArticleTypePlatformConfig;
|
||||||
use App\Domain\Channel\Repository\ArticleTypePlatformConfigRepositoryInterface;
|
use App\Domain\Channel\Repository\ArticleTypePlatformConfigRepositoryInterface;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
@ -46,6 +47,21 @@ final class DoctrineArticleTypePlatformConfigRepository implements ArticleTypePl
|
||||||
->getResult();
|
->getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function findByArticleTypeAndPlatformType(ArticleType $articleType, string $platformType): ?ArticleTypePlatformConfig
|
||||||
|
{
|
||||||
|
/** @var ArticleTypePlatformConfig|null */
|
||||||
|
return $this->em->getRepository(ArticleTypePlatformConfig::class)
|
||||||
|
->createQueryBuilder('c')
|
||||||
|
->join('c.platform', 'p')
|
||||||
|
->where('c.articleType = :articleType')
|
||||||
|
->andWhere('p.type = :platformType')
|
||||||
|
->setParameter('articleType', $articleType)
|
||||||
|
->setParameter('platformType', $platformType)
|
||||||
|
->setMaxResults(1)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
|
||||||
public function save(ArticleTypePlatformConfig $config): void
|
public function save(ArticleTypePlatformConfig $config): void
|
||||||
{
|
{
|
||||||
$this->em->persist($config);
|
$this->em->persist($config);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ namespace App\Tests\Integration\Infrastructure\Channel\Ebay;
|
||||||
use App\Domain\Article\Article;
|
use App\Domain\Article\Article;
|
||||||
use App\Domain\Article\ArticleCondition;
|
use App\Domain\Article\ArticleCondition;
|
||||||
use App\Domain\Article\ArticleType;
|
use App\Domain\Article\ArticleType;
|
||||||
|
use App\Domain\Channel\ArticleTypePlatformConfig;
|
||||||
|
use App\Domain\Channel\Platform;
|
||||||
|
use App\Domain\Channel\Repository\ArticleTypePlatformConfigRepositoryInterface;
|
||||||
use App\Infrastructure\Channel\Ebay\EbayAdapter;
|
use App\Infrastructure\Channel\Ebay\EbayAdapter;
|
||||||
use App\Infrastructure\Channel\Ebay\EbayInventoryApiClient;
|
use App\Infrastructure\Channel\Ebay\EbayInventoryApiClient;
|
||||||
use App\Infrastructure\Channel\Ebay\EbayOAuthClient;
|
use App\Infrastructure\Channel\Ebay\EbayOAuthClient;
|
||||||
|
|
@ -35,45 +38,47 @@ final class EbayAdapterIntegrationTest extends TestCase
|
||||||
private string $createdListingId = '';
|
private string $createdListingId = '';
|
||||||
private bool $userTokenAvailable = false;
|
private bool $userTokenAvailable = false;
|
||||||
|
|
||||||
|
private static function env(string $key, string $default = ''): string
|
||||||
|
{
|
||||||
|
$val = $_SERVER[$key] ?? getenv($key);
|
||||||
|
|
||||||
|
return \is_string($val) ? $val : $default;
|
||||||
|
}
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$clientId = $_SERVER['EBAY_CLIENT_ID'] ?? getenv('EBAY_CLIENT_ID');
|
$clientId = self::env('EBAY_CLIENT_ID');
|
||||||
$clientSecret = $_SERVER['EBAY_CLIENT_SECRET'] ?? getenv('EBAY_CLIENT_SECRET');
|
$clientSecret = self::env('EBAY_CLIENT_SECRET');
|
||||||
$apiBaseUrl = $_SERVER['EBAY_API_BASE_URL'] ?? getenv('EBAY_API_BASE_URL');
|
$apiBaseUrl = self::env('EBAY_API_BASE_URL');
|
||||||
$oauthBaseUrl = $_SERVER['EBAY_OAUTH_BASE_URL'] ?? getenv('EBAY_OAUTH_BASE_URL');
|
$oauthBaseUrl = self::env('EBAY_OAUTH_BASE_URL');
|
||||||
$marketplaceId = $_SERVER['EBAY_MARKETPLACE_ID'] ?? getenv('EBAY_MARKETPLACE_ID') ?: 'EBAY_DE';
|
$marketplaceId = self::env('EBAY_MARKETPLACE_ID', 'EBAY_DE');
|
||||||
|
|
||||||
if (!$clientId || !$clientSecret || !$apiBaseUrl || !$oauthBaseUrl) {
|
if ('' === $clientId || '' === $clientSecret || '' === $apiBaseUrl || '' === $oauthBaseUrl) {
|
||||||
$this->markTestSkipped('EBAY_* env vars not set');
|
$this->markTestSkipped('EBAY_* env vars not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
$http = HttpClient::create();
|
$http = HttpClient::create();
|
||||||
$cache = new ArrayAdapter();
|
$cache = new ArrayAdapter();
|
||||||
|
|
||||||
$oauth = new EbayOAuthClient(
|
$oauth = new EbayOAuthClient($http, $cache, $clientId, $clientSecret, $oauthBaseUrl);
|
||||||
$http,
|
|
||||||
$cache,
|
|
||||||
(string) $clientId,
|
|
||||||
(string) $clientSecret,
|
|
||||||
(string) $oauthBaseUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->apiClient = new EbayInventoryApiClient(
|
$this->apiClient = new EbayInventoryApiClient($http, $oauth, $apiBaseUrl, $marketplaceId);
|
||||||
$http,
|
|
||||||
$oauth,
|
|
||||||
(string) $apiBaseUrl,
|
|
||||||
(string) $marketplaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->adapter = new EbayAdapter($this->apiClient);
|
$articleType = new ArticleType('Notebook');
|
||||||
|
$platform = new Platform('ebay', 'eBay');
|
||||||
|
// Sandbox category 177 = Notebooks; no policies needed for sandbox withdraw/stock tests
|
||||||
|
$platformConfig = new ArticleTypePlatformConfig($articleType, $platform, '177');
|
||||||
|
|
||||||
$userToken = $_SERVER['EBAY_USER_TOKEN'] ?? getenv('EBAY_USER_TOKEN');
|
$configRepo = $this->createStub(ArticleTypePlatformConfigRepositoryInterface::class);
|
||||||
$this->userTokenAvailable = (bool) $userToken;
|
$configRepo->method('findByArticleTypeAndPlatformType')->willReturn($platformConfig);
|
||||||
|
|
||||||
// Build a realistic sandbox article for listing tests
|
$this->adapter = new EbayAdapter($this->apiClient, $configRepo, $marketplaceId);
|
||||||
$type = new ArticleType('Notebook');
|
|
||||||
|
$this->userTokenAvailable = '' !== self::env('EBAY_USER_TOKEN');
|
||||||
|
|
||||||
|
// Build a realistic sandbox article for listing tests (reuse same $articleType so config matches)
|
||||||
$this->article = new Article(
|
$this->article = new Article(
|
||||||
$type,
|
$articleType,
|
||||||
'SS3K-TEST-'.time(),
|
'SS3K-TEST-'.time(),
|
||||||
'INV-TEST-001',
|
'INV-TEST-001',
|
||||||
1,
|
1,
|
||||||
|
|
@ -98,7 +103,7 @@ final class EbayAdapterIntegrationTest extends TestCase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_publish_listing_creates_live_sandbox_listing(): void
|
public function testPublishListingCreatesLiveSandboxListing(): void
|
||||||
{
|
{
|
||||||
if (!$this->userTokenAvailable) {
|
if (!$this->userTokenAvailable) {
|
||||||
$this->markTestSkipped(
|
$this->markTestSkipped(
|
||||||
|
|
@ -116,7 +121,7 @@ final class EbayAdapterIntegrationTest extends TestCase
|
||||||
$this->article->setEbayListingId($listingId);
|
$this->article->setEbayListingId($listingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_update_stock_changes_quantity(): void
|
public function testUpdateStockChangesQuantity(): void
|
||||||
{
|
{
|
||||||
if (!$this->userTokenAvailable) {
|
if (!$this->userTokenAvailable) {
|
||||||
$this->markTestSkipped('EBAY_USER_TOKEN not set — see test_publish_listing_creates_live_sandbox_listing');
|
$this->markTestSkipped('EBAY_USER_TOKEN not set — see test_publish_listing_creates_live_sandbox_listing');
|
||||||
|
|
@ -131,7 +136,7 @@ final class EbayAdapterIntegrationTest extends TestCase
|
||||||
$this->addToAssertionCount(1);
|
$this->addToAssertionCount(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_deactivate_listing_withdraws_offer(): void
|
public function testDeactivateListingWithdrawsOffer(): void
|
||||||
{
|
{
|
||||||
if (!$this->userTokenAvailable) {
|
if (!$this->userTokenAvailable) {
|
||||||
$this->markTestSkipped('EBAY_USER_TOKEN not set — see test_publish_listing_creates_live_sandbox_listing');
|
$this->markTestSkipped('EBAY_USER_TOKEN not set — see test_publish_listing_creates_live_sandbox_listing');
|
||||||
|
|
@ -146,7 +151,7 @@ final class EbayAdapterIntegrationTest extends TestCase
|
||||||
$this->addToAssertionCount(1);
|
$this->addToAssertionCount(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_deactivate_listing_is_noop_when_no_listing_id(): void
|
public function testDeactivateListingIsNoopWhenNoListingId(): void
|
||||||
{
|
{
|
||||||
// No listing ID set — should silently return, no API call
|
// No listing ID set — should silently return, no API call
|
||||||
$this->adapter->deactivateListing($this->article);
|
$this->adapter->deactivateListing($this->article);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ namespace App\Tests\Unit\Infrastructure\Channel\Ebay;
|
||||||
use App\Domain\Article\Article;
|
use App\Domain\Article\Article;
|
||||||
use App\Domain\Article\ArticleCondition;
|
use App\Domain\Article\ArticleCondition;
|
||||||
use App\Domain\Article\ArticleType;
|
use App\Domain\Article\ArticleType;
|
||||||
|
use App\Domain\Channel\ArticleTypePlatformConfig;
|
||||||
|
use App\Domain\Channel\Platform;
|
||||||
|
use App\Domain\Channel\Repository\ArticleTypePlatformConfigRepositoryInterface;
|
||||||
use App\Infrastructure\Channel\Ebay\EbayAdapter;
|
use App\Infrastructure\Channel\Ebay\EbayAdapter;
|
||||||
use App\Infrastructure\Channel\Ebay\EbayInventoryApiClient;
|
use App\Infrastructure\Channel\Ebay\EbayInventoryApiClient;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
|
@ -15,25 +18,35 @@ use PHPUnit\Framework\TestCase;
|
||||||
final class EbayAdapterTest extends TestCase
|
final class EbayAdapterTest extends TestCase
|
||||||
{
|
{
|
||||||
private EbayInventoryApiClient&MockObject $apiClient;
|
private EbayInventoryApiClient&MockObject $apiClient;
|
||||||
|
private ArticleTypePlatformConfigRepositoryInterface&MockObject $configRepo;
|
||||||
private EbayAdapter $adapter;
|
private EbayAdapter $adapter;
|
||||||
private Article $article;
|
private Article $article;
|
||||||
|
private ArticleTypePlatformConfig $platformConfig;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->apiClient = $this->createMock(EbayInventoryApiClient::class);
|
$this->apiClient = $this->createMock(EbayInventoryApiClient::class);
|
||||||
$this->adapter = new EbayAdapter($this->apiClient);
|
$this->configRepo = $this->createMock(ArticleTypePlatformConfigRepositoryInterface::class);
|
||||||
$this->article = new Article(new ArticleType('Notebook'), 'NB-001', 'INV-001', 1, ArticleCondition::Good);
|
$this->adapter = new EbayAdapter($this->apiClient, $this->configRepo, 'EBAY_DE');
|
||||||
|
|
||||||
|
$articleType = new ArticleType('Notebook');
|
||||||
|
$platform = new Platform('ebay', 'eBay');
|
||||||
|
$this->platformConfig = new ArticleTypePlatformConfig($articleType, $platform, '177');
|
||||||
|
|
||||||
|
$this->article = new Article($articleType, 'NB-001', 'INV-001', 1, ArticleCondition::Good);
|
||||||
$this->article->setEbayTitle('Dell Latitude 5520');
|
$this->article->setEbayTitle('Dell Latitude 5520');
|
||||||
$this->article->setListingPrice('299.00');
|
$this->article->setListingPrice('299.00');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_get_type_returns_ebay(): void
|
public function testGetTypeReturnsEbay(): void
|
||||||
{
|
{
|
||||||
$this->assertSame('ebay', $this->adapter->getType());
|
$this->assertSame('ebay', $this->adapter->getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_publish_listing_calls_upsert_create_and_publish(): void
|
public function testPublishListingCallsUpsertCreateAndPublish(): void
|
||||||
{
|
{
|
||||||
|
$this->configRepo->method('findByArticleTypeAndPlatformType')->willReturn($this->platformConfig);
|
||||||
|
|
||||||
$this->apiClient->expects($this->once())->method('upsertInventoryItem');
|
$this->apiClient->expects($this->once())->method('upsertInventoryItem');
|
||||||
$this->apiClient->expects($this->once())->method('createOffer')->willReturn('offer-123');
|
$this->apiClient->expects($this->once())->method('createOffer')->willReturn('offer-123');
|
||||||
$this->apiClient->expects($this->once())->method('publishOffer')->with('offer-123')->willReturn('listing-456');
|
$this->apiClient->expects($this->once())->method('publishOffer')->with('offer-123')->willReturn('listing-456');
|
||||||
|
|
@ -43,7 +56,57 @@ final class EbayAdapterTest extends TestCase
|
||||||
$this->assertSame('listing-456', $listingId);
|
$this->assertSame('listing-456', $listingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_deactivate_listing_withdraws_offer(): void
|
public function testPublishListingThrowsWhenNoPlatformConfig(): void
|
||||||
|
{
|
||||||
|
$this->configRepo->method('findByArticleTypeAndPlatformType')->willReturn(null);
|
||||||
|
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('No eBay platform config found for ArticleType');
|
||||||
|
|
||||||
|
$this->adapter->publishListing($this->article);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPublishListingIncludesPoliciesWhenSet(): void
|
||||||
|
{
|
||||||
|
$this->platformConfig->setFulfillmentPolicyId('FP-123');
|
||||||
|
$this->platformConfig->setPaymentPolicyId('PP-456');
|
||||||
|
$this->platformConfig->setReturnPolicyId('RP-789');
|
||||||
|
$this->platformConfig->setMerchantLocationKey('WAREHOUSE-1');
|
||||||
|
$this->configRepo->method('findByArticleTypeAndPlatformType')->willReturn($this->platformConfig);
|
||||||
|
|
||||||
|
$this->apiClient->expects($this->once())->method('upsertInventoryItem');
|
||||||
|
$this->apiClient->expects($this->once())
|
||||||
|
->method('createOffer')
|
||||||
|
->with($this->callback(static function (array $body): bool {
|
||||||
|
return isset($body['listingPolicies']['fulfillmentPolicyId'])
|
||||||
|
&& 'FP-123' === $body['listingPolicies']['fulfillmentPolicyId']
|
||||||
|
&& 'PP-456' === $body['listingPolicies']['paymentPolicyId']
|
||||||
|
&& 'RP-789' === $body['listingPolicies']['returnPolicyId']
|
||||||
|
&& 'WAREHOUSE-1' === $body['merchantLocationKey'];
|
||||||
|
}))
|
||||||
|
->willReturn('offer-123');
|
||||||
|
$this->apiClient->method('publishOffer')->willReturn('listing-456');
|
||||||
|
|
||||||
|
$this->adapter->publishListing($this->article);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPublishListingOmitsPoliciesWhenNotSet(): void
|
||||||
|
{
|
||||||
|
$this->configRepo->method('findByArticleTypeAndPlatformType')->willReturn($this->platformConfig);
|
||||||
|
|
||||||
|
$this->apiClient->expects($this->once())->method('upsertInventoryItem');
|
||||||
|
$this->apiClient->expects($this->once())
|
||||||
|
->method('createOffer')
|
||||||
|
->with($this->callback(static function (array $body): bool {
|
||||||
|
return !isset($body['listingPolicies']) && !isset($body['merchantLocationKey']);
|
||||||
|
}))
|
||||||
|
->willReturn('offer-123');
|
||||||
|
$this->apiClient->method('publishOffer')->willReturn('listing-456');
|
||||||
|
|
||||||
|
$this->adapter->publishListing($this->article);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeactivateListingWithdrawsOffer(): void
|
||||||
{
|
{
|
||||||
$this->article->setEbayListingId('offer-123');
|
$this->article->setEbayListingId('offer-123');
|
||||||
|
|
||||||
|
|
@ -52,7 +115,7 @@ final class EbayAdapterTest extends TestCase
|
||||||
$this->adapter->deactivateListing($this->article);
|
$this->adapter->deactivateListing($this->article);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_deactivate_listing_is_noop_when_no_listing_id(): void
|
public function testDeactivateListingIsNoopWhenNoListingId(): void
|
||||||
{
|
{
|
||||||
$this->apiClient->expects($this->never())->method('withdrawOffer');
|
$this->apiClient->expects($this->never())->method('withdrawOffer');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ menu.ai_prompts: 'KI-Prompts'
|
||||||
menu.translations: Übersetzungen
|
menu.translations: Übersetzungen
|
||||||
menu.users: Benutzer
|
menu.users: Benutzer
|
||||||
menu.logs: Logs
|
menu.logs: Logs
|
||||||
|
menu.section_ebay: eBay
|
||||||
|
menu.ebay_platform_configs: 'Kategorie-Konfigurationen'
|
||||||
menu.section_sales: Verkauf
|
menu.section_sales: Verkauf
|
||||||
menu.orders: Bestellungen
|
menu.orders: Bestellungen
|
||||||
menu.customers: Kunden
|
menu.customers: Kunden
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ menu.ai_prompts: 'AI Prompts'
|
||||||
menu.translations: Translations
|
menu.translations: Translations
|
||||||
menu.users: Users
|
menu.users: Users
|
||||||
menu.logs: Logs
|
menu.logs: Logs
|
||||||
|
menu.section_ebay: eBay
|
||||||
|
menu.ebay_platform_configs: 'Category Configurations'
|
||||||
menu.section_sales: Sales
|
menu.section_sales: Sales
|
||||||
menu.orders: Orders
|
menu.orders: Orders
|
||||||
menu.customers: Customers
|
menu.customers: Customers
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue