I am trying to write a price alterer module to add initial European VAT support to Ubercart, so prices are shown including tax before the checkout. However, I'm running into a problem at checkout where the price alterer function appears to be called twice, resulting in both the cart block and the order total preview pane showing incorrect values.

A simplified version of my code is below; this doubles the price as it goes through the handler to make it easier to see what's going on.

 function uc_pricetest_uc_price_handler() {
  return array(
    'alter' => array(
      'title' => t('Test price alterer'),
      'description' => t('Test price alterer'),
      'callback' => 'uc_pricetest_price_handler_alter',
    ),
  );
}

function uc_pricetest_price_handler_alter(&$price, &$context, &$options) {
  switch ($context['location']) {
    case 'product-view':
    case 'cart-item':
    case 'cart-checkout-item':
    case 'cart-review-item':
      $price['price'] *= 2;
  }
} 

This works fine for the product-view, cart-item and cart-review-item contexts on all pages except the checkout - a product that is priced at £1 is correctly doubled to £2 in node views, the cart block and the cart page. However, upon reaching the checkout, the cart block suddenly doubles the prices again, so the item is shown as £4, and the order preview pane behaves even more strangely:

                Subtotal: £1.00
           Free shipping: £0.00
Subtotal excluding taxes: £2.00
                     VAT: £0.30
             Order total: £1.30

(VAT is added by uc_taxes and set to 15%)

If I comment out the 'cart-checkout-item' case then the cart block and order total preview prices are shown correctly - but I want the cart checkout pane to show the altered prices. I'm somewhat confused by the different things I could test for in $context but cart-checkout-item seemed the only way to be able to alter prices in the cart checkout pane.

Maybe the cart checkout pane should use cart-item instead as its price location? I don't think this would explain the double alteration going on in the cart block, though. Or am I not going about this correctly? I haven't found much documentation to go on and the issue thread where the price handler was implemented was hard to follow, so this has largely been trial-and-error so far!

Comments

longwave’s picture

Status: Active » Needs review
StatusFileSize
new621 bytes

Looking more into the Ubercart code it seems that the price location of 'cart-checkout-item' is used twice, once in uc_cart_checkout_form() and again in theme_cart_review_table(), which appears to be the source of this issue.

Changing theme_cart_review_table() to use 'cart-review-item' instead appears to fix this for my needs (see attached patch), as this location is used in uc_checkout_pane_cart() already, though I'm not sure whether this should be just 'cart-item' instead, or if we need an entirely new location name for this.

longwave’s picture

Title: Price alterer called twice during checkout? » Price alterer contexts and VAT support

Further to this there seems to be no real standard for the 'location' or 'class' context variables passed to a price alterer. At the moment I'm looking at altering prices in the following locations before/during checkout to show prices including VAT across the site I am developing this for:

product-view
product-tapir-table
catalog-grid-product
views-price-handler
cart-item
cart-review-item

However, how should I make this extensible enough to include other modules that may want to display prices without requiring specific support in my module? If module authors are allowed to make up their own price location names I don't see how this will be possible, other than providing a textarea for store admins to enter the locations where prices should be altered - but how would they discover the location names? The 'class' context doesn't seem to be standardised enough for this either.

Regarding views-price-handler, I'm also unsure whether to alter prices here by default, or write a separate handler for displaying prices that include VAT, as I can see a need for both of these. Or maybe Ubercart should provide separate altered and unaltered views handlers for price fields?

cha0s’s picture

Perhaps we should extend the price_handler to include an array of location names, which would make it easy to see what a price handler implements...

I had thought of something similar before. The price handlers would use that list of location names as its 'default' for a variable_get(), and would apply its alteration to that list. That'd also allow the store admin to select by hand exactly which locations would get the price applied by the handler. Do you think that's overkill?

I think providing an altered price field to views would be a good idea.

longwave’s picture

Do you mean something like a hook_uc_price_locations() that should be implemented by any module that calls uc_price()? If this could include a description this might work to provide at an admin UI where store owners can select which price handlers to apply where. Without a translatable description a lot of store owners will not know which to select. I think in such a hook, providing standard 'class' context strings that relate to the source of the price would also be useful.

However, this could all indeed be overkill! If something like this was implemented, there are already at least 89 different price location contexts used in Ubercart core alone, which will surely make any admin UI difficult to use without either standardising or somehow grouping these further.

Many of these locations are likely never even used for price alteration, as they will only be needed in an 'original' revision - this seems to be the case for the credit card gateway and report prices at least. After looking through the code I am not sure why, for example, uc_paypal_ec_checkout() needs to call uc_price() for a formatting a price using a possible custom formatter - shouldn't this always be formatted by the module itself to the format PayPal expects?

cha0s’s picture

Well, I've been mulling over this for a bit and I have some more to say.

First off, I'd like to say the VAT module you have is great! That's exactly the kind of simple, elegant client code to the Price API I was counting on.

Well, you might think the alterer is a little convoluted, but I agree with you. The good part is, I think we're still free to make a couple small changes if it's gonna make life better.

Let me say that in a perfect world (one we can get an awful lot closer to if we make some changes) the function would look like this:

function uc_vat_price_handler_alter(&$price, &$context, &$options) {
  $node = $context['subject']['node'];
  foreach (uc_taxes_rate_load() as $tax) {
    if (in_array($node->type, $tax->taxed_product_types) && ($tax->shippable == 0 || $node->shippable == 1)) {
      $price['price'] *= 1 + $tax->rate;
    }
  }
}

Now, it may not be worth it to change the product kit stuff, because you solved it in not-too-many lines of code. I do feel that it's clunky, but it'll work.

How can we trim down that large alterer to this tiny chunk of code? As I mentioned before, maintaining a list of the locations that are being passed into uc_price(). If the issue of 'overkill' is simply the UI that will result, I think it can be handled. Why not use some AHAH and have all the alterers and the formatter in a select box. Selecting one would load the next select box of modules implementing the locations hook, and selecting one of those would finally bring up a multiselect of which locations are to be handled by the alterer/formatter. This would default to another new element, 'locations' that would be returned from uc_price_handler(). My last comment on this vision may have been a bit confusing, hopefully I cleared up what I meant by that.

So now, by doing this, we're only calling the alterer/formatter if it's a handled location. This eliminates any logic that has to test whether we have a location match or not.

Moving along, in the VAT module we still need the select branch to determine the location type because of the tricky location of the node we want (ignoring the product kit exception for the moment). My second proposal is that we should unify the location of these elements. Perhaps rename 'subject' to 'subjects' and put everything in 'extras' into 'subjects'. That would eliminate this location-specific resource acquisition problem.

Here's what the code looks like if these ideas are put into play:

function uc_vat_price_handler_alter(&$price, &$context, &$options) {
  // Product kits use the derivative items for tax.
  if ($context['location'] == 'product-kit-view') {
    $nodes = $context['subjects']['node']->products;
  }
  // Otherwise, the product node.
  else {
    $nodes = array($context['subjects']['node']);
  }
  
  // Apply all the product nodes' taxes.
  foreach ($nodes as $node) {
    foreach (uc_taxes_rate_load() as $tax) {
      if (in_array($node->type, $tax->taxed_product_types) && ($tax->shippable == 0 || $node->shippable == 1)) {
        $price['price'] += $node->sell_price * $tax->rate;
      }
    }
  }
}

Much better defined logic, IMO. Not so haphazard and the best part is, if for whatever reason the store owner wants more or less locations handled, no code involved whatsoever, it's UI. :) What do you think?

P.S. Duplicate location naming is definitely a bug, so your patch is good.

longwave’s picture

My product kit code doesn't apparently work in all circumstances, if say the product kit discount feature is used. I'm not even sure how VAT should be calculated here if some components of a kit include VAT and some don't, and then a discounted price is given for the full kit. It also looks like I'll have to check separately for product kit nodes when they are displayed by the catalog module, though I need to look at the code more and run some tests to figure out how to handle this properly.

Having said that, moving toward your second example alterer function seems like the way to go. I am not sure why there is the split between 'subject' and 'extras' and as you say unifying these would be better. Maybe ensuring a consistent class context such as 'product' and 'node' for any time a product-type node is passed would avoid the need for UI entirely? In the VAT case at least, I think products should always be considered for alteration, but line items and order total prices do not need to be altered as VAT on these is applied as a line item anyway. What are the other use cases of price alterers? Would they even need to know the specific location of a price, or is a common 'class' context enough?

There's already a feature request for supporting uc_discounts_alt, which I think would also need the VAT applying if the discount applies to a specific product - in that case, uc_discounts_alt could call uc_price with the same 'product' and 'node' class context and this could hopefully then be handled without any UI options or specific support in uc_vat itself. For whole-order discounts, uc_discounts_alt could use 'line-item' and/or 'order' class contexts, and uc_vat would either not get called to alter the price, or by testing the class names it could figure out by itself that it doesn't need to affect that particular price.

Island Usurper’s picture

A lot of the development of the price handler as been driven by my discount module (uc_discount). The 'subject' and 'extras' were split because of #449688: Price adjustments not carrying through to checkout. Since attributes change the cart item's price, I needed some way to determine when to use the node's price and when to replace it with the cart item's price. I wanted to pass in both since there shouldn't be a reason for each alterer to call node_load() when that exact node is already available.

I'm really glad to hear you working on a VAT price handler though. The only way to find all the limitations of a system as big as this one is to actually try to use it. The more different types of handlers we have implemented, the more robust and flexible uc_price() will be, I think.

cha0s’s picture

Actually a lot of the development of this API was for VAT. Looking over the discount module and the cart checkout pane, I'm still not sure if I see a reason for separating them into subject and extras. I noticed in your issue you said, "I have a patch here that takes all of the $context['subject']s that have more than one object in them, and put the ones that aren't supposed to be altered into $context['extras']", but you can see in this case longwave does need to alter the data in 'extras'.

Couldn't the branch in uc_discount be preserved by doing something like this:

  else if (isset($context['subject']['cart_item']) && isset($context['subject']['node'])) {

While still allowing us to keep the data in one place?

attiks’s picture

subscribe

Vitamate’s picture

subscribing

longwave’s picture

For those who are subscribing to this issue, please also check out http://drupal.org/project/uc_vat - the development release should work for showing VAT-inclusive prices on products and in the cart, though product kit support is currently buggy. Please report any issues you find with the module in the uc_vat issue queue.

longwave’s picture

Product kit support, including discounted product kits, has been fixed in uc_vat. However, I can see that if two price alterers are enabled, the calculation will be incorrect, as the module can only know the original sell_price (and discount) for each product kit item. If another alterer modifies the full kit price, the module cannot know which individual products were affected.

uc_product_kit does call uc_price() when displaying kit items in the cart, but not when calculating the kit total before this point. Perhaps some of the kit price logic in uc_product_kit_update() should be moved to uc_product_kit_view() and uc_price() calls should be added for each kit item? This should mean uc_vat can just handle product nodes as normal and ignore product_kit nodes entirely, as each product in the kit would already have any VAT added by the time uc_product_kit calculates the kit total.

longwave’s picture

Further to this I've just had a request from my customer to support uc_upsell in uc_vat, and I don't think that will be the last module that needs to be supported.

uc_upsell already calls uc_price() to display prices, but currently does not supply any context. We really need some more documentation for uc_price() so other developers know what they should be passing when calling this function.

torgospizza’s picture

Yeah, I had to change uc_upsell to support uc_price() for multicurrency support, and wasn't quite sure what $context was really for. I now have somewhat a better idea. @longwave, if you can make a patch for that and explain to me a little bit why you did what you did, that'll help me make improvements (as well as help me figure out a way to make this a little more abstract / flexible with regard to the VAT stuff.)

criznach’s picture

I'm looking at similar issues adapting uc_discounts_alt for use with multicurrency. Because the cart display shows various subtotals that have to add up, the calculations are sometimes off by a small amount depending on how the selected currency's rounding is done. For example, when converting from GBP to USD, the order total might appear to be off by $0.01. This is because the displayed discount values were rounded to two decimal places and the total discount is now less. Anyone have any suggestions or experience dealing with this?

torgospizza’s picture

criznach, I've removed the rounding functionality from multicurrency and I'm leaving that up to the price handler. But it's still going to need your help. I'll be posting a patch about it soon.

I'm late to the rest of this issue but I wanted to show my support (a HUGE +1) to adding a "locations" array of some kind to the uc_price_handler() hook. It's absolutely essential that other modules be able to announce their locations so that price alterer functions can be run on them.

Please see my comment here, where I had to add a condition to watch for Upsell's block location to the Multicurrency module: http://drupal.org/node/488990#comment-1697324

IMO this is pretty awkward and leads to less flexibility in the long run. It means that when a new $context['location'] is created by a price-altering module, that location is protected from the alter functions and immediately has restrictions placed upon it, due to the way the price altering is handled on cart block, checkout subtotal pane, etc.

cha0s’s picture

StatusFileSize
new2.03 KB
new18.41 KB

Here's the first step of the proposal, the part where administrators get full control over configurations for each location and alterer. The new UI takes hints from the permissions matrix. There was also some HTML that was removed from the form generations, as well as making the UI look generally better.

The patch preserves full backwards compatibility with the previous behavior by assuming that no 'locations' element in the hook_uc_price_handler() means that the alterer handles all locations.

Hitting the price API settings page nukes the price cache now, so there's no need to submit. Like the modules page and the menu cache.

I also attached a patch for the uc_vat module that shows the difference in code that the new change will make.

For my next trick, I will show how we can change the logic in uc_discount to allow it to work, while removing the 'extras' element. But not tonight! I can't believe how late I stayed up working on this!

Please note, not all of the locations have been accounted for in the new hook_uc_price_locations(). In my opinion, significant review needs to be done on the way the invocation of the price API was implemented in core Ubercart. For instance there are places where one location is used for multiple prices, and where multiple locations are used for what is essentially one price being printed in multiple places. This auditing/review process will take place next, followed by investigation of unifying the 'subject' and 'extras' fields in the context.

@longwave, please do test the uc_vat patch! It's always possible I missed something. ;) NB: Don't submit the price handler settings form to test VAT, as I haven't implemented the new hook_uc_price_locations() for them yet. If you do submit it, you can 'Reset locations' to make it work again.

cha0s’s picture

Assigned: Unassigned » cha0s
StatusFileSize
new44.93 KB
new39.85 KB

Screenshots.

Island Usurper’s picture

Status: Needs review » Needs work

I certainly agree that we need to work on figuring out the best practices in using the Price API, and what we have going on now isn't "best". However, I'm not sure that this is going in the right direction either.

I had thought of the locations as identifiers that can be used to uniquely distinguish the places where the API is used. I would use them kind of like CSS id selectors, that is, when I only want to affect that particular one. From looking at the screenshots, I'm already worried that most people will have a hard time figuring out which checkboxes they need to mark. Imagine what it'll be like when it's 10 times longer.

To continue the analogy, the rest of the context is like CSS classes. There are common elements among different sets of prices, and somehow the price handlers need to be able to identify them so that they can act. I had thought to use the keys in the subject, but that's proven to be fairly awkward even in the simple cases. Now I'm thinking we can classify them as "cases", such as "nodes", "cart items", and "totals" which are used when we're looping over another case to add them up. These are all different because there are differing amounts of data available and they are in different places in the workflow. We still have to be careful that alterers aren't applied twice to the same price. But that should be easier now since I don't believe any particular price should go through the same case more than once.

Does this make sense, or have I missed something obvious? Or both? ;)

torgospizza’s picture

After reading your comment Lyle, and looking at the matrix proposal cha0s came up with - I'm not sure. I do like the idea of 'cases' - that seems to make more sense than a "location". I don't think you should be able to create locations and locations, because by the end of the day you could have 100 locations.

But if things are simplified a lot, that will mean cleaner code and a better UI, if one is even needed at that point. For instance for my Upsell module I could set the $context['location'] to "product" or something that would automatically hook into the uc_multicurrency module's handler function. In reciprocation, THAT hook should know that it will always be looking for the "product" (or "node") and "total" cases, because those are the situations when multicurrency will be needed.

Perhaps even another option that allows administrators to decide if they want the handler alterations to occur on the product level or at the subtotal level? I think that's one of those things that VAT scenarios require, that VAT is calculated after subtotal but before shipping? Things of that nature.

I think this is a hugely complex system, and to get it right will take a lot of ideas, but I think we are heading in the right direction.

cha0s’s picture

Well, the point is administrators won't even have to touch the UI normally. The modules like uc_vat handle their own locations easily enough. This is for when somebody implements a custom module with new locations and they want those locations to be altered by uc_vat. They go into the admin settings and make it so, they don't hack uc_vat. A normal user will never need to figure out which boxes to mark.

The 'cases' idea sounds interesting. I would definitely be interested to see a working patch taking that idea into account.

attiks’s picture

#20: VAT is calculated after subtotal but before shipping?

I can only speak for Belgium (but probably the same for all other EC countries)

VAT has to be calculated on line items since it's possible that multiple VAT rates exist (0, 6, 12, 21) and depending on the shipping services VAT is in effect or not (shipping with the Belgian post office is excluded from VAT, but shipping with DHL, UPS, .. is 21%).

For a B2C sites, you normally display the VAT totals on the bill/invoice so you get something like this:
product A - 12,1 euro (VAT rate 21%)
product B - 10 euro (VAT rate 0%)
subtotal including VAT 22,1 euro
vat 0% on amount 10 euro = 0 euro
vat 21% on amount 12,1 euro = 2,1 euro
subtotal excluding VAT = 20 euro
to be paid 22,1 euro

i hope it makes things clear

torgospizza’s picture

#22 - I was only spitballing. I don't know any of the real ways VAT works (there are other issues for that) but what I'm saying is that the process needs to be flexible enough to accomplish all kinds of tax rules - for instance, on our site we would tax the products and NOT the shipping, but perhaps there is a company where they are required to as well? For instance if they ship within their own state, or if they are a drop-shipper, they need to include fees for shipping from a different point of origin?

Using price alterers in this way is the best way to allow a centralized API that all modules - shipping, tax, discounts, upsell - can hook into and keep track of what prices get calculated where, and how. The fact that it was originally used to handle VAT at this point is a bit more superfluous now.

longwave’s picture

I think the "cases" idea as explained in #19 and #20 is probably a better approach for this. cha0s's idea in #17 adds complexity for the site admin which I don't believe is needed - the modules should be able to autoconfigure themselves wherever possible, so depending on a more general "case" than lots of individual locations should work better. A price alterer that does depend on location could have its own config page for this if necessary. Also, uc_vat now has four different places it looks for the relevant node object, now Views support has been fixed and attribute support has been added, so simplifying it to the extent of the patch in #17 doesn't seem possible:

  switch ($context['location']) {
    case 'product-view':            // Standard product node view
    case 'product-kit-view':        // Product kit node view
    case 'product-tapir-table':     // Catalog table view
    case 'catalog-grid-product':    // Catalog grid view
    case 'product-upsell':          // Prices displayed by uc_upsell
      $node = $context['subject']['node'];
      break;

    case 'cart-item':               // Cart item
    case 'cart-review-item':        // Cart checkout review page
    //case 'cart-checkout-item': // Enabling this causes incorrect prices in the cart block!
      $node = $context['extras']['node'];
      break;

    case 'views-price-handler':     // Prices displayed by Views module
      $node = node_load($context['subject']['node']->nid);
      break;

    case 'product-attribute-form-element': // Product node option dropdown
    case 'attribute-form-default-option':  // Product attributes page
      $node = node_load($context['subject']['option']->nid);
      break;
  }

Maybe this "cases" idea isn't even needed, if the "product-view" location could be reused. I still can't think of a use case for having the five separate locations in the first part of the above switch statement. Couldn't they just all use "product-view" as their location?

@Island Usurper: I would still like the patch in #1 to be reviewed while this is being resolved, as I believe a location name is incorrectly being used twice for two separate locations here.

cha0s’s picture

Ok, I can see the location matrix idea is dead in the water. :)

Let's talk cases. Or perhaps as longwave suggested, cases aren't necessary and we need to rethink the way we're currently doing locations.

I have broken down prices into a few groups, and welcome any suggestions on this list:

  • Refunds
  • Line Item
    • Tax
    • Shipping
  • Product
    • Order
    • Attribute
  • Order
    • Total
    • Balance
    • Payment
  • Charge
    • Authorization
    • Capture
    • Recurring
  • Cart
    • Item
    • Total
    • Subtotal
  • Others I haven't thought of?

Notice (and is already apparent in the location strings currently) there is actually a hierarchy present here. I propose we make location an array instead of a string to better represent this. For instance, a price that was a cart total would have a location of array('cart', 'total') whereas a cart subtotal would have array('cart', 'subtotal') . Perhaps this will allow price handlers to have a bit looser-grain control than they have now. Instead of 5 cases as longwave showed, this could theoretically be reduced to 2 array_shift()'s, first to see that it was a product, then next, to see whether the next value is 'attribute' or not (since they need to be handled differently).

Opinions appreciated, I can see us getting closer to a solution all the time!

P.S. I think it's ugly to force Price API clients to node_load() for the views and attributes as we are, but maybe it would be really inefficient if we assume and do it for every price?

longwave’s picture

There can be many other types of line item, such as coupon and discount, otherwise that list looks good for now.

I'm wondering if price alterers should be allowed (or even invoked at all) after an order has been finalised. Are there any use cases for modifying prices past this point? The order object and any subsequent payments etc should be a fixed record of the prices charged and paid, and I can't think of a scenario where modifying these would be useful. Multicurrency may be a valid use case but even here the price and currency should be fixed after an order is submitted, surely? And as in my example from #4, I still can't think of a useful reason for specifically modifying (for example) the price sent to the PayPal gateway; in the case of allowing multicurrency via PayPal I think it would be better to drupal_alter() all the data before sending it PayPal, so the currency code can also be changed at this point.

I'm also thinking a location array is yet more complexity to add here, it would be simpler to name them as dash separated strings as the locations are now, e.g. cart-total, cart-subtotal, etc. Modules can always split the strings or use substring matching to decide whether they want to modify a set of prices? This may also remove the need for the 'class' context, as I think we could just reuse the name provided here as the CSS class for the price. Maybe it's a bit late for this now but perhaps uc_price's signature could be changed to uc_price($price_info, $location, $context, $options) - forcing callers to provide a location string, and then $context can simply contain $context['node'], $context['attribute'], $context['order'], etc.?

Regarding node_load() in price alterers, I think it should be left up to the price alterer module, as it is now. The full node object isn't always needed and in data-heavy displays generated by Views and similar modules, it's a performance hit that isn't always necessary. uc_vat unfortunately needs to do this as it has to test the 'type' and 'shippable' fields to see which taxes to apply, and it also needs the subproducts list in the case of product kits.

torgospizza’s picture

+1 to what you guys are saying here. Personally I prefer arrays over strings, because it seems like it takes out the step where you have to break the string by a delimiter at which point you're converting it to an array anyways.

cha0s I did think of a couple things based on longwave's comment.

1) line items are a bit fuzzier in the sense that they can include many variations as he mentioned. This is important to us especially as we use coupons and certificates quite heavily.

2) Perhaps there should be a tiny bit more complexity added to this list. As longwave suggested, I think he's right that the price should be "frozen" once the order has been finalized. That being said, I'm aware of a current limitation with uc_multicurrency where the prices from past orders are still being altered by the current exchange rate. (At least, I believe this to be an issue that requires a core patch.) So perhaps, like with shipping, we have a "post checkout" and "pre checkout" setting of some kind? I'm not sure quite how to approach that concept, though, and I'm not sure if there should also be an additional "currency" field added the uc_order* tables. (Zen cart has currency fields out of the box.)

Sorry for going on a little tangent.

cha0s’s picture

Yeah, since we're talking about breaking the location string to determine the hierarchy of the price, we might as well provide it as an array, IMO.

I agree that some prices really shouldn't be changed. I forgot to mention it (I left them out on purpose up there) but I was wondering if reports are one of those cases as well. When we're talking about record of transactions, I'm not sure if we should be altering that price. Then again, is it desirable to allow a multi-currency module change these prices? Anyways, I think an easier way to handle this is by adding another level of hierarchy at the top... maybe 'active' and 'archive'? It seems like this is unnecessary to me though, since for instance 'order, total' will probably always be 'archive', whereas 'cart, item' will probably always be 'active'. Can you think of a case where another level is actually needed?

The suggestion to get rid of the class element has merit. Especially if we change location to an array and give a class for each level of location, I think that would work fine...

I like the idea of $context only containing the actual object data. However, we'll have to move revision into its own arg too. Do you think that would make the function signature clunky? It might not be too bad.

torgospizza’s picture

That's a good point, in the cases when "order, total" is used, that is after all calculations has happened and the price is essentially frozen. So it's almost like you would block any further price alterations from taking place if a price reveals that as its $context. Does that make sense?

Because in this case, your price would be calculated like cart, item > cart, subtotal > cart, total and then once the order goes to post-checkout it would be set as order, total and order, balance. In other words, "cart, total" should still be malleable because the user could go back and change their discount, which would in turn affect all of the line_items and subtotals to new values. Therefore, essentially all $contexts at that point would be alterable except anything past order, total.

I'm not sure if this affects your hierarchy at all, which I like BTW. But it almost seems like there should be one of those cases moved to be a child of the other and not a linear hierarchy. i.e.:

Product
Order
 (etc.)
Cart
  |_ subtotal
  |  |_ Total
  |
  |_ Order
     |_ Subtotal
         |_ Total

For some reason I feel like the "Order" context should be a child of the "Cart" context, because essentially an Order is a cart that has moved to completed, unlike comparing Cart to Product and vice-versa for the other parent contexts in your hierarchy. Instead of a completely new element it's essentially an element that has taken on a new ... uh, context. :)

cha0s’s picture

I think I disagree that the hierarchy should look like cart->order, since we could access order prices after the cart is long gone... know what I mean? Again though, do we want to? I understand the 'don't make it possible to screw up records' for sure, is there a possibility of multicurrency wanting to alter/format that price even after the order is finalized? That's what I'm wondering. Or do we just say WYSIWYG?

I'm leaning towards making prices like the order total, reports 'final' prices. I want to know if anyone has an idea of if they'd ever need to be altered, even if for purely aesthetic reasons....

torgospizza’s picture

I guess in essence the number itself wouldn't be altered if it was always returned as the store's default currency. In other words if my shop is default USD but I have a 2,000 JPY order, which is the approx conversion of a $20 USD product, then there are two choices: show the order info as $20 USD (the total in the default currency) or 2000 JPY (the total that the user saw after all price handlers were utilized).

The former is the easiest; to do the latter you'd need to know at least the currency used, and potentially the exchange rate at the time - but that would get messy and you probably would only want the final total, not the USD-to-JPY numbers in their entirety.

At least I wouldn't think so - I am a big fan of keeping things simple, so IMO the best choices are a tossup between "adjusted total with currency used", or just "total in store default currency".

attiks’s picture

my 2 cents

#30 Same here, don't add an hierarchy cart->order and make all prices final once an order is placed. Regarding the multi currency problem, it shouldn't alter the prices, just how they are displayed.

#31 in Belgium if you sell something in a different currency you have to keep track of the amount in that currency as well as the counterpart in euro, the easy way out is just to sell everything in euro's and let your customers do their own calculations. So for me personally, i'm in favor of keeping things simple.

Island Usurper’s picture

longwave, about the patch, there's already a 'cart-review-item' location. I'm also not sure how the two 'cart-checkout-item' locations are behaving differently since they both are based off uc_cart_get_contents(). There shouldn't be any way alterers are being run twice. Is the subject different is some way?

longwave’s picture

Island Usurper, try my example price alterer from the first post to see what happens when you alter the 'cart-checkout-item' location, I am not sure exactly why but on the checkout page prices get altered twice in the cart block without my patch from #1. Alternatively you can download and configure the current version of uc_vat, and uncomment the cart-checkout-item line in the switch statement, this shows the same problem.

rszrama’s picture

Issue tags: +price, +ubercamp sprint
criznach’s picture

Regarding #32 regarding #30 & #31 :)

I tried updating uc_discounts_alt to work with multicurrency, and found that it's not easy to display every subtotal and line item in the altered currency. Because of rounding and truncation, the subtotals rarely add up to exactly the order total as they do in the base currency. To customers who do the math, this looks like an error.

This particular client really wanted to see the order total in the selected currency, so I proposed showing the base currency total, and the selected currency total, but nothing else.

torgospizza’s picture

Please see these other issues. We're brainstorming how to fix the uc_price function to manage all potential situations.

http://drupal.org/node/505496
http://drupal.org/node/505498

Island Usurper’s picture

Assigned: cha0s » Unassigned
Priority: Normal » Critical
Status: Needs work » Needs review

Here's a first effort at putting cases in the price contexts. I settled on the key "type", and so far it has the following possible values:

  • amount
  • product
  • attribute_option
  • cart_item
  • order_product
  • payment
  • line_item

Since I know that TR wants the multicurrency module to be able to use historic exchange rates for the order total, I will probably have to add an "order_total" type to this list. However, there is probably some more work to be done on the payment methods in order for them to get the needed currency data from 3rd party modules and pay for orders in other currencies.

The idea I've had is that the types determine which part of the subject the price info comes from. This is a very strong hint to the alterer functions that indicates how the pieces of the subject are related, and how it should calculate its alterations. This information will go into the docs for uc_price() eventually, but right now I'm just getting it out of my notebook so we all can look at it.

The field data has moved out of the subject. It shows specifically where the price data comes from on the main subject object. This object is denoted with a *.

amount:
No subject information.
product:
Subject
node
Field
  • list_price
  • cost
  • sell_price (default)
attribute_option:
Subject
  • attribute
  • option*
cart_item:
Subject
  • node
  • cart_item*
  • cart (maybe optional?)
Field
price
order_product:
Subject
  • order
  • node
  • product*
payment:
Subject
payment
line_item:
Subject
  • order
  • line_item*

<dl>s are fun. :)

Island Usurper’s picture

StatusFileSize
new57.84 KB

Considering how many issues were tagged "ubercamp sprint", this patch will probably be invalidated before the end of the day. Here's a patch that includes Bazaar revision data that should merge fairly easily with any changes that get committed. Use bzr merge [filename] in your working directory to get the changes.

cha0s’s picture

The direction this has taken is confusing me.

On the one hand, I see the merit of 'types'. On the other hand, are we effectively obsoleting locations? Are types enough to specify for each price and context, or do we keep locations as the 'id' you referred to in your CSS analogy, Lyle?

If we're phasing out locations, ok. If not, why are types and locations separate things? Can I get input on my idea for hierarchical locations? Essentially, your 'types' would be $location[0], and the more specific stuff is on the next level(s) down. Does it make sense to have locations and types?

Also, in Denver Sammy suggested that we predefine the binding between revision and prices earlier than call-time. I thought that was one of the points of consolidating the price types, was so we could identify the patterns and map revisions to the different combinations. At least, that's what I remember from our conversation. I don't know if this is strictly necessary, but it lets you be able to do for instance: uc_price_registry_type_history($type); and know what (alterer(s), formatter, theme) touched the price.

There's no way to know that now, so to store the extra data needed for proper bookkeeping will have to be handled by us, instead of being opened to allow contribs to handle this.

I'm not seeing how the stuff we talked about in Denver is going to be solved doing it this way... make your plans known... :)

We can still implement the handler registry, and I could throw it out there (I have it), but I'd like to have some discussion about this first. I don't feel like we should be taking shots in the dark.

Island Usurper’s picture

Status: Needs review » Needs work

My primary motivation right now is to make using uc_price() as easy as possible. I guess that means taking out the locations if that's what makes sense. I don't mean for the patch in #39 to be the final version, necessarily. I'm just putting some code out there to get things started.

While it's good to add useful features like the alterer registry, or making some price types default to the original or altered revision, I feel like they are big complicated things to code. As long as we have a good base to work on, we can still have a 2.0 release without them. A point release later, even one that changes the API a little should be fine.

I also feel like we're all looking at the situation and seeing different problems. That shouldn't mean that our solutions are mutually exclusive, but I hope no one begrudges me for thinking that the problem with usability is the most important. ;)

So, on to the specific input you asked for. Hierarchical locations/types look like an additional burden on the developer that may not end up being necessary. I'd like for everyone who has made a price alterer to try to use just the types and see if there are any gaps to fill in. (TR has already mentioned that he needs access to the order object when calculating order totals in another currency.) If more specificity is needed, then we can allow for it. I suspect most cases need just the base type, though.

Sammy's idea of matching price types to revisions really only works when deciding between original and altered. A module might want to use the numeric, formatted, and themed versions of a price at the same time, and I can't think of a good reason to say they can't when all three are being generated anyway. Bitmasks have come up several times to represent revisions, and I think it would work.

I'll put up a new merge/patch to take out the locations and add in order totals as a type. I may even see what using bitmasks for revisions looks like.

Island Usurper’s picture

Status: Needs work » Needs review
StatusFileSize
new71.38 KB

Here's the updated patch that adds the "order_total" type and removes the location. That's in its own commit, so I can revert that if someone really really needs it.

So, contrib authors, does this work for you? Are there any more features that are absolutely essential for a 2.0 release of Ubercart?

Island Usurper’s picture

Here's the patch to uc_discount that takes advantage of the changes made here: #516592: Use new price context in alter handler The logic is a bit easier to understand than it was. Don't forget that the data that was in 'extras' is now included in the 'subject'.

Island Usurper’s picture

StatusFileSize
new71.9 KB

One final update to the patch. There was a typo in uc_cart.module, and the subtotal line item now treats the products' prices as 'cart_item' instead of 'order_total' in the cart-preview $op. This allowed the discounts to be included in the subtotal, and it makes sense in the workflow.

Al01’s picture

EU display VAT issue: Just realized that this patch takes at the shipping modules the location just leaving revision (formatted) & type (amount). We need more information to ask CA if these methods are taxed.

Island Usurper’s picture

The shipping quotes are returning just the amount that is quoted, but those are placed into line items, which are a type of price used in Ubercart. In all but a couple of places, the order is part of the subject, so it should be possible to adjust the amount of the line item in those places, or with the hook_line_item_alter().

That won't work right now, though, on the payment checkout pane with the line items preview. Since an order doesn't exist yet, none is passed along to the AHAH callback, so there isn't an order in the subject there. Somehow, that needs to be fixed, but I don't see how to do that without messing up all of the javascript that's going on there. It would affect uc_payment, uc_quote, uc_taxes, and uc_discount.

hanoii’s picture

Interested in this, just starting to look at uc_vat and this issue. I think I have to review some posts a few times, but I am wondering. Something I am in the needs of is to specify taxes on a per attribute basis. Say I want gift warp to always have VAT regardless the product setting. Do you think this new way for uc_price() api will support that. For now, I am also thinking that there's no real UI for someone to configure tax rules per attributes, is that so? What do you think the is the best way of implementing this kind of features. I will be soon looking and trying to contribute to uc_vat but I am interested in the outcome of this issue and any other information I might need to understand what's being thought for a full VAT support on Ubercart.

Thanks.
a.=

Island Usurper’s picture

StatusFileSize
new75.27 KB

Just an update to take care of some conflicts when applying the merge to the latest Ubercart.

Island Usurper’s picture

StatusFileSize
new76.95 KB

'Nother conflict fixing update. uc_weightquote now has multiple methods, just like uc_flatrate.

Island Usurper’s picture

Status: Needs review » Fixed

So, I guess no one has any problems with going in this direction. I'm not sure how VAT is going to work out, but I think this helps it get there.

Committed.

torgospizza’s picture

I've been away from this thread a bit and therefore have missed the meat of the changes. Could you give me, for the benefit of myself and others who were away from the discussion, a quick code example of how one might use the uc_price handlers now? It looks fairly similar to how it was before, but any changes would be good to see in practice (at least until the Documentation is fleshed out). Thanks, Lyle!

fenstrat’s picture

Agreed, a brief overview of how the changes should be implemented would be really handy.

Island Usurper’s picture

I guess the best example is still how prices are shown on the product page. This code is based on what's found in uc_product_view(), but it's condensed for readability.

  $context = array(
    'revision' => 'themed',  // Revisions are still the same.
                             // Can be original, altered, formatted, formatted-original, themed, or themed-original.
    'type' => 'product',  // Listed in comment #38 above (plus 'order_total'). They determine what are expected values in 'subject',
    'class' => array(  // HTML classes added to the wrapping <div> in the theme function.
      'product',
      'sell',
    ),
    'subject' => array(  // Data objects that price alterers may use to decide how the price should be changed. One of these should
      'node' => $node,   // contain the actual value given as the first parameter to uc_price(), and it should be found in the place
    ),                   // denoted by 'field'.
    'field' => 'sell_price',
  );

  $options = array( // Formatting options.
    'label' => FALSE,
  );

  $node->content['sell_price'] = array('#value' => uc_price($node->sell_price, $context, $options),

Here's another example taken from the subtotal line item. It adds up the total value of the order's products.

  $context = array(
    'revision' => 'altered',
    'type' => 'cart_item',
  );
  $subtotal = 0;
  foreach ($arg1 as $item) {
    $price_info = array(
      'price' => $item->price,  // Since cart items only have a single "price" field, it isn't necessary to specify it in the context.
      'qty' => ($item->qty) ? $item->qty : 1,
    );
    $context['subject'] = array(  // The type is 'cart_item', so the cart item and the node it is based on are part of the subject.
      'cart_item' => $item,  // The 'cart_item' object is understood to be the main subject because that is where the price comes from.
      'node' => node_load($item->nid),
    );
    $total = uc_price($price_info, $context);
    $subtotal += $total;  // The "original" and "altered" revisions return the numeric value so it can be added to the subtotal.
  }
torgospizza’s picture

Brilliant. Thanks!

hanoii’s picture

Question. I don't know if this have gone into the code, I think not from what I've seen, but it seems like for us users of uc_vat there's still a patch for ubercart needed to be commited, is that so? Just to know how to follow both developments together and because that patch, if needed, might slip the ubercart queue.

Patch is from Island Usurper @ http://drupal.org/node/518916#comment-1892980

What would happen with that? Just wondering why wasn't included in the recent RCs.

Status: Fixed » Closed (fixed)

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

joachim’s picture

Note that the $node object you get when you're coming from the Views price field handler isn't actually a true node.
If your price alteration needs things like node type, taxonomy terms (to do discounts or vat based on that data) then it might not be there.
See #579170: views price field handler needs more data so price alterations are correctly processed