Release 7.1.0

This commit is contained in:
Alberto G. Viu
2025-09-11 10:20:45 +02:00
parent d91a29c6b7
commit 2aafa38085
129 changed files with 688 additions and 131 deletions
@@ -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);
@@ -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;
@@ -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;
}
@@ -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;
}
}
@@ -0,0 +1,116 @@
<?php declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\Subscription\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Shopware\Commercial\Subscription\Checkout\Order\Generation\GenerateSubscriptionOrder;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Adapter\Console\ShopwareStyle;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Uuid\Uuid;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Messenger\MessageBusInterface;
#[AsCommand(
name: 'vrpayment:subscription:generate',
description: 'Manually dispatches a message to generate a recurring subscription order.',
)]
class GenerateSubscriptionOrderCommand extends Command
{
public function __construct(
private readonly MessageBusInterface $bus,
private readonly ?EntityRepository $subscriptionRepository
) {
parent::__construct();
}
protected function configure(): void
{
$this->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();
}
}
@@ -100,6 +100,7 @@ class CheckoutSubscriber implements EventSubscriberInterface
{
return [
CheckoutConfirmPageLoadedEvent::class => ['onConfirmPageLoaded', 1],
"subscription." . CheckoutConfirmPageLoadedEvent::class => ['onConfirmPageLoaded', 1],
MailBeforeValidateEvent::class => ['onMailBeforeValidate', 1],
];
}
+1 -1
View File
@@ -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',
];
}
+16 -6
View File
@@ -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()) {
@@ -5,12 +5,21 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!-- Commands -->
<service id="VRPaymentPayment\Core\Checkout\Subscription\Command\GenerateSubscriptionOrderCommand">
<argument type="service" id="Symfony\Component\Messenger\MessageBusInterface"/>
<argument type="service" id="subscription.repository" on-invalid="null" />
<tag name="console.command"/>
</service>
<!-- Services -->
<service id="VRPaymentPayment\Core\Checkout\PaymentHandler\VRPaymentPaymentHandler">
<argument type="service" id="VRPaymentPayment\Core\Checkout\Cart\CustomCartPersister"/>
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
<argument type="service" id="Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler"/>
<argument type="service" id="Shopware\Core\System\SalesChannel\Context\SalesChannelContextService"/>
<argument type="service" id="order_transaction.repository"/>
<argument type="service" id="subscription.repository" on-invalid="null" />
<call method="setLogger">
<argument type="service" id="monolog.logger.vrpayment_payment"/>
</call>