diff --git a/Build/phpstan-baseline.neon b/Build/phpstan-baseline.neon index 2e11cf9b..a93ad23f 100644 --- a/Build/phpstan-baseline.neon +++ b/Build/phpstan-baseline.neon @@ -1038,6 +1038,96 @@ parameters: count: 1 path: ../Classes/Domain/Finisher/Form/AddToCartFinisherInterface.php + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\DatabaseWriter\:\:jsonEncodeWithThrowable\(\) has parameter \$dataToEncode with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/DatabaseWriter.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\Log\:\:__construct\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/Log.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\Log\:\:error\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/Log.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\Log\:\:getArguments\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/Log.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\Log\:\:info\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/Log.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\Log\:\:notice\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/Log.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\Log\:\:warning\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/Log.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\LogInterface\:\:__construct\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/LogInterface.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\LogInterface\:\:error\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/LogInterface.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\LogInterface\:\:getArguments\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/LogInterface.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\LogInterface\:\:info\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/LogInterface.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\LogInterface\:\:notice\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/LogInterface.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Model\\LogInterface\:\:warning\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Model/LogInterface.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Repository\\LogRepository\:\:findAllByIdentifier\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Repository/LogRepository.php + + - + message: '#^Method Extcode\\Cart\\Domain\\Log\\Repository\\LogRepository\:\:insert\(\) has parameter \$fieldValues with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: ../Classes/Domain/Log/Repository/LogRepository.php + - message: '#^Method Extcode\\Cart\\Domain\\Model\\Cart\:\:getCart\(\) should return Extcode\\Cart\\Domain\\Model\\Cart\\Cart\|null but returns mixed\.$#' identifier: return.type @@ -5472,56 +5562,3 @@ parameters: count: 1 path: ../ext_emconf.php - - - message: '#^Cannot access an offset on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''1588829280'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 2 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''MAIL'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''SYS'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''cart'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''fluid'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''namespaces'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''partialRootPaths'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php - - - - message: '#^Cannot access offset ''templateRootPaths'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../ext_localconf.php diff --git a/Classes/Controller/Backend/Order/PaymentController.php b/Classes/Controller/Backend/Order/PaymentController.php index 76dc559c..e8ea0607 100644 --- a/Classes/Controller/Backend/Order/PaymentController.php +++ b/Classes/Controller/Backend/Order/PaymentController.php @@ -12,21 +12,35 @@ */ use Extcode\Cart\Controller\Backend\ActionController; +use Extcode\Cart\Domain\Log\LogServiceInterface; +use Extcode\Cart\Domain\Log\Model\Log; use Extcode\Cart\Domain\Model\Order\Payment; use Extcode\Cart\Domain\Repository\Order\PaymentRepository; use Extcode\Cart\Event\Order\UpdateServiceEvent; +use InvalidArgumentException; use Psr\Http\Message\ResponseInterface; use TYPO3\CMS\Extbase\Utility\LocalizationUtility; class PaymentController extends ActionController { public function __construct( - protected PaymentRepository $paymentRepository + private readonly PaymentRepository $paymentRepository, + private readonly LogServiceInterface $logService, ) {} public function updateAction(Payment $payment): ResponseInterface { $this->paymentRepository->update($payment); + $this->logService->write( + Log::info( + $this->getOrderItemUid($payment), + 'updatePayment', + 'Payment was set to ' . $payment->getStatus() . '.', + [ + 'time' => time(), + ] + ) + ); $event = new UpdateServiceEvent($payment); $this->eventDispatcher->dispatch($event); @@ -40,4 +54,15 @@ public function updateAction(Payment $payment): ResponseInterface return $this->redirect('show', 'Backend\Order\Order', null, ['orderItem' => $payment->getItem()]); } + + private function getOrderItemUid(Payment $payment): int + { + $orderItemUid = $payment->getItem()?->getUid(); + + if (is_null($orderItemUid)) { + throw new InvalidArgumentException('Method should only called for persisted orders.', 1774715307); + } + + return $orderItemUid; + } } diff --git a/Classes/Controller/Backend/Order/ShippingController.php b/Classes/Controller/Backend/Order/ShippingController.php index f2cfed3e..f1b97aee 100644 --- a/Classes/Controller/Backend/Order/ShippingController.php +++ b/Classes/Controller/Backend/Order/ShippingController.php @@ -12,21 +12,35 @@ */ use Extcode\Cart\Controller\Backend\ActionController; +use Extcode\Cart\Domain\Log\LogServiceInterface; +use Extcode\Cart\Domain\Log\Model\Log; use Extcode\Cart\Domain\Model\Order\Shipping; use Extcode\Cart\Domain\Repository\Order\ShippingRepository; use Extcode\Cart\Event\Order\UpdateServiceEvent; +use InvalidArgumentException; use Psr\Http\Message\ResponseInterface; use TYPO3\CMS\Extbase\Utility\LocalizationUtility; class ShippingController extends ActionController { public function __construct( - protected ShippingRepository $shippingRepository + private readonly ShippingRepository $shippingRepository, + private readonly LogServiceInterface $logService, ) {} public function updateAction(Shipping $shipping): ResponseInterface { $this->shippingRepository->update($shipping); + $this->logService->write( + Log::info( + $this->getOrderItemUid($shipping), + 'updateShipping', + 'Shipping was set to ' . $shipping->getStatus() . '.', + [ + 'time' => time(), + ] + ) + ); $event = new UpdateServiceEvent($shipping); $this->eventDispatcher->dispatch($event); @@ -40,4 +54,15 @@ public function updateAction(Shipping $shipping): ResponseInterface return $this->redirect('show', 'Backend\Order\Order', null, ['orderItem' => $shipping->getItem()]); } + + private function getOrderItemUid(Shipping $shipping): int + { + $orderItemUid = $shipping->getItem()?->getUid(); + + if (is_null($orderItemUid)) { + throw new InvalidArgumentException('Method should only called for persisted orders.', 1774715307); + } + + return $orderItemUid; + } } diff --git a/Classes/Domain/Log/DatabaseWriter.php b/Classes/Domain/Log/DatabaseWriter.php new file mode 100644 index 00000000..2aba1e54 --- /dev/null +++ b/Classes/Domain/Log/DatabaseWriter.php @@ -0,0 +1,58 @@ +getData(); + + $log = $recordData['log'] ?? null; + if (($log instanceof LogInterface) === false) { + return $this; + } + unset($recordData['log']); + + $fieldValues = [ + 'log_level' => $log->getLogLevel()->value, + 'item' => $log->getOrderItemId(), + 'type' => $log->getType(), + 'message' => $log->getMessage(), + 'arguments' => $this->jsonEncodeWithThrowable($log->getArguments()), + 'request_id' => $record->getRequestId(), + 'time_micro' => $record->getCreated(), + 'level' => $record->getLevel(), + 'data' => $this->jsonEncodeWithThrowable($recordData), + ]; + + $logRepository = GeneralUtility::makeInstance(LogRepository::class); + $logRepository->insert($fieldValues); + + return $this; + } + + public function jsonEncodeWithThrowable(array $dataToEncode): string + { + $data = ''; + if (!empty($dataToEncode)) { + // Fold an exception into the message, and string-ify it into recordData so it can be jsonified. + if (isset($dataToEncode['exception']) && $dataToEncode['exception'] instanceof Throwable) { + $dataToEncode['exception'] = (string)$dataToEncode['exception']; + } + $data = '- ' . json_encode($dataToEncode); + } + + return $data; + } +} diff --git a/Classes/Domain/Log/LogService.php b/Classes/Domain/Log/LogService.php new file mode 100644 index 00000000..167c4ec5 --- /dev/null +++ b/Classes/Domain/Log/LogService.php @@ -0,0 +1,27 @@ +logger->log( + $log->getLogLevel()->value, + $log->getMessage(), + [ + 'log' => $log, + ] + ); + } +} diff --git a/Classes/Domain/Log/LogServiceInterface.php b/Classes/Domain/Log/LogServiceInterface.php new file mode 100644 index 00000000..9d45842e --- /dev/null +++ b/Classes/Domain/Log/LogServiceInterface.php @@ -0,0 +1,12 @@ +logLevel; + } + + public function getOrderItemId(): int + { + return $this->orderItemId; + } + + public function getType(): string + { + return $this->type; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getArguments(): array + { + return $this->arguments; + } + +} diff --git a/Classes/Domain/Log/Model/LogInterface.php b/Classes/Domain/Log/Model/LogInterface.php new file mode 100644 index 00000000..bb4ce8a8 --- /dev/null +++ b/Classes/Domain/Log/Model/LogInterface.php @@ -0,0 +1,54 @@ +queryBuilder = $connectionPool + ->getQueryBuilderForTable(self::TABLE_NAME) + ; + } + + public function insert( + array $fieldValues, + ): void { + // for cleanup of table + $fieldValues['crdate'] = time(); + + $queryBuilder = clone $this->queryBuilder; + $queryBuilder + ->insert(self::TABLE_NAME) + ->values($fieldValues) + ->executeStatement() + ; + } + + public function findAllByIdentifier( + string $identifier, + ): array { + $queryBuilder = clone $this->queryBuilder; + $queryBuilder + ->select('*') + ->from(self::TABLE_NAME) + ->where( + $queryBuilder->expr()->eq( + 'identifier', + $queryBuilder->createNamedParameter($identifier) + ) + ) + ; + + return $queryBuilder + ->executeQuery() + ->fetchAllAssociative(); + } + +} diff --git a/Classes/Service/MailHandler.php b/Classes/Service/MailHandler.php index f18a2640..b485aadb 100644 --- a/Classes/Service/MailHandler.php +++ b/Classes/Service/MailHandler.php @@ -9,10 +9,14 @@ * LICENSE file that was distributed with this source code. */ +use Exception; +use Extcode\Cart\Domain\Log\LogServiceInterface; +use Extcode\Cart\Domain\Log\Model\Log; use Extcode\Cart\Domain\Model\Cart\Cart; use Extcode\Cart\Domain\Model\Order\AddressInterface; use Extcode\Cart\Domain\Model\Order\Item; use Extcode\Cart\Event\Mail\AttachmentEvent; +use InvalidArgumentException; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ServerRequestInterface; use Symfony\Component\Mime\Address; @@ -45,7 +49,8 @@ class MailHandler implements SingletonInterface public function __construct( private readonly ConfigurationManagerInterface $configurationManager, private readonly EventDispatcherInterface $eventDispatcher, - private readonly MailerInterface $mailer + private readonly MailerInterface $mailer, + private readonly LogServiceInterface $logService, ) { $this->setPluginSettings(); } @@ -310,7 +315,7 @@ public function sendBuyerMail(Item $orderItem): void ->from($fromAddress) ->setTemplate('Mail/' . ucfirst($status) . '/Buyer') ->format(FluidEmail::FORMAT_HTML) - ->assign('settings', $this->pluginSettings['settings']) + ->assign('settings', $this->pluginSettings['settings'] ?? []) ->assign('cart', $this->cart) ->assign('orderItem', $orderItem); @@ -328,11 +333,35 @@ public function sendBuyerMail(Item $orderItem): void $this->addAttachments('buyer', $orderItem, $email); - if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) { + if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface) { $email->setRequest($GLOBALS['TYPO3_REQUEST']); } - $this->mailer->send($email); + try { + $this->mailer->send($email); + $this->logService->write( + Log::info( + $this->getOrderItemUid($orderItem), + 'sendBuyerMail', + 'Mail was send to buyer.', + [ + 'time' => time(), + ] + ) + ); + } catch (Exception $e) { + $this->logService->write( + Log::error( + $this->getOrderItemUid($orderItem), + 'sendBuyerMail', + 'Mail could not send to buyer.', + [ + 'time' => time(), + 'exception' => $e->__toString(), + ] + ) + ); + } } /** @@ -340,6 +369,9 @@ public function sendBuyerMail(Item $orderItem): void */ public function sendSellerMail(Item $orderItem): void { + if (is_null($orderItem->getUid())) { + throw new InvalidArgumentException('Method should only called for persisted orders.', 1774715307); + } $sellerEmailTo = $this->getSellerEmailTo(); if (empty($this->getSellerEmailFrom()) || empty($sellerEmailTo)) { return; @@ -356,7 +388,7 @@ public function sendSellerMail(Item $orderItem): void ->from($fromAddress) ->setTemplate('Mail/' . ucfirst($status) . '/Seller') ->format(FluidEmail::FORMAT_HTML) - ->assign('settings', $this->pluginSettings['settings']) + ->assign('settings', $this->pluginSettings['settings'] ?? []) ->assign('cart', $this->cart) ->assign('orderItem', $orderItem); @@ -376,11 +408,35 @@ public function sendSellerMail(Item $orderItem): void $this->addAttachments('seller', $orderItem, $email); - if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) { + if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface) { $email->setRequest($GLOBALS['TYPO3_REQUEST']); } - $this->mailer->send($email); + try { + $this->mailer->send($email); + $this->logService->write( + Log::info( + $this->getOrderItemUid($orderItem), + 'sendSellerMail', + 'Mail was send to seller.', + [ + 'time' => time(), + ] + ) + ); + } catch (Exception $e) { + $this->logService->write( + Log::error( + $this->getOrderItemUid($orderItem), + 'sendSellerMail', + 'Mail could not send to seller.', + [ + 'time' => time(), + 'exception' => $e->__toString(), + ] + ) + ); + } } public function addAttachments(string $type, Item $orderItem, FluidEmail $email): void @@ -394,8 +450,30 @@ public function addAttachments(string $type, Item $orderItem, FluidEmail $email) foreach ($attachments as $attachment) { if (file_exists($attachment)) { $email->attachFromPath($attachment); + } else { + $this->logService->write( + Log::warning( + $this->getOrderItemUid($orderItem), + 'addAttachments', + 'Mail could add attachment ' . $attachment . ' to mail.', + [ + 'time' => time(), + ] + ) + ); } } } } + + private function getOrderItemUid(Item $orderItem): int + { + $orderItemUid = $orderItem->getUid(); + + if (is_null($orderItemUid)) { + throw new InvalidArgumentException('Method should only called for persisted orders.', 1774715307); + } + + return $orderItemUid; + } } diff --git a/Documentation/Changelog/12.0/Feature-752-AddLogForOrders.rst b/Documentation/Changelog/12.0/Feature-752-AddLogForOrders.rst new file mode 100644 index 00000000..cc2e5d72 --- /dev/null +++ b/Documentation/Changelog/12.0/Feature-752-AddLogForOrders.rst @@ -0,0 +1,21 @@ +.. include:: ../../Includes.rst.txt + +=========================================== +Feature: #752 - Add LogInterface for orders +=========================================== + +See `Issue 752 `__ + +Description +=========== + +Currently there is no logging for an order. An editor can +change the shipping status, but nobody knows, when this is +happend. + +Impact +====== + +No direct impact. + +.. index:: API diff --git a/Documentation/Changelog/12.0/Index.rst b/Documentation/Changelog/12.0/Index.rst new file mode 100644 index 00000000..dec9057d --- /dev/null +++ b/Documentation/Changelog/12.0/Index.rst @@ -0,0 +1,20 @@ +.. include:: ../../Includes.rst.txt + +12.0 Changes +============ + +**Table of contents** + +.. contents:: + :local: + :depth: 1 + +Features +-------- + +.. toctree:: + :maxdepth: 1 + :titlesonly: + :glob: + + Feature-* diff --git a/Documentation/Changelog/Index.rst b/Documentation/Changelog/Index.rst index f4c73d9d..bc90b960 100644 --- a/Documentation/Changelog/Index.rst +++ b/Documentation/Changelog/Index.rst @@ -10,6 +10,7 @@ ChangeLog :maxdepth: 5 :titlesonly: + 12.0/Index 11.7/Index 11.3/Index 11.1/Index diff --git a/Documentation/Introduction/Index.rst b/Documentation/Introduction/Index.rst index 20694a56..49eb4266 100644 --- a/Documentation/Introduction/Index.rst +++ b/Documentation/Introduction/Index.rst @@ -86,13 +86,6 @@ Examples of websites which use this extension as e-commerce solution. `www.liebman-design-import.com `__ -.. figure:: ../Images/Examples/weingut-isele.de.png - :width: 640 - :alt: Cart of Weingut Isele - :class: with-shadow - - `www.weingut-isele.de `__ - **Table of contents** .. toctree:: @@ -101,4 +94,3 @@ Examples of websites which use this extension as e-commerce solution. Support/Index Sponsoring/Index - NoteOfThanks/Index diff --git a/Documentation/Introduction/NoteOfThanks/Index.rst b/Documentation/Introduction/NoteOfThanks/Index.rst deleted file mode 100644 index 0d5e7181..00000000 --- a/Documentation/Introduction/NoteOfThanks/Index.rst +++ /dev/null @@ -1,15 +0,0 @@ -.. include:: ../../Includes.rst.txt - -============== -Note of thanks -============== - -A big thank you goes `Tritum GmbH `__ for the many hours I was allowed to work on Cart. - -In particular, I would like to thank Björn. He always has an open ear. He contributed his opinion to many questions -and decisions. Without him, Cart would not be what it is today. - -Another thanks goes to the testers for their feedback and understanding when I made changes to the data model again and -again. - -A big thank you also goes out to all the supporters on github. diff --git a/Documentation/Introduction/Sponsoring/Index.rst b/Documentation/Introduction/Sponsoring/Index.rst index a2fce503..78671a83 100644 --- a/Documentation/Introduction/Sponsoring/Index.rst +++ b/Documentation/Introduction/Sponsoring/Index.rst @@ -9,8 +9,5 @@ If there is a feature that has not yet been implemented in Cart, you can contact There is also the possibility to support the further development independently of new functions. * Ask for an invoice. -* `GitHub Sponsors `_ * `paypal.me `_ -Sponsors --------- diff --git a/Documentation/guides.xml b/Documentation/guides.xml index 3ce88ac9..3ed5ce12 100644 --- a/Documentation/guides.xml +++ b/Documentation/guides.xml @@ -11,9 +11,9 @@ interlink-shortcode="extcode/cart" /> diff --git a/README.md b/README.md index aee99e82..a2c4e1cc 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,11 @@ Sometimes minor versions also result in minor adjustments to own templates or co | Cart | TYPO3 | PHP | Support/Development | |--------|------------|-----------|--------------------------------------| -| 11.x.x | 13.4 | 8.2 - 8.5 | Features, Bugfixes, Security Updates | -| 10.x.x | 12.4 | 8.1 - 8.5 | Bugfixes, Security Updates | -| 9.x.x | 12.4 | 8.1 - 8.4 | Security Updates | -| 8.x.x | 10.4, 11.5 | 7.2+ | Security Updates | +| 12.x.x | 14.1 | 8.2 - 8.5 | Features, Bugfixes, Security Updates | +| 11.x.x | 13.4 | 8.2 - 8.5 | Bugfixes, Security Updates | +| 10.x.x | 12.4 | 8.1 - 8.5 | Security Updates | +| 9.x.x | 12.4 | 8.1 - 8.4 | | +| 8.x.x | 10.4, 11.5 | 7.2+ | | | 7.x.x | 10.4 | 7.2 - 7.4 | | | 6.x.x | 9.5 | 7.2 - 7.4 | | | 5.x.x | 8.7 | 7.0 - 7.4 | | @@ -79,7 +80,6 @@ News uses **semantic versioning** which basically means for you, that ## 4. Sponsoring * Ask for an invoice. -* [GitHub Sponsors](https://github.com/sponsors/extcode) * [PayPal.Me](https://paypal.me/extcart) [1]: https://docs.typo3.org/typo3cms/extensions/cart/ diff --git a/Tests/Functional/Service/MailHandlerTest.php b/Tests/Functional/Service/MailHandlerTest.php new file mode 100644 index 00000000..cf1b3eb9 --- /dev/null +++ b/Tests/Functional/Service/MailHandlerTest.php @@ -0,0 +1,384 @@ +testExtensionsToLoad[] = 'extcode/cart'; + $this->testExtensionsToLoad[] = 'typo3conf/ext/cart/Tests/Fixtures/cart_example'; + + $this->configurationToUseInTestInstance = [ + 'LOG' => [ + 'Extcode' => [ + 'Cart' => [ + 'Tests' => [ + 'writerConfiguration' => [ + LogLevel::INFO => [ + DatabaseWriter::class => [], + ], + ], + ], + ], + ], + ], + ]; + + parent::setUp(); + + $this->importPHPDataSet(__DIR__ . '/../../Fixtures/BaseDatabase.php'); + } + + #[Test] + public function logSucessAfterEmailToBuyerWasSend(): void + { + $mockBuilder = $this->getMockBuilderForMailHandlerClass(); + $mockBuilder->onlyMethods( + [ + 'getBuyerEmailFrom', + 'getBuyerEmailName', + ] + ); + $mailHandler = $mockBuilder->getMock(); + $mailHandler->method('getBuyerEmailFrom')->willReturn('buyerEmailFrom@example.com'); + $mailHandler->method('getBuyerEmailName')->willReturn('Buyer Email Name'); + + $mailHandler->sendBuyerMail( + $this->createStubForOrderItem() + ); + + $logEntries = $this->getAllRecords('tx_cart_domain_model_order_log'); + self::assertCount( + 1, + $logEntries + ); + self::assertIsArray($logEntries[0]); + + self::assertArrayIsEqualToArrayIgnoringListOfKeys( + [ + 'log_level' => 'info', + 'item' => 142, + 'type' => 'sendBuyerMail', + 'message' => 'Mail was send to buyer.', + 'level' => 'info', + ], + $logEntries[0], + [ + 'uid', + 'request_id', + 'crdate', + 'time_micro', + 'data', + 'arguments', + ] + ); + + self::assertIsString( + $logEntries[0]['arguments'] + ); + $arguments = json_decode( + ltrim($logEntries[0]['arguments'], '- '), + true + ); + self::assertIsArray( + $arguments + ); + self::assertArrayHasKey( + 'time', + $arguments + ); + } + + #[Test] + public function logErrorIfEmailToBuyerCouldNotSend(): void + { + $mockBuilder = $this->getMockBuilderForMailHandlerClass(mailerThrowException: true); + $mockBuilder->onlyMethods( + [ + 'getBuyerEmailFrom', + 'getBuyerEmailName', + ] + ); + $mailHandler = $mockBuilder->getMock(); + $mailHandler->method('getBuyerEmailFrom')->willReturn('buyerEmailFrom@example.com'); + $mailHandler->method('getBuyerEmailName')->willReturn('Buyer Email Name'); + + $mailHandler->sendBuyerMail( + $this->createStubForOrderItem() + ); + + $logEntries = $this->getAllRecords('tx_cart_domain_model_order_log'); + self::assertCount( + 1, + $logEntries + ); + self::assertIsArray($logEntries[0]); + + self::assertArrayIsEqualToArrayIgnoringListOfKeys( + [ + 'log_level' => 'error', + 'item' => 142, + 'type' => 'sendBuyerMail', + 'message' => 'Mail could not send to buyer.', + 'level' => 'error', + ], + $logEntries[0], + [ + 'uid', + 'request_id', + 'crdate', + 'time_micro', + 'data', + 'arguments', + ] + ); + + self::assertIsString( + $logEntries[0]['arguments'] + ); + $arguments = json_decode( + ltrim($logEntries[0]['arguments'], '- '), + true + ); + self::assertIsArray( + $arguments + ); + self::assertArrayHasKey( + 'time', + $arguments + ); + self::assertArrayHasKey( + 'exception', + $arguments + ); + self::assertIsString( + $arguments['exception'] + ); + self::assertStringStartsWith( + 'Exception in ', + $arguments['exception'] + ); + self::assertStringContainsString( + '/cart/Tests/Functional/Service/MailHandlerTest.php', + $arguments['exception'] + ); + } + + #[Test] + public function logSucessAfterEmailToSellerWasSend(): void + { + $mockBuilder = $this->getMockBuilderForMailHandlerClass(); + $mockBuilder->onlyMethods( + [ + 'getSellerEmailTo', + 'getSellerEmailFrom', + 'getSellerEmailName', + ] + ); + $mailHandler = $mockBuilder->getMock(); + $mailHandler->method('getSellerEmailTo')->willReturn('sellerEmailTo@example.com'); + $mailHandler->method('getSellerEmailFrom')->willReturn('sellerEmailFrom@example.com'); + $mailHandler->method('getSellerEmailName')->willReturn('Seller Email Name'); + + $mailHandler->sendSellerMail( + $this->createStubForOrderItem() + ); + + $logEntries = $this->getAllRecords('tx_cart_domain_model_order_log'); + self::assertCount( + 1, + $logEntries + ); + self::assertIsArray($logEntries[0]); + + self::assertArrayIsEqualToArrayIgnoringListOfKeys( + [ + 'log_level' => 'info', + 'item' => 142, + 'type' => 'sendSellerMail', + 'message' => 'Mail was send to seller.', + 'level' => 'info', + ], + $logEntries[0], + [ + 'uid', + 'request_id', + 'crdate', + 'time_micro', + 'data', + 'arguments', + ] + ); + + self::assertIsString( + $logEntries[0]['arguments'] + ); + $arguments = json_decode( + ltrim($logEntries[0]['arguments'], '- '), + true + ); + self::assertIsArray( + $arguments + ); + self::assertArrayHasKey( + 'time', + $arguments + ); + } + + #[Test] + public function logErrorIfEmailToSellerCouldNotSend(): void + { + $mockBuilder = $this->getMockBuilderForMailHandlerClass(mailerThrowException: true); + $mockBuilder->onlyMethods( + [ + 'getSellerEmailTo', + 'getSellerEmailFrom', + 'getSellerEmailName', + ] + ); + $mailHandler = $mockBuilder->getMock(); + $mailHandler->method('getSellerEmailTo')->willReturn('sellerEmailTo@example.com'); + $mailHandler->method('getSellerEmailFrom')->willReturn('sellerEmailFrom@example.com'); + $mailHandler->method('getSellerEmailName')->willReturn('Seller Email Name'); + + $mailHandler->sendSellerMail( + $this->createStubForOrderItem() + ); + + $logEntries = $this->getAllRecords('tx_cart_domain_model_order_log'); + self::assertCount( + 1, + $logEntries + ); + self::assertIsArray($logEntries[0]); + + self::assertArrayIsEqualToArrayIgnoringListOfKeys( + [ + 'log_level' => 'error', + 'item' => 142, + 'type' => 'sendSellerMail', + 'message' => 'Mail could not send to seller.', + 'level' => 'error', + ], + $logEntries[0], + [ + 'uid', + 'request_id', + 'crdate', + 'time_micro', + 'data', + 'arguments', + ] + ); + + self::assertIsString( + $logEntries[0]['arguments'] + ); + $arguments = json_decode( + ltrim($logEntries[0]['arguments'], '- '), + true + ); + self::assertIsArray( + $arguments + ); + self::assertArrayHasKey( + 'time', + $arguments + ); + self::assertArrayHasKey( + 'exception', + $arguments + ); + self::assertIsString( + $arguments['exception'] + ); + self::assertStringStartsWith( + 'Exception in ', + $arguments['exception'] + ); + self::assertStringContainsString( + '/cart/Tests/Functional/Service/MailHandlerTest.php', + $arguments['exception'] + ); + } + + /** + * @return MockBuilder + */ + private function getMockBuilderForMailHandlerClass(bool $mailerThrowException = false): MockBuilder + { + $configurationManager = self::createStub(ConfigurationManagerInterface::class); + + $eventDispatcher = self::createStub(EventDispatcherInterface::class); + + $mailer = self::createStub(MailerInterface::class); + if ($mailerThrowException) { + $mailer->method('send')->willThrowException(new Exception()); + } + + $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); + $logService = GeneralUtility::makeInstance( + LogService::class, + $logger, + ); + + $mockBuilder = $this->getMockBuilder(MailHandler::class); + $mockBuilder->setConstructorArgs( + [ + $configurationManager, + $eventDispatcher, + $mailer, + $logService, + ] + ); + + return $mockBuilder; + } + + private function createStubForOrderItem(): OrderItem&Stub + { + $billingAddress = self::createStub(BillingAddress::class); + $billingAddress->method('getEmail')->willReturn('billingAddress@example.com'); + + $payment = self::createStub(Payment::class); + $payment->method('getStatus')->willReturn('open'); + + $orderItem = self::createStub(OrderItem::class); + $orderItem->method('getBillingAddress')->willReturn($billingAddress); + $orderItem->method('getPayment')->willReturn($payment); + $orderItem->method('getUid')->willReturn(142); + + return $orderItem; + } + +} diff --git a/ext_emconf.php b/ext_emconf.php index 4169be74..105ca46a 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -4,7 +4,7 @@ 'title' => 'Cart', 'description' => 'Shopping Cart(s) for TYPO3', 'category' => 'plugin', - 'version' => '11.7.2', + 'version' => '12.0.0', 'state' => 'stable', 'author' => 'Daniel Gohlke', 'author_email' => 'ext@extco.de', @@ -12,9 +12,9 @@ 'constraints' => [ 'depends' => [ 'php' => '8.2.0-8.4.99', - 'typo3' => '13.4.0-13.4.99', - 'extbase' => '13.4.0-13.4.99', - 'fluid' => '13.4.0-13.4.99', + 'typo3' => '14.1.0-14.4.99', + 'extbase' => '14.1.0-14.1.99', + 'fluid' => '14.1.0-14.1.99', ], 'conflicts' => [], 'suggests' => [], diff --git a/ext_localconf.php b/ext_localconf.php index 3d5add58..426e4017 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -11,73 +11,116 @@ use Extcode\Cart\Controller\Cart\PaymentController; use Extcode\Cart\Controller\Cart\ProductController; use Extcode\Cart\Controller\Cart\ShippingController; +use Extcode\Cart\Domain\Log\DatabaseWriter; +use TYPO3\CMS\Core\Log\LogLevel; +use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Extbase\Utility\ExtensionUtility; -// configure plugins +(static function (string $extKey) { + if (is_array($GLOBALS['TYPO3_CONF_VARS'] ?? null) === false) { + throw new Exception('$GLOBALS[\'TYPO3_CONF_VARS\'] is not an array', 1774601240); + } -ExtensionUtility::configurePlugin( - 'Cart', - 'MiniCart', - [ - CartPreviewController::class => 'show', - CurrencyController::class => 'update', - ], - [ - CartPreviewController::class => 'show', - CurrencyController::class => 'update', - ] -); + ArrayUtility::mergeRecursiveWithOverrule( + $GLOBALS['TYPO3_CONF_VARS'], + [ + 'LOG' => [ + 'Extcode' => [ + 'Cart' => [ + 'Domain' => [ + 'Log' => [ + 'LogService' => [ + 'writerConfiguration' => [ + LogLevel::INFO => [ + DatabaseWriter::class => [], + ], + ], + ], + ], + ], + ], + ], + ], + // view paths for TYPO3 Mail API + 'MAIL' => [ + 'templateRootPaths' => [ + '1588829280' => 'EXT:cart/Resources/Private/Templates/', + ], + 'partialRootPaths' => [ + '1588829280' => 'EXT:cart/Resources/Private/Partials/', + ], + ], + 'SYS' => [ + 'fluid' => [ + 'namespaces' => [ + 'cart' => [ + 1 => 'Extcode\\Cart\\ViewHelpers', + ], + ], + ], + ], + ] + ); -ExtensionUtility::configurePlugin( - 'Cart', - 'Cart', - [ - CartController::class => 'show, clear, update', - CountryController::class => 'update', - CouponController::class => 'add, remove', - CurrencyController::class => 'update', - OrderController::class => 'show, create', - PaymentController::class => 'update', - ProductController::class => 'add, remove', - ShippingController::class => 'update', - ], - [ - CartController::class => 'show, clear, update', - CountryController::class => 'update', - CouponController::class => 'add, remove', - CurrencyController::class => 'update', - OrderController::class => 'show, create', - PaymentController::class => 'update', - ProductController::class => 'add, remove', - ShippingController::class => 'update', - ] -); + // configure plugins + ExtensionUtility::configurePlugin( + 'Cart', + 'MiniCart', + [ + CartPreviewController::class => 'show', + CurrencyController::class => 'update', + ], + [ + CartPreviewController::class => 'show', + CurrencyController::class => 'update', + ] + ); -ExtensionUtility::configurePlugin( - 'Cart', - 'Currency', - [ - CurrencyController::class => 'edit, update', - ], - [ - CurrencyController::class => 'edit, update', - ] -); + ExtensionUtility::configurePlugin( + 'Cart', + 'Cart', + [ + CartController::class => 'show, clear, update', + CountryController::class => 'update', + CouponController::class => 'add, remove', + CurrencyController::class => 'update', + OrderController::class => 'show, create', + PaymentController::class => 'update', + ProductController::class => 'add, remove', + ShippingController::class => 'update', + ], + [ + CartController::class => 'show, clear, update', + CountryController::class => 'update', + CouponController::class => 'add, remove', + CurrencyController::class => 'update', + OrderController::class => 'show, create', + PaymentController::class => 'update', + ProductController::class => 'add, remove', + ShippingController::class => 'update', + ] + ); -ExtensionUtility::configurePlugin( - 'Cart', - 'Order', - [ - \Extcode\Cart\Controller\Order\OrderController::class => 'list, show', - ], - [ - \Extcode\Cart\Controller\Order\OrderController::class => 'list, show', - ] -); + ExtensionUtility::configurePlugin( + 'Cart', + 'Currency', + [ + CurrencyController::class => 'edit, update', + ], + [ + CurrencyController::class => 'edit, update', + ] + ); -// register "cart:" namespace -$GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['cart'][] = 'Extcode\\Cart\\ViewHelpers'; + ExtensionUtility::configurePlugin( + 'Cart', + 'Order', + [ + \Extcode\Cart\Controller\Order\OrderController::class => 'list, show', + ], + [ + \Extcode\Cart\Controller\Order\OrderController::class => 'list, show', + ] + ); -// view paths for TYPO3 Mail API -$GLOBALS['TYPO3_CONF_VARS']['MAIL']['templateRootPaths']['1588829280'] = 'EXT:cart/Resources/Private/Templates/'; -$GLOBALS['TYPO3_CONF_VARS']['MAIL']['partialRootPaths']['1588829280'] = 'EXT:cart/Resources/Private/Partials/'; +})('cart'); diff --git a/ext_tables.sql b/ext_tables.sql index acc70928..686dc4f6 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -237,6 +237,27 @@ CREATE TABLE tx_cart_domain_model_coupon ( INDEX `parent` (pid), INDEX `t3ver_oid` (t3ver_oid,t3ver_wsid), ); +# +# Table structure for table 'tx_cart_domain_model_order_log' +# +CREATE TABLE tx_cart_domain_model_order_log ( + uid int(11) NOT NULL auto_increment, + item int(11) DEFAULT '0' NOT NULL, + type varchar(255) DEFAULT '' NOT NULL, + + request_id varchar(13) DEFAULT '' NOT NULL, + crdate int(11) DEFAULT '0' NOT NULL, + time_micro float DEFAULT '0' NOT NULL, + log_level varchar(10) DEFAULT 'info' NOT NULL, + level varchar(10) DEFAULT 'info' NOT NULL, + message text, + data text, + + arguments mediumtext, + + PRIMARY KEY (uid), +); + # # Table structure for table 'tx_cart_domain_model_cart' # @@ -269,3 +290,4 @@ CREATE TABLE tx_cart_domain_model_tag ( INDEX `parent` (pid), INDEX `t3ver_oid` (t3ver_oid,t3ver_wsid), ); +