@@ -965,18 +983,23 @@ La tokenizzazione non è disponibile per i checkout degli ospiti.
Tokenization
-
+
8.7
+ Pagamenti ricorrenti
+
+
+
+ 8.8
Key Features
- 8.8
+ 8.9
Risoluzione dei Problemi
- 8.9
+ 8.10
FAQs
diff --git a/docs/it/resource/api-key.png b/docs/it/resource/api-key.png
new file mode 100644
index 0000000..c74fe13
Binary files /dev/null and b/docs/it/resource/api-key.png differ
diff --git a/docs/it/resource/application-users.png b/docs/it/resource/application-users.png
new file mode 100644
index 0000000..51a97b9
Binary files /dev/null and b/docs/it/resource/application-users.png differ
diff --git a/docs/it/resource/assign-role.png b/docs/it/resource/assign-role.png
new file mode 100644
index 0000000..616a1dd
Binary files /dev/null and b/docs/it/resource/assign-role.png differ
diff --git a/docs/it/resource/bogus-processor.png b/docs/it/resource/bogus-processor.png
new file mode 100644
index 0000000..db79bb1
Binary files /dev/null and b/docs/it/resource/bogus-processor.png differ
diff --git a/docs/it/resource/capture-transaction.png b/docs/it/resource/capture-transaction.png
new file mode 100644
index 0000000..5fb2044
Binary files /dev/null and b/docs/it/resource/capture-transaction.png differ
diff --git a/docs/it/resource/cc-disable.png b/docs/it/resource/cc-disable.png
new file mode 100644
index 0000000..88048c8
Binary files /dev/null and b/docs/it/resource/cc-disable.png differ
diff --git a/docs/it/resource/cc-enable.png b/docs/it/resource/cc-enable.png
new file mode 100644
index 0000000..9b7e655
Binary files /dev/null and b/docs/it/resource/cc-enable.png differ
diff --git a/docs/it/resource/connectors.png b/docs/it/resource/connectors.png
new file mode 100644
index 0000000..056a892
Binary files /dev/null and b/docs/it/resource/connectors.png differ
diff --git a/docs/it/resource/loading-roles.png b/docs/it/resource/loading-roles.png
new file mode 100644
index 0000000..5d0d501
Binary files /dev/null and b/docs/it/resource/loading-roles.png differ
diff --git a/docs/it/resource/name-processor.png b/docs/it/resource/name-processor.png
new file mode 100644
index 0000000..d028107
Binary files /dev/null and b/docs/it/resource/name-processor.png differ
diff --git a/docs/it/resource/order-confirmation-email.png b/docs/it/resource/order-confirmation-email.png
new file mode 100644
index 0000000..b7e8d57
Binary files /dev/null and b/docs/it/resource/order-confirmation-email.png differ
diff --git a/docs/it/resource/payment-method-configuration.png b/docs/it/resource/payment-method-configuration.png
new file mode 100644
index 0000000..c84e6c0
Binary files /dev/null and b/docs/it/resource/payment-method-configuration.png differ
diff --git a/docs/it/resource/payment-methods.png b/docs/it/resource/payment-methods.png
new file mode 100644
index 0000000..4bbea90
Binary files /dev/null and b/docs/it/resource/payment-methods.png differ
diff --git a/docs/it/resource/payment-settings.png b/docs/it/resource/payment-settings.png
new file mode 100644
index 0000000..bd758a5
Binary files /dev/null and b/docs/it/resource/payment-settings.png differ
diff --git a/docs/it/resource/plugin-configuration.png b/docs/it/resource/plugin-configuration.png
new file mode 100644
index 0000000..1fa6bd4
Binary files /dev/null and b/docs/it/resource/plugin-configuration.png differ
diff --git a/docs/it/resource/plugin-installation.png b/docs/it/resource/plugin-installation.png
new file mode 100644
index 0000000..5b32274
Binary files /dev/null and b/docs/it/resource/plugin-installation.png differ
diff --git a/docs/it/resource/refund-transaction.png b/docs/it/resource/refund-transaction.png
new file mode 100644
index 0000000..a03d38c
Binary files /dev/null and b/docs/it/resource/refund-transaction.png differ
diff --git a/docs/it/resource/roles.png b/docs/it/resource/roles.png
new file mode 100644
index 0000000..6dd3548
Binary files /dev/null and b/docs/it/resource/roles.png differ
diff --git a/docs/it/resource/save-role.png b/docs/it/resource/save-role.png
new file mode 100644
index 0000000..b5d30cb
Binary files /dev/null and b/docs/it/resource/save-role.png differ
diff --git a/docs/it/resource/shopware_6_stage_graph_delivery.svg b/docs/it/resource/shopware_6_stage_graph_delivery.svg
new file mode 100644
index 0000000..03e5a1d
--- /dev/null
+++ b/docs/it/resource/shopware_6_stage_graph_delivery.svg
@@ -0,0 +1,3 @@
+
+
+
Hold Open Open
Transaction ful... Transaction c... Canceled Transaction decline / fail / void
Transaction de... 1
2
3
Viewer does not support full SVG 1.1
\ No newline at end of file
diff --git a/docs/it/resource/shopware_6_stage_graph_order.svg b/docs/it/resource/shopware_6_stage_graph_order.svg
new file mode 100644
index 0000000..89a9ea7
--- /dev/null
+++ b/docs/it/resource/shopware_6_stage_graph_order.svg
@@ -0,0 +1,3 @@
+
+
+
In Progress Paid Open
Transaction Invoice paid / not applicable
Transaction Inv... Transaction a... Transaction fai... Canceled Transaction decline / void
Transaction de... Failed 1
4
2
3
Viewer does not support full SVG 1.1
\ No newline at end of file
diff --git a/docs/it/resource/state_graph_order.svg b/docs/it/resource/state_graph_order.svg
new file mode 100644
index 0000000..46dbc35
--- /dev/null
+++ b/docs/it/resource/state_graph_order.svg
@@ -0,0 +1,2 @@
+
+
[Not supported by viewer] [Not supported by viewer] [Not supported by viewer] [Not supported by viewer] [Not supported by viewer] [Not supported by viewer] [Not supported by viewer] Depending on configuration
[Not supported by viewer] Transaction decline / void
[Not supported by viewer] [Not supported by viewer] [Not supported by viewer] [Not supported by viewer] [Not supported by viewer] [Not supported by viewer] [Not supported by viewer]
\ No newline at end of file
diff --git a/docs/it/resource/state_graph_payment_state.svg b/docs/it/resource/state_graph_payment_state.svg
new file mode 100644
index 0000000..d09a614
--- /dev/null
+++ b/docs/it/resource/state_graph_payment_state.svg
@@ -0,0 +1,3 @@
+
+
+
In Progress Paid Open
Transaction complete / fulfill
Transaction com... Transaction a... Transaction fai... Canceled Transaction decline / void
Transaction de... Failed 1
4
2
3
Viewer does not support full SVG 1.1
\ No newline at end of file
diff --git a/docs/it/resource/token.png b/docs/it/resource/token.png
new file mode 100644
index 0000000..15be72e
Binary files /dev/null and b/docs/it/resource/token.png differ
diff --git a/docs/it/resource/user.png b/docs/it/resource/user.png
new file mode 100644
index 0000000..5673a04
Binary files /dev/null and b/docs/it/resource/user.png differ
diff --git a/docs/it/resource/void-transaction.png b/docs/it/resource/void-transaction.png
new file mode 100644
index 0000000..8037d28
Binary files /dev/null and b/docs/it/resource/void-transaction.png differ
diff --git a/docs/it/resource/webhook-listeners.png b/docs/it/resource/webhook-listeners.png
new file mode 100644
index 0000000..7b7f2b4
Binary files /dev/null and b/docs/it/resource/webhook-listeners.png differ
diff --git a/docs/it/resource/webhooks.png b/docs/it/resource/webhooks.png
new file mode 100644
index 0000000..27a58e1
Binary files /dev/null and b/docs/it/resource/webhooks.png differ
diff --git a/src/Core/Api/Transaction/Service/TransactionService.php b/src/Core/Api/Transaction/Service/TransactionService.php
index 20851c0..f8ea0dd 100644
--- a/src/Core/Api/Transaction/Service/TransactionService.php
+++ b/src/Core/Api/Transaction/Service/TransactionService.php
@@ -16,22 +16,23 @@ use Shopware\Core\{
};
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
-use VRPayment\Sdk\{
- Model\AddressCreate,
- Model\ChargeAttempt,
- Model\CreationEntityState,
- Model\CriteriaOperator,
- Model\EntityQuery,
- Model\EntityQueryFilter,
- Model\EntityQueryFilterType,
- Model\Gender,
- Model\LineItemAttributeCreate,
- Model\LineItemCreate,
- Model\LineItemType,
- Model\Transaction,
- Model\TransactionCreate,
- Model\TransactionPending,
- Model\TransactionState,
+use VRPayment\Sdk\Model\{
+ AddressCreate,
+ ChargeAttempt,
+ CreationEntityState,
+ CriteriaOperator,
+ EntityQuery,
+ EntityQueryFilter,
+ EntityQueryFilterType,
+ Gender,
+ LineItemAttributeCreate,
+ LineItemCreate,
+ LineItemType,
+ TokenizationMode,
+ Transaction,
+ TransactionCreate,
+ TransactionPending,
+ TransactionState,
};
use VRPaymentPayment\Core\{
Api\OrderDeliveryState\Handler\OrderDeliveryStateHandler,
@@ -195,6 +196,25 @@ class TransactionService
return $redirectUrl;
}
+ /**
+ * Creates the transaction in the portal using the SDK.
+ *
+ * @return void
+ */
+ public function createRecurringTransaction(TransactionCreate $sdkTransactionCreate, string $spaceId = ""): Transaction {
+ $settings = $this->settingsService->getSettings();
+ if (empty($spaceId)) {
+ $spaceId = $settings->getSpaceId();
+ }
+
+ $sdkTransaction = $settings->getApiClient()->getTransactionService()->create($spaceId, $sdkTransactionCreate);
+ if ($sdkTransaction->valid()) {
+ return $settings->getApiClient()->getTransactionService()->processWithoutUserInteraction($spaceId, $sdkTransaction->getId());
+ }
+
+ throw new \Exception("The transacion is not valid and could not be created.");
+ }
+
/**
* @param \Shopware\Core\Checkout\Payment\Cart\PaymentTransactionStruct $transaction
* @param \Shopware\Core\Framework\Context $context
@@ -391,7 +411,7 @@ class TransactionService
* @throws \VRPayment\Sdk\Http\ConnectionException
* @throws \VRPayment\Sdk\VersioningException
*/
- public function read(int $transactionId, string $salesChannelId): Transaction
+ public function read(int $transactionId, string $salesChannelId = ""): Transaction
{
$settings = $this->settingsService->getSettings($salesChannelId);
return $settings->getApiClient()->getTransactionService()->read($settings->getSpaceId(), $transactionId);
@@ -598,7 +618,8 @@ class TransactionService
->setCustomerEmailAddress($customer->getEmail())
->setCustomerId($customerId)
->setSuccessUrl($homeUrl . '?success')
- ->setFailedUrl($homeUrl . '?fail');
+ ->setFailedUrl($homeUrl . '?fail')
+ ->setTokenizationMode(TokenizationMode::FORCE_CREATION);
$transactionService = $settings->getApiClient()->getTransactionService();
$transaction = $transactionService->create($settings->getSpaceId(), $transactionPayload);
diff --git a/src/Core/Api/WebHooks/Strategy/WebHookTransactionStrategy.php b/src/Core/Api/WebHooks/Strategy/WebHookTransactionStrategy.php
index a100f45..f0b75f7 100644
--- a/src/Core/Api/WebHooks/Strategy/WebHookTransactionStrategy.php
+++ b/src/Core/Api/WebHooks/Strategy/WebHookTransactionStrategy.php
@@ -9,12 +9,13 @@ use Shopware\Core\{
Checkout\Cart\CartException,
Framework\Context,
System\StateMachine\Exception\IllegalTransitionException};
-use VRPayment\Sdk\{
- Model\RefundState,
- Model\Transaction,
- Model\TransactionInvoiceState,
- Model\TransactionState,
- Model\TransactionInvoice,};
+use VRPayment\Sdk\Model\{
+ RefundState,
+ Transaction,
+ TransactionInvoiceState,
+ TransactionState,
+ TransactionInvoice,
+ Token};
use VRPaymentPayment\Core\{
Api\WebHooks\Service\WebHooksService,
Api\WebHooks\Struct\WebHookRequest,
@@ -51,7 +52,7 @@ class WebHookTransactionStrategy extends WebHookStrategyBase implements WebhookS
* @throws \VRPayment\Sdk\Http\ConnectionException ConnectionException.
* @throws \VRPayment\Sdk\VersioningException VersioningException.
*/
- public function getTransaction(WebHookRequest $request) {
+ public function getTransaction(WebHookRequest $request): Transaction {
return $this->settings->getApiClient()
->getTransactionService()
->read($request->getSpaceId(), $request->getEntityId());
@@ -60,7 +61,7 @@ class WebHookTransactionStrategy extends WebHookStrategyBase implements WebhookS
/**
* @inheritDoc
*/
- public function getOrderIdByTransaction($transaction): string
+ public function getOrderIdByTransaction(Transaction $transaction): string
{
/** @var \VRPayment\Sdk\Model\Transaction $transaction */
return $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
@@ -96,7 +97,7 @@ class WebHookTransactionStrategy extends WebHookStrategyBase implements WebhookS
*/
public function process(WebHookRequest $request): Response
{
- return $this->updateTransaction($request, $this->getContext());
+ return $this->processTransaction($request, $this->getContext());
}
/**
@@ -107,16 +108,17 @@ class WebHookTransactionStrategy extends WebHookStrategyBase implements WebhookS
* @param Context $context The operational context providing settings and environment for transaction processing.
* @return Response Returns a JSON response indicating the result of the transaction update operation.
*/
- private function updateTransaction(WebHookRequest $request, Context $context): Response
+ private function processTransaction(WebHookRequest $request, Context $context): Response
{
$status = Response::HTTP_UNPROCESSABLE_ENTITY;
try {
/** @var \Shopware\Core\Checkout\Order\OrderEntity $order */
$transaction = $this->getTransaction($request);
- $orderId = $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
- if(!empty($orderId) && !$transaction->getParent()) {
- $this->executeLocked($orderId, $context, function () use ($orderId, $transaction, $context, $request) {
+ $token = $transaction->getToken();
+ $orderId = $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_ID];
+ if (!empty($orderId) && !$transaction->getParent()) {
+ $this->executeLocked($orderId, $context, function () use ($orderId, $transaction, $context, $request, $token) {
$this->transactionService->upsert($transaction, $context);
$orderTransactionId = $transaction->getMetaData()[TransactionPayload::VRPAYMENT_METADATA_ORDER_TRANSACTION_ID];
$orderTransaction = $this->getOrderTransaction($orderId, $context);
@@ -143,6 +145,16 @@ class WebHookTransactionStrategy extends WebHookStrategyBase implements WebhookS
$this->unholdDelivery($orderId, $context);
break;
case TransactionState::AUTHORIZED:
+ if ($token instanceof Token) {
+ // Update orderTransaction with the authorized token:
+ $data = [
+ 'id' => $orderTransactionId,
+ 'customFields' => [
+ TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_TOKEN => $token->getId(),
+ ],
+ ];
+ $this->container->get('order_transaction.repository')->update([$data], $context);
+ }
$this->orderTransactionStateHandler->process($orderTransactionId, $context);
$this->sendEmail($transaction, $context, $orderId);
break;
diff --git a/src/Core/Api/WebHooks/Strategy/WebhookStrategyActionsInterface.php b/src/Core/Api/WebHooks/Strategy/WebhookStrategyActionsInterface.php
index a2e25de..250db67 100644
--- a/src/Core/Api/WebHooks/Strategy/WebhookStrategyActionsInterface.php
+++ b/src/Core/Api/WebHooks/Strategy/WebhookStrategyActionsInterface.php
@@ -57,5 +57,5 @@ interface WebhookStrategyActionsInterface {
* @param Transaction|TransactionInvoiceState|Refund|mixed $transaction The transaction object from which the order ID should be extracted.
* @return string The order ID as a string.
*/
- public function getOrderIdByTransaction($transaction): string;
+ public function getOrderIdByTransaction(Transaction $transaction): string;
}
diff --git a/src/Core/Checkout/PaymentHandler/VRPaymentPaymentHandler.php b/src/Core/Checkout/PaymentHandler/VRPaymentPaymentHandler.php
index 5de412d..fd8c9bb 100644
--- a/src/Core/Checkout/PaymentHandler/VRPaymentPaymentHandler.php
+++ b/src/Core/Checkout/PaymentHandler/VRPaymentPaymentHandler.php
@@ -4,8 +4,10 @@ namespace VRPaymentPayment\Core\Checkout\PaymentHandler;
use Psr\Log\LoggerInterface;
use Shopware\Core\{
+ Checkout\Order\OrderEntity,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity,
Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler,
+ Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates,
Checkout\Payment\Cart\PaymentTransactionStruct,
Checkout\Payment\Cart\PaymentHandler\AbstractPaymentHandler,
Checkout\Payment\Cart\PaymentHandler\PaymentHandlerType,
@@ -16,9 +18,11 @@ use Shopware\Core\{
Framework\Context,
Framework\DataAbstractionLayer\EntityRepository,
Framework\DataAbstractionLayer\Search\Criteria,
+ Framework\DataAbstractionLayer\Search\Filter\EqualsFilter,
Framework\DataAbstractionLayer\Search\Sorting\FieldSorting,
Framework\Struct\Struct,
Framework\Validation\DataBag\RequestDataBag,
+ System\StateMachine\Aggregation\StateMachineState\StateMachineStateEntity,
System\SalesChannel\Context\SalesChannelContextService,
System\SalesChannel\Context\SalesChannelContextServiceParameters
};
@@ -33,7 +37,9 @@ use Symfony\Component\{
HttpFoundation\Request
};
use VRPayment\Sdk\Model\TransactionState;
-use VRPaymentPayment\Core\Api\Transaction\Service\TransactionService;
+use VRPaymentPayment\Core\Api\Transaction\Service\TransactionService as PluginTransactionService;
+use VRPaymentPayment\Core\Util\Payload\TransactionPayload;
+
/**
@@ -50,9 +56,9 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
private CustomCartPersister $cartPersister;
/**
- * @var \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService
+ * @var \VRPaymentPayment\Core\Api\Transaction\Service\PluginTransactionService
*/
- protected $transactionService;
+ protected $pluginTransactionService;
/**
* @var \Psr\Log\LoggerInterface
@@ -67,28 +73,26 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
protected EntityRepository $orderTransactionRepository;
+ protected ?EntityRepository $subscriptionRepository;
+
/**
* VRPaymentPaymentHandler constructor.
- *
- * @param \VRPaymentPayment\Core\Api\Transaction\Service\TransactionService $transactionService
- * @param \Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler $orderTransactionStateHandler
- * @param SalesChannelContextService $salesChannelContextService
- * @param EntityRepository $orderTransactionRepository
*/
public function __construct(
CustomCartPersister $cartPersister,
- TransactionService $transactionService,
+ PluginTransactionService $pluginTransactionService,
OrderTransactionStateHandler $orderTransactionStateHandler,
SalesChannelContextService $salesChannelContextService,
- EntityRepository $orderTransactionRepository
+ EntityRepository $orderTransactionRepository,
+ ?EntityRepository $subscriptionRepository,
) {
$this->cartPersister = $cartPersister;
- $this->transactionService = $transactionService;
+ $this->pluginTransactionService = $pluginTransactionService;
$this->orderTransactionStateHandler = $orderTransactionStateHandler;
$this->salesChannelContextService = $salesChannelContextService;
$this->orderTransactionRepository = $orderTransactionRepository;
+ $this->subscriptionRepository = $subscriptionRepository;
}
-
/**
* @param \Psr\Log\LoggerInterface $logger
*
@@ -134,16 +138,15 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
$redirectUrl = $transaction->getReturnUrl();
if ($orderTransaction->getOrder()->getAmountTotal() > 0) {
- $transactionId = $_SESSION['transactionId'] ?? null;
+ $transactionId = $request->getSession()->get('transactionId');
if ($transactionId === null) {
- $this->transactionService->createPendingTransaction($transaction, $salesChannelContext);
+ $this->pluginTransactionService->createPendingTransaction($salesChannelContext);
}
- $redirectUrl = $this->transactionService->create($transaction, $salesChannelContext);
+ $redirectUrl = $this->pluginTransactionService->create($transaction, $salesChannelContext);
}
return new RedirectResponse($redirectUrl);
-
} catch (\Throwable $e) {
- unset($_SESSION['transactionId']);
+ $request->getSession()->remove('transactionId');
$errorMessage = 'An error occurred during the communication with external payment gateway : ' . $e->getMessage();
$this->logger->critical($errorMessage);
throw PaymentException::customerCanceled($transaction->getOrderTransaction()->getId(), $errorMessage);
@@ -174,12 +177,12 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
)->getEntities()->first();
if ($orderTransaction->getOrder()->getAmountTotal() > 0) {
- $transactionEntity = $this->transactionService->getByOrderId(
+ $transactionEntity = $this->pluginTransactionService->getByOrderId(
$orderTransaction->getOrder()->getId(),
$context
);
- $vRPaymentTransaction = $this->transactionService->read(
+ $vRPaymentTransaction = $this->pluginTransactionService->read(
$transactionEntity->getTransactionId(),
$transactionEntity->getSalesChannelId()
);
@@ -189,7 +192,7 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
':orderId' => $orderTransaction->getOrder()->getId(),
':salesChannelName' => $transactionEntity->getSalesChannelId(),
]);
- unset($_SESSION['transactionId']);
+ $request->getSession()->remove('transactionId');
$this->logger->info($errorMessage);
throw PaymentException::customerCanceled($transaction->getOrderTransaction()->getId(), $errorMessage);
}
@@ -217,10 +220,240 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
string $paymentMethodId,
Context $context
): bool {
- if ($type === PaymentHandlerType::RECURRING) {
- return false;
- }
+ // Both PaymentHandlerType::RECURRING and PaymentHandlerType::REFUND are supported
+ //TODO: check that the payment method really supports recurring.
+ // In order to do that, we need to get this information in when synching the payment methods.
+ // The payment methods in the portal are managed by their Connectors. The Connectors need
+ // to support the recurrin and the refunding. These values are 1453357059666L and 1453351315899L for
+ // tokenization and refunding respectively.
return true;
}
+ public function recurring(
+ PaymentTransactionStruct $transaction,
+ Context $context
+ ): void {
+ if ($this->subscriptionRepository === null || !class_exists(\Shopware\Commercial\Subscription\Entity\Subscription\SubscriptionEntity::class)) {
+ throw PaymentException::paymentTypeUnsupported(
+ $transaction->getOrderTransactionId(),
+ 'Shopware Commercial plugin with Subscription feature is not installed or active. Recurring payments cannot be processed.'
+ );
+ }
+
+ if ($transaction->isRecurring() === false) {
+ //TODO: Provide payment-method-id instead of order-transaction-id
+ throw PaymentException::paymentTypeUnsupported($transaction->getOrderTransaction()->getId(), PaymentHandlerType::RECURRING);
+ }
+
+ $recurringData = $transaction->getRecurring();
+ $newTransactionId = $transaction->getOrderTransactionId();
+
+ if ($recurringData === null) {
+ throw PaymentException::recurringInterrupted($newTransactionId, 'Recurring payment data is missing from the transaction struct.');
+ }
+
+ try {
+ // Get information about the subscription
+ $subscriptionId = $recurringData->getSubscriptionId();
+ $criteria = new Criteria([$subscriptionId]);
+ $criteria->addAssociation('orders.transactions.stateMachineState');
+
+ /** @var SubscriptionEntity|null $subscription */
+ $subscription = $this->subscriptionRepository->search($criteria, $context)->get($subscriptionId);
+
+ if ($subscription === null) {
+ throw PaymentException::recurringInterrupted($newTransactionId, sprintf('Subscription with ID "%s" could not be found.', $subscriptionId));
+ }
+
+ // Find the original order and transaction
+ $orders = $subscription->getOrders();
+ if ($orders === null || $orders->count() === 0) {
+ throw PaymentException::recurringInterrupted($newTransactionId, 'No orders found associated with the subscription.');
+ }
+
+ $orders->sort(fn (OrderEntity $a, OrderEntity $b) => $a->getCreatedAt() <=> $b->getCreatedAt());
+ /** @var OrderEntity|null $originalOrder */
+ $originalOrder = $orders->first();
+
+ $originalTransactions = $originalOrder->getTransactions();
+
+ if ($originalTransactions === null) {
+ throw PaymentException::recurringInterrupted($newTransactionId, 'No transactions found on the original order.');
+ }
+
+ /** @var OrderTransactionEntity|null $originalTransaction */
+ $originalTransaction = $originalTransactions->filter(
+ fn (OrderTransactionEntity $t) => $t->getStateMachineState()?->getTechnicalName() === OrderTransactionStates::STATE_PAID
+ )->first();
+
+ if ($originalTransaction === null) {
+ throw PaymentException::recurringInterrupted($newTransactionId, 'A successful, paid transaction could not be found on the original order to retrieve payment details.');
+ }
+
+ $newOrderTransaction = $this->orderTransactionRepository->search(
+ (new Criteria([$newTransactionId]))
+ ->addAssociation('order'), $context
+ )->getEntities()->first();
+ $orderNumber = $newOrderTransaction->getOrder()->getOrderNumber();
+
+ // Access the custom fields for getting the original transaction details
+ $customFields = $originalTransaction->getCustomFields();
+ // The tokenReference is not really needed because it's also stored in the original transaction
+ $tokenReference = $customFields[TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_TOKEN] ?? null;
+ $spaceId = (string) $customFields[TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_SPACE_ID] ?? null;
+ $sdkTransactionId = $customFields[TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_TRANSACTION_ID] ?? null;
+
+ if ($sdkTransactionId === null || $spaceId === null) {
+ throw PaymentException::recurringInterrupted($newTransactionId, 'Required original transaction ID and spaceId is missing from order transaction custom fields.');
+ }
+
+ /** @var \VRPayment\Sdk\Model\Transaction $originalSdkTransaction */
+ $originalSdkTransaction = $this->pluginTransactionService->read($sdkTransactionId, "");
+
+ //TODO: Consider moving this logic to its own function for improved readability
+ $sdkTransactionCreate = new \VRPayment\Sdk\Model\TransactionCreate;
+
+ // Build the new transaction based on the original transaction
+ $sdkTransactionCreate->setCurrency($originalSdkTransaction->getCurrency());
+ $sdkTransactionCreate->setBillingAddress($this->addressCreateFromSdk($originalSdkTransaction->getBillingAddress()));
+ $sdkTransactionCreate->setShippingAddress($this->addressCreateFromSdk($originalSdkTransaction->getShippingAddress()));
+ $sdkTransactionCreate->setShippingMethod($originalSdkTransaction->getShippingMethod());
+ $sdkTransactionCreate->setCustomerEmailAddress($originalSdkTransaction->getCustomerEmailAddress());
+ $sdkTransactionCreate->setCustomerId($originalSdkTransaction->getCustomerId());
+ $sdkTransactionCreate->setLanguage($originalSdkTransaction->getLanguage());
+ // Get the merchant reference from the new Order, not the original one
+ $sdkTransactionCreate->setMerchantReference($orderNumber);
+ $sdkTransactionCreate->setInvoiceMerchantReference($originalSdkTransaction->getInvoiceMerchantReference());
+
+ $lineItems = $originalSdkTransaction->getLineItems();
+ $lineItemsCreate = [];
+ foreach ($lineItems as $lineItem) {
+ $lineItemsCreate[] = $this->lineItemCreateFromSdk($lineItem);
+ }
+ if (count($lineItemsCreate) > 0) {
+ $sdkTransactionCreate->setLineItems($lineItemsCreate);
+ }
+
+ $sdkTransactionCreate->setSuccessUrl($originalSdkTransaction->getSuccessUrl());
+ $sdkTransactionCreate->setToken($originalSdkTransaction->getToken());
+ $sdkTransactionCreate->setTokenizationMode($originalSdkTransaction->getTokenizationMode());
+ $sdkTransactionCreate->setMetaData($originalSdkTransaction->getMetaData());
+
+ // Create the new recurring transaction
+ $newSdkTransaction = $this->pluginTransactionService->createRecurringTransaction($sdkTransactionCreate, $spaceId);
+
+ // Set the new state for the new order transaction
+ if (in_array($newSdkTransaction->getState(), [TransactionState::AUTHORIZED, TransactionState::COMPLETED, TransactionState::CONFIRMED, TransactionState::FULFILL])) {
+ $this->orderTransactionStateHandler->paid($newTransactionId, $context);
+ } elseif (in_array($newSdkTransaction->getState(), [TransactionState::DECLINE, TransactionState::FAILED, TransactionState::VOIDED])) {
+ $this->orderTransactionStateHandler->fail($newTransactionId, $context);
+ } elseif (in_array($newSdkTransaction->getState(), [TransactionState::PENDING, TransactionState::PROCESSING])) {
+ $this->orderTransactionStateHandler->process($newTransactionId, $context);
+ } else {
+ $this->orderTransactionStateHandler->reopen($newTransactionId, $context);
+ }
+
+ $data = [
+ 'id' => $newTransactionId,
+ 'customFields' => [
+ TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_TRANSACTION_ID => $newSdkTransaction->getId(),
+ TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_SPACE_ID => $spaceId,
+ TransactionPayload::ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_TOKEN => $tokenReference,
+ ],
+ ];
+
+ // Update the new order transaction with the new transaction details
+ $this->orderTransactionRepository->update([$data], $context);
+ $this->pluginTransactionService->upsert($newSdkTransaction, $context);
+ }
+ catch (\Throwable $e) {
+ $errorMessage = 'An error occurred during the communication with external payment gateway : ' . $e->getMessage();
+ $this->logger->critical($errorMessage);
+ throw PaymentException::recurringInterrupted($transaction->getOrderTransactionId(), $errorMessage);
+ }
+ }
+
+ /**
+ * Creates a new AddressCreate instance from the given SDK Address model.
+ *
+ * @param \VRPayment\Sdk\Model\Address $address The address model from the SDK.
+ * @return \VRPayment\Sdk\Model\AddressCreate The newly created AddressCreate instance.
+ */
+ private function addressCreateFromSdk(\VRPayment\Sdk\Model\Address $address): \VRPayment\Sdk\Model\AddressCreate {
+ $addressCreate = new \VRPayment\Sdk\Model\AddressCreate;
+
+ $addressCreate->setCity($address->getCity());
+ $addressCreate->setCommercialRegisterNumber($address->getCommercialRegisterNumber());
+ $addressCreate->setCountry($address->getCountry());
+ $addressCreate->setDateOfBirth($address->getDateOfBirth());
+ $addressCreate->setDependentLocality($address->getDependentLocality());
+ $addressCreate->setEmailAddress($address->getEmailAddress());
+ $addressCreate->setFamilyName($address->getFamilyName());
+ $addressCreate->setGender($address->getGender());
+ $addressCreate->setGivenName($address->getGivenName());
+ $addressCreate->setMobilePhoneNumber($address->getMobilePhoneNumber());
+ $addressCreate->setOrganizationName($address->getOrganizationName());
+ $addressCreate->setPhoneNumber($address->getPhoneNumber());
+ $addressCreate->setPostalState($address->getPostalState());
+ $addressCreate->setPostcode($address->getPostcode());
+ $addressCreate->setSalesTaxNumber($address->getSalesTaxNumber());
+ $addressCreate->setSalutation($address->getSalutation());
+ $addressCreate->setSocialSecurityNumber($address->getSocialSecurityNumber());
+ $addressCreate->setSortingCode($address->getSortingCode());
+ $addressCreate->setStreet($address->getStreet());
+
+ return $addressCreate;
+ }
+
+ /**
+ * Creates a LineItemCreate object from a given SDK LineItem.
+ *
+ * This method takes a \VRPayment\Sdk\Model\LineItem instance and transforms it into a
+ * \VRPayment\Sdk\Model\LineItemCreate object, which can be used for further processing
+ * or integration with the VRPayment payment SDK.
+ *
+ * @param \VRPayment\Sdk\Model\LineItem $lineItem The line item from the SDK to convert.
+ * @return \VRPayment\Sdk\Model\LineItemCreate The created LineItemCreate object.
+ */
+ private function lineItemCreateFromSdk(\VRPayment\Sdk\Model\LineItem $lineItem): \VRPayment\Sdk\Model\LineItemCreate
+ {
+ $lineItemCreate = new \VRPayment\Sdk\Model\LineItemCreate();
+
+ $lineItemCreate->setAmountIncludingTax($lineItem->getAmountIncludingTax());
+
+ $attributes = $lineItem->getAttributes();
+ $attributesCreate = [];
+ foreach ($attributes as $id => $attribute) {
+ $attributeCreate = new \VRPayment\Sdk\Model\LineItemAttributeCreate();
+ $attributeCreate->setLabel($attribute->getLabel());
+ $attributeCreate->setValue($attribute->getValue());
+ $attributesCreate[$id] = $attributeCreate;
+ }
+ if (count($attributesCreate) > 0) {
+ $lineItemCreate->setAttributes($attributesCreate);
+ }
+
+ $lineItemCreate->setDiscountIncludingTax($lineItem->getDiscountIncludingTax());
+ $lineItemCreate->setName($lineItem->getName());
+ $lineItemCreate->setQuantity($lineItem->getQuantity());
+ $lineItemCreate->setShippingRequired($lineItem->getShippingRequired());
+ $lineItemCreate->setSku($lineItem->getSku());
+
+ $taxes = $lineItem->getTaxes();
+ $taxesCreate = [];
+ foreach ($taxes as $tax) {
+ $taxCreate = new \VRPayment\Sdk\Model\TaxCreate();
+ $taxCreate->setRate($tax->getRate());
+ $taxCreate->setTitle($tax->getTitle());
+ $taxesCreate[] = $taxCreate;
+ }
+ if (count($taxesCreate) > 0) {
+ $lineItemCreate->setTaxes($taxesCreate);
+ }
+
+ $lineItemCreate->setType($lineItem->getType());
+ $lineItemCreate->setUniqueId($lineItem->getUniqueId());
+
+ return $lineItemCreate;
+ }
}
diff --git a/src/Core/Checkout/Subscription/Command/GenerateSubscriptionOrderCommand.php b/src/Core/Checkout/Subscription/Command/GenerateSubscriptionOrderCommand.php
new file mode 100644
index 0000000..8719a5b
--- /dev/null
+++ b/src/Core/Checkout/Subscription/Command/GenerateSubscriptionOrderCommand.php
@@ -0,0 +1,116 @@
+addArgument('subscriptionIdentifier', InputArgument::REQUIRED, 'The ID or Number of the subscription to process.');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new ShopwareStyle($input, $output);
+
+ if ($this->subscriptionRepository === null || !class_exists(\Shopware\Commercial\Subscription\Entity\Subscription\SubscriptionEntity::class)) {
+ $io->error('Subscription functionality is not available in this Shopware instance. Please ensure the Subscription plugin is installed and enabled.');
+ return self::FAILURE;
+ }
+
+ $identifier = $input->getArgument('subscriptionIdentifier');
+
+ if (!is_string($identifier)) {
+ $io->error('Invalid Subscription ID provided.');
+ return self::FAILURE;
+ }
+ $subscriptionId = $this->findSubscriptionId($identifier, $io);
+
+ if ($subscriptionId === null) {
+ // Error message is already printed in findSubscriptionId
+ return self::FAILURE;
+ }
+
+ $io->text(sprintf('Forcing next schedule for subscription ID: %s', $subscriptionId));
+ $this->forceNextSchedule($subscriptionId);
+
+ $io->title('Subscription Order Generation');
+ $io->text(sprintf('Dispatching GenerateSubscriptionOrder message for subscription ID: %s', $subscriptionId));
+
+ $this->bus->dispatch(new GenerateSubscriptionOrder($subscriptionId));
+
+ $io->success('Message dispatched successfully!');
+ $io->note('Ensure a message consumer is running to process the queue: "bin/console messenger:consume async"');
+
+ return self::SUCCESS;
+ }
+
+ /**
+ * Set the next schedule date to the current time, so it will be processed immediately.
+ *
+ * @param string $subscriptionId
+ * @return void
+ */
+ private function forceNextSchedule(string $subscriptionId): void
+ {
+ $context = Context::createDefaultContext();
+ $this->subscriptionRepository->update([
+ [
+ 'id' => $subscriptionId,
+ 'nextSchedule' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
+ ]
+ ], $context);
+ }
+
+ private function findSubscriptionId(string $identifier, ShopwareStyle $io): ?string
+ {
+ $context = Context::createDefaultContext();
+ if (Uuid::isValid($identifier)) {
+ // Check if a subscription with this ID actually exists
+ $result = $this->subscriptionRepository->searchIds(new Criteria([$identifier]), $context);
+ if ($result->firstId()) {
+ return $identifier;
+ }
+ $io->error(sprintf('No subscription found with ID "%s".', $identifier));
+ return null;
+ }
+
+ // If not a UUID, assume it's a subscription number
+ $criteria = new Criteria();
+ $criteria->addFilter(new EqualsFilter('subscriptionNumber', $identifier));
+ $result = $this->subscriptionRepository->searchIds($criteria, $context);
+
+ if ($result->firstId() === null) {
+ $io->error(sprintf('No subscription found with number "%s".', $identifier));
+ return null;
+ }
+
+ return $result->firstId();
+ }
+}
diff --git a/src/Core/Storefront/Checkout/Subscriber/CheckoutSubscriber.php b/src/Core/Storefront/Checkout/Subscriber/CheckoutSubscriber.php
index 9baf214..9109457 100644
--- a/src/Core/Storefront/Checkout/Subscriber/CheckoutSubscriber.php
+++ b/src/Core/Storefront/Checkout/Subscriber/CheckoutSubscriber.php
@@ -100,6 +100,7 @@ class CheckoutSubscriber implements EventSubscriberInterface
{
return [
CheckoutConfirmPageLoadedEvent::class => ['onConfirmPageLoaded', 1],
+ "subscription." . CheckoutConfirmPageLoadedEvent::class => ['onConfirmPageLoaded', 1],
MailBeforeValidateEvent::class => ['onMailBeforeValidate', 1],
];
}
diff --git a/src/Core/Util/Analytics/Analytics.php b/src/Core/Util/Analytics/Analytics.php
index 474d11c..2a02274 100644
--- a/src/Core/Util/Analytics/Analytics.php
+++ b/src/Core/Util/Analytics/Analytics.php
@@ -25,7 +25,7 @@ class Analytics {
self::SHOP_SYSTEM => 'shopware',
self::SHOP_SYSTEM_VERSION => '6',
self::SHOP_SYSTEM_AND_VERSION => 'shopware-6',
- self::PLUGIN_SYSTEM_VERSION => '7.0.1',
+ self::PLUGIN_SYSTEM_VERSION => '7.1.0',
];
}
diff --git a/src/Core/Util/Payload/TransactionPayload.php b/src/Core/Util/Payload/TransactionPayload.php
index ebc1e14..30368e8 100644
--- a/src/Core/Util/Payload/TransactionPayload.php
+++ b/src/Core/Util/Payload/TransactionPayload.php
@@ -56,6 +56,7 @@ class TransactionPayload extends AbstractPayload
public const ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_SPACE_ID = 'vrpayment_space_id';
public const ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_TRANSACTION_ID = 'vrpayment_transaction_id';
+ public const ORDER_TRANSACTION_CUSTOM_FIELDS_VRPAYMENT_TOKEN = 'vrpayment_token';
public const VRPAYMENT_METADATA_SALES_CHANNEL_ID = 'salesChannelId';
public const VRPAYMENT_METADATA_ORDER_ID = 'orderId';
@@ -457,7 +458,7 @@ class TransactionPayload extends AbstractPayload
*/
protected function addOptionalLineItems(array &$lineItems): void
{
- if (count($this->order->getShippingCosts()->getCalculatedTaxes()) === 1) {
+ if ($this->order->getShippingCosts() && $this->order->getShippingTotal() > 0) {
if ($shippingLineItem = $this->getShippingLineItem()) {
$lineItems[] = $shippingLineItem;
}
@@ -728,12 +729,21 @@ class TransactionPayload extends AbstractPayload
{
$lineItem = null;
- $lineItemPriceTotal = array_sum(array_map(static function (LineItemCreate $lineItem) {
- return $lineItem->getAmountIncludingTax();
- }, $lineItems));
+ // Calculate total of all current line items
+ $lineItemPriceTotal = array_sum(array_map(static fn(LineItemCreate $li) => $li->getAmountIncludingTax(), $lineItems));
- $adjustmentPrice = $this->order->getAmountTotal() - $lineItemPriceTotal;
- $adjustmentPrice = self::round($adjustmentPrice);
+ $this->logger->debug("LineItem price total before adjustment: $lineItemPriceTotal");
+ // Get shipping total including taxes from the order
+ $shippingCosts = $this->order->getShippingCosts();
+ $shippingTotal = $shippingCosts ? self::round($shippingCosts->getTotalPrice()) : 0.0;
+
+ // Add shipping to the line items total if it's not already included
+ $hasShippingLineItem = array_filter($lineItems, static fn(LineItemCreate $li) => $li->getType() === LineItemType::SHIPPING);
+ if (!$hasShippingLineItem && $shippingTotal > 0) {
+ $lineItemPriceTotal += $shippingTotal;
+ }
+
+ $adjustmentPrice = self::round($this->order->getAmountTotal() - $lineItemPriceTotal);
if (abs($adjustmentPrice) != 0) {
if ($this->settings->isLineItemConsistencyEnabled()) {
diff --git a/src/Resources/config/services/core/checkout.xml b/src/Resources/config/services/core/checkout.xml
index efeef09..ee1861e 100644
--- a/src/Resources/config/services/core/checkout.xml
+++ b/src/Resources/config/services/core/checkout.xml
@@ -5,12 +5,21 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
+
+
+
+
+
+
+
+
+