Problem

The PurchasableEntityInterface specifies a getPrice() method that should allow you to add orderItems for any entity that implements it. The DefaultPriceResolver does not support this method, and instead requires a price field. Any Entity implementing PurchasableEntityInterface and computing it's price in getPrice() causes Fatal Errors whenever the OrderRefresh service is used:

OrderItem::setUnitPrice() must be an instance of Drupal\commerce_price\Price, null given

Solution
DefaultPriceResolver should call the getPrice() method on the purchasable entity instead of looking for a field value.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

rlmumford created an issue. See original summary.

rlmumford’s picture

This major bug was introduced by #2941401. I can't see any documentation there discussing why this was introduced. The recommended solution at the top of the ticket still makes use of the getPrice method.

rlmumford’s picture

Title: OrderRefresh does not support all PurchasableEntityInterface entities » OrderRefresh does no longer supports all PurchasableEntityInterface entities
rlmumford’s picture

Status: Active » Needs review
FileSize
2.43 KB

Here is a patch that hits the stated aim of issue #2941401 without breaking all other entities that implement PurchasableEntityInterface.

rlmumford’s picture

Component: Developer experience » Price
bojanz’s picture

Title: OrderRefresh does no longer supports all PurchasableEntityInterface entities » Backwards compatibility break in DefaultPriceResolver (due to list_price support)
Status: Needs review » Needs work

This would be an even bigger BC break.

We have two options here for fixing DefaultPriceResolver:
1) Add a getListPrice() to PurchasableEntityInterface, make the method then call either getPrice() or getListPrice() based on the field name.
2) Make the method use getPrice() if $field_name is "price", then use hasField and get for others.

Solution depends on whether we want to require any purchasable entity to have a list price. Our initial thinking was that we didn't want to impose that requirement since list prices are display only.

Ultimately, the real problem is that we have no test coverage for a custom entity type implementing PurchasableEntityInterface. We need to add it, otherwise this situation will happen again.

bojanz’s picture

Status: Needs work » Needs review
FileSize
833 bytes

So, the most minimal fix is this.

We still need to decide whether to introduce PurchasableEntityInterface::getListPrice() though. Thoughts?

rlmumford’s picture

Alternatively, we could add a new interface "ContextAwarePurchasableEntityInterface" that defines some method of getting different prices in different contexts. Depending on fields seems to be a bad idea as we can't enforce those with the interface.

This new interface could define getContextualPrice that takes Context as an argument.

DefaultPriceResolver would check whether the Entity implemented the new interface; if it did pass the context through to getContextualPrice, if not just call getPrice.

rlmumford’s picture

This patch adds a new interface "VariablePricePurchasableEntityInterface" that defines the method "getContextualPrice(Context $context)".

DefaultPriceResolver calls the contextual price method if the new interface is implemented.

ProductVariationInterface now extends VariablePricePurchasableEntityInterface instead of PurchasableEntityInterface

The behaviour of PurchasableEntityInterface is entirely unchanged.

I'm not convinced about the names I've used for these interfaces and methods but I think the approach is workable.

rlmumford’s picture

FileSize
3.86 KB

I missed some changes in that last patch so I've hidden it to prevent confusion.

The last submitted patch, 9: 3005098-9.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 10: 3005098-10.patch, failed testing. View results

bojanz’s picture

There is no use case for introducing such an interface, cause we have nowhere in Commerce to consume it from.

The current context field_name is set in a field formatter, if you don't have a field you don't have a field formatter either.

This brings us back to #7, and the question of whether it is desirable to also have getListPrice() on the PurchasedEntityInterface.

rlmumford’s picture

I don't understand what the original use case was for splitting them out like that, so I can't comment on the field formatter point. Why does one price field need to be able to display a different field? (I might have completely misunderstood that).

You're right that #7 is the most minimal fix and resolves the BC Break.

I would be against adding getListPrice to PurchasableEntityInterface because that would just be another BC Break. Also, once you have to idea of a ListPrice/Price distinction I think you open to door to all kinds of other prices.

rlmumford’s picture

I would be happy to mark #7 as RTBC as long as we're sure that 'field_name' isn't going to be passed as 'list_price' to an entity that doesn't have that field by mistake.

Should we always default back to getPrice if the list_price is NULL?

  • bojanz committed 0f78ee4 on 8.x-2.x
    Issue #3005098 by rlmumford, bojanz: Backwards compatibility break in...
bojanz’s picture

Status: Needs work » Fixed

I would be happy to mark #7 as RTBC as long as we're sure that 'field_name' isn't going to be passed as 'list_price' to an entity that doesn't have that field by mistake.

I can confirm that.

Added a comment to explain why we're invoking the getter, and committed #7.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.