A new update in my on-going saga! 
I finally seem to understand how the tax is calculated on the order.
- The Order object is not retained as part of the session
- Each of the "checkout" pages seem to create a new order object each time
- The order constructor will invoke the function "cart" if it is not passed a specific order ID
- "Cart" grabs data from the session and builds the order
- "Cart" calculates the tax while building the order module
This makes my modification a little tricky. Dig inside includes/classes/order.php to understand why.
First, the shipping and billing address are pulled from the database, based upon the customer ID and the sendto/billto values in the session. Sendto and Billto seem to be unique address ID values, indicating WHICH of the customer's stored addresses will be used for this order:
Code:
$shipping_address_query = "select ab.entry_firstname, ab.entry_lastname, ab.entry_company,
ab.entry_street_address, ab.entry_suburb, ab.entry_postcode,
ab.entry_city, ab.entry_zone_id, z.zone_name, ab.entry_country_id,
c.countries_id, c.countries_name, c.countries_iso_code_2,
c.countries_iso_code_3, c.address_format_id, ab.entry_state
from " . TABLE_ADDRESS_BOOK . " ab
left join " . TABLE_ZONES . " z on (ab.entry_zone_id = z.zone_id)
left join " . TABLE_COUNTRIES . " c on (ab.entry_country_id = c.countries_id)
where ab.customers_id = '" . (int)$_SESSION['customer_id'] . "'
and ab.address_book_id = '" . (int)$_SESSION['sendto'] . "'";
$shipping_address = $db->Execute($shipping_address_query);
(A similar block pulls the billing address.)
Next, there is a block to retrieve the $tax_address:
Code:
$tax_address_query = "select ab.entry_country_id, ab.entry_zone_id
from " . TABLE_ADDRESS_BOOK . " ab
left join " . TABLE_ZONES . " z on (ab.entry_zone_id = z.zone_id)
where ab.customers_id = '" . (int)$_SESSION['customer_id'] . "'
and ab.address_book_id = '" . (int)($this->content_type == 'virtual' ? $_SESSION['billto'] : $_SESSION['sendto']) . "'";
$tax_address = $db->Execute($tax_address_query);
(The real code has more cases, but this illustrates the idea.)
Again, the data is pulled from the database depending upon the selected customer_id and billto/sendto address.
The addresses get moved into $this->delivery and $this->billing a little further down in the module, becoming the addresses for the order object.
Immediately afterwards, we get into the real meat of the tax calculations. The system reads through all the products in the $_SESSION['cart'] object:
Code:
$products = $_SESSION['cart']->get_products();
for ($i=0, $n=sizeof($products); $i<$n; $i++) {
if (($i/2) == floor($i/2)) {
$rowClass="rowEven";
} else {
$rowClass="rowOdd";
}
$this->products[$index] = array('qty' => $products[$i]['quantity'],
'name' => $products[$i]['name'],
'model' => $products[$i]['model'],
'tax' => zen_get_tax_rate($products[$i]['tax_class_id'], $tax_address->fields['entry_country_id'], $tax_address->fields['entry_zone_id']),
'tax_description' => zen_get_tax_description($products[$i]['tax_class_id'], $tax_address->fields['entry_country_id'], $tax_address->fields['entry_zone_id']),
'price' => $products[$i]['price'],
'final_price' => $products[$i]['price'] + $_SESSION['cart']->attributes_price($products[$i]['id']),
'onetime_charges' => $_SESSION['cart']->attributes_price_onetime_charges($products[$i]['id'], $products[$i]['quantity']),
'weight' => $products[$i]['weight'],
'products_priced_by_attribute' => $products[$i]['products_priced_by_attribute'],
'product_is_free' => $products[$i]['product_is_free'],
'products_discount_type' => $products[$i]['products_discount_type'],
'products_discount_type_from' => $products[$i]['products_discount_type_from'],
'id' => $products[$i]['id'],
'rowClass' => $rowClass);
More code follows to deal with attributes, but I left that out.
Each order->products[$index] object gets a tax and tax_description, based upon the product's tax_class_id, and the country and zone codes for the $tax_address.
A little further down, in a well-commented section, the tax calculation is performed. Each product's tax rate and price get multiplied to arrive at the final tax amount. The tax calculation is the last step in the "products" loop. It all gets repeated for everything in the cart.
My problem is that I want to lightly touch this process, replacing the $tax_address country and zone codes with a new one, based on the store.
I would also like to update the $shipping_address with a new shipping address, again based upon the address of my store.
It would be nice if there were a hook in the middle of this module, so I could change the values AFTER the database call, but BEFORE the calculations. No such luck. Here my problems start:
- I want shipping address to be an address NOT associated with the client
- I want the tax address to be fixed, and NOT based on any address associated with the client
Zen Cart assumes that tax address and shipping address will always be in an address attached to the client record.
WHEW.
I cannot just update something in the session object before the order is created -- updating the billto/shipto will not get the result I need. Any of the "notify" events that happen before "new order" in the headers are useless for my purposes.
I CAN, however, tie into the "notify" events that happen at the END of the headers. The solution is a little ugly, but I can update the order object after it is created, but before the page is rendered:
- Retrieve a list of all the products in the order (order->products)
- Iterate through all the products, and replace the 'tax' and 'tax_description' values for each one
- Use the same basic code that appears in the cart function, but use the country and zone ID of my store, not the one associated with the customer's addresses
- Update the order->delivery value to reflect the store address
- Reset the order->info subtotal, tax, and tax_groups indicators to zero
- Repeat the whole block of logic labeled "calculate taxes" in the order module, recalculating all the taxes for the order
- Rerun a small bit of code to update the order->info['total'] value with the taxes if needed - in "cart" is is one of the last few statements listed
Then, I should have a new order object with the updated tax values.
This solution gets pretty ugly, though, because various checking is done in some of the modules, like checkout_confirmation/header_php.pmp, to see if the amount of the order is greater or less than certain credits.
I am pretty sure I will need to repeat all that code in my observer module, since I am changing the values AFTER the header module has ended. I may need different code in my observer, depending upon which checkout module has called it. This could be done with an ugly "select case" statement.
I am not looking forward to coding and testing this. Unfortunately the design of Zen Cart's checkout process really works against what I am trying to do. The checkout process assumes that the addresses are in the database and attached to the client's record. If anyone has a more elegant solution, let me know.
It will take me a while to get through this. I will post the results once I get done, but do not expect it anytime soon.
Too bad. I thought this would be an easy fix. It turns out to be quite complicated.