Tutorial Extension  1.0.0
SellerDeck Extensions - Tutorial Extension
CTax.php
1 <?php
2 
3 /**
4  * CTax.php - Implementation file for Tax Module class.
5  *
6  * @package SellerDeck Extensions
7  *
8  * @author Péter Erdődi
9  * @copyright © SellerDeck Ltd 2015. All rights reserved.
10  */
11 
12 namespace SDExtension;
13 
14 class CTax
15  {
16 
17  /**
18  * @var object $m_oCatalogDB Catalog DB
19  */
20  protected $m_oCatalogDB = null;
21 
22  /**
23  * @var object $m_oShippingDB Shipping DB
24  */
25  protected $m_oShippingDB = null;
26 
27  /**
28  * @var string $m_sCountry Country
29  */
30  protected $m_sCountry = null;
31 
32  /**
33  * @var array $m_aTaxDefinition Tax Definition
34  */
35  protected $m_aTaxDefinition = null;
36 
37  /**
38  * @var array $m_aTaxModel Tax Model
39  */
40  protected $m_aTaxModel = null;
41 
42  /**
43  * @var array $m_aTaxBandNames Tax Band Names
44  */
45  protected $m_aTaxBandNames = [];
46 
47  /**
48  * @var array $m_aProductTaxes Product Taxes
49  */
50  protected $m_aProductTaxes = [];
51 
52  /**
53  * @var array $m_nCustomTaxNo Custom Tax No
54  */
55  protected $m_nCustomTaxNo = 0;
56 
57  /**
58  * @var array $m_aRoundingRules Rounding Rules
59  */
60  protected $m_aRoundingRules = [
61  TAX_ROUND_TRUNCATION => "down",
62  TAX_ROUND_SCIENTIFIC_DOWN => "halfdown",
63  TAX_ROUND_SCIENTIFIC_NORMAL => "normal",
64  TAX_ROUND_CEILING => "up",
65  TAX_ROUND_BANKERS => "bankers",
66  ];
67 
68  /**
69  * @var array $m_aRoundingGroups Rounding Groups
70  */
71  protected $m_aRoundingGroups = [
72  TAX_ROUND_PER_LINE => "line",
73  TAX_ROUND_PER_ITEM => "item",
74  TAX_ROUND_PER_ORDER => "order",
75  ];
76 
77  /**
78  * @var bool $m_bSimpleTax TRUE if Simple Tax applies, FALSE, if Advanced
79  */
80  protected $m_bSimpleTax = true;
81 
82  /**
83  * __construct - Object constructor method
84  *
85  * @access public
86  * @param object $oCatalogDB Catalog Database Object
87  * @param object $oShippingDB Shipping Database Object
88  * @return void
89  */
90  public function __construct($oCatalogDB, $oShippingDB)
91  {
92  $this->m_oCatalogDB = $oCatalogDB;
93  $this->m_oShippingDB = $oShippingDB;
94  }
95 
96  /**
97  * GetCountry - Gets Country from DB
98  *
99  * @access public
100  * @return string Country
101  */
102  public function GetCountry()
103  {
104  if ($this->m_sCountry == null)
105  {
106  $oSetupQueryResult = $this->m_oCatalogDB->Select('Setup', ['Country']);
107  $this->m_sCountry = select_get($oSetupQueryResult, 'Country', 0);
108  }
109  return $this->m_sCountry;
110  }
111 
112  /**
113  * GetTaxDefinition - Gets Tax Definition
114  *
115  * @access public
116  * @return array Tax Definition
117  */
118  public function GetTaxDefinition()
119  {
120  return $this->m_aTaxDefinition;
121  }
122 
123  /**
124  * GenerateOpaqueShipDataElements - Generates Opaque Shipping Data Elements
125  *
126  * @access public
127  * @param string $sCountryName Country name
128  * @return string Opaque Shipping Data
129  */
130  public function GenerateOpaqueShipDataElements($sCountryName)
131  {
132  $oQueryResult = $this->m_oCatalogDB->Select('Countries', ['nCountryID'], [['sCountryCode', $sCountryName]]);
133  $nCountryID = select_get($oQueryResult, 'nCountryID', 0);
134  $oQueryResult = $this->m_oShippingDB->Select('ZoneMembers', ['ZoneID'], [['RegionID', $nCountryID]]);
135  $nZoneID = select_get($oQueryResult, 'ZoneID');
136  return [$nCountryID, $nZoneID];
137  }
138 
139  /**
140  * GenerateOpaqueShipData - Generates Opaque Shipping Data
141  *
142  * @access public
143  * @param string $sCountry Country name
144  * @param int $nCost Shipping Cost
145  * @return string Opaque Shipping Data
146  */
147  public function GenerateOpaqueShipData($sCountry, $nCost)
148  {
149  list($nCountryID, $nZoneID) = $this->GenerateOpaqueShipDataElements($sCountry);
150  $sOpaqueShipData = "ShippingClass;Unknown;ShippingZone;$nZoneID;BasisTotal;0.000000;Simple;$nCost;NoRecalculation;";
151  return $sOpaqueShipData;
152  }
153 
154  /**
155  * GetInternationalShipping - Gets International Shipping
156  *
157  * Find the matching value in sCountryCode in the Countries table of the SellerDeck database and lookup nCountryID for the record
158  * Cross reference nCountryID against RegionID in the ZoneMembers table of the shipping database to get the zone name (ZoneID)
159  * For each Zone containing the country, look up by name (ZoneID) against the ZoneName in the Zones table.
160  * If ANY zone containing the country has bInternational set to -1, set the international shipping flag for the order (see SD-2782).
161  *
162  * @access public
163  * @param string $sCountryName Country name
164  * @return bool Opaque Shipping Data
165  */
166  public function GetInternationalShipping($sCountryName)
167  {
168  list($nCountryID, $nZoneID) = $this->GenerateOpaqueShipDataElements($sCountryName);
169  $oQueryResult = $this->m_oShippingDB->Select('Zones', '*', [['ZoneID', $nZoneID], ['bInternational', true]]);
170  if (is_object($oQueryResult))
171  {
172  $aZones = $oQueryResult->fetch(\PDO::FETCH_ASSOC);
173  return !empty($aZones);
174  }
175  return false;
176  }
177 
178  /**
179  * GenerateOpaqueShipDataForOrderDetail - Generates Opaque Shipping Data for OrderDetail
180  *
181  * @access public
182  * @param array $aProduct Product array
183  * @return string Opaque Shipping Data
184  */
185  public function GenerateOpaqueShipDataForOrderDetail($aProduct)
186  {
187  return ";ALT_WEIGHT=" . arr_get($aProduct, 'sAltWeight', '')
188  . ";EXCLUDE_FROM_SHIP=" . abs(arr_get($aProduct, 'bExcludeFromShippingCalc', 0))
189  . ";SHIP_CATEGORY=" . arr_get($aProduct, 'sShippingCategory', 0)
190  . ";SHIP_QUANTITY=" . arr_get($aProduct, 'nShippingQuantity', 1)
191  . ";SHIP_SUPPLEMENT=" . arr_get($aProduct, 'dShippingSupplement', 0)
192  . ";SHIP_SUPPLEMENT_ONCE=" . arr_get($aProduct, 'bShipSupplAppliedOnce', 0)
193  . ";HAND_SUPPLEMENT=" . arr_get($aProduct, 'dHandlingSupplement', 0)
194  . ";HAND_SUPPLEMENT_ONCE=" . arr_get($aProduct, 'bHandSupplAppliedOnce', 0)
195  . ";EXCLUDE_PARENT=" . arr_get($aProduct, '', 0)
196  . ";SEP_LINE=" . arr_get($aProduct, 'bShipSeparately', '')
197  . ";USE_ASSOC_SHIP=" . arr_get($aProduct, '', '')
198  . ";";
199  }
200 
201  /**
202  * GenerateTaxModelOpaqueData - Generates Tax Model Opaque Data
203  *
204  * Tax model information
205  *
206  * eTaxModel=eTaxBy=Shipping Tax 1 Opaque data=Shipping Tax 2 Opaque Data=Handling Tax 1 Opaque Data=Handling Tax 2 Opaque Data
207  *
208  * eTaxModel 0 - simple
209  * 1 - advanced
210  *
211  * eTaxBy 0 - always tax
212  * 1 - tax by invoice address
213  * 2 - tax by delivery address
214  *
215  * example: 1=2=0=0=0=Zero-Rated=0=0=0=??=0=0=0=??=0=0=0=??=
216  *
217  * @access public
218  * @param string $sCountryName Country Name
219  * @return string Tax Model Opaque Data
220  */
221  public function GenerateTaxModelOpaqueData($sCountryName)
222  {
223  if (null == $sCountryName)
224  {
225  return "0=0=0=0=0==0=0=0==0=0=0==0=0=0==";
226  }
227  if ($this->m_bSimpleTax)
228  {
229  $nTaxModel = TAX_MODEL_SIMPLE;
230  $aTaxes = $this->GetSimpleTaxes($this->GetTaxModel());
231  }
232  else
233  {
234  $nTaxModel = TAX_MODEL_ADVANCED;
235  $aTaxes = $this->GetCountryTaxes($sCountryName);
236  }
237 
238  $nTaxBy = TAX_BY_ALWAYS_TAX;
239  $sShippingTax1OpaqueData = arr_get($aTaxes, '1.sShippingOpaqueData', "0=0=0=");
240  $sShippingTax2OpaqueData = arr_get($aTaxes, '2.sShippingOpaqueData', "0=0=0=");
241  $sHandlingTax1OpaqueData = arr_get($aTaxes, '1.sHandlingOpaqueData', "0=0=0=");
242  $sHandlingTax2OpaqueData = arr_get($aTaxes, '2.sHandlingOpaqueData', "0=0=0=");
243  $sShippingTax1BandName = arr_get($this->m_aTaxBandNames, $this->ParseProductTaxData($sShippingTax1OpaqueData)->nBandID, '');
244  $sShippingTax2BandName = arr_get($this->m_aTaxBandNames, $this->ParseProductTaxData($sShippingTax2OpaqueData)->nBandID, '');
245  $sHandlingTax1BandName = arr_get($this->m_aTaxBandNames, $this->ParseProductTaxData($sHandlingTax1OpaqueData)->nBandID, '');
246  $sHandlingTax2BandName = arr_get($this->m_aTaxBandNames, $this->ParseProductTaxData($sHandlingTax2OpaqueData)->nBandID, '');
247  if (empty(trim($sShippingTax1OpaqueData)))
248  {
249  $sShippingTax1OpaqueData = "0=0=0=";
250  }
251  if (empty(trim($sShippingTax2OpaqueData)))
252  {
253  $sShippingTax2OpaqueData = "0=0=0=";
254  }
255  if (empty(trim($sHandlingTax1OpaqueData)))
256  {
257  $sHandlingTax1OpaqueData = "0=0=0=";
258  }
259  if (empty(trim($sHandlingTax2OpaqueData)))
260  {
261  $sHandlingTax2OpaqueData = "0=0=0=";
262  }
263  $sTaxModelOpaqueData = "$nTaxModel=$nTaxBy=$sShippingTax1OpaqueData$sShippingTax1BandName=$sShippingTax2OpaqueData$sShippingTax2BandName=$sHandlingTax1OpaqueData$sHandlingTax1BandName=$sHandlingTax2OpaqueData$sHandlingTax2BandName=";
264  return $sTaxModelOpaqueData;
265  }
266 
267  /**
268  * IsTaxing - Do we include tax, when simple tax is selected?
269  *
270  * @access public
271  * @return array Tax query result
272  */
273  public function IsTaxing()
274  {
275  $oQueryResult = $this->m_oCatalogDB->Select(
276  'TaxSetup', [['TaxZones', 'bIncludeTax1']], [[['TaxSetup', 'nModelID'], 0]], [
277  'join' => [
278  ['left', 'TaxZones', [[['TaxZones', 'nModelID'], ['TaxSetup', 'nModelID']]]],
279  ],
280  ]
281  );
282  if ($oQueryResult === false)
283  {
284  return false;
285  }
286  $aTaxZonesResult = $oQueryResult->fetch(\PDO::FETCH_ASSOC);
287  if ($oQueryResult === false)
288  {
289  //
290  // Advanced Taxing
291  //
292  return true;
293  }
294  $bIncludeTax1 = (bool) abs(arr_get($aTaxZonesResult, 'bIncludeTax1', true));
295  return $bIncludeTax1;
296  }
297 
298  /**
299  * GetSimpleTaxes - Get Simple Taxes
300  *
301  * @access public
302  * @param array $aTaxModel Tax Model details
303  * @return array Tax query result
304  */
305  public function GetSimpleTaxes($aTaxModel)
306  {
307  $aTaxes = [];
308  for ($nIndex = 1; $nIndex <= 2; $nIndex++)
309  {
310  if (arr_get($aTaxModel, 'bIncludeTax' . $nIndex . 'Always', 1) != 0)
311  {
312  $oQueryResult = $this->m_oCatalogDB->Select(
313  'Taxes', [['Taxes', '*']], [['nTaxID', arr_get($aTaxModel, 'nTax' . $nIndex . 'ID', 1)]]
314  );
315  if ($oQueryResult === false)
316  {
317  return null;
318  }
319  $aTaxes[$nIndex] = $oQueryResult->fetch(\PDO::FETCH_ASSOC);
320  }
321  }
322  return $aTaxes;
323  }
324 
325  /**
326  * GetCountryTaxes - Get Country Taxes
327  *
328  * @access public
329  * @param string $sCountryName Country Name
330  * @return array Tax query result
331  */
332  public function GetCountryTaxes($sCountryName)
333  {
334  return array_filter([
335  1 => $this->GetCountryTax($sCountryName, 1),
336  2 => $this->GetCountryTax($sCountryName, 2),
337  ]);
338  }
339 
340  /**
341  * GetTaxModel - Get Tax Model
342  *
343  * @access public
344  * @return array|bool Tax query result
345  */
346  public function GetTaxModel()
347  {
348  if ($this->m_aTaxModel == null)
349  {
350  $oQueryResult = $this->m_oCatalogDB->Select(
351  'TaxSetup', [['TaxModels', '*'], 'sShippingTax1OpaqueData', 'sShippingTax2OpaqueData', 'sHandlingTax1OpaqueData', 'sHandlingTax2OpaqueData', 'bTaxZonesRestrictive', 'bTaxInclusiveMode'], [], [
352  'join' => [
353  ['left', 'TaxModels', [[['TaxSetup', 'nModelID'], ['TaxModels', 'nModelID']]]],
354  ],
355  ]
356  );
357  if ($oQueryResult === false)
358  {
359  return null;
360  }
361  return $oQueryResult->fetch(\PDO::FETCH_ASSOC);
362  }
363  else
364  {
365  return $this->m_aTaxModel;
366  }
367  }
368 
369  /**
370  * GetCountryTax - Get Country Tax
371  *
372  * @access public
373  * @param string $sCountryName Country Name
374  * @return array|bool Tax query result
375  */
376  public function GetCountryTax($sCountryName, $nTaxNo = 1)
377  {
378  if (!in_array($nTaxNo, [1, 2]))
379  {
380  return null;
381  }
382  $oQueryResult = $this->m_oCatalogDB->Select(
383  'Taxes', [['Taxes', '*'], ['TaxZones', 'bAllowTax1UserExemption'], ['TaxZones', 'bAllowTax2UserExemption'], ['TaxZones', 'bTaxOtherTaxes']], [['sCountryName', $sCountryName]], [
384  'join' => [
385  ['left', 'TaxZones', [[['Taxes', 'nTaxID'], ['TaxZones', 'nTax' . $nTaxNo . 'ID']]]],
386  ['left', 'TaxZoneMembers', [[['TaxZones', 'nZoneID'], ['TaxZoneMembers', 'nTaxZoneID']]]],
387  ['left', 'Countries', [[['TaxZoneMembers', 'nRegionID'], ['Countries', 'nCountryID']]]],
388  ],
389  ]
390  );
391  if ($oQueryResult === false)
392  {
393  return null;
394  }
395  return $oQueryResult->fetch(\PDO::FETCH_ASSOC);
396  }
397 
398  /**
399  * GenerateTaxOpaqueData - Generates Tax Opaque Data
400  *
401  * Tax 1 information
402  * TaxName=RoundRule=RoundGroup=TaxOtherTaxesFlag=LowTreshold=HighTreshold= AllowExemption=TaxID
403  *
404  * TaxName - tax name
405  * RoundRule - tax rounding rule
406  * Reference to [TaxRules].[nTaxRulesID]
407  * RoundGroup- tax rounding group
408  * 0 = round tax per order line
409  * 1 = round tax per order item
410  * 2 = round tax per order
411  * TaxOtherTaxesFlag - whether tax is cumulative
412  * 0 = false
413  * 1 = true
414  * LowThreshold - low threshold (integer)
415  * HighThreshold- high threshold (integer)
416  * AllowExemption - whether the user exemption is allowed
417  * 0 = disallowed
418  * 1 = allowed
419  * TaxID - tax ID
420  *
421  * @access public
422  * @param string $sCountryName Country Name
423  * @return string Tax Opaque Data
424  */
425  public function GenerateTaxOpaqueData($sCountryName, $nTaxIndex)
426  {
427  if (null == $sCountryName)
428  {
429  return "";
430  }
431  $aTaxes = $this->GetCountryTax($sCountryName, $nTaxIndex);
432 
433  $sTaxName = $aTaxes['sTaxName'];
434  $sTaxID = $aTaxes['nTaxID'];
435  $nRoundRule = $aTaxes['nRoundingRule'];
436  $sRoundGroup = $aTaxes['nRoundingGroup'];
437  $nTaxOtherTaxesFlag = $aTaxes['bTaxOtherTaxes'];
438  $nLowThreshold = $aTaxes['nRoundingGroup'];
439  $nHighThreshold = $aTaxes['nHighThreshold'];
440  $sAllowExemption = $aTaxes['bAllowTax' . $nTaxIndex . 'UserExemption'];
441 
442  $sTaxOpaqueData = "$sTaxName=$nRoundRule=$sRoundGroup=$nTaxOtherTaxesFlag=$nLowThreshold=$nHighThreshold=$sAllowExemption=$sTaxID=";
443  return $sTaxOpaqueData;
444  }
445 
446  /**
447  * GetTaxBands - Gets Tax Bands
448  *
449  * @access public
450  * @return string Tax Opaque Data
451  */
452  public function GetTaxBands()
453  {
454  $sCountry = $this->GetCountry();
455  $oQueryResult = $this->m_oCatalogDB->Select(
456  'TaxBands', [['TaxBands', '*']], [['sCountryName', $sCountry]], [
457  'join' => [
458  ['left', 'TaxZones', [[['TaxBands', 'nTaxID'], ['TaxZones', 'nTax1ID']]]],
459  ['left', 'TaxZoneMembers', [[['TaxZones', 'nZoneID'], ['TaxZoneMembers', 'nTaxZoneID']]]],
460  ['left', 'Countries', [[['TaxZoneMembers', 'nRegionID'], ['Countries', 'nCountryID']]]],
461  ],
462  ]
463  );
464  $aTaxBands = $oQueryResult->fetch(\PDO::FETCH_ASSOC);
465  return $aTaxBands;
466  }
467 
468  /**
469  * GenerateTaxOpaqueDataForOrderDetail - Generates Tax Opaque Data
470  *
471  *
472  * Shipping /Handling Tax opaque data
473  * Band ID=Band Rate=Custom Rate=Band Name=
474  *
475  * Band ID string for the band ID
476  * Band Rate string for the band rate
477  * Custom Rate string for the custom rate
478  * Band Name string for the band name
479  *
480  * @access public
481  * @param object $oTaxBand Tax Band
482  * @return string Tax Opaque Data
483  */
484  public function GenerateTaxOpaqueDataForOrderDetail($oTaxBand)
485  {
486  return $oTaxBand->nBandID . "=" . $oTaxBand->dBandRate . "=" . $oTaxBand->nCustomRate . "=" . $this->m_aTaxBandNames[$oTaxBand->nBandID] . "=";
487  }
488 
489  /**
490  * NormalizeTaxBand - Normalize Tax Band
491  *
492  * @access public
493  * @param array | object $vTaxBand Tax Band
494  * @return object Normalized Tax Band
495  */
496  public function NormalizeTaxBand($vTaxBand)
497  {
498  if (is_array($vTaxBand))
499  {
500  $vTaxBand = (object) array_intersect_key($vTaxBand, array_flip(["nBandID", "dBandRate", "nCustomRate"]));
501  $vTaxBand->nCustomRate = $vTaxBand->nCustomRate? : 0;
502  }
503  $vTaxBand->dBandRate = (double) $vTaxBand->dBandRate;
504  return $vTaxBand;
505  }
506 
507  /**
508  * ParseProductTaxData - Parses Product Tax Data
509  *
510  * @access public
511  * @param string $sTaxOpaqueData Tax Opaque Data
512  * @param arraz $aDefaultTaxBandAttributes Default Tax Band Attributes
513  * @return object Tax data
514  */
515  public function ParseProductTaxData($sTaxOpaqueData, $aDefaultTaxBandAttributes = [])
516  {
517  $oDefault = (object) ["nBandID" => arr_get($aDefaultTaxBandAttributes, 'id', 0), "dBandRate" => float2sdnum(arr_get($aDefaultTaxBandAttributes, 'rate', 0.0)), "nCustomRate" => 0.0, "sName" => arr_get($aDefaultTaxBandAttributes, 'name', '')];
518  if (empty($sTaxOpaqueData))
519  {
520  return $oDefault;
521  }
522  $aTaxOpaqueData = explode("=", $sTaxOpaqueData);
523  array_pop($aTaxOpaqueData);
524  if (count($aTaxOpaqueData) != 3)
525  {
526  return $oDefault;
527  }
528  $oTaxOpaqueData = (object) array_combine(["nBandID", "dBandRate", "nCustomRate"], $aTaxOpaqueData);
529  return $oTaxOpaqueData;
530  }
531 
532  /**
533  * LoadTaxes - Loads Taxes from DataBase
534  *
535  * @access public
536  * @param string $sCountryName Country Name
537  * @return array Tax Band Data
538  */
539  public function LoadTaxes($sCountryName)
540  {
541  if (!isset($this->m_aTaxDefinition[$sCountryName]))
542  {
543  $this->m_aTaxDefinition[$sCountryName] = [
544  '@attributes' =>
545  [
546  'taxinclusiveprices' => 'yes',
547  ],
548  ];
549  $nTaxCount = 0;
550  $aTaxes = [];
551  $aTaxModel = $this->GetTaxModel();
552  if (arr_get($aTaxModel, 'nModelID', TAX_MODEL_SIMPLE) == TAX_MODEL_SIMPLE)
553  {
554  $this->m_bSimpleTax = true;
555  $aTaxes = $this->GetSimpleTaxes($aTaxModel);
556  }
557  else
558  {
559  $this->m_bSimpleTax = false;
560  $aTaxes = $this->GetCountryTaxes($sCountryName);
561  }
562  foreach ($aTaxes as $aTax)
563  {
564  $nTaxCount++;
565  $this->m_aTaxDefinition[$sCountryName]["tax$nTaxCount"]["@attributes"] = [
566  'id' => $aTax['nTaxID'],
567  'name' => iconv('WINDOWS-1256', 'UTF-8', $aTax['sTaxName']),
568  'roundingmethod' => $this->m_aRoundingRules[$aTax['nRoundingRule']],
569  'roundinglevel' => $this->m_aRoundingGroups[$aTax['nRoundingGroup']],
570  ];
571  $oTaxBandsQueryResult = $this->m_oCatalogDB->Select('TaxBands', '*', [['nTaxID', $aTax['nTaxID']]]);
572  if (false === $oTaxBandsQueryResult)
573  {
574  continue;
575  }
576  $aTaxBands = $oTaxBandsQueryResult->fetchAll();
577  foreach ($aTaxBands as $nTaxBandIndex => $aTaxBand)
578  {
579 // $aTaxBand['dBandRate'] = sdnum2float(float2sdnum($aTaxBand['dBandRate']));
580  $nBandID = $aTaxBand['nBandID'];
581  $sBandName = iconv('WINDOWS-1256', 'UTF-8', $aTaxBand['sBandName']);
582  $this->m_aTaxBandNames[$nBandID] = $this->m_aTaxBandNames[$aTaxBand[$nBandID]] ? : $sBandName;
583  $this->m_aTaxDefinition[$sCountryName]["tax$nTaxCount"]["band"][] = [
584  '@attributes' => [
585  'id' => $nBandID,
586  'name' => $sBandName,
587  'rate' => $aTaxBand['dBandRate'],
588  ],
589  ];
590  }
591  $this->m_aTaxBandNames[0] = $this->m_aTaxBandNames[0] ? : "Zero-Rated";
592  $this->m_aTaxDefinition[$sCountryName]["tax$nTaxCount"]["band"][] = [
593  '@attributes' => [
594  'id' => 0,
595  'name' => "Zero&amp;#45;Rated",
596  'rate' => "0.000000",
597  ],
598  ];
599  $this->m_aTaxBandNames[1] = $this->m_aTaxBandNames[1] ? : "Exempt";
600  $this->m_aTaxDefinition[$sCountryName]["tax$nTaxCount"]["band"][] = [
601  '@attributes' => [
602  'id' => 1,
603  'name' => "Exempt",
604  'rate' => "0.000000",
605  ],
606  ];
607  }
608  $this->m_aTaxDefinition[$sCountryName]["@attributes"]["taxcount"] = $nTaxCount;
609  }
610  return $this->m_aTaxDefinition[$sCountryName];
611  }
612 
613  /**
614  * TransformOrderToXMLArray - Transforms Order to XML Array
615  *
616  * @access public
617  * @param object $oOrder Order
618  * @return array Tax Band Data
619  */
620  public function TransformOrderToXMLArray($oOrder)
621  {
622  $aProcessedOrder = $oOrder;
623  return $aProcessedOrder;
624  }
625 
626  /**
627  * AddCustomTaxToDefinition - Add Custom Tax To Definition
628  *
629  * @access public
630  * @param string $sCountryName Country Name
631  * @param int $nTaxIndex Tax Index
632  */
633  public function AddCustomTaxToDefinition($sCountryName, $nTaxIndex)
634  {
635  $this->m_aTaxDefinition[$sCountryName]["tax$nTaxIndex"]["band"][] = [
636  '@attributes' => [
637  'id' => "c100-$nTaxIndex",
638  'name' => "Custom tax 100%",
639  'rate' => 100,
640  ],
641  ];
642  $this->m_aTaxDefinition[$sCountryName]["tax$nTaxIndex"]["band"][] = [
643  '@attributes' => [
644  'id' => "c000-$nTaxIndex",
645  'name' => "Custom tax 0%",
646  'rate' => 0,
647  ],
648  ];
649  }
650 
651  /**
652  * SetCustomTax - Sets Custom Tax
653  *
654  * @access public
655  * @param string $sCountryName Country Name
656  * @param int $nTaxIndex Tax Index
657  */
658  public function SetCustomTax($sCountryName, $nTaxIndex, $aOrderDetailTaxXML, $nCustomRate)
659  {
660  $this->AddCustomTaxToDefinition($sCountryName, $nTaxIndex);
661  $dRate = $nCustomRate / sdnum2float($aOrderDetailTaxXML['@attributes']['price']);
662  if (isset($aOrderDetailTaxXML['lineband'][0]['@attributes']['applypercent']))
663  {
664  $nOtherTaxIndex = 3 - $nTaxIndex;
665  $aOrderDetailTaxXML['lineband'][0]['@attributes']['tax' . $nTaxIndex . 'bandid'] = "c000-$nTaxIndex";
666  $aOrderDetailTaxXML['lineband'][1]['@attributes']['applypercent'] -= $dRate;
667  $aOrderDetailTaxXML['lineband'][1]['@attributes']['tax' . $nTaxIndex . 'bandid'] = "c000-$nTaxIndex";
668  $aOrderDetailTaxXML['lineband'][2]['@attributes']['applypercent'] = $dRate;
669  $aOrderDetailTaxXML['lineband'][2]['@attributes']['tax' . $nTaxIndex . 'bandid'] = "c100-$nTaxIndex";
670  $aOrderDetailTaxXML['lineband'][2]['@attributes']['tax' . $nOtherTaxIndex . 'bandid'] = "c000-$nOtherTaxIndex";
671  }
672  else
673  {
674  $aOrderDetailTaxXML['lineband'][0]['@attributes']['applypercent'] = $dRate;
675  $aOrderDetailTaxXML['lineband'][0]['@attributes']['tax' . $nTaxIndex . 'bandid'] = "c100-$nTaxIndex";
676  $aOrderDetailTaxXML['lineband'][1]['@attributes']['applypercent'] = 100.00 - $dRate;
677  $aOrderDetailTaxXML['lineband'][1]['@attributes']['tax' . $nTaxIndex . 'bandid'] = "c000-$nTaxIndex";
678  }
679  return $aOrderDetailTaxXML;
680  }
681 
682  /**
683  * AddTaxBands - Adds Tax Bands
684  *
685  * @access public
686  * @param string $sCountryName Country Name
687  * @param array $aOrderProducts Order Products
688  * @param array $aOrderDetailTaxXMLs Order Detail Tax XML
689  * @return array Modified Order Detail Tax XML
690  */
691  public function AddTaxBands($sCountryName, $aOrderProducts, $aOrderDetailTaxXMLs)
692  {
693  foreach ($aOrderDetailTaxXMLs as $nOrderDetailID => $aOrderDetailTaxXML)
694  {
695  $sProductReference = arr_get($aOrderDetailTaxXML, '@attributes.reference', 0);
696  $aOrderProduct = arr_get($aOrderProducts, $sProductReference, []);
697  $aOrderDetailTaxXMLs[$nOrderDetailID]['@attributes']['orderdetailid'] = $nOrderDetailID;
698  $aTaxBands = $this->GetTaxBandsFromDefinition($sCountryName, $aOrderProduct);
699  $aTaxBandIDs = $this->GetTaxBandIDs($aTaxBands);
700  foreach ($aTaxBands as $nTaxIndex => $oTaxBand)
701  {
702  $aOrderDetailTaxXMLs[$nOrderDetailID]['@attributes']['tax' . $nTaxIndex . 'rate'] = (double) $oTaxBand->dBandRate;
703  $aOrderDetailTaxXMLs[$nOrderDetailID]['@attributes']['tax' . $nTaxIndex . 'opaquedata'] = $this->GenerateTaxOpaqueDataForOrderDetail($oTaxBand);
704  }
705  switch ($this->CountCustom($aTaxBandIDs))
706  {
707  case 0:
708  foreach ($aTaxBandIDs as $nTaxIndex => $nTaxBandID)
709  {
710  $aOrderDetailTaxXMLs[$nOrderDetailID]['@attributes']['tax' . $nTaxIndex . 'bandid'] = $nTaxBandID;
711  }
712  break;
713  case 1:
714  $nCustomTaxIndex = arr_get($aTaxBandIDs, 1, 0) == 6 ? 1 : 2;
715  $nNonCustomTaxIndex = 3 - $nCustomTaxIndex;
716  $nCustomRate = $aTaxBands[$nCustomTaxIndex]->nCustomRate;
717  $nNonCustomTaxBandID = $aTaxBandIDs[$nNonCustomTaxIndex];
718  $aOrderDetailTaxXMLs[$nOrderDetailID] = $this->SetCustomTax($sCountryName, $nCustomTaxIndex, $aOrderDetailTaxXMLs[$nOrderDetailID], $nCustomRate);
719  $aOrderDetailTaxXMLs[$nOrderDetailID]['lineband'][0]['@attributes']['tax' . $nNonCustomTaxIndex . 'bandid'] = $nNonCustomTaxBandID;
720  $aOrderDetailTaxXMLs[$nOrderDetailID]['lineband'][1]['@attributes']['tax' . $nNonCustomTaxIndex . 'bandid'] = $nNonCustomTaxBandID;
721  break;
722  case 2:
723  foreach ($aTaxBands as $nTaxIndex => $oTaxBand)
724  {
725  $aOrderDetailTaxXMLs[$nOrderDetailID] = $this->SetCustomTax($sCountryName, $nTaxIndex, $aOrderDetailTaxXMLs[$nOrderDetailID], $oTaxBand->nCustomRate);
726  }
727  break;
728  }
729  }
730  return $aOrderDetailTaxXMLs;
731  }
732 
733  /**
734  * GetFirstTaxBand - Get First Tax Band
735  *
736  * @access public
737  * @param string $sCountryName Country Name
738  * @param int $nTaxIndex Tax Index
739  * @return array Tax Band IDs
740  */
741  public function GetFirstTaxBand($sCountryName, $nTaxIndex)
742  {
743  $aTaxBandsTmp = arr_get($this->m_aTaxDefinition, "$sCountryName.tax$nTaxIndex.band", []);
744  $aTaxBandTmp = is_array($aTaxBandsTmp) ? array_shift($aTaxBandsTmp) : [];
745  return arr_get($aTaxBandTmp, "@attributes", []);
746  }
747 
748  /**
749  * GetTaxBandsFromDefinition - Get Tax Bands From Definition
750  *
751  * @access public
752  * @param string $sCountryName Country Name
753  * @param array $aOrderProduct Order Product data
754  * @return array Tax Band IDs
755  */
756  public function GetTaxBandsFromDefinition($sCountryName, $aOrderProduct)
757  {
758  $aTaxBands = [];
759  $nTaxCount = arr_get($this->m_aTaxDefinition, "$sCountryName.@attributes.taxcount", 1);
760  $nSpecTaxDiff = $this->m_bSimpleTax ? 0 : 2;
761  for ($nTaxIndex = 1; $nTaxIndex <= $nTaxCount; $nTaxIndex++)
762  {
763  $nTaxOpaqueDataNo = arr_get($this->m_aTaxDefinition, "$sCountryName.tax$nTaxIndex.@attributes.id", $nTaxIndex + $nSpecTaxDiff) - $nSpecTaxDiff;
764  $sTaxOpaqueData = arr_get($aOrderProduct, 'sTax' . $nTaxOpaqueDataNo . 'OpaqueData', '');
765  $aDefaultTaxBandAttributes = $this->GetFirstTaxBand($sCountryName, $nTaxIndex);
766  $aTaxBands[$nTaxIndex] = $this->ParseProductTaxData($sTaxOpaqueData, $aDefaultTaxBandAttributes);
767  }
768  return $aTaxBands;
769  }
770 
771  /**
772  * GetTaxBandIDs - Get Tax Band IDs
773  *
774  * @access public
775  * @param array $aTaxBands Tax Bands
776  * @return array Tax Band IDs
777  */
778  public function GetTaxBandIDs($aTaxBands)
779  {
780  return array_map(function($oTaxBand)
781  {
782  return $oTaxBand->nBandID;
783  }, $aTaxBands);
784  }
785 
786  /**
787  * GetShippingLine - Get Shipping Line
788  *
789  * @access public
790  * @param double $dShipping Shipping
791  * @return array Shipping Order Line
792  */
793  public function GetShippingLine($dShipping, $sCountryName)
794  {
795  $aShipping = [
796  '@attributes' =>
797  [
798  'id' => 'shipping',
799  'price' => $dShipping,
800  'qty' => 1,
801  'groupid' => 'discountedcart',
802  ]
803  ];
804  // <orderline id="shipping" price="350.000000" qty="1" tax1bandid="301shipping"/>
805  // <orderline id="shipping" price="1130.000000" qty="1" tax1bandid="301" tax2bandid="402"/>
806  // TODO!!
807  if ($this->m_bSimpleTax)
808  {
809  $nTaxModel = TAX_MODEL_SIMPLE;
810  $aTaxes = $this->GetSimpleTaxes($this->GetTaxModel());
811  }
812  else
813  {
814  $nTaxModel = TAX_MODEL_ADVANCED;
815  $aTaxes = $this->GetCountryTaxes($sCountryName);
816  }
817  $nTaxBy = TAX_BY_ALWAYS_TAX;
818  $nTaxCount = $this->m_aTaxDefinition[$sCountryName]["@attributes"]["taxcount"];
819  $sShippingTax1OpaqueData = arr_get($aTaxes, '1.sShippingOpaqueData', "0=0=0=");
820  $oTaxOpaqueData = $this->ParseProductTaxData($sShippingTax1OpaqueData);
821  if ($oTaxOpaqueData->nBandID != 5) // Pro-Rata
822  {
823  $aShipping['@attributes']['tax1bandid'] = $oTaxOpaqueData->nBandID;
824  }
825  if ($nTaxCount > 1)
826  {
827  $sShippingTax2OpaqueData = arr_get($aTaxes, '2.sShippingOpaqueData', "0=0=0=");
828  $oTaxOpaqueData = $this->ParseProductTaxData($sShippingTax2OpaqueData);
829  if ($oTaxOpaqueData->nBandID != 5) // Pro-Rata
830  {
831  $aShipping['@attributes']['tax2bandid'] = $oTaxOpaqueData->nBandID;
832  }
833  }
834  return $aShipping;
835  }
836 
837  /**
838  * CountCustom - Count Custom
839  *
840  * @access public
841  * @param array $aTaxBandIDs Tax Band IDs
842  * @return int No of Custom Taxes
843  */
844  public function CountCustom($aTaxBandIDs)
845  {
846  $aCountValues = array_count_values($aTaxBandIDs);
847  return arr_get($aCountValues, 6, 0);
848  }
849 
850  /**
851  * ProcessTaxes - Processing Taxes (Class Entry Point)
852  *
853  * @access public
854  * @param string $sCountryName Country Name
855  * @param array $aOrderProducts Order Products
856  * @param array $aOrderDetailTaxXMLs Order Detail Tax XML
857  * @param double $dShipping Shipping
858  * @return
859  */
860  public function ProcessTaxes($sCountryName, $aOrderProducts, $aOrderDetailTaxXMLs, $dShipping = 0)
861  {
862  if (!class_exists('CTaxModule'))
863  {
864  throw new \SDExtension\SDException\SDException("CTaxModule not found");
865  }
866  $this->LoadTaxes($sCountryName); // $this->m_aTaxDefinition[$sCountryName]
867  $aCustomer = ['@attributes' => ['tax1exempt' => 'no',]];
868  $aZone = ['@attributes' => ['tax1applies' => 'yes',]];
869  $nTaxCount = $this->m_aTaxDefinition[$sCountryName]["@attributes"]["taxcount"];
870  if ($nTaxCount > 1)
871  {
872  $aCustomer['@attributes']['tax2exempt'] = 'no';
873  $aZone['@attributes']['tax2applies'] = 'yes';
874  }
875  $aOrderDetailTaxXMLs = $this->AddTaxBands($sCountryName, $aOrderProducts, $aOrderDetailTaxXMLs);
876  $aOrderDetailTaxXMLs = [
877  'group' => [
878  '@attributes' => ['id' => 'discountedcart'],
879  'group' => [
880  '@attributes' => ['id' => 'cart'],
881  'orderline' => $aOrderDetailTaxXMLs,
882  ]
883  ]
884  ];
885  $aOrderDetailTaxXMLs['orderline'] = $this->GetShippingLine($dShipping, $sCountryName);
886  $oTaxModule = new \CTaxModule();
887  $aMessage = [
888  'taxdefinition' => $this->m_aTaxDefinition[$sCountryName],
889  'customer' => $aCustomer,
890  'zone' => $aZone,
891  'order' => $aOrderDetailTaxXMLs,
892  ];
893  $oTaxXML = \SDExtension\Helper\Array2XML::createXML("message", $aMessage);
894  $sTaxXML = $oTaxXML->saveXML();
895  $sTaxModuleOutputXML = $oTaxModule->Process($sTaxXML);
896  $aTaxModuleOutputXML = \SDExtension\Helper\XML2Array::createArray($sTaxModuleOutputXML);
897  if (arr_get($aTaxModuleOutputXML, 'MESSAGE.ORDER.GROUP.GROUP.ORDERLINE.LINEBAND', null) == null)
898  {
899  foreach (arr_get($aTaxModuleOutputXML, 'MESSAGE.ORDER.GROUP.GROUP.ORDERLINE', []) as $nKey => $aOrderLine)
900  {
901  $aTaxModuleOutputXML['MESSAGE']['ORDER']['GROUP']['GROUP']['ORDERLINE']["tmp$nKey"] = $aOrderLine;
902  unset($aTaxModuleOutputXML['MESSAGE']['ORDER']['GROUP']['GROUP']['ORDERLINE'][$nKey]);
903  }
904  foreach (arr_get($aTaxModuleOutputXML, 'MESSAGE.ORDER.GROUP.GROUP.ORDERLINE', []) as $sKey => $aOrderLine)
905  {
906  $aTaxModuleOutputXML['MESSAGE']['ORDER']['GROUP']['GROUP']['ORDERLINE'][arr_get($aOrderLine, '@attributes.ORDERDETAILID', 0)] = $aOrderLine;
907  unset($aTaxModuleOutputXML['MESSAGE']['ORDER']['GROUP']['GROUP']['ORDERLINE'][$sKey]);
908  }
909  }
910  else
911  {
912  $aOrderLine = arr_get($aTaxModuleOutputXML, 'MESSAGE.ORDER.GROUP.GROUP.ORDERLINE', []);
913  $nOrderDetailID = arr_get($aTaxModuleOutputXML, 'MESSAGE.ORDER.GROUP.GROUP.ORDERLINE.@attributes.ORDERDETAILID', []);
914  unset($aTaxModuleOutputXML['MESSAGE']['ORDER']['GROUP']['GROUP']['ORDERLINE']);
915  $aTaxModuleOutputXML['MESSAGE']['ORDER']['GROUP']['GROUP']['ORDERLINE'][$nOrderDetailID] = $aOrderLine;
916  }
917  \SDExtension\Helper\CLogger::get(LOG_CHANNEL_NAME)->addDebug("TaxModule REQUEST: " . $sTaxXML);
918  \SDExtension\Helper\CLogger::get(LOG_CHANNEL_NAME)->addDebug("TaxModule RESPONSE: " . str_replace("\t\t", " ", $sTaxModuleOutputXML));
919  return $aTaxModuleOutputXML;
920  }
921 
922  }
static get($sChannel="default", $sLogRoot="")
Definition: CLogger.php:90
CountCustom($aTaxBandIDs)
Definition: CTax.php:844
SetCustomTax($sCountryName, $nTaxIndex, $aOrderDetailTaxXML, $nCustomRate)
Definition: CTax.php:658
GenerateOpaqueShipData($sCountry, $nCost)
Definition: CTax.php:147
GetTaxBandIDs($aTaxBands)
Definition: CTax.php:778
GetSimpleTaxes($aTaxModel)
Definition: CTax.php:305
GenerateOpaqueShipDataForOrderDetail($aProduct)
Definition: CTax.php:185
GenerateOpaqueShipDataElements($sCountryName)
Definition: CTax.php:130
LoadTaxes($sCountryName)
Definition: CTax.php:539
GenerateTaxOpaqueDataForOrderDetail($oTaxBand)
Definition: CTax.php:484
AddTaxBands($sCountryName, $aOrderProducts, $aOrderDetailTaxXMLs)
Definition: CTax.php:691
static & createXML($node_name, $arr=array(), $namespace=null)
Definition: Array2XML.php:62
GetFirstTaxBand($sCountryName, $nTaxIndex)
Definition: CTax.php:741
NormalizeTaxBand($vTaxBand)
Definition: CTax.php:496
TransformOrderToXMLArray($oOrder)
Definition: CTax.php:620
ParseProductTaxData($sTaxOpaqueData, $aDefaultTaxBandAttributes=[])
Definition: CTax.php:515
ProcessTaxes($sCountryName, $aOrderProducts, $aOrderDetailTaxXMLs, $dShipping=0)
Definition: CTax.php:860
GenerateTaxOpaqueData($sCountryName, $nTaxIndex)
Definition: CTax.php:425
GetShippingLine($dShipping, $sCountryName)
Definition: CTax.php:793
GetTaxBandsFromDefinition($sCountryName, $aOrderProduct)
Definition: CTax.php:756
__construct($oCatalogDB, $oShippingDB)
Definition: CTax.php:90
GetTaxDefinition()
Definition: CTax.php:118
GenerateTaxModelOpaqueData($sCountryName)
Definition: CTax.php:221
AddCustomTaxToDefinition($sCountryName, $nTaxIndex)
Definition: CTax.php:633
GetCountryTax($sCountryName, $nTaxNo=1)
Definition: CTax.php:376
GetInternationalShipping($sCountryName)
Definition: CTax.php:166
GetCountryTaxes($sCountryName)
Definition: CTax.php:332
static & createArray($input_xml)
Definition: XML2Array.php:49