diff --git a/composer.lock b/composer.lock index b99068b..602c015 100644 --- a/composer.lock +++ b/composer.lock @@ -2762,12 +2762,12 @@ "source": { "type": "git", "url": "https://github.com/strictify/form-mapper-bundle.git", - "reference": "939818b29cafffcba94d93334b56859e0f45eeab" + "reference": "485ef9a26dc50f20fc4ff3f9e470cf742015bb35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/strictify/form-mapper-bundle/zipball/939818b29cafffcba94d93334b56859e0f45eeab", - "reference": "939818b29cafffcba94d93334b56859e0f45eeab", + "url": "https://api.github.com/repos/strictify/form-mapper-bundle/zipball/485ef9a26dc50f20fc4ff3f9e470cf742015bb35", + "reference": "485ef9a26dc50f20fc4ff3f9e470cf742015bb35", "shasum": "" }, "require": { @@ -2827,7 +2827,7 @@ "source": "https://github.com/strictify/form-mapper-bundle/tree/master", "issues": "https://github.com/strictify/form-mapper-bundle/issues" }, - "time": "2024-03-11T16:57:31+00:00" + "time": "2024-04-08T10:20:01+00:00" }, { "name": "strictify/lazy", diff --git a/functions.php b/functions.php index 438bf51..b3d11d1 100644 --- a/functions.php +++ b/functions.php @@ -162,3 +162,17 @@ function implode_non_empty(string $separator, array $parts): ?string return implode($separator, $filtered) ?: null; } + +/** + * @param non-empty-list $lookup + */ +function any_key_exists(array $lookup, array $haystack): bool +{ + foreach ($lookup as $item) { + if (array_key_exists($item, $haystack)) { + return true; + } + } + + return false; +} diff --git a/src/Command/DummyCommand.php b/src/Command/DummyCommand.php index 12c7e16..70e620f 100644 --- a/src/Command/DummyCommand.php +++ b/src/Command/DummyCommand.php @@ -1,11 +1,13 @@ -companyRepository->findOneBy(['name' => 'Strictify']) ?? throw new LogicException(); - $this->zohoSync->downloadAll($company); + + $this->zohoImprovedManager->downloadAll($company); return Command::SUCCESS; } diff --git a/src/DTO/Zoho/Item.php b/src/DTO/Zoho/Item.php index c4e37a1..cac7f99 100644 --- a/src/DTO/Zoho/Item.php +++ b/src/DTO/Zoho/Item.php @@ -4,21 +4,28 @@ namespace App\DTO\Zoho; +use Override; use function is_string; -class Item +class Item implements ZohoSingleResultInterface { + /** + * @param non-empty-string|int $itemId + */ public function __construct( - private string $itemId, + private string|int $itemId, private string $name, private ?string $description, private ?float $rate, - private ?string $taxId, + private string|int|null $taxId, ) { } - public function getItemId(): string + /** + * @return non-empty-string|int + */ + public function getItemId(): string|int { return $this->itemId; } @@ -39,9 +46,9 @@ public function getRate(): ?float } /** - * @return non-empty-string|null + * @return non-empty-string|int|null */ - public function getTaxId(): ?string + public function getTaxId(): string|int|null { $taxId = $this->taxId; if (is_string($taxId) && $taxId !== '') { @@ -50,4 +57,10 @@ public function getTaxId(): ?string return null; } + + #[Override] + public function getId(): string + { + return (string)$this->getItemId(); + } } diff --git a/src/DTO/Zoho/Items.php b/src/DTO/Zoho/Items.php index b860311..def6ed7 100644 --- a/src/DTO/Zoho/Items.php +++ b/src/DTO/Zoho/Items.php @@ -4,7 +4,12 @@ namespace App\DTO\Zoho; -class Items +use LogicException; + +/** + * @implements ZohoMappingInterface + */ +class Items implements ZohoMappingInterface { /** * @param list $items @@ -12,8 +17,19 @@ class Items public function __construct( public int $code, public string $message, - public array $items, + public ?Item $item = null, + public array $items = [], ) { } + + public function getOne(): object + { + return $this->item ?? throw new LogicException(); + } + + public function getMany(): array + { + return $this->items; + } } diff --git a/src/DTO/Zoho/Tax.php b/src/DTO/Zoho/Tax.php index 486a5d8..8592aae 100644 --- a/src/DTO/Zoho/Tax.php +++ b/src/DTO/Zoho/Tax.php @@ -4,17 +4,25 @@ namespace App\DTO\Zoho; -class Tax +use Override; + +class Tax implements ZohoSingleResultInterface { + /** + * @param non-empty-string|int $taxId + */ public function __construct( - private string $taxId, + private string|int $taxId, private string $taxName, private float $taxPercentage, ) { } - public function getTaxId(): string + /** + * @return non-empty-string|int + */ + public function getTaxId(): string|int { return $this->taxId; } @@ -28,4 +36,10 @@ public function getTaxPercentage(): float { return $this->taxPercentage; } + + #[Override] + public function getId(): string + { + return (string)$this->getTaxId(); + } } diff --git a/src/DTO/Zoho/Taxes.php b/src/DTO/Zoho/Taxes.php index 6274491..c7bb7a6 100644 --- a/src/DTO/Zoho/Taxes.php +++ b/src/DTO/Zoho/Taxes.php @@ -4,7 +4,12 @@ namespace App\DTO\Zoho; -class Taxes +use LogicException; + +/** + * @implements ZohoMappingInterface + */ +class Taxes implements ZohoMappingInterface { /** * @param list $taxes @@ -12,8 +17,19 @@ class Taxes public function __construct( public int $code, public string $message, - public array $taxes, + private array $taxes, + private ?Tax $tax = null, ) { } + + public function getOne(): object + { + return $this->tax ?? throw new LogicException(); + } + + public function getMany(): array + { + return $this->taxes; + } } diff --git a/src/DTO/Zoho/Warehouse.php b/src/DTO/Zoho/Warehouse.php index b6cb17b..e05995d 100644 --- a/src/DTO/Zoho/Warehouse.php +++ b/src/DTO/Zoho/Warehouse.php @@ -4,22 +4,33 @@ namespace App\DTO\Zoho; -class Warehouse +class Warehouse implements ZohoSingleResultInterface { + /** + * @param non-empty-string|int $warehouseId + */ public function __construct( - private string $warehouseId, + private string|int $warehouseId, private string $warehouseName, ) { } + /** + * @return non-empty-string + */ public function getWarehouseId(): string { - return $this->warehouseId; + return (string)$this->warehouseId; } public function getName(): string { return $this->warehouseName; } + + public function getId(): string + { + return (string)$this->warehouseId; + } } diff --git a/src/DTO/Zoho/Warehouses.php b/src/DTO/Zoho/Warehouses.php index 4273abf..4ba5a06 100644 --- a/src/DTO/Zoho/Warehouses.php +++ b/src/DTO/Zoho/Warehouses.php @@ -4,7 +4,12 @@ namespace App\DTO\Zoho; -class Warehouses +use LogicException; + +/** + * @implements ZohoMappingInterface + */ +class Warehouses implements ZohoMappingInterface { /** * @param list $warehouses @@ -12,8 +17,19 @@ class Warehouses public function __construct( public int $code, public string $message, - public array $warehouses, + private ?Warehouse $warehouse = null, + public array $warehouses = [], ) { } + + public function getOne(): object + { + return $this->warehouse ?? throw new LogicException(); + } + + public function getMany(): array + { + return $this->warehouses; + } } diff --git a/src/DTO/Zoho/ZohoMappingInterface.php b/src/DTO/Zoho/ZohoMappingInterface.php new file mode 100644 index 0000000..044409d --- /dev/null +++ b/src/DTO/Zoho/ZohoMappingInterface.php @@ -0,0 +1,21 @@ + + */ + public function getMany(): array; +} diff --git a/src/DTO/Zoho/ZohoSingleResultInterface.php b/src/DTO/Zoho/ZohoSingleResultInterface.php new file mode 100644 index 0000000..f29ded3 --- /dev/null +++ b/src/DTO/Zoho/ZohoSingleResultInterface.php @@ -0,0 +1,13 @@ +id ??= $this->doCreateUuid(); diff --git a/src/Entity/Product/Product.php b/src/Entity/Product/Product.php index dfab763..c509494 100644 --- a/src/Entity/Product/Product.php +++ b/src/Entity/Product/Product.php @@ -77,6 +77,9 @@ public function setDescription(?string $description): void } #[Override] + /** + * @return non-empty-string|null + */ public function getZohoId(): ?string { $zohoId = $this->zohoId; diff --git a/src/Entity/ZohoAwareInterface.php b/src/Entity/ZohoAwareInterface.php index f875bed..f07799b 100644 --- a/src/Entity/ZohoAwareInterface.php +++ b/src/Entity/ZohoAwareInterface.php @@ -4,10 +4,19 @@ namespace App\Entity; +use App\Entity\Company\Company; + interface ZohoAwareInterface { + public function getCompany(): Company; + /** * @return non-empty-string|null */ public function getZohoId(): ?string; + + /** + * @return non-empty-string + */ + public function getId(): string; } diff --git a/src/Form/DataComparator/MoneyComparator.php b/src/Form/DataComparator/MoneyComparator.php index 042435c..db6a96c 100644 --- a/src/Form/DataComparator/MoneyComparator.php +++ b/src/Form/DataComparator/MoneyComparator.php @@ -7,9 +7,7 @@ use Override; use Money\Money; use Strictify\FormMapper\Service\Comparator\DataComparatorInterface; -use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; -#[AutoconfigureTag('strictify_form_mapper.comparator')] class MoneyComparator implements DataComparatorInterface { #[Override] diff --git a/src/Message/Zoho/ZohoPutEntityMessage.php b/src/Message/Zoho/ZohoPutEntityMessage.php new file mode 100644 index 0000000..fa8d1e0 --- /dev/null +++ b/src/Message/Zoho/ZohoPutEntityMessage.php @@ -0,0 +1,60 @@ + + */ + private string $class; + + /** + * @var non-empty-string + */ + private string $zohoId; + + /** + * @param TAction $action + */ + public function __construct(ZohoAwareInterface $entity, #[ExpectedValues(['put', 'delete'])] private string $action = 'put') + { + $this->class = get_class($entity); + $this->zohoId = $entity->getZohoId() ?? throw new LogicException(); + } + + /** + * @return class-string + */ + public function getClass(): string + { + return $this->class; + } + + /** + * @return non-empty-string + */ + public function getZohoId(): string + { + return $this->zohoId; + } + + /** + * @return TAction + */ + public function getAction(): string + { + return $this->action; + } +} diff --git a/src/Message/Zoho/ZohoSyncProductMessage.php b/src/Message/Zoho/ZohoSyncProductMessage.php deleted file mode 100644 index 34d7512..0000000 --- a/src/Message/Zoho/ZohoSyncProductMessage.php +++ /dev/null @@ -1,38 +0,0 @@ -id = $product->getId(); - } - - public function getId(): string - { - return $this->id; - } - - /** - * @return TAction - */ - public function getAction(): string - { - return $this->action; - } -} diff --git a/src/Message/Zoho/ZohoSyncTaxMessage.php b/src/Message/Zoho/ZohoSyncTaxMessage.php deleted file mode 100644 index 844876e..0000000 --- a/src/Message/Zoho/ZohoSyncTaxMessage.php +++ /dev/null @@ -1,38 +0,0 @@ -id = $tax->getId(); - } - - public function getId(): string - { - return $this->id; - } - - /** - * @return TAction - */ - public function getAction(): string - { - return $this->action; - } -} diff --git a/src/Message/Zoho/ZohoSyncWarehouseMessage.php b/src/Message/Zoho/ZohoSyncWarehouseMessage.php deleted file mode 100644 index a28031d..0000000 --- a/src/Message/Zoho/ZohoSyncWarehouseMessage.php +++ /dev/null @@ -1,38 +0,0 @@ -id = $warehouse->getId(); - } - - public function getId(): string - { - return $this->id; - } - - /** - * @return TAction - */ - public function getAction(): string - { - return $this->action; - } -} diff --git a/src/Message/ZohoSyncMessage.php b/src/Message/ZohoSyncMessage.php deleted file mode 100644 index dae5659..0000000 --- a/src/Message/ZohoSyncMessage.php +++ /dev/null @@ -1,22 +0,0 @@ -id = $company->getId(); - } - - public function getId(): string - { - return $this->id; - } -} diff --git a/src/Service/Zoho/Model/SyncInterface.php b/src/Service/Zoho/Model/SyncInterface.php new file mode 100644 index 0000000..24e6bdb --- /dev/null +++ b/src/Service/Zoho/Model/SyncInterface.php @@ -0,0 +1,66 @@ + + */ +#[AutoconfigureTag(name: self::class)] +interface SyncInterface +{ + /** + * @param non-empty-string $zohoId + * + * @return TEntity|null + */ + public function findEntityByZohoId(string $zohoId): ZohoAwareInterface|null; + + /** + * @param TEntity $entity + * + * @return non-empty-array + */ + public function createPutPayload(ZohoAwareInterface $entity): array; + + /** + * @return class-string + */ + public function getEntityClass(): string; + + /** + * @return class-string + */ + public function getMappingClass(): string; + + /** + * @return non-empty-string + */ + public function getBaseUrl(): string; + + /** + * When an entity from @see getEntityName is updated, return iterable of messages to be executed after succesful flush. + * + * @no-named-arguments + * + * @param TEntity $entity + * + * @return iterable + */ + public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable; + + /** + * @param TEntity $entity + * @param TK $mapping + */ + public function map(ZohoAwareInterface $entity, object $mapping): void; +} diff --git a/src/Service/Zoho/Sync/Model/SyncInterface.php b/src/Service/Zoho/Sync/Model/SyncInterface.php deleted file mode 100644 index 8613de0..0000000 --- a/src/Service/Zoho/Sync/Model/SyncInterface.php +++ /dev/null @@ -1,35 +0,0 @@ - - */ - public function getEntityName(): string; - - /** - * When an entity from @see getEntityName is updated, return iterable of messages to be executed after succesful flush. - * - * @no-named-arguments - * - * @param T $entity - * - * @return iterable - */ - public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable; - - public function downloadAll(Company $company): void; -} diff --git a/src/Service/Zoho/Sync/ProductSync.php b/src/Service/Zoho/Sync/ProductSync.php index 5776b50..d478ecd 100644 --- a/src/Service/Zoho/Sync/ProductSync.php +++ b/src/Service/Zoho/Sync/ProductSync.php @@ -7,110 +7,89 @@ use Override; use Money\Money; use Money\Currency; -use App\DTO\Zoho\Items; use App\Entity\Tax\Tax; -use InvalidArgumentException; use App\Entity\Product\Product; -use App\Entity\Company\Company; -use App\Service\Zoho\ZohoClient; use App\DTO\Zoho\Item as ZohoItem; use App\Entity\ZohoAwareInterface; +use App\DTO\Zoho\Items as ZohoItems; use App\Repository\Tax\TaxRepository; -use App\Message\Zoho\ZohoSyncProductMessage; +use App\Service\Zoho\Model\SyncInterface; +use App\Message\Zoho\ZohoPutEntityMessage; use App\Repository\Product\ProductRepository; -use App\Service\Zoho\Sync\Model\SyncInterface; -use Symfony\Component\Messenger\Attribute\AsMessageHandler; -use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use function is_float; use function is_string; use function strip_tags; use function array_key_exists; /** - * @implements SyncInterface + * @implements SyncInterface */ class ProductSync implements SyncInterface { public function __construct( - private ZohoClient $zohoClient, - private TaxRepository $repository, private ProductRepository $productRepository, + private TaxRepository $taxRepository, ) { } - #[AsMessageHandler] - public function __invoke(ZohoSyncProductMessage $message): void + #[Override] + public function map(ZohoAwareInterface $entity, object $mapping): void { - try { - if (!$product = $this->productRepository->find($message->getId())) { - return; - } - if (!is_string($zohoId = $product->getZohoId())) { - return; - } - // @see https://www.zoho.com/inventory/api/v1/items/#overview - $url = sprintf('/items/%s', $zohoId); - - match ($message->getAction()) { - 'remove' => $this->zohoClient->delete($product->getCompany(), $url), - 'update' => $this->zohoClient->put($product->getCompany(), $url, data: [ - 'rate' => (float)($product->getPrice()->getAmount()) / 100, - 'name' => $product->getName(), - 'description' => strip_tags($product->getDescription() ?? ''), - ]), - }; - } catch (InvalidArgumentException $e) { - throw new UnrecoverableMessageHandlingException(previous: $e); - } + $rate = $mapping->getRate(); + $rate = is_float($rate) ? $rate : 0; + $price = new Money((int)($rate * 100), new Currency('USD')); + $entity->setPrice($price); + + $entity->setName($mapping->getName()); + $entity->setDescription($mapping->getDescription()); + + $tax = $this->findTax($mapping); + $entity->setTax($tax); } #[Override] - public function getEntityName(): string + public function getBaseUrl(): string { - return Product::class; + return '/items'; } #[Override] - public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable + public function findEntityByZohoId(string $zohoId): Product|null { - if (!array_key_exists('name', $changeSet) && !array_key_exists('description', $changeSet) && !array_key_exists('price', $changeSet)) { - return; - } - - yield new ZohoSyncProductMessage($entity, 'update'); + return $this->productRepository->findOneBy(['zohoId' => $zohoId]); } #[Override] - public function downloadAll(Company $company): void + public function getMappingClass(): string { - $zohoItems = $this->zohoClient->get($company, '/items', Items::class); - foreach ($zohoItems->items as $zohoItem) { - $product = $this->getProduct($zohoItem, $company); - - $rate = $zohoItem->getRate(); - $rate = is_float($rate) ? $rate : 0; - $price = new Money((int)($rate * 100), new Currency('USD')); - $product->setPrice($price); - - $product->setName($zohoItem->getName()); - $product->setDescription($zohoItem->getDescription()); + return ZohoItems::class; + } - $tax = $this->findTax($zohoItem); - $product->setTax($tax); - } - $this->productRepository->flush(); + #[Override] + public function getEntityClass(): string + { + return Product::class; } - private function getProduct(ZohoItem $zohoItem, Company $company): Product + #[Override] + public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable { - if ($product = $this->productRepository->findOneBy(['zohoId' => $zohoItem->getItemId()])) { - return $product; + if (!array_key_exists('name', $changeSet) && !array_key_exists('value', $changeSet)) { + return; } - $product = new Product($company, name: $zohoItem->getName(), zohoId: $zohoItem->getItemId()); - $this->productRepository->persist($product); - return $product; + yield new ZohoPutEntityMessage($entity, 'put'); + } + + #[Override] + public function createPutPayload(ZohoAwareInterface $entity): array + { + return [ + 'rate' => (float)($entity->getPrice()->getAmount()) / 100, + 'name' => $entity->getName(), + 'description' => strip_tags($entity->getDescription() ?? ''), + ]; } private function findTax(ZohoItem $zohoItem): ?Tax @@ -119,6 +98,6 @@ private function findTax(ZohoItem $zohoItem): ?Tax return null; } - return $this->repository->findOneBy(['zohoId' => $zohoTaxId]); + return $this->taxRepository->findOneBy(['zohoId' => $zohoTaxId]); } } diff --git a/src/Service/Zoho/Sync/TaxSync.php b/src/Service/Zoho/Sync/TaxSync.php index d423195..2c98542 100644 --- a/src/Service/Zoho/Sync/TaxSync.php +++ b/src/Service/Zoho/Sync/TaxSync.php @@ -5,56 +5,53 @@ namespace App\Service\Zoho\Sync; use Override; -use App\DTO\Zoho\Taxes; use App\Entity\Tax\Tax; -use App\Entity\Company\Company; use App\DTO\Zoho\Tax as ZohoTax; -use App\Service\Zoho\ZohoClient; use App\Entity\ZohoAwareInterface; +use App\DTO\Zoho\Taxes as ZohoTaxes; use App\Repository\Tax\TaxRepository; -use App\Message\Zoho\ZohoSyncTaxMessage; -use App\Service\Zoho\Sync\Model\SyncInterface; -use App\Message\Zoho\ZohoSyncWarehouseMessage; -use Symfony\Component\Messenger\Attribute\AsMessageHandler; -use function sprintf; -use function is_string; +use App\Service\Zoho\Model\SyncInterface; +use App\Message\Zoho\ZohoPutEntityMessage; use function array_key_exists; /** - * @implements SyncInterface + * @implements SyncInterface */ class TaxSync implements SyncInterface { public function __construct( - private ZohoClient $zohoClient, - private TaxRepository $repository, + private TaxRepository $taxRepository, ) { } - #[AsMessageHandler] - public function __invoke(ZohoSyncWarehouseMessage $message): void + #[Override] + public function map(ZohoAwareInterface $entity, object $mapping): void { - if (!$tax = $this->repository->find($message->getId())) { - return; - } - if (!is_string($zohoId = $tax->getZohoId())) { - return; - } - // @see https://www.zoho.com/inventory/api/v1/taxes/#overview - $url = sprintf('/settings/taxes/%s', $zohoId); + $entity->setName($mapping->getTaxName()); + $entity->setValue((int)$mapping->getTaxPercentage()); + } + + #[Override] + public function getBaseUrl(): string + { + return '/settings/taxes'; + } + + #[Override] + public function findEntityByZohoId(string $zohoId): Tax|null + { + return $this->taxRepository->findOneBy(['zohoId' => $zohoId]); + } - match ($message->getAction()) { - 'remove' => $this->zohoClient->delete($tax->getCompany(), $url), - 'update' => $this->zohoClient->put($tax->getCompany(), $url, data: [ - 'tax_name' => $tax->getName(), - 'tax_percentage' => $tax->getValue(), - ]), - }; + #[Override] + public function getMappingClass(): string + { + return ZohoTaxes::class; } #[Override] - public function getEntityName(): string + public function getEntityClass(): string { return Tax::class; } @@ -66,29 +63,15 @@ public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable return; } - yield new ZohoSyncTaxMessage($entity, 'update'); + yield new ZohoPutEntityMessage($entity, 'put'); } #[Override] - public function downloadAll(Company $company): void + public function createPutPayload(ZohoAwareInterface $entity): array { - $zohoTaxes = $this->zohoClient->get($company, '/settings/taxes', Taxes::class); - foreach ($zohoTaxes->taxes as $zohoTax) { - $tax = $this->getTax($zohoTax, $company); - $tax->setName($zohoTax->getTaxName()); - $tax->setValue((int)$zohoTax->getTaxPercentage()); - } - $this->repository->flush(); - } - - private function getTax(ZohoTax $zohoTax, Company $company): Tax - { - if ($tax = $this->repository->findOneBy(['zohoId' => $zohoTax->getTaxId()])) { - return $tax; - } - $tax = new Tax($company, $zohoTax->getTaxName(), $zohoTax->getTaxPercentage(), zohoId: $zohoTax->getTaxId()); - $this->repository->persist($tax); - - return $tax; + return [ + 'tax_name' => $entity->getName(), + 'tax_percentage' => $entity->getValue(), + ]; } } diff --git a/src/Service/Zoho/Sync/WarehouseSync.php b/src/Service/Zoho/Sync/WarehouseSync.php index 20db593..a2dfc07 100644 --- a/src/Service/Zoho/Sync/WarehouseSync.php +++ b/src/Service/Zoho/Sync/WarehouseSync.php @@ -5,86 +5,71 @@ namespace App\Service\Zoho\Sync; use Override; -use App\DTO\Zoho\Warehouses; -use App\Entity\Company\Company; -use App\Service\Zoho\ZohoClient; use App\Entity\ZohoAwareInterface; use App\Entity\Warehouse\Warehouse; +use App\Message\Zoho\ZohoPutEntityMessage; use App\DTO\Zoho\Warehouse as ZohoWarehouse; -use App\Service\Zoho\Sync\Model\SyncInterface; -use App\Message\Zoho\ZohoSyncWarehouseMessage; +use App\DTO\Zoho\Warehouses as ZohoWarehouses; use App\Repository\Warehouse\WarehouseRepository; -use Symfony\Component\Messenger\Attribute\AsMessageHandler; -use function is_string; +use App\Service\Zoho\Model\SyncInterface; use function array_key_exists; /** - * @implements SyncInterface + * @implements SyncInterface */ class WarehouseSync implements SyncInterface { public function __construct( - private ZohoClient $zohoClient, private WarehouseRepository $warehouseRepository, ) { } - #[AsMessageHandler] - public function __invoke(ZohoSyncWarehouseMessage $message): void + #[Override] + public function map(ZohoAwareInterface $entity, object $mapping): void { - if (!$warehouse = $this->warehouseRepository->find($message->getId())) { - return; - } - if (!is_string($zohoId = $warehouse->getZohoId())) { - return; - } - // @see https://www.zoho.com/inventory/api/v1/multi-warehouse/#overview - $url = sprintf('/settings/warehouses/%s', $zohoId); - - match ($message->getAction()) { - 'remove' => $this->zohoClient->delete($warehouse->getCompany(), $url), - 'update' => $this->zohoClient->put($warehouse->getCompany(), $url, data: [ - 'warehouse_name' => $warehouse->getName(), - ]), - }; + $entity->setName($mapping->getName()); } #[Override] - public function getEntityName(): string + public function getBaseUrl(): string { - return Warehouse::class; + return '/settings/warehouses'; } #[Override] - public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable + public function findEntityByZohoId(string $zohoId): Warehouse|null { - if (!array_key_exists('name', $changeSet)) { - return; - } + return $this->warehouseRepository->findOneBy(['zohoId' => $zohoId]); + } - yield new ZohoSyncWarehouseMessage($entity, 'update'); + #[Override] + public function getMappingClass(): string + { + return ZohoWarehouses::class; } #[Override] - public function downloadAll(Company $company): void + public function getEntityClass(): string { - $warehouses = $this->zohoClient->get($company, '/settings/warehouses', Warehouses::class); - foreach ($warehouses->warehouses as $zohoWarehouse) { - $warehouse = $this->getWarehouse($zohoWarehouse, $company); - $warehouse->setName($zohoWarehouse->getName()); - } - $this->warehouseRepository->flush(); + return Warehouse::class; } - private function getWarehouse(ZohoWarehouse $zohoWarehouse, Company $company): Warehouse + #[Override] + public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable { - if ($product = $this->warehouseRepository->findOneBy(['zohoId' => $zohoWarehouse->getWarehouseId()])) { - return $product; + if (!array_key_exists('name', $changeSet) && !array_key_exists('value', $changeSet)) { + return; } - $warehouse = new Warehouse($company, $zohoWarehouse->getName(), zohoId: $zohoWarehouse->getWarehouseId()); - $this->warehouseRepository->persist($warehouse); - return $warehouse; + yield new ZohoPutEntityMessage($entity, 'put'); + } + + #[Override] + public function createPutPayload(ZohoAwareInterface $entity): array + { + return [ + 'warehouse_name' => $entity->getName(), + ]; } } diff --git a/src/Service/Zoho/ZohoClient.php b/src/Service/Zoho/ZohoClient.php index b6e8bd5..1700c4f 100644 --- a/src/Service/Zoho/ZohoClient.php +++ b/src/Service/Zoho/ZohoClient.php @@ -92,6 +92,39 @@ public function delete(Company $company, string $url): void $this->request($company, 'DELETE', $url); } + public function getConnectUrl(): string + { + return sprintf('%s/oauth/v2/auth?scope=%s&client_id=%s&state=testing&response_type=code&redirect_uri=%s&access_type=offline', + $this->baseUrl, + 'ZohoInventory.FullAccess.all', + $this->clientId, + $this->generateRedirectUrl(), + ); + } + + public function getToken(string $code, Company $company): void + { + $url = sprintf('%s/oauth/v2/token?code=%s&client_id=%s&client_secret=%s&redirect_uri=%s&grant_type=authorization_code', + $this->baseUrl, + $code, + $this->clientId, + $this->clientSecret, + $this->generateRedirectUrl(), + ); + + $response = $this->httpClient->request('POST', $url); + $data = $response->getContent(); + $dto = $this->treeMapper->map(Token::class, Source::json($data)->camelCaseKeys()); + + $company->setZohoAccessToken($dto->getAccessToken()); + $company->setZohoRefreshToken($dto->getRefreshToken()); + if (is_int($expiresIn = $dto->getExpiresIn())) { + $expiresAt = new DateTimeImmutable('+' . $expiresIn . ' seconds'); + $company->setZohoExpiresAt($expiresAt); + } + $this->tokenRefreshed = true; + } + /** * @param non-empty-array|null $payload * @@ -129,39 +162,6 @@ private function request(Company $company, string $method, string $url, ?array $ return $response->getContent(false); } - public function getConnectUrl(): string - { - return sprintf('%s/oauth/v2/auth?scope=%s&client_id=%s&state=testing&response_type=code&redirect_uri=%s&access_type=offline', - $this->baseUrl, - 'ZohoInventory.FullAccess.all', - $this->clientId, - $this->generateRedirectUrl(), - ); - } - - public function getToken(string $code, Company $company): void - { - $url = sprintf('%s/oauth/v2/token?code=%s&client_id=%s&client_secret=%s&redirect_uri=%s&grant_type=authorization_code', - $this->baseUrl, - $code, - $this->clientId, - $this->clientSecret, - $this->generateRedirectUrl(), - ); - - $response = $this->httpClient->request('POST', $url); - $data = $response->getContent(); - $dto = $this->treeMapper->map(Token::class, Source::json($data)->camelCaseKeys()); - - $company->setZohoAccessToken($dto->getAccessToken()); - $company->setZohoRefreshToken($dto->getRefreshToken()); - if (is_int($expiresIn = $dto->getExpiresIn())) { - $expiresAt = new DateTimeImmutable('+' . $expiresIn . ' seconds'); - $company->setZohoExpiresAt($expiresAt); - } - $this->tokenRefreshed = true; - } - private function refreshToken(Company $company): string { $url = sprintf('%s/oauth/v2/token?refresh_token=%s&client_id=%s&client_secret=%s&grant_type=refresh_token', diff --git a/src/Service/Zoho/ZohoManager.php b/src/Service/ZohoImprovedManager.php similarity index 59% rename from src/Service/Zoho/ZohoManager.php rename to src/Service/ZohoImprovedManager.php index 4b50c8e..a897482 100644 --- a/src/Service/Zoho/ZohoManager.php +++ b/src/Service/ZohoImprovedManager.php @@ -2,21 +2,22 @@ declare(strict_types=1); -namespace App\Service\Zoho; +namespace App\Service; use Override; use Generator; use App\Entity\Company\Company; -use App\Message\ZohoSyncMessage; -use App\Service\Zoho\Sync\TaxSync; +use App\Service\Zoho\ZohoClient; use App\Entity\ZohoAwareInterface; -use App\Service\Zoho\Sync\ProductSync; +use App\Service\Zoho\Sync\TaxSync; use App\Message\AsyncMessageInterface; +use App\Service\Zoho\Sync\ProductSync; use App\Service\Zoho\Sync\WarehouseSync; +use App\Service\Zoho\Model\SyncInterface; use Doctrine\ORM\Event\PreUpdateEventArgs; +use App\Message\Zoho\ZohoPutEntityMessage; use App\Repository\Company\CompanyRepository; use Symfony\Contracts\Service\ResetInterface; -use App\Service\Zoho\Sync\Model\SyncInterface; use Symfony\Component\Messenger\Stamp\DelayStamp; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\DependencyInjection\ServiceLocator; @@ -27,9 +28,8 @@ use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use function is_a; use function sprintf; -use function array_keys; -class ZohoManager implements ResetInterface, PreUpdateEventListenerInterface, PostFlushEventListenerInterface +class ZohoImprovedManager implements ResetInterface, PreUpdateEventListenerInterface, PostFlushEventListenerInterface { private bool $syncEnabled = true; @@ -46,16 +46,25 @@ public function __construct( private ServiceLocator $syncs, private CompanyRepository $companyRepository, private MessageBusInterface $messageBus, + private ZohoClient $zohoClient, ) { } #[AsMessageHandler] - public function __invoke(ZohoSyncMessage $message): void + public function __invoke(ZohoPutEntityMessage $message): void { - $id = $message->getId(); - $company = $this->companyRepository->find($id) ?? throw new UnrecoverableMessageHandlingException(sprintf('Company %s does not exist.', $id)); - $this->downloadAll($company); + $entityClassName = $message->getClass(); + $sync = $this->findSyncForEntity($entityClassName) ?? throw new UnrecoverableMessageHandlingException(sprintf('Entity %s not supported', $entityClassName)); + $zohoId = $message->getZohoId(); + if (!$entity = $sync->findEntityByZohoId($zohoId)) { + return; + } + $url = $sync->getBaseUrl() . '/' . $zohoId; + match ($message->getAction()) { + 'delete' => $this->zohoClient->delete($entity->getCompany(), $url), + 'put' => $this->zohoClient->put($entity->getCompany(), $url, data: $sync->createPutPayload($entity)), + }; } #[Override] @@ -74,10 +83,17 @@ public function downloadAll(Company $company): void $this->syncEnabled = false; foreach ($this->getSyncsInOrder() as $serviceName) { $sync = $this->syncs->get($serviceName); - $sync->downloadAll($company); + $mapperClass = $sync->getMappingClass(); + $data = $this->zohoClient->get($company, $sync->getBaseUrl(), $mapperClass); + foreach ($data->getMany() as $item) { + $zohoId = $item->getId(); + if ($entity = $sync->findEntityByZohoId($zohoId)) { + $sync->map($entity, $item); + } + } + // in case tagged service didn't flush its entities, do it here + $this->companyRepository->flush(); } - // in case tagged service didn't flush its entities, do it here - $this->companyRepository->flush(); $this->syncEnabled = true; } @@ -93,14 +109,11 @@ public function preUpdate(PreUpdateEventArgs $event): void if (!$entity instanceof ZohoAwareInterface) { return; } - - foreach ($this->getAllSyncServices() as $sync) { - if (!is_a($entity, $sync->getEntityName(), true)) { - continue; - } - foreach ($sync->onUpdate($entity, $changeSet) as $message) { - $this->pendingUpdateMessages[] = $message; - } + if (!$sync = $this->findSyncForEntity($entity)) { + return; + } + foreach ($sync->onUpdate($entity, $changeSet) as $message) { + $this->pendingUpdateMessages[] = $message; } } @@ -116,14 +129,20 @@ public function postFlush(): void } /** - * @return Generator + * @template TEntity of ZohoAwareInterface + * + * @param class-string|TEntity $entityClassName */ - private function getAllSyncServices(): Generator + private function findSyncForEntity(string|object $entityClassName): SyncInterface|null { - $identifiers = array_keys($this->syncs->getProvidedServices()); - foreach ($identifiers as $identifier) { - yield $this->syncs->get($identifier); + foreach ($this->getSyncsInOrder() as $serviceName) { + $sync = $this->syncs->get($serviceName); + if (is_a($entityClassName, $sync->getEntityClass(), true)) { + return $sync; + } } + + return null; } /**