Best way to realize multiple shipments in one Shopware cart? - shopware

I have build a marketplace with Shopware 6 where every vendor has their own logistics and warehouse. I want to allow customers to order from multiple vendors in one cart.
What is the smartest way to structure this?
Should I put all CartItems from one Vendor in a CartItem of type Container? Did anyone every do that? I mean there is this container CartItem so it just makes sense, when you use it in a way like that, or?
Thank you for your help.

Shopware 6 has been designed with multiple deliveries per order being a possibility. However it has not yet been implemented fully. Currently it is expected that there is one delivery per order by default, hence why you will often find template references like order.deliveries.first() and such.
For now you'll have to implement the handling of multiple deliveries yourself.
After an order has been placed you can update an order to be split into multiple deliveries:
$order = [
[
'id' => $orderId,
'deliveries' => [
[
'stateId' => $stateId,
'shippingMethodId' => $firstShippingMethodId,
'shippingCosts' => new CalculatedPrice(10, 10, new CalculatedTaxCollection(), new TaxRuleCollection()),
'shippingDateEarliest' => date(\DATE_ISO8601),
'shippingDateLatest' => date(\DATE_ISO8601),
'shippingOrderAddressId' => $shippingOrderAddressId,
'positions' => [
[
'price' => new CalculatedPrice(10, 10, new CalculatedTaxCollection(), new TaxRuleCollection()),
'orderLineItemId' => $firstOrderLineItemId,
],
],
],
[
'stateId' => $stateId,
'shippingMethodId' => $secondShippingMethodId,
'shippingCosts' => new CalculatedPrice(10, 10, new CalculatedTaxCollection(), new TaxRuleCollection()),
'shippingDateEarliest' => date(\DATE_ISO8601),
'shippingDateLatest' => date(\DATE_ISO8601),
'shippingOrderAddressId' => $shippingOrderAddressId,
'positions' => [
[
'price' => new CalculatedPrice(10, 10, new CalculatedTaxCollection(), new TaxRuleCollection()),
'orderLineItemId' => $secondOrderLineItemId,
],
],
],
],
],
];
$this->orderRepository->update($order, $context);
If you need to split the deliveries before the order has been placed, during the checkout, you may want to register a custom cart processor.
<service id="My\CustomPlugin\Checkout\Cart\Delivery\DeliveryProcessor">
<argument type="service" id="Shopware\Core\Checkout\Cart\Delivery\DeliveryBuilder"/>
<argument type="service" id="Shopware\Core\Checkout\Cart\Delivery\DeliveryCalculator"/>
<argument type="service" id="shipping_method.repository"/>
<!-- priority -5001 so it runs after the original delivery processor -->
<tag name="shopware.cart.processor" priority="-5001"/>
</service>
Have a look at the original DeliveryProcessor for reference. In the process method you could then split the original delivery into a collection of multiple delivers and set them to the cart.

Related

Shopware filter is using parents price also for products where only variants are being used

We have a sage integration to shopware 6. They integrate product variants as well.
BUT: For the parent product of variant products - they put into the price field 999.999€. The product variants indeed have correct prices.
The problem is: We use the client-api to fetch all products of a category and with that also their filters. (see https://shopware.stoplight.io/docs/store-api/cf2592a37b40b-fetch-a-product-listing-by-category)
The price filter does show the very high 999.999€ price, even though no product variant has this price. Only the (not used/shown) parents.
Is there a way around this? Especially without maintaining the Parent-Price manually? Ideally the Shopware API would not include the prices of the parent products... Can this be configured maybe?
Any hint appreciated :wink:
Example response from /store-api/product-listing/CATEGORY_ID
"aggregations": {
"price": {
"min": "249.0000",
"max": "999999.9900",
"avg": null,
"sum": null,
"apiAlias": "price_aggregation"
},
I called the api endpoint for a category that contains variant products. Their parent products have 999.999€ as a price. As a filter aggregation for price I get a max price of 999999 where I would expect the highest price of all products and variant products. But not having the price of the variants parents included.
One possible approach would be to subscribe to the ProductListingCriteriaEvent and wrap the original price aggregation in a FilterAggregation. With the filter aggregation you can limit the range of the inner StatsAggregation named price.
class MyProductListingSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
// set a priority for the listener below -100
// so it is executed after the original aggregations are set
return [
ProductListingCriteriaEvent::class => [
['enhanceCriteria', -101],
],
ProductSearchCriteriaEvent::class => [
['enhanceCriteria', -101],
],
];
}
public function enhanceCriteria(ProductListingCriteriaEvent $event): void
{
$criteria = $event->getCriteria();
$aggregations = $criteria->getAggregations();
$criteria->resetAggregations(); // remove all aggregations
foreach ($aggregations as $aggregation) {
if ($aggregation->getName() !== 'price') {
// re-add aggregations not name 'price'
$criteria->addAggregation($aggregation);
continue;
}
// wrap original aggregation and set a `RangeFilter` for the gross price
$filterAggregration = new FilterAggregation(
'filter-price-stats',
$aggregation,
[
new RangeFilter(
'cheapestPrice.gross',
[
RangeFilter::LT => 999999.99,
]
)
]
);
$criteria->addAggregation($filterAggregration);
}
}
}
This should yield a max price for the filter with the exception of prices equal to or larger than 999999.99.
The problem was the indexing behavior. We had products on this installation before, which were deleted. New products were imported then. Something must have gone wrong with the indexing.
What solved our issue was refreshing the index using the console
.bin/console dal:refresh:index

Use PDF mediaId in Email Attachment

I am using the "api/_action/mail-template/send" action in a Shopware App to send an email to the customer and I want to add a document which was created before. If I understand it correct, I only have to give the mediaId as parameter to the request. The problem is, that the file can not be found ('detail' => 'File not found at path: media/29/50/18/1660063117/invoice_2544.pdf'),because the PDF files are in the files directory.
What am I doing wrong? Are PDFs a special case?
Thanks
Danny
Unfortunately you are correct with your assumption. The primary keys you provide with mediaIds may only refer to files stored in the public filesystem. There's already a feature flag in place that will allow to provide documentIds specifically instead, but since you're developing an app I doubt you'll be able to activate said feature flag for this purpose and the feature flag is only to be removed with the upcoming major release 6.5.0.0.
What you can do however is provide raw binary data for your attachments with the request payload key binAttachments. You can find how this data is handled in the MailFactory service.
There's the endpoint POST /api/_action/order/document/download you can request with a payload documentIds that will give you the files binary content (You essentially download the file). You can provide multiple ids but it will merge all documents into one, so you might want to do one request per document if you want to keep the files separated as attachments.
You can then use the binary data you retrieved as content of your binAttachments.
PHP example with GuzzleHttp\Client:
$response = $client->post($host . '/api/_action/order/document/download', [
'json' => [
'documentIds' => ['b0b3ac0ca218473babaffc7f0a800f36'],
],
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $accessToken,
],
]);
$client->post($host . '/api/_action/mail-template/send', [
'form_params' => [
'recipients' => [
'test1#example.com' => 'Test user 1',
],
'salesChannelId' => '6923fd0d41e04521a2a259ba13f20bc3',
'contentHtml' => 'officia Lorem non aute',
'contentPlain' => 'eu veniam m',
'subject' => 'sed adipisicing',
'senderName' => 'enim occaecat aliquip veniam',
'senderEmail' => 'test#example.com',
'binAttachments' => [
[
'content' => $response->getBody()->getContents(),
'fileName' => 'test.pdf',
'mimeType' => 'application/pdf',
],
],
],
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
],
]);
While this is certainly a workaround, as of now this might be your best bet to achieve this purely with an app.

How to capture funds later when using Stripe's Checkout feature?

I am using Stripe's Checkout feature. My product takes a few minutes to generate, so I want to place a hold on the funds, only charging the customer once their product is ready. Is it possible to do so without refactoring away from using this feature?
This code works for me:
<?php
$amount = 50;
$stripe = new \Stripe\StripeClient(env('STRIPE_SECRET'));
$session = $stripe->checkout->sessions->create([
'payment_method_types' => ['card'],
'success_url' => 'reservation.success'.'?id={CHECKOUT_SESSION_ID}',
'cancel_url' => 'reservation.cancel',
'payment_intent_data' => [
'capture_method' => 'manual',
],
'line_items' => [[
'price_data' => [
'currency' => "eur",
'product_data'=> [
'name'=> "My awesome product",
],
'unit_amount'=> $amount * 100,
],
'quantity' => 1
]],
'mode' => 'payment',
]);
return redirect()->to($session->url);
This was done in laravel. You might need to adapt it a little to your needs.
Of course you need to
adapt $amount to the amount you want to collect in €.
change the currency if you need to
set the route for success_url
set the route for cancel_url
After calling this code the user is redirected to the session url, to make the payment, which is then reserved on his card.
You can then capture the payment later via:
<?php
$stripe = new \Stripe\StripeClient(env('STRIPE_SECRET'));
$session_id = 'THE_CHECKOUT_SESSION_ID';
$payment_intent = $stripe->checkout->sessions->retrieve($session_id)-payment_intent;
$intent = \Stripe\PaymentIntent::retrieve($payment_intent);
// Capture all
$intent->capture();
// Alternative: Capture only a part of the reserved amount
// $intent->capture(['amount_to_capture' => 750]);
See also https://stripe.com/docs/payments/capture-later

Algolia - excluding results with specific values

I'm trying to figure out how to exclude specific records from my search results.
My records look like this:
eventId:bcf9e22b-adb1-11e6-b058-0623f336d3b3
name:Event Name
fullAddress:1964 Long Beach Rd, Toronto, ON, Canada
latitude:44.998148000000
longitude:-79.319882400000
eventStartAt:1469948400
objectID:eyJpZCI6ImJjZjllMjJiLWFkYjEtMTFlNi1iMDU4LTA2MjNmMzM2ZDNiMyJ9
So while searching for event name I'm trying to exclude certain records that have specific eventId.
I'm using Symfony bundle provided by Algolia so my code looks like this:
$results = $this->get('algolia.indexer')->search(
$this->get('doctrine.orm.entity_manager'),
'MyBundle:Entity',
'Random Event Name',
[
'numericFilters' => 'eventStartAt > 1469948000',
'minWordSizefor1Typo' => 2,
'filters' => 'NOT eventId:b2c7a242-e17d-11e6-b058-0623f336d3b3',
]
);
Which produces request body:
{
"params": "numericFilters=eventStartAt+%3E+1469948000&minWordSizefor1Typo=2&filters=NOT+eventId%3Ab2c7a242-e17d-11e6-b058-0623f336d3b3&query=Random+Event+Name"
}
Problem is that NOT eventId:b2c7a242-e17d-11e6-b058-0623f336d3b3 filter does not seem to be applied at all, and record with eventId of b2c7a242-e17d-11e6-b058-0623f336d3b3 is still present in my results.

Yii2 Search model without using GridView

There is a serch form on the mainpage of a realestate agency. The data about objects is stored in the table "realty" that uses relations. For example, there are related tables category (residential, commercial, land plots), deal (buy, sell, rent), object_type (apartment, house, office).
Then different categories have different properties and and there are three bootstrap tabs in the search form: residential, commercial, land plots. Under each tab there are selects and input fields that are specific for the choosen tab.
In the most cases, the examples of using Search model are given within a gridview.
Is it possible to adapt Search model logic so that it could return the array of results from the table 'realty' based on the values indicated in the search form on the mainpage ?
Yes, of course you can. you have several options:
Solution 1 - worst solution but the answer to your question
modify the search function of the model (or create a new function). The search functions usually looks like this
public function search($params)
{
$query = Bookmark::find()->where('status <> "deleted"');
$dataProvider = new ActiveDataProvider([
'query' => $query,
'pagination' => [
'pageSize' => Yii::$app->session->get(get_parent_class($this) . 'Pagination'),
],
]);
if (!($this->load($params) && $this->validate())) {
return $dataProvider;
}
$query->andFilterWhere([
'status' => $this->status,
'id' => $this->id,
'reminder' => $this->reminder,
'order' => $this->order,
'update_time' => $this->update_time,
'update_by' => $this->update_by,
'create_time' => $this->create_time,
'create_by' => Yii::$app->user->id,
]);
$query->andFilterWhere(['like', 'name', $this->name])
->andFilterWhere(['like', 'url', $this->url]);
return $dataProvider;
}
You can change it to something like
public function search($params)
{
$query = Bookmark::find()->where('status <> "deleted"');
if (!($this->load($params) && $this->validate())) {
THROW AN ERROR SOMEHOW HERE
}
$query->andFilterWhere([
'status' => $this->status,
'id' => $this->id,
'reminder' => $this->reminder,
'order' => $this->order,
'update_time' => $this->update_time,
'update_by' => $this->update_by,
'create_time' => $this->create_time,
'create_by' => Yii::$app->user->id,
]);
$query->andFilterWhere(['like', 'name', $this->name])
->andFilterWhere(['like', 'url', $this->url]);
return $query->all();
}
however this will return to you all the records because ActiveDataProvider takes care of the pagination based on the query given.
Solution 2, a better solution
read the first example here http://www.yiiframework.com/doc-2.0/yii-data-activedataprovider.html . You can call ->getModels() on an ActiveDataProvider to get the actual records. No changes needed to the search function. Do whatever you want with the array.
Solution 3 and what I use all the time
Use ActiveDataProvider with a ListView. The list view allows you to create the list of records however you want, it does not have to be a table. I personally do this in many places and it works quite well. I sometimes transform arrays to an ArrayDataProvider just to use it. More about data providers here: http://www.yiiframework.com/doc-2.0/guide-output-data-providers.html

Resources