PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / classes / order / order-item-fee.php

order-item-fee.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/classes/order/order-item-fee.php

319 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\Classes\Order;
4
5 use StoreEngine\Classes\enums\ProductTaxStatus;
6 use StoreEngine\Classes\Exceptions\StoreEngineException;
7 use StoreEngine\Classes\Order;
8 use StoreEngine\Classes\Tax;
9 use StoreEngine\Utils\Formatting;
10 use StoreEngine\Utils\NumberUtil;
11 use StoreEngine\Utils\TaxUtil;
12
13 /**
14 * Order line item representing an additional fee.
15 */
16 class OrderItemFee extends AbstractOrderItem {
17 protected array $meta_key_to_props = [
18 '_tax_class' => 'tax_class',
19 '_tax_status' => 'tax_status',
20 '_amount' => 'amount',
21 '_line_total' => 'total',
22 '_line_tax' => 'total_tax',
23 '_line_tax_data' => 'taxes',
24 ];
25
26 /**
27 * Data stored in meta keys.
28 *
29 * @var array
30 */
31 protected array $internal_meta_keys = [];
32
33 /**
34 * Order Data array. This is the core order data exposed in APIs
35 *
36 * @var array
37 */
38 protected array $extra_data = [
39 'tax_class' => '',
40 'tax_status' => ProductTaxStatus::TAXABLE,
41 'amount' => '',
42 'total' => '',
43 'total_tax' => '',
44 'taxes' => [
45 'total' => [],
46 ],
47 ];
48
49 protected function read_data(): array {
50 return array_merge( parent::read_data(), [
51 'tax_class' => $this->get_metadata( '_tax_class' ),
52 'tax_status' => $this->get_metadata( '_tax_status' ),
53 'amount' => $this->get_metadata( '_amount' ),
54 'total' => $this->get_metadata( '_line_total' ),
55 'total_tax' => $this->get_metadata( '_line_tax' ),
56 'taxes' => $this->get_metadata( '_line_tax_data' ),
57 ] );
58 }
59
60 /**
61 * Get item costs grouped by tax class.
62 *
63 * @param Order $order Order object.
64 *
65 * @return array
66 */
67 protected function get_tax_class_costs( Order $order ): array {
68 $order_item_tax_classes = $order->get_items_tax_classes();
69 $costs = array_fill_keys( $order_item_tax_classes, 0 );
70 $costs['non-taxable'] = 0;
71
72 foreach ( $order->get_items( array( 'line_item', 'fee', 'shipping' ) ) as $item ) {
73 if ( 0 > $item->get_total() ) {
74 continue;
75 }
76 if ( ProductTaxStatus::TAXABLE !== $item->get_tax_status() ) {
77 $costs['non-taxable'] += $item->get_total();
78 } elseif ( 'inherit' === $item->get_tax_class() ) {
79 $inherit_class = reset( $order_item_tax_classes );
80 $costs[ $inherit_class ] += $item->get_total();
81 } else {
82 $costs[ $item->get_tax_class() ] += $item->get_total();
83 }
84 }
85
86 return array_filter( $costs );
87 }
88
89 /**
90 * Calculate item taxes.
91 *
92 * @param array $calculate_tax_for Location data to get taxes for. Required.
93 *
94 * @return bool True if taxes were calculated.
95 * @throws StoreEngineException
96 */
97 public function calculate_taxes( array $calculate_tax_for = [] ): bool {
98 if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) {
99 return false;
100 }
101 // Use regular calculation unless the fee is negative.
102 if ( 0 <= $this->get_total() ) {
103 return parent::calculate_taxes( $calculate_tax_for );
104 }
105
106 if ( TaxUtil::is_tax_enabled() && $this->get_order() ) {
107 // Apportion taxes to order items, shipping, and fees.
108 $order = $this->get_order();
109 $tax_class_costs = $this->get_tax_class_costs( $order );
110 $total_costs = NumberUtil::array_sum( $tax_class_costs );
111 $discount_taxes = array();
112 if ( $total_costs ) {
113 foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) {
114 if ( 'non-taxable' === $tax_class ) {
115 continue;
116 }
117 $proportion = $tax_class_cost / $total_costs;
118 $cart_discount_proportion = $this->get_total() * $proportion;
119 $calculate_tax_for['tax_class'] = $tax_class;
120 $tax_rates = Tax::find_rates( $calculate_tax_for );
121 $discount_taxes = Formatting::array_merge_recursive_numeric( $discount_taxes, Tax::calc_tax( $cart_discount_proportion, $tax_rates ) );
122 }
123 }
124 $this->set_taxes( array( 'total' => $discount_taxes ) );
125 } else {
126 $this->set_taxes( false );
127 }
128
129 do_action( 'storeengine/order/item_fee_after_calculate_taxes', $this, $calculate_tax_for );
130
131 return true;
132 }
133
134 /*
135 |--------------------------------------------------------------------------
136 | Setters
137 |--------------------------------------------------------------------------
138 */
139
140 /**
141 * Set fee amount.
142 *
143 * @param string|int|float $value Amount.
144 */
145 public function set_amount( $value ) {
146 $this->set_prop( 'amount', Formatting::format_decimal( $value ) );
147 }
148
149 /**
150 * Set tax class.
151 *
152 * @param string $value Tax class.
153 *
154 * @throws StoreEngineException
155 */
156 public function set_tax_class( string $value ) {
157 if ( $value && ! in_array( $value, Tax::get_tax_class_slugs(), true ) ) {
158 $this->error( 'order_item_fee_invalid_tax_class', __( 'Invalid tax class', 'storeengine' ) );
159 }
160
161 $this->set_prop( 'tax_class', $value );
162 }
163
164 /**
165 * Set tax_status.
166 *
167 * @param string $value Tax status.
168 */
169 public function set_tax_status( string $value ) {
170 if ( in_array( $value, [ ProductTaxStatus::TAXABLE, ProductTaxStatus::NONE ], true ) ) {
171 $this->set_prop( 'tax_status', $value );
172 } else {
173 $this->set_prop( 'tax_status', ProductTaxStatus::TAXABLE );
174 }
175 }
176
177 /**
178 * Set total.
179 *
180 * @param string|int|float $amount Fee amount (do not enter negative amounts).
181 */
182 public function set_total( $amount ) {
183 $this->set_prop( 'total', Formatting::format_decimal( $amount ) );
184 }
185
186 /**
187 * Set total tax.
188 *
189 * @param string|int|float $amount Amount.
190 */
191 public function set_total_tax( $amount ) {
192 $this->set_prop( 'total_tax', Formatting::format_decimal( $amount ) );
193 }
194
195 /**
196 * Set taxes.
197 *
198 * This is an array of tax ID keys with total amount values.
199 *
200 * @param array|string $raw_tax_data Raw tax data.
201 */
202 public function set_taxes( $raw_tax_data ) {
203 $raw_tax_data = maybe_unserialize( $raw_tax_data );
204 $tax_data = [ 'total' => [] ];
205 if ( ! empty( $raw_tax_data['total'] ) ) {
206 $tax_data['total'] = array_map( [ Formatting::class, 'format_decimal' ], $raw_tax_data['total'] );
207 }
208 $this->set_prop( 'taxes', $tax_data );
209
210 if ( TaxUtil::tax_round_at_subtotal() ) {
211 $this->set_total_tax( NumberUtil::array_sum( $tax_data['total'] ) );
212 } else {
213 $this->set_total_tax( NumberUtil::array_sum( array_map( [
214 Formatting::class,
215 'round_tax_total',
216 ], $tax_data['total'] ) ) );
217 }
218 }
219
220 /*
221 |--------------------------------------------------------------------------
222 | Getters
223 |--------------------------------------------------------------------------
224 */
225
226 /**
227 * Get fee amount.
228 *
229 * @param string $context What the value is for. Valid values are 'view' and 'edit'.
230 *
231 * @return string|int|float
232 */
233 public function get_amount( string $context = 'view' ) {
234 return $this->get_prop( 'amount', $context );
235 }
236
237 /**
238 * Get order item name.
239 *
240 * @param string $context What the value is for. Valid values are 'view' and 'edit'.
241 *
242 * @return string
243 */
244 public function get_name( string $context = 'view' ): string {
245 $name = $this->get_prop( 'name', $context );
246 if ( 'view' === $context ) {
247 return $name ? $name : __( 'Fee', 'storeengine' );
248 } else {
249 return $name;
250 }
251 }
252
253 /**
254 * Get order item type.
255 *
256 * @return string
257 */
258 public function get_type(): string {
259 return 'fee';
260 }
261
262 /**
263 * Get tax class.
264 *
265 * @param string $context What the value is for. Valid values are 'view' and 'edit'.
266 *
267 * @return string
268 */
269 public function get_tax_class( string $context = 'view' ): string {
270 return $this->get_prop( 'tax_class', $context );
271 }
272
273 /**
274 * Get tax status.
275 *
276 * @param string $context What the value is for. Valid values are 'view' and 'edit'.
277 *
278 * @return string
279 */
280 public function get_tax_status( string $context = 'view' ): string {
281 return $this->get_prop( 'tax_status', $context );
282 }
283
284 /**
285 * Get total fee.
286 *
287 * @param string $context What the value is for. Valid values are 'view' and 'edit'.
288 *
289 * @return string|int|float
290 */
291 public function get_total( string $context = 'view' ) {
292 return $this->get_prop( 'total', $context );
293 }
294
295 /**
296 * Get total tax.
297 *
298 * @param string $context What the value is for. Valid values are 'view' and 'edit'.
299 *
300 * @return string|int|float
301 */
302 public function get_total_tax( string $context = 'view' ) {
303 return $this->get_prop( 'total_tax', $context );
304 }
305
306 /**
307 * Get fee taxes.
308 *
309 * @param string $context What the value is for. Valid values are 'view' and 'edit'.
310 *
311 * @return array
312 */
313 public function get_taxes( string $context = 'view' ): array {
314 return $this->get_prop( 'taxes', $context );
315 }
316 }
317
318 // End of file order-item-fee.php.
319