Problem/motivation
There are several different use cases for bundling products. This module aims to support those use cases in sub-modules, with core API and interfaces in the base module. We need to define the base interfaces they can implement.
Proposed resolution
Provide interfaces supporting different product bundle scenarios. To start:
Core CommerceBundle interfaces:
BundleInterface {
getBasePrice()
}
* Not sure if this is where we would specify a base price
BundleWithSKUInterface {
getSKUConstraints()
}BundleWithoutSKUInterface {
(?)
}* Not sure if these *SKUInterfaces should be their own interfaces or instead settings on the base *BundleInterface interface(s).
StaticBundleInterface implements BundleInterface {
getItems()
}DynamicBundleInterface implements BundleInterface {
getAllowedItems()
setSelectedItems()
getSelectedItems()
}ShippingOverrideBundleInterface {
getShippingOverride() // bool
getShippingParams()
}PricingOverrideBundleInterface {
getPricingOverride() // bool
getTotalPrice()
}Core Commerce Bundle Item interface(s):
BundleItemInterface {
getEntity()
getQuantity()
getUnitPrice()
}* Note: this bundle item references purchasable entities (i.e. product variations). A bundle of 'products' (which are not themselves purchasable entities) would be a different BundleItem type perhaps with more complex logic.
Core Commerce Bundle item entity:
<?php
Drupal\commerce_product_bundle\Entity;
class BundleItem implements BundleItemInterface {
protected $entity;
protected $qty;
protected $unitPrice;
__construct(PurchasableEntityInterface $entity, int $qty, Price $unit_price) {
$this->entity = $entity;
$this->qty = $qty;
$this->unitPrice = $unit_price;
}
getEntity()
getQuantity()
getUnitPrice()
}
Example implementation (the "static bundle with sku and pricing+shipping override" use case):
<?php
Drupal\commerce_product_bundle_static\Entity\StaticBundle
class StaticProductVariationBundle implements StaticBundleInterface, BundleWithSKUInterface, ShippingOverrideInterface, PricingOverrideInterface {
/** @var \Drupal\commerce_product_bundle\Entity\BundleItemInterface[] $items */
protected $items = [];
/** @var array $configuration */
protected $configuration;
/** @var \Drupal\commerce_price\Resolver\PriceResolverInterface $resolver */
protected $priceResolver;
/** @var \Drupal\commerce\AvailabilityManagerInterface $availabilityManager */
protected $availabilityManager;
/** @var \Drupal\commerce_price\Price $basePrice */
protected $basePrice;
__construct($items, $price_resolver, $availability_manager, $configuration) {
$this->items = []
$this->priceResolver = $price_resolver;
$this->availabilityManager = $availability_manager;
$this->configuration = $configuration;
$this->basePrice = new Price($configuration['base_price'])
}
public function getShippingOverride() {
return $this->configuration['override_shipping'];
}
public function getPricingOverride() {
return $this->configuration['override_pricing'];
}
public function getBasePrice() {
return $this->basePrice;
}
public function getTotalPrice() {
if ($this->getPricingOverride()) {
// Calculate the overridden price.
$price = $this->getBasePrice();
foreach ($items as $item) {
$quantity = $item->getQuantity();
$unit_price = $item->getUnitPrice();
$item_price = $unit_price->multiply($quantity);
$price->add($item_price);
}
return $price;
}
else {
// Compute sum of all purchasable entities' resolved prices
$price = new \Price();
foreach ($items as $item) {
$entity = $item->getEntity();
$quantity = $item->getQuantity();
$unit_price = $this->priceResolver($entity);
$item_price = $unit_price->multiply($quantity);
$price->add($item_price);
}
return $price;
}
}
}Remaining tasks
- Finish defining these interfaces as use cases are worked out.
Completed tasks
- Identified bundling use cases with or without implementation of unique SKU, Shipping overrides, and Pricing overrides
Comments
Comment #2
steveoliver commentedAdded BundleItem interface and example implementation.
Comment #3
steveoliver commentedLinking parent [meta] issue.
Comment #4
olafkarsten commentedHmm, I wonder if we can pull up the getItems() method to the base interface and let it return the current state in case of dynamic bundles. I think, if you look at a dynamic bundle to an fixed point in time, it should look like a static one. In the beginning return the defaults, inbetween selected ones and after the order was placed, the items are fixed anyway.
If I understand that correctly, the getPricingOverride() is a Flag. Would hasPriceOverride() than be a better name?
Comment #5
olafkarsten commentedMe thinking loud: If we make the bundleItems purchasables entities, we get simpler bundles and more flexibel bundle items, right? But I'm not sure about the resolver thingies. Totally unfinished unfinished thought, but wanted to share this. Not sure I have time today evening for irc
Core Commerce Bundle Item interface(s):
Core Commerce Bundle item entity:
We can create BundleItems with no overriden price, if we need to or override the price in the bundle item. Our bundle Items could contain any sort of additional logic, like overriding the order item title.
The static bundle implemenation than seems somewhat simpler.
Example implementation (the "static bundle with sku and pricing+shipping override" use case):
Shouldn't the bundle implement PurchasableEntityInterface. Should we let the BaseBundle extend that interface?
If we do it this way, we could even reference a static bundle as an bundle item, right? Not sure though if that is of any help
Do we really need a baseprice in the sense of: Set a bundle baseprice and add the item prices? I've come across cases were someone asked for a fixed bundle price setting without worrying about the bundle item prices. Maybe there are use case where we never need to know about the bundle items prices. So if there is a baseprice, no need to compute anything. But if we need it for something, I'm totally fine with it. That is not the most important detail. The question is - should the bundle item return its price or should we really grab the referenced entity in the bundle?
I think that some taxresolver simple could grab the items (bundle->getItems()) - looping over them, get the referenced entity if it need to, but get the price from the bundle item, as that is the base for calculating the taxes on a per item base. If the tax calculation is based on the bundle total - its easy anyway.
Comment #6
olafkarsten commentedWe have some interfaces in code that differ somewhat. Instead of updating the fat issue here, lets discuss proposed changes in separat issues.