Shopware 6: Tax Free with domestic delivery in DE (TaxDecorator & CartRuleLoader) - shopware6

Some customers with a domestic shipping address within Germany are still tax exempt (e.g. NATO, or US Army).
Since this is considered an export delivery.
How can this be implemented? The only solution I have found is to completely override/replace CartRuleLoader
What I have done so far: I created a custom field in Customers, where a store editor can specify a customer as tax free (even within Germany).
Then I overwrote the class TaxDetector:
class TaxDetector extends \Shopware\Core\Checkout\Cart\Tax\TaxDetector
{
public function isNetDelivery(SalesChannelContext $context): bool
{
$customer = $context->getCustomer();
if ($customer && ($cf = $customer->getCustomFields()) && !empty($cf['mycustomfield']) && $cf['mycustomfield'] === 'nato') {
return true;
}
return parent::isNetDelivery($context);
}
public function getTaxState(SalesChannelContext $context): string
{
$customer = $context->getCustomer();
if ($customer && ($cf = $customer->getCustomFields()) && !empty($cf['mycustomfield']) && $cf['mycustomfield'] === 'nato') {
return CartPrice::TAX_STATE_FREE;
}
return parent::getTaxState($context);
}
}
The problem is that Shopware 6 checks the validity of the tax status in CartRuleLoader when completing an order. And here the test is done in private functions that I cannot override.
load -> validateTaxFree -> detectTaxType
Any suggestions on how to solve this more eligant and futerprove then override all CartRuleLoader?

This is indeed a little tricky.
When you look at the method CartRuleLoader::detectTaxType you can find these lines:
$currency = $context->getCurrency();
$currencyTaxFreeAmount = $currency->getTaxFreeFrom();
$isReachedCurrencyTaxFreeAmount = $currencyTaxFreeAmount > 0 && $cartNetAmount >= $currencyTaxFreeAmount;
if ($isReachedCurrencyTaxFreeAmount) {
return CartPrice::TAX_STATE_FREE;
}
It's arguably a little hacky but you could "trick" Shopware to return the tax free state by setting the tax free amount of the currency to a minimal amount.
Therefore you could decorate Shopware\Core\System\SalesChannel\Context\SalesChannelContextFactory.
<service id="MyPlugin\Core\System\SalesChannel\Context\SalesChannelContextFactoryDecorator" decorates="Shopware\Core\System\SalesChannel\Context\SalesChannelContextFactory" decoration-priority="-999">
<argument type="service" id="MyPlugin\Core\System\SalesChannel\Context\SalesChannelContextFactoryDecorator.inner"/>
</service>
In the decorator you could then have your logic and set the tax free amount:
<?php declare(strict_types=1);
namespace MyPlugin\Core\System\SalesChannel\Context;
use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
use Shopware\Core\System\SalesChannel\Context\AbstractSalesChannelContextFactory;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
class SalesChannelContextFactoryDecorator extends AbstractSalesChannelContextFactory
{
private AbstractSalesChannelContextFactory $decorated;
public function __construct(
AbstractSalesChannelContextFactory $decorated,
) {
$this->decorated = $decorated;
}
public function getDecorated(): AbstractSalesChannelContextFactory
{
return $this->decorated;
}
public function create(string $token, string $salesChannelId, array $options = []): SalesChannelContext
{
$context = $this->getDecorated()->create($token, $salesChannelId, $options);
// ... your logic
if (...) {
$context->getCurrency()->setTaxFreeFrom(0.00001);
$context->setTaxState(CartPrice::TAX_STATE_FREE);
}
return $context;
}
}
Edit: It looks like this could not be enough as there is a security check which checks if the previous state wasn't tax free and resets it otherwise.
if ($previous !== CartPrice::TAX_STATE_FREE) {
$context->setTaxState($previous);
}
You might have to additionally set the tax state inside your decorator (updated the above example). The tax free amount would then make it stick.

Related

Exclude specific products from Product Indexer in Shopware 6

We have four specific products with a massive amount of variants. When running the Product Indexer we run out of memory because of these products.
So we want to exclude these specific products from the Product Indexer Job.
My first approach was to use the ProductIndexerEvent, but the event is dispatched at the end of the handle() method :
(vendor/shopware/core/Content/Product/DataAbstractionLayer/ProductIndexer.php:187),
which is probably too late.
What is the best approach to implement that behaviour?
I would advise against excluding products from being indexed. There's business logic relying on the data being indexed.
If you're confident in what you're doing and know about the consequences, you could decorate the ProductIndexer service.
<service id="Foo\MyPlugin\ProductIndexerDecorator" decorates="Shopware\Core\Content\Product\DataAbstractionLayer\ProductIndexer">
<argument type="service" id="Foo\MyPlugin\ProductIndexerDecorator.inner"/>
</service>
In the decorator you would have to deconstruct the original event, filter the WriteResult instances by excluded IDs and then pass the reconstructed event to the decorated service.
class ProductIndexerDecorator extends EntityIndexer
{
const FILTERED_IDS = ['9b180c61ddef4dad89e9f3b9fa13f3be'];
private EntityIndexer $decorated;
public function __construct(EntityIndexer $decorated)
{
$this->decorated = $decorated;
}
public function getDecorated(): EntityIndexer
{
return $this->decorated;
}
public function getName(): string
{
return $this->getDecorated()->getName();
}
public function iterate($offset): ?EntityIndexingMessage
{
return $this->getDecorated()->iterate($offset);
}
public function update(EntityWrittenContainerEvent $event): ?EntityIndexingMessage
{
$originalEvents = clone $event->getEvents();
if (!$originalEvents) {
return $this->getDecorated()->update($event);
}
$event->getEvents()->clear();
/** #var EntityWrittenEvent $writtenEvent */
foreach ($originalEvents as $writtenEvent) {
if ($writtenEvent->getEntityName() !== 'product') {
$event->getEvents()->add($writtenEvent);
continue;
}
$results = [];
foreach ($writtenEvent->getWriteResults() as $result) {
if (\in_array($result->getPrimaryKey(), self::FILTERED_IDS, true)) {
continue;
}
$results[] = $result;
}
$event->getEvents()->add(new EntityWrittenEvent('product', $results, $event->getContext()));
}
return $this->getDecorated()->update($event);
}
public function handle(EntityIndexingMessage $message): void
{
$data = array_diff($message->getData(), self::FILTERED_IDS);
$newMessage = new ProductIndexingMessage($data, $message->getOffset(), $message->getContext(), $message->forceQueue());
$this->getDecorated()->handle($newMessage);
}
public function getTotal(): int
{
return $this->getDecorated()->getTotal();
}
public function getOptions(): array
{
return $this->getDecorated()->getOptions();
}
}

Is Sylius PayumBundle handling payment details incorrectly?

I am testing Bitbag/PayUPlugin and I was stopped by gateway API with error "Required data missing".
After some debugging, I realised that Sylius Payment entity, specifically "details" property, is not fulfilled with data.
After change condition on line 53:
https://github.com/Sylius/Sylius/blob/4e06a4dfb8dc56731470016bb97165f3025947b7/src/Sylius/Bundle/PayumBundle/Action/CapturePaymentAction.php#L53
to
if ($status->isNew() || $status->isUnknown()) {
payment gateway seems to work correctly.
Is it a bug or am I doing something wrong ?
Sylius/Sylius v1.4.6
Bitbag/PayUPlugin v1.8.0
Unlikely there is an error in PayumBundle/CapturePaymentAction (because more people used PayumBundle than PayUPlugin, so probability of bug is less), conceptually payment object status at the beginning should be "new" instead of "unknown", so the condition should work.
So you should find out https://github.com/BitBagCommerce/SyliusPayUPlugin/blob/master/src/Action/StatusAction.php#L58 class, why it doesn't reach markNew() line.
I guess the BitBagCommerce/SyliusPayUPlugin is dead as this issue hasn't been addressed yet, since July.
In order to fix this I had to decorate the StatusAction class:
<?php
declare(strict_types=1);
namespace App\Payment\PayU;
use BitBag\SyliusPayUPlugin\Action\StatusAction;
use BitBag\SyliusPayUPlugin\Bridge\OpenPayUBridgeInterface;
use Payum\Core\Action\ActionInterface;
use Payum\Core\Bridge\Spl\ArrayObject;
use Payum\Core\Exception\RequestNotSupportedException;
use Payum\Core\Request\GetStatusInterface;
final class StatusActionDecorator implements ActionInterface
{
private $action;
public function __construct(StatusAction $action)
{
$this->action = $action;
}
public function setApi($api): void
{
$this->action->setApi($api);
}
public function execute($request): void
{
/** #var $request GetStatusInterface */
RequestNotSupportedException::assertSupports($this, $request);
$model = ArrayObject::ensureArrayObject($request->getModel());
$status = $model['statusPayU'] ?? null;
if (empty($status) || OpenPayUBridgeInterface::NEW_API_STATUS === $status) {
$request->markNew();
return;
}
if (OpenPayUBridgeInterface::PENDING_API_STATUS === $status) {
$request->markPending();
return;
}
if (OpenPayUBridgeInterface::CANCELED_API_STATUS === $status) {
$request->markCanceled();
return;
}
if (OpenPayUBridgeInterface::WAITING_FOR_CONFIRMATION_PAYMENT_STATUS === $status) {
$request->markSuspended();
return;
}
if (OpenPayUBridgeInterface::COMPLETED_API_STATUS === $status) {
$request->markCaptured();
return;
}
$request->markUnknown();
}
public function supports($request): bool
{
return $this->action->supports($request);
}
}
then in the services.yaml:
App\Payment\PayU\StatusActionDecorator:
decorates: bitbag.payu_plugin.action.status
arguments: ['#App\Payment\PayU\StatusActionDecorator.inner']

Set default combination automaticly based on stock Prestashop 1.7

i need Prestashop to:
Check stock of combinations from a product.
If a combination is out of stock, set default combination to a different combination which is in stock.
This way the shop will not show : 'out of stock' at the product, on the category listing. Makes no sense, because the product is not out of stock, only 1 combination is out of stock.
Another solution will be: the out of stock sticker to check if there are any combinations in stock.
Please do not advise any modules.
Example given:
T-shirt in size Small, Medium and Large.
Small: 0 stock, Medium: 10 stock, Large: 10 stock.
Prestashop will now show T-shirt as out of stock on the frontend, unless i manually check Medium or Large as a default size.
You will need to modify ProductController(/controllers/front/ProductController.php)
Basically what i did was to check if the default attribute has stock. If there is stock,then there is no need to look else where.
If there is no stock, then get all the product attributes and loop through the attributes. Once attribute with stock is found, delete the old default attribute and set the found as default attribute.
Add this methods inside the productController Class
/*
start set default based on stock
*/
public function getProductAttributes()
{
$sql = "SELECT * FROM " . _DB_PREFIX_ . "product_attribute WHERE
id_product = ". (int) $this->product->id;
return Db::getInstance()->executeS($sql);
}
public function getProductStock($id_product_attribute)
{
return StockAvailable::getQuantityAvailableByProduct($this->product->id,
$id_product_attribute, $this->context->shop->id);
}
public function checkDefault()
{
$id_product_attribute = Product::getDefaultAttribute($this->product->id);
$stk = (int) $this->getProductStock($id_product_attribute);
if( $stk > 0){
return true;
}
return false;
}
public function checkAttributeStock( $id_product_attribute)
{
$stk = $this->getProductStock($id_product_attribute);
if( $stk > 0){
return true;
}
return false;
}
public function setDefaultBasedOnStock()
{
if($this->checkDefault()){
return true;
}
$product_atts = $this->getProductAttributes();
if(count($product_atts) >0){
foreach($product_atts as $attri){
if($this->checkAttributeStock($attri['id_product_attribute'])){
$this->product->deleteDefaultAttributes();
$this->product->setDefaultAttribute($attri['id_product_attribute']);
break;
}
}
}
}
/*
end set default based on stock
*/
Then you need to call setDefaultBasedOnStock method inside init Method just after parent::init();
public function init()
{
parent::init();
$this->setDefaultBasedOnStock();
// here is the rest of init code ......
//Do not delete the rest of init code
}
Please do not delete the rest of init() method code, just add this line $this->setDefaultBasedOnStock(); below parent::init();
The best way will be to override productController
Instead of modifying productController class, you can create productController override class inside /override/controllers/front/ folder
class ProductController extends ProductControllerCore {
// add all the methods above
// including the init method
}
// After add override you may need to clear cache if prestashop cache is enabled.

Create different objects based on multiple parameters

I have a REST API. I need to create presentation (DTO) object, but the construction of this object depends on request - it differs in 15%.
I wonder what pattern should I use.
My case:
//presentation-DTO
class Item {
private $name;
private $price;
private $tags;
private $liked; //is Liked by logged user
...
public function __construct(Item $item, bool $liked, ...)
{
$this->name = $item->getName();
$this->price = $item->getPrice();
$this->tags = $item->getTags();
$this->liked = $liked;
...
}
}
When user is not logged in - I don't need $liked
When showing list of items - I don't need $tags
And there are more attributes that works as above.
My first idea was to use Builder principle.
$itemBuilder = new ItemBuilder();
$itemBuilder->setItem($item);
...
if($user) {
$itemBuilder->setUserLiked($userLiked);
...
}
return $itemBuilder->build();
It solves my problem with too many parameters in constructor.
But still, I also don't need all parameters to be constructed - eg. I don't need tags (on lists). As I use lazy load, I don't want my dto constructor to call them.
So I thought, maybe Factory.. but then my problem with too many (and optional) parameters is returning.
How will you solve this?
Sorry I don't have required points to make a comment hence an answer.
What are you trying to do with the Item class. Your class is Item and first parameter is also of type Item. I cannot visualizes how its going to work.
I will prefer to keep business login to set proper properties in a separate class:
/**
* A class for business logic to set the proper properties
*/
class ItemProperties {
private $item;
public $isLogin = false;
public $showList = false;
.....
public function __construct(Item &$item) {
// set all properties;
}
public function getProperties() {
$retVal = [];
if($this->isLogin == true) {
$retVal['liked'] = true;
}
if($this->showList == true) {
$retVal['tags'] = $this->item->getTags();
}
if(....) {
$retVal['...'] = $this->item->.....();
}
return $retVal;
}
}
/**
* DTO
*/
class Item {
public function __construct(ItemProperties $itemProps) {
$this->setItemProps($itemProps);
}
// If you prefer lazy loading here...maybe make it public
// and remove call from constructor.
private function setItemProps(&$itemProps) {
$properties = $itemProps->getProperties();
foreach($properties AS $propName => $propValue) {
$this->$propName = $propValue;
}
}
}
// Usage:
$itemProps = new ItemProperties($Item);
// set other properties if you need to...
$itemProps->isLogin = false;
$item = new Item($itemProps);

Replacing a series of if statements with an interface-based approach

so I am trying to write a discounts method that will apply discount(s) on a product.
The current vanilla code goes like so:
void ApplyDiscount(List<DiscountRule> discountRules, Product objProduct)
{
foreach (var discountRule in discountRules)
{
// this is a very simple way of deciding on the available discounts
if (discountRule.Type==DiscountType.Percent)
{
// Process for percentage discount
}
if (discountRule.Type==DiscountType.Free)
{
// Process for flat discount
}
// and so on , there are like 5 more types,
// not mentioned here for the case of brevity.
}
}
What this method does is take a list of discount rules and apply on the product.
The discount rules are fetched by executing a SP # the server and that returns the
available discounts for that product.
The code review for this resulted in the following comment:
Please use an interface based approach and try to get rid of the IFs!
I can get rid of the IFs but they will be replaced by SWITCH.
How do I go about using an interface?
May be this question is not constructive, but I would want to know if some OOPs gurus here can guide me in writing this better.
Regards.
An interface / virtual-dispatch approach might look something like this:
// First we loosely define how a "discount" can be used.
// This could also be an abstract class, if common base-class
// functionality is desired.
public interface IDiscount
{
// This is called to apply this discount to a particular product
void ApplyDiscount(Product product);
}
// Here's one implementation that applies a percentage discount
public class PercentDiscount : IDiscount
{
private decimal m_percent;
public PercentDiscount(decimal percent) {
m_percent = percent;
}
#region IDiscount implementation
public void ApplyDiscount(Product product) {
product.Price -= product.Price * m_discount;
}
#endregion
}
// Here's another implementation that makes a product free
public class FreeDiscount : IDiscount
{
public FreeDiscount() {
}
#region IDiscount implementation
public void ApplyDiscount(Product product) {
product.Price = 0;
}
#endregion
}
public class SomeClass {
// Now applying the discounts becomes much simpler! Note that this function
// takes a collection of IDiscounts, and applies them in a consistent way,
// by just calling IDiscount.ApplyDiscount()
void ApplyDiscounts(IEnumerable<IDiscount> discounts, Product product) {
foreach (var discount in discounts) {
discount.ApplyDiscount(product);
}
}
}
Note that I also changed ApplyDiscounts to take an IEnumerable<T> instead of List<T>. This allows any arbitrary collection type to be passed, and also doesn't allow the function to inadvertently modify the collection.