/**
  * like test_REG_final_price_matches_total_of_filtering_line_item_tree,
  * but makes sure the tickets have sub-prices, because that has shown to have some
  * bugs with calculations so far
  */
 function test_REG_final_price_matches_total_of_filtering_line_item_tree__with_sub_line_items()
 {
     $transaction = $this->new_typical_transaction(array('ticket_types' => 2, 'fixed_ticket_price_modifiers' => 2));
     //add another ticket purchase for one of the same events
     $event1 = EEM_Event::instance()->get_one(array(array('Registration.TXN_ID' => $transaction->ID())));
     $event_line_item = EEM_Line_Item::instance()->get_one(array(array('TXN_ID' => $transaction->ID(), 'OBJ_type' => 'Event', 'OBJ_ID' => $event1->ID())));
     $discount = $this->new_model_obj_with_dependencies('Line_Item', array('LIN_type' => EEM_Line_Item::type_line_item, 'LIN_name' => 'event discount', 'LIN_total' => -8, 'LIN_unit_price' => -8, 'LIN_percent' => 0, 'LIN_quantity' => 1, 'LIN_parent' => $event_line_item->ID(), 'LIN_percent' => null, 'LIN_order' => count($event_line_item->children())));
     $transaction->total_line_item()->recalculate_pre_tax_total();
     //and add an unrelated purchase
     EEH_Line_Item::add_unrelated_item($transaction->total_line_item(), 'Transaction-Wide Discount', -5);
     $totals = EEH_Line_Item::calculate_reg_final_prices_per_line_item($transaction->total_line_item());
     //		honestly the easiest way to confirm the total was right is to visualize the tree
     //		var_dump( $totals );
     //		EEH_Line_Item::visualize( $transaction->total_line_item() );
     //for each registration on the tranasction, verify the REG_final_price
     //indicated by EEH_Line_Item::calculate_reg_final_prices_per_line_item matches
     //what the line item filters would have returned
     EEH_Autoloader::register_line_item_filter_autoloaders();
     foreach ($transaction->registrations() as $registration) {
         $ticket_line_item = EEM_Line_Item::instance()->get_line_item_for_registration($registration);
         $reg_final_price_from_line_item_helper = $totals[$ticket_line_item->ID()];
         //now get the line item filter's final price
         $filters = new EE_Line_Item_Filter_Collection();
         $filters->add(new EE_Single_Registration_Line_Item_Filter($registration));
         $line_item_filter_processor = new EE_Line_Item_Filter_Processor($filters, $transaction->total_line_item());
         $filtered_line_item_tree = $line_item_filter_processor->process();
         $reg_final_price_from_line_item_filter = $filtered_line_item_tree->total();
         $this->assertLessThan(0.2, abs($reg_final_price_from_line_item_filter - $reg_final_price_from_line_item_helper));
     }
 }
 /**
  * Updates the registration' final prices based on the current line item tree (taking into account
  * discounts, taxes, and other line items unrelated to tickets.)
  * @param EE_Transaction $transaction
  * @param boolean $save_regs whether to immediately save registrations in this function or not
  * @return void
  */
 public function update_registration_final_prices($transaction, $save_regs = true)
 {
     $reg_final_price_per_ticket_line_item = EEH_Line_Item::calculate_reg_final_prices_per_line_item($transaction->total_line_item());
     foreach ($transaction->registrations() as $registration) {
         $line_item = EEM_Line_Item::instance()->get_line_item_for_registration($registration);
         if (isset($reg_final_price_per_ticket_line_item[$line_item->ID()])) {
             $registration->set_final_price($reg_final_price_per_ticket_line_item[$line_item->ID()]);
             if ($save_regs) {
                 $registration->save();
             }
         }
     }
     //and make sure there's no rounding problem
     $this->fix_reg_final_price_rounding_issue($transaction);
 }
 /**
  * Verifies discounts only apply to the their sibling ticket line item's REG_final_prices
  * @group 8541
  */
 function test_calculate_reg_final_prices_per_line_item__discount_only_for_one_event_subtotal()
 {
     $grand_total = $this->new_model_obj_with_dependencies('Line_Item', array('LIN_name' => 'total', 'LIN_type' => EEM_Line_Item::type_total, 'LIN_total' => 0));
     $subtotal_a = $this->new_model_obj_with_dependencies('Line_Item', array('LIN_name' => 'subtotal_a', 'LIN_type' => EEM_Line_Item::type_sub_total, 'LIN_total' => 0, 'LIN_unit_price' => 0, 'LIN_quantity' => 0, 'LIN_parent' => $grand_total->ID(), 'LIN_order' => 0));
     $subtotal_b = $this->new_model_obj_with_dependencies('Line_Item', array('LIN_name' => 'subtotal_b', 'LIN_type' => EEM_Line_Item::type_sub_total, 'LIN_total' => 0, 'LIN_unit_price' => 0, 'LIN_quantity' => 0, 'LIN_parent' => $grand_total->ID(), 'LIN_order' => 1));
     $ticket_line_item_a = $this->new_model_obj_with_dependencies('Line_Item', array('LIN_name' => 'ticket_line_item_a', 'LIN_type' => EEM_Line_Item::type_line_item, 'LIN_is_taxable' => false, 'LIN_total' => 10, 'LIN_unit_price' => 10, 'LIN_quantity' => 1, 'LIN_parent' => $subtotal_a->ID(), 'LIN_order' => 1, 'OBJ_type' => 'Ticket'));
     $ticket_line_item_b = $this->new_model_obj_with_dependencies('Line_Item', array('LIN_name' => 'ticket_line_item_b', 'LIN_type' => EEM_Line_Item::type_line_item, 'LIN_is_taxable' => false, 'LIN_total' => 10, 'LIN_unit_price' => 10, 'LIN_quantity' => 1, 'LIN_parent' => $subtotal_b->ID(), 'LIN_order' => 1, 'OBJ_type' => 'Ticket'));
     $discount_for_b = $this->new_model_obj_with_dependencies('Line_Item', array('LIN_name' => 'discount_for_b', 'LIN_type' => EEM_Line_Item::type_line_item, 'LIN_is_taxable' => false, 'LIN_total' => -5, 'LIN_unit_price' => 0, 'LIN_percent' => -50, 'LIN_quantity' => 1, 'LIN_parent' => $subtotal_b->ID(), 'LIN_order' => 100));
     $taxes_subtotal = $this->new_model_obj_with_dependencies('Line_Item', array('LIN_name' => 'taxes', 'LIN_type' => EEM_Line_Item::type_tax_sub_total, 'LIN_percent' => 0, 'LIN_parent' => $grand_total->ID(), 'LIN_order' => 1));
     $totals = EEH_Line_Item::calculate_reg_final_prices_per_line_item($grand_total);
     //		var_dump($totals);
     //		EEH_Line_Item::visualize( $grand_total );
     //now verify the discount only applied to event B's ticket, not event A's
     $this->assertEquals(10, $totals[$ticket_line_item_a->ID()]);
     $this->assertEquals(5, $totals[$ticket_line_item_b->ID()]);
 }
 /**
  * Calculates the registration's final price, taking into account that they
  * need to not only help pay for their OWN ticket, but also any transaction-wide surcharges and taxes,
  * and receive a portion of any transaction-wide discounts.
  * eg1, if I buy a $1 ticket and brent buys a $9 ticket, and we receive a $5 discount
  * then I'll get 1/10 of that $5 discount, which is $0.50, and brent will get
  * 9/10ths of that $5 discount, which is $4.50. So my final price should be $0.50
  * and brent's final price should be $5.50.
  *
  * In order to do this, we basically need to traverse the line item tree calculating
  * the running totals (just as if we were recalculating the total), but when we identify
  * regular line items, we need to keep track of their share of the grand total.
  * Also, we need to keep track of the TAXABLE total for each ticket purchase, so
  * we can know how to apply taxes to it. (Note: "taxable total" does not equal the "pretax total"
  * when there are non-taxable items; otherwise they would be the same)
  *
  * @param EE_Line_Item $line_item
  * @param array $billable_ticket_quantities 		array of EE_Ticket IDs and their corresponding quantity that
  *                                          									can be included in price calculations at this moment
  * @return array 		keys are line items for tickets IDs and values are their share of the running total,
  *                                          plus the key 'total', and 'taxable' which also has keys of all the ticket IDs. Eg
  *                                          array(
  *                                          12 => 4.3
  *                                          23 => 8.0
  *                                          'total' => 16.6,
  *                                          'taxable' => array(
  *                                          12 => 10,
  *                                          23 => 4
  *                                          ).
  *                                          So to find which registrations have which final price, we need to find which line item
  *                                          is theirs, which can be done with
  *                                          `EEM_Line_Item::instance()->get_line_item_for_registration( $registration );`
  */
 public static function calculate_reg_final_prices_per_line_item(EE_Line_Item $line_item, $billable_ticket_quantities = array())
 {
     //init running grand total if not already
     if (!isset($running_totals['total'])) {
         $running_totals['total'] = 0;
     }
     if (!isset($running_totals['taxable'])) {
         $running_totals['taxable'] = array('total' => 0);
     }
     foreach ($line_item->children() as $child_line_item) {
         switch ($child_line_item->type()) {
             case EEM_Line_Item::type_sub_total:
                 $running_totals_from_subtotal = EEH_Line_Item::calculate_reg_final_prices_per_line_item($child_line_item, $billable_ticket_quantities);
                 //combine arrays but preserve numeric keys
                 $running_totals = array_replace_recursive($running_totals_from_subtotal, $running_totals);
                 $running_totals['total'] += $running_totals_from_subtotal['total'];
                 $running_totals['taxable']['total'] += $running_totals_from_subtotal['taxable']['total'];
                 break;
             case EEM_Line_Item::type_tax_sub_total:
                 //find how much the taxes percentage is
                 if ($child_line_item->percent() != 0) {
                     $tax_percent_decimal = $child_line_item->percent() / 100;
                 } else {
                     $tax_percent_decimal = EE_Taxes::get_total_taxes_percentage() / 100;
                 }
                 //and apply to all the taxable totals, and add to the pretax totals
                 foreach ($running_totals as $line_item_id => $this_running_total) {
                     //"total" and "taxable" array key is an exception
                     if ($line_item_id === 'taxable') {
                         continue;
                     }
                     $taxable_total = $running_totals['taxable'][$line_item_id];
                     $running_totals[$line_item_id] += $taxable_total * $tax_percent_decimal;
                 }
                 break;
             case EEM_Line_Item::type_line_item:
                 // ticket line items or ????
                 if ($child_line_item->OBJ_type() === 'Ticket') {
                     // kk it's a ticket
                     if (isset($running_totals[$child_line_item->ID()])) {
                         //huh? that shouldn't happen.
                         $running_totals['total'] += $child_line_item->total();
                     } else {
                         //its not in our running totals yet. great.
                         if ($child_line_item->is_taxable()) {
                             $taxable_amount = $child_line_item->unit_price();
                         } else {
                             $taxable_amount = 0;
                         }
                         // are we only calculating totals for some tickets?
                         if (isset($billable_ticket_quantities[$child_line_item->OBJ_ID()])) {
                             $quantity = $billable_ticket_quantities[$child_line_item->OBJ_ID()];
                             $running_totals[$child_line_item->ID()] = $quantity ? $child_line_item->unit_price() : 0;
                             $running_totals['taxable'][$child_line_item->ID()] = $quantity ? $taxable_amount : 0;
                         } else {
                             $quantity = $child_line_item->quantity();
                             $running_totals[$child_line_item->ID()] = $child_line_item->unit_price();
                             $running_totals['taxable'][$child_line_item->ID()] = $taxable_amount;
                         }
                         $running_totals['taxable']['total'] += $taxable_amount * $quantity;
                         $running_totals['total'] += $child_line_item->unit_price() * $quantity;
                     }
                 } else {
                     // it's some other type of item added to the cart
                     // it should affect the running totals
                     // basically we want to convert it into a PERCENT modifier. Because
                     // more clearly affect all registration's final price equally
                     $line_items_percent_of_running_total = $running_totals['total'] > 0 ? $child_line_item->total() / $running_totals['total'] + 1 : 1;
                     foreach ($running_totals as $line_item_id => $this_running_total) {
                         //the "taxable" array key is an exception
                         if ($line_item_id === 'taxable') {
                             continue;
                         }
                         // update the running totals
                         // yes this actually even works for the running grand total!
                         $running_totals[$line_item_id] = $line_items_percent_of_running_total * $this_running_total;
                         if ($child_line_item->is_taxable()) {
                             $running_totals['taxable'][$line_item_id] = $line_items_percent_of_running_total * $running_totals['taxable'][$line_item_id];
                         }
                     }
                 }
                 break;
         }
     }
     return $running_totals;
 }