/** * 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; }