Shopware PWA: Disable buying Product before it is Released - progressive-web-apps

Is there a way to disable a product for buying before the release date.
We added a release date for a product, the product has also an available stock. By doing this it is possible to add the product to the cart and buy it. Even if the release date is still in the future. The reason is that the product should automatically become availalbe when the release date (time) is reached, without the requirement to update the stock or anything.
Also disableing the product is no option, because it should be shown as coming soon.
The question is about how to disable it in a shopware-pwa setup, the store should not be allowed to add products to a chart nor to sell them before the release date. Disabeling this on the UI side only does not solve the problem because bots/scripts would still be able to do it by calling the API.
I did not find a setting for this. Does it require to update the logic of the shop?

To secure this server-side you could register your own data collector for the cart and validate the line item in there. Then if the release date has not yet been reached, you can add an error and prevent the product from being added to the cart. It's important to have this collect the data after the ProductCartProcessor, hence the priority lower than 5000, otherwise the release date won't be available in the payload.
<service id="Foo\MyPlugin\Content\Product\Cart\CustomCartCollector">
<tag name="shopware.cart.collector" priority="4999"/>
</service>
class CustomCartCollector implements CartDataCollectorInterface
{
public function collect(CartDataCollection $data, Cart $original, SalesChannelContext $context, CartBehavior $behavior): void
{
foreach ($original->getLineItems() as $lineItem) {
$releaseDate = $lineItem->getPayloadValue('releaseDate');
if (!$releaseDate) {
continue;
}
$releaseDate = new \DateTime($releaseDate);
$now = new \DateTime();
if ($releaseDate > $now) {
$original->addErrors(new ProductNotFoundError($lineItem->getLabel() ?: $lineItem->getId()));
$original->getLineItems()->remove($lineItem->getId());
}
}
}
}

So I had a quick look and I didn't find any reference to the releaseDate in the /store-api/checkout/cart/line-item route either.
But what you can do is to add a subscriber to the BeforeLineItemAddedEvent and make that check there yourself. You'd want to throw an exception here, if the releaseDate for the product hasn't been reached yet.
Here is a little example of such a subscriber:
class ReleaseDateSubscriber implements EventSubscriberInterface
{
private EntityRepositoryInterface $productRepository;
public function __construct(EntityRepositoryInterface $productRepository)
{
$this->productRepository = $productRepository;
}
public static function getSubscribedEvents(): array
{
return [
BeforeLineItemAddedEvent::class => 'onLineItemAdded'
];
}
public function onLineItemAdded(BeforeLineItemAddedEvent $event): void
{
$lineItem = $event->getLineItem();
$productId = $lineItem->getId();
$context = $event->getContext();
/** #var ProductEntity $product */
$product = $this->productRepository->search(new Criteria([$productId]), $context)->first();
$releaseDate = $product->getReleaseDate();
// Do what you want with the release date
$releaseDateReached = true;
if(!$releaseDateReached){
throw new ProductNotReleasedException();
}
}
}
For the UX it would obviously be nice to hide the "Add to cart"-button if the product hasn't been released yet, but you mentioned that already so I assume you can help yourself for this.

Related

Bukkit How to change an int in the config file then be able to change it again without reloading (Custom config file class.))

Okay so I am making a custom feature for my OP-Prison server, one of the things that I need to do is get an integer from the players.yml file, check if it is >= one, if it is take away one, save it and then if it is still above one then they can repeat the action untill it's 0.
The issue comes with the fact that I have to restart the server for the file to change, and even when I do, it will only go down by one integer at a time, before having to reload it again.
GUI Creation code:
Main main = Main.getPlugin(Main.class);
#SuppressWarnings("unused")
private FileControl fc;
#SuppressWarnings("unused")
private FileControl playerfc;
public static String inventoryname = Utils.chat(Main.pl.getFileControl().getConfig().getString("Backpacks.White.InventoryName"));
public List<Player> WhiteOpened = new ArrayList<>();
public static Inventory whiteBackpack(Player player) {
Inventory whiteBackpack = Bukkit.createInventory(null, 27, (inventoryname));
UUID uuid = player.getUniqueId();
whiteBackpack.setItem(10,
new ItemCreator(Material.INK_SACK).setData(8)
.setDisplayname(Utils.chat("&fCommon Packages &8» &f&l" + Main.pl.getPlayerFile().getConfig().getInt("Users." + uuid + ".Packages.Common")))
.getItem());
return whiteBackpack;
}
Code for updating the config + item when the Commonpackage is clicked:
#EventHandler
public void whiteBackpackInteract(InventoryClickEvent event) {
Player player = (Player) event.getWhoClicked();
UUID uuid = player.getUniqueId();
ItemStack clicked = event.getCurrentItem();
String title = event.getInventory().getName();
if (title.equals(inventoryname)) {
// Making it so that the item cannot be moved
event.setCancelled(true);
if (clicked != null) {
if (event.getSlot() == 10) {
// Getting the user's common packages section in the config and checking if it is greater than or equal to 1.
if (Main.pl.getPlayerFile().getConfig().getInt("Users." + uuid + ".Packages.Common") >= 1) {
// Saving the user's common package section to 'currentCommon'
Integer currentCommon = Main.pl.getPlayerFile().getConfig().getInt("Users." + uuid + ".Packages.Common");
// Taking away one from 'currentCommon' and saving it to 'newCommon'
Integer newCommon = currentCommon - 1;
// Getting the 'players.yml' file
File file = new File(main.getDataFolder(), "players.yml");
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
// Checking if the current common keys is greater than or equal to 1
if (currentCommon >= 1) {
try {
//Now, Here's where the error lies.
//Gets the player's common package count and sets it to the 'newCommon' count
config.set("Users." + uuid + ".Packages.Common", newCommon);
//Saves the players.yml file
config.save(file);
} catch (IOException e) {
e.printStackTrace();
}
// Updates the inventory they're currently in (Atleast it's meant to...)
player.updateInventory();
// Sends them a message (This is just for testing purposes, making sure it's working.)
player.sendMessage(Utils.chat("&8(&9Vexil&8) &fCommon Package"));
}
}
}
}
}
}
If there is any other code that you need, just ask I'll happily provide it for you.
Right now, you need to restart the server for it to save the data to the file. This should not happen, since you are calling the method config.save(file). The following is simply speculation, but it's the only cause that I think can easily explain what is going on.
In the object that is returned by getPlayerFile().getConfig(), there is likely a variable that stores a FileConfiguration object. That variable houses all the data from the players.yml file. In your whiteBackpackInteract() method, you load the data all over again. You then continue on to write to this NEW FileConfiguration variable, rather than the one that is stored in getPlayerfile().getConfig(). Since you then proceed to save to the file directly, the variables stored in the getPlayerfile().getConfig() is never told that you changed some values around. To fix this, you need to change the following:
config.set("Users." + uuid + ".Packages.Common", newCommon);
config.save(file);
to this:
Main.pl.getPlayerFile().getConfig().set("Users." + uuid + ".Packages.Common", newCommon);
Main.pl.getPlayerFile().getConfig().save(file);
and then delete this line of code:
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
This should solve your problem entirely. If it does not, I would recommend not using your friend's custom config API and instead just use the ones that are built in. Using third party code that you don't properly understand can very often lead to problems such as this.
The following are not the bugs, but are suggestions to help improve your code:
You should be sure to put your comments ABOVE or to the RIGHT over the code they describe. People read from top to bottom, so the comments (before I made the suggested edit to your post) were all below the code they describe.
Typically, you want to try to make sure that if code doesn't need to be run, it isn't. Since the int newCommon is not used until inside that if statement, you should move it in there.
You are using Main.getPlugin();
Now while that doesn't seem like such a bad thing, your getting an unassigned variable, I have no idea how it is working but you're assigning Main to Main. There are 2 proper ways to actually get the main class.
The first, and generally best way, is to use dependency injection.
So basically,
public class Main extends JavaPlugin {
#Override
public void onEnable() {
BackpackListener listener new Backpacklistener(this);
getServer().getPluginManager().registerEvents(listener, this);
}
}
public class BackpackListener implements Listener {
private Main instance;
private BackpackUtil util;
public BackpackListener(Main instance) {
this.instance = instance;
util = new BackpackUtil();
}
#EventHandler
public void onClick(InventoryClickEvent event) {
//code
util.whiteBackpack(instance);
}
public class BackpackUtil {
public Inventory whiteBackpack(Main instance) {
FileConfiguration config = instance.getConfig();
//Do things
instance.saveConfig();
}
}
The next way you can do it is less optimal, and frowned upon, but still an easier option.
public class Main() {
public static Main instance;
#Override
public void onEnable() {
instance = this;
}
}
public class ConfigHelper() {
Main instance = Main.instance;
FileConfiguration config = instance.getConfig();
//Do things
instance.saveConfig();
}
It's good to get out of the habit of using the second method (It's called a singleton), because normally the main class will change, or have multiple instances, etc... but with Spigot there can only be one main instance and one thread.

How to give a fixed Uid to my Action

Hy,
I'm trying to call my action with allways a fixed Uid (configured by TS) so I could put a plugin on my page to register for a specific Event. And don't have to go over a Event List click the Event click register.
I tried the following which did not work out:
public function newAction(
\XYZ\xyz\Domain\Model\Registration $newRegistration = NULL,
\XYZ\xyz\Domain\Model\Event $event = 'DD8B2164290B40DA240D843095A29904'
)
The next didn't one work either!
public function newAction(
\XYZ\xyz\Domain\Model\Registration $newRegistration = NULL,
\XYZ\xyz\Domain\Model\Event $event = Null
) {
$myinstance = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
'XYZ\\xyz\\Domain\\Model\\Event'
);
$event = $myinstance->findByUid('DD8B2164290B40DA240D843095A29904');
.......
}
So I was woundering is there a way to give my fixed Uid to the action?
In TYPO3 calling Extbase actions is done in the routing and dispatching components - to pass anything from the outside that is different from a numeric uid value a custom property TypeConverter would have to be implemented that transforms a particular string pattern into a value domain object of type Event.
However, there's a simpler approach by using configuration:
1) Provide configuration in TypoScript
Extbase uses a strong naming convention based on the extension name and optionally the plugin name. Thus, either tx_myextension or tx_myextension_someplugin can be used - latter is more specific for for according somePlugin. Besides that settings are automatically forwarded and provided in an Extbase controller context - accessible by $this->settings.
plugin.tx_xyz {
settings {
newActionEventIdentifier = DD8B2164290B40DA240D843095A29904
}
}
2) Retrieve data via repository
\XYZ\xyz\Domain\Repository\EventRepository
Use a dedicated EventRepository::findByIdentifier(string) method to retrieve the data. The property names are just assumptions since there are no explicit mentions how exactly the event data is persisted and whether it is persisted in a relational DBMS at all.
<?php
namespace XYZ\xyz\Domain\Repository;
class EventRepository
{
public function findByIdentifier($identifier)
{
$query = $this->createQuery();
$query->matching(
$query->equals('event_id', $identifier)
);
return $query->execute();
}
}
3) Putting all together in the according controller
The $event property was removed from the action since that entity is pre-defined and cannot be submitted from the outside (and to support the string to Event entity transformation a custom TypeConverter would be required as mentioned earlier).
public function newAction(
\XYZ\xyz\Domain\Model\Registration $newRegistration = null
) {
$event = $this->eventRepository->findByIdentifier(
$this->settings['newActionEventIdentifier']
);
if ($event === null) {
throw new \RuntimeException('No event found', 1522070079);
}
// the regular controller tasks
$this->view->assign(...);
}

Magento 2: Get Product Stock Quantity and Other Stock Information

Magento 2: Get Product Stock Quantity and Other Stock Information
How to get the product stock quantity and information in magento 2
If we look at the StockItemRepository class the get method wants parameter $stockItemId, not $productId. Reference:
https://github.com/magento/magento2/blob/develop/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php#L202
I've seen many sites where stock item id IS NOT the same as product id and we should not assume it's the same ID.
To get this working you could use \Magento\CatalogInventory\Model\Stock\Item class instead and load the model by product_id field instead. I am also aware of the website_id and stock_id fields, but as far as I know it's not used (yet) and also existed in M1.
It should look something like this (code not tested):
<?php
namespace Vendor\Module\Model;
use \Magento\CatalogInventory\Model\Stock\Item;
class Mymodel
{
/**
* #var Item
*/
protected $stockItem;
/**
* Mymodel constructor.
*
* #param Item $stockItem
*/
public function __construct(Item $stockItem)
{
$this->stockItem = $stockItem;
}
/**
* Description
*
* #param $productModel
*/
public function getStockQtyByProductId($productModel)
{
try {
$stockItem = $this->stockItem->load($productModel->getId(), 'product_id');
return $stockItem->getQty();
} catch (\Exception $e) {
echo 'Something went wrong and was not handled: ' . $e->getMessage();
exit;
}
}
}
if you have product object then just use following:
echo $_product->getExtensionAttributes()->getStockItem()->getQty();
conplete object can be find as follow:
var_dump($_product->getExtensionAttributes()->getStockItem()->getData());
Actually this operation should be performed using \Magento\CatalogInventory\Api\StockRegistryInterface and here we can obtain \Magento\CatalogInventory\Api\Data\StockItemInterface, by product id or sku and we can use bunch of usefull methods to get stock information - linked product. For general stock information I recommend explore other service contracts declared in Magento\CatalogInventory\Api
Example of usage:
<?php
namespace Test\Test\Model;
class Test
{
protected $_stockRegistry;
public function __construct(\Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry)
{
$this->_stockRegistry = $stockRegistry;
}
public function getStockItem($productId)
{
return $this->_stockRegistry->getStockItem($productId);
}
}
this code help you to get product quantity
<?php
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$StockState = $objectManager->get('\Magento\CatalogInventory\Api\StockStateInterface');
echo $StockState->getStockQty($productId);
?>
If you have the product object and do not want to use the other classes, You can try in the following way.
// For phtml file
$prodObj = $_product->load($_product->getId()); // $_product object in list.phtml
$stockItem = $prodObj->getExtensionAttributes()->getStockItem();
$stockQty = $stockItem->getQty(); // $stockItemData = $stockItem->getData();
// For php class file
$stockItem = $prodObj->getExtensionAttributes()->getStockItem();
$stockQty = $stockItem->getQty(); // $stockItemData = $stockItem->getData();
Credits:
https://github.com/magento/magento2/issues/7057#issuecomment-256052729
Actually \Magento\CatalogInventory\Api\Data\StockStatusInterface
should answer to all your questions.
Long story short:
Magento has StockItem entity which represents amount (Qty) of specific product (productId) on a concrete stock (stockId).
StockItemInterface should be used when you would like to "write" data into the data storage (like update amount of products to sync up Magento with your ERP system or to make deduction of stock during the checkout process).
StockStatusInterface is opposite to it. It should be used to "read" data for representation (on front-end). Consider StockStatus as an index which contains aggregated stock data for each specific product.
So, if you would like to get product stock status (in stock, out of stock) by product_id.
You need using StockStatusRepositoryInterface::getList(StockStatusCriteriaInterface $searchCriteria);
get StockStatus entity for specified product
/** #var \Magento\CatalogInventory\Api\StockStatusCriteriaInterfaceFactory $stockStatusCriteriaFactory **/
$criteria = $stockStatusCriteriaFactory->create();
$criteria->setProductsFilter($productId);
/** #var \Magento\CatalogInventory\Api\Data\StockStatusRepositoryInterface $stockStatusRepository **/
$result = $stockStatusRepository->getList($criteria);
$stockStatus = current($result->getItems());
$stockStatus->getProductId(); // product id
$stockStatus->getQty(); // quantity of specified product
$stockStatus->getStockStatus(); // Could be
// Magento\CatalogInventory\Model\Stock\Status::STATUS_OUT_OF_STOCK = 0;
// or
// Magento\CatalogInventory\Model\Stock\Status::STATUS_IN_STOCK = 1;

How can we set second picklist value dynamically with string value is know in CRM Dynamics 4.0

I have two pick-list in Car details entity. I'm setting the Model (cir_model) Picklist value with from the input parameter (that is CrmNumber) of Custom Workflow activity and it's working as expected, and the second pick-list Marque (cir_marque) will be set logically using the Model pick-list.
Logic should be if Model is set to 'Ac Ace' then Marque should be set to 'Ac'. Take value 'Ac' using Split() from the string 'Ac Ace'.
Normally in C# this can be done easily but in CRM 4.0 how this can be achieve (How I'll set 'Ac' to Marque)
public static DependencyProperty modelProperty = DependencyProperty.Register("model",
typeof(int), typeof(CreateCardetails));
[CrmInput("Model")]
public int model
{
get
{
return (int)base.GetValue(modelProperty);
}
set
{
base.SetValue(modelProperty, value);
}
}
public static DependencyProperty ContactProperty =
DependencyProperty.Register("Contact", typeof(Lookup), typeof(CreateCardetails));
[CrmInput("Contact ID")]
[CrmReferenceTarget("contact")]
public Lookup Contact
{
get
{
return (Lookup)base.GetValue(ContactProperty);
}
set
{
base.SetValue(ContactProperty, value);
}
}
protected override ActivityExecutionStatus Execute(ActivityExecutionContext
executionContext)
{
//Create an car details record which will be linked to the contact record
DynamicEntity cardetails = new DynamicEntity("cir_cardetails");
cardetails["cir_carsdetailsid"] = Contact;
//Setting the picklist value of Model
Picklist modelPickList = new Picklist();
modelPickList.Value = model.Value;
cardetails.Properties.Add(new PicklistProperty("cir_model",modelPickList));
/*
Here the logic should be done for setting Marque (cir_model) value
Picklist marquePickList = new Picklist();
marquePickList.Value = ???
cardetails.Properties.Add(new PicklistProperty("cir_marque",marquePickList));
*/
//Creating the car details record
Guid carkey = crmService.Create(cardetails);
}
How we can set the Marque value logically, I have left the code blank for this like below
/*
Here the logic should be done for setting Marque (cir_marque) value
Picklist marquePickList = new Picklist();
marquePickList.Value = ???
cardetails.Properties.Add(new PicklistProperty("cir_marque",marquePickList));
*/
Please arrange to help me out on this, all suggestions are welcome.
There is no language CRM 4.0, in CRM 4.0 you code in c#. The only thing that change is the way you work with new types.
In Workflow you don't work with controls, you work with entities and the related attributes. So you "just" need to get the attribute cir_model, do a subtring and find the available options in Marque and set the corrected value. Check this sample from SDK.
You can use JavaScript or C# (Plug-In, Workflow) to accomplish this. There are some considerations to think of when choosing which approach to use.
If you want the user to be able to see the result in real time (when they select) then you can use JavaScript.
If you don't care for the user to see the result, or there is data coming in from an outside source (not the user form), then think about using a plugin.
I don't think you should have to use a WF to do this, plugins are just as easy to write and will happen instantaneously instead of waiting for the async process to complete.

Tridion Workflows - How to get the Component at the Activity in Event Handler

I need to get the component associated to a Activity at the event system.
I try to get the component ID using:
public void OnActivityInstanceFinishPost(ActivityInstance activityInstance, string finishMessage, string nextActivity, string dynamicAssignee)
{
if (activityInstance.ProcessInstance.ProcessDefinition.Title.Equals("Component Process IESE"))
{
if (activityInstance.ActivityDefinition.Title.Equals("Create or Edit Component"))
{
WFE workflow = tdse.GetWFE();
try
{
Component comp = (Component)activityInstance.ProcessInstance.Item;
XMLReadFilter filter = new XMLReadFilter();
String processHistoryId = activityInstance.ProcessInstance.ID.Replace("131076", "131080");
ProcessHistory hist = (ProcessHistory)tdse.GetObject(activityInstance.ProcessInstance.ID, EnumOpenMode.OpenModeView, Constants.URINULL, filter);
}
catch (Exception e)
{ }
}
}
}
we try different options:
Component comp = (Component)activityInstance.ProcessInstance.Item;
But this solution returns a null.
Then I found in internet the next solution:
XMLReadFilter filter = new XMLReadFilter();
String processHistoryId = activityInstance.ProcessInstance.ID.Replace("131076", "131080");
ProcessHistory hist = (ProcessHistory)tdse.GetObject(activityInstance.ProcessInstance.ID, EnumOpenMode.OpenModeView, Constants.URINULL, filter);
Component comp = hist.Item as Component;
But the ProcessHistory object is null.
How can I determine the component associated to the activityInstance?
Thank you.
After reviewing the functionality needed by Guskermitt, I've shown him a neater way to do what he needs to do. In short, EventSystem is not needed in this case.
His goal is to send an email after a component has been approved, the approach will be the following:
Add to workflow a new automatic activity.
Create a new .NET assembly, in this case a C# class to do what he needs to do.
Register the assembly in the GAC.
Add logic in the new automatic activity in workflow to use the .NET assembly.
2#
[ProgId("WfHelper")]
[ComVisible(true)]
public class Helper
{
public void SendMail(string workItemId)
{
var session = new Session();
.
.
.
4#
dim helper
set helper = CreateObject("WfHelper")
call helper.SendMail(CurrentWorkItem.ID)
set helper = nothing
FinishActivity “Email has been sent"
ActivityInstance has a WorkItems property (inherited from Activity) that contains a reference to your Component.
OnActivityInstanceFinishPost means that your activity is finished. Therefore there is no more work item associated with it. However, you are getting the process instance and the work item associated with that. If you get null there, then it suggests your workflow process is done and the component has moved out of workflow. From looking at your code, it is quite likely that your ProcessInstance is completed (it won't be null, but it won't have any item associated with it).
I suspect that you've read this post http://www.tridiondeveloper.com/autopublishing-on-workflow-finish suggesting to look in the history. Have you looked into the history via the CM GUI, is the history item there? If it isn't, that's why you get null. A workflow process gets moved to history when it is completed. So double check that you are indeed on the last workflow activity before looking at the history.
By looking at your code, the error seems to be that you are trying to get a history object using activityInstance.ProcessInstance.ID. GetObject() should return an item, but your cast to a ProcessHistory should break and then you quietly eat the exception. You need to pass in the History ID, not the ProcessInstance ID as follows:
ProcessHistory hist = (ProcessHistory)tdse.GetObject(processHistoryId, EnumOpenMode.OpenModeView, Constants.URINULL, filter);