PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.3
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.3
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / app / Services / Tax / AdminOrderTaxService.php

AdminOrderTaxService.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.3, at app/Services/Tax/AdminOrderTaxService.php

180 lines 8.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\Tax;
4
5 use FluentCart\Api\StoreSettings;
6 use FluentCart\App\Modules\Tax\TaxCalculator;
7 use FluentCart\App\Modules\Tax\TaxModule;
8 use FluentCart\Framework\Support\Arr;
9
10 /**
11 * AdminOrderTaxService
12 *
13 * Calculates tax for admin-created or admin-edited orders.
14 * Accepts plain items + address arrays instead of a live cart session.
15 * Mirrors the config-building logic in TaxModule::calculateCartTax().
16 *
17 * @since 1.3.11
18 */
19 class AdminOrderTaxService
20 {
21 /**
22 * Calculate tax for a set of admin order items and a billing/shipping address.
23 *
24 * @param array $items Each item: ['post_id', 'object_id', 'qty', 'subtotal', 'discount_total']
25 * @param array $address ['country', 'state', 'city', 'postcode']
26 *
27 * @return array{tax_total: int, shipping_tax: int, tax_lines: array, tax_behavior: int, tax_country: string}|null
28 * Returns null when tax is disabled or country is missing.
29 */
30 /**
31 * Pick the correct tax address based on the store's tax_calculation_basis setting.
32 * Falls back: shipping → billing when no shipping country; store → store config.
33 *
34 * @param string $basis 'billing' | 'shipping' | 'store'
35 * @param array|null $billingAddress ['country', 'state', 'city', 'postcode']
36 * @param array|null $shippingAddress ['country', 'state', 'city', 'postcode']
37 * @return array
38 */
39 public static function resolveAddressForBasis($basis, $billingAddress, $shippingAddress = null)
40 {
41 if ($basis === 'store') {
42 $s = new StoreSettings();
43 return [
44 'country' => $s->get('store_country', ''),
45 'state' => $s->get('store_state', ''),
46 'city' => $s->get('store_city', ''),
47 'postcode' => $s->get('store_postcode', ''),
48 ];
49 }
50
51 if ($basis === 'shipping' && !empty($shippingAddress) && !empty($shippingAddress['country'])) {
52 return $shippingAddress;
53 }
54
55 return $billingAddress ?: [];
56 }
57
58 public static function calculate($items, $address, $taxSettings = null)
59 {
60 if (!TaxModule::isTaxEnabled()) {
61 return null;
62 }
63
64 $country = Arr::get($address, 'country', '');
65 $state = Arr::get($address, 'state', '');
66 $city = Arr::get($address, 'city', '');
67 $postCode = Arr::get($address, 'postcode', '');
68
69 if (empty($country)) {
70 return null;
71 }
72
73 // Build line-item array in the shape TaxCalculator expects:
74 // key fields: post_id, object_id, subtotal, discount_total, shipping_charge, quantity.
75 // Fee lines (is_fee) mirror Cart::buildFeeCartItem(): post_id/object_id 0,
76 // quantity 1 — TaxCalculator applies the first product's rates to them.
77 $lineItems = [];
78 foreach ($items as $item) {
79 $isFee = !empty($item['is_fee']);
80 $subtotal = (int) Arr::get($item, 'subtotal', 0);
81 $discountTotal = (int) Arr::get($item, 'discount_total', 0);
82 $qty = (int) Arr::get($item, 'qty', Arr::get($item, 'quantity', 1));
83
84 $lineItems[] = [
85 // Order-item id, when the caller has one. Custom lines all share
86 // post_id/object_id 0:0, so the id is the only per-line identity
87 // the patch step can match on.
88 'id' => $isFee ? 0 : (int) Arr::get($item, 'id', 0),
89 'post_id' => $isFee ? 0 : (int) Arr::get($item, 'post_id', Arr::get($item, 'product_id', 0)),
90 'object_id' => $isFee ? 0 : (int) Arr::get($item, 'object_id', Arr::get($item, 'variation_id', 0)),
91 'subtotal' => $subtotal,
92 'discount_total' => $discountTotal,
93 'shipping_charge' => (int) Arr::get($item, 'shipping_charge', 0),
94 'quantity' => $isFee ? 1 : $qty,
95 'is_fee' => $isFee,
96 'title' => (string) Arr::get($item, 'title', ''),
97 'other_info' => Arr::get($item, 'other_info', []),
98 ];
99 }
100
101 if (empty($lineItems)) {
102 return null;
103 }
104
105 try {
106 if ($taxSettings === null) {
107 $taxSettings = (new TaxModule())->getSettings();
108 }
109 $taxCalculator = new TaxCalculator($lineItems, [
110 'inclusive' => false,
111 'country' => $country,
112 'state' => $state,
113 'city' => $city,
114 'postcode' => $postCode,
115 'tax_rounding' => Arr::get($taxSettings, 'tax_rounding', 'item'),
116 ]);
117
118 $taxedLines = $taxCalculator->getTaxedLines();
119
120 // Split fee lines out of the taxed lines — same convention as
121 // TaxModule::calculateCartTax(). Fee lines never enter line_items
122 // (no post_id to patch); their tax is reported via fee_tax /
123 // fee_tax_lines. Note: getTotalTax() and getTaxLinesByRates()
124 // already include the fee portion; getExclusiveTaxTotal() excludes
125 // fee lines — identical to the aggregates checkout stores.
126 $productLines = [];
127 $feeTax = 0;
128 $feeTaxLines = [];
129 foreach ($taxedLines as $taxedLine) {
130 if (!empty($taxedLine['is_fee'])) {
131 $feeLineTax = (int) Arr::get($taxedLine, 'tax_amount', 0);
132 $feeTax += $feeLineTax;
133 $feeTaxLines[] = [
134 'label' => Arr::get($taxedLine, 'title', ''),
135 'tax_amount' => $feeLineTax,
136 'inclusive' => (bool) Arr::get($taxedLine, 'line_meta.tax_config.inclusive', false),
137 ];
138 } else {
139 $productLines[] = $taxedLine;
140 }
141 }
142
143 $lineItemsResult = array_values(array_map(function ($lineItem) {
144 return [
145 'id' => (int) Arr::get($lineItem, 'id', 0),
146 'post_id' => (int) Arr::get($lineItem, 'post_id', 0),
147 'object_id' => (int) Arr::get($lineItem, 'object_id', 0),
148 'tax_amount' => (int) Arr::get($lineItem, 'tax_amount', 0),
149 'line_meta' => Arr::get($lineItem, 'line_meta', []),
150 'recurring_tax' => (int) Arr::get($lineItem, 'other_info.recurring_tax', 0),
151 'signup_fee_tax' => (int) Arr::get($lineItem, 'other_info.signup_fee_tax', 0),
152 'signup_fee_tax_config' => Arr::get($lineItem, 'signup_fee_tax_config', []),
153 ];
154 }, $productLines));
155
156 return [
157 'tax_total' => $taxCalculator->getTotalTax(),
158 'exclusive_tax_total' => $taxCalculator->getExclusiveTaxTotal(),
159 'shipping_tax' => $taxCalculator->getShippingTax(),
160 'shipping_tax_lines' => $taxCalculator->getShippingTaxByRates(),
161 'fee_tax' => $feeTax,
162 'fee_tax_lines' => $feeTaxLines,
163 'tax_lines' => $taxCalculator->getTaxLinesByRates(),
164 'tax_behavior' => $taxCalculator->getTaxBehaviorValue(),
165 'store_tax_behavior' => $taxCalculator->getStoreTaxBehaviorValue(),
166 'tax_country' => $taxCalculator->getTaxCountry(),
167 'line_items' => $lineItemsResult,
168 ];
169 } catch (\Exception $e) {
170 // Log but never block order creation
171 fluent_cart_warning_log(
172 'AdminOrderTaxService: calculation failed',
173 get_class($e) . ': ' . wp_strip_all_tags($e->getMessage()),
174 ['module_name' => 'tax', 'log_type' => 'api']
175 );
176 return null;
177 }
178 }
179 }
180