MS_Addon_Taxamo_Api

Taxamo API functions.

Defined (1)

The class is defined in the following location(s).

/app/addon/taxamo/class-ms-addon-taxamo-api.php  
  1. class MS_Addon_Taxamo_Api extends MS_Controller { 
  2.  
  3. static protected $Countries = null; 
  4. static protected $Countries_Prefix = null; 
  5. static protected $Countries_Vat = null; 
  6.  
  7. /** 
  8. * Returns tax information that must be applied to the specified amount. 
  9. * This function first checks the user-metadata for logged-in users and 
  10. * takes the details that are stored in the user metadata table. 
  11. * If the user is not logged in or no metadata are found then the Taxamo API 
  12. * is queried to get the default tax details for the country that 
  13. * Taxamo automatically detects. 
  14. * @since 1.0.0 
  15. * @api 
  16. * @param numeric $amount The amount without taxes. 
  17. * @return object { 
  18. * Tax information 
  19. * string $country Tax country (2-digit code) 
  20. * string $name Tax name 
  21. * numeric $rate Tax rate (percent) 
  22. * numeric $amount Tax amount 
  23. * } 
  24. */ 
  25. static public function tax_info( $amount = 0 ) { 
  26. static $Info = null; 
  27.  
  28. if ( null === $Info ) { 
  29. $settings = MS_Factory::load( 'MS_Model_Settings' ); 
  30.  
  31. try { 
  32. $profile = self::get_tax_profile(); 
  33. $tax_number = null; 
  34. if ( $profile->use_vat_number ) { 
  35. $tax_number = $profile->vat_number; 
  36. $resp = self::taxamo()->calculateSimpleTax( 
  37. null // buyer_credit_card_prefix 
  38. , $tax_number // buyer_tax_number 
  39. , null // product_type 
  40. , $profile->tax_country->code // force_country_code 
  41. , null // quantity 
  42. , null // unit_price 
  43. , null // total_amount 
  44. , null // tax_deducted 
  45. , 100 // amount 
  46. , $profile->tax_country->code // billing_country_code 
  47. , $settings->currency // currency_code 
  48. , null // order_date 
  49. ); 
  50.  
  51. // Prepare the result object. 
  52. if ( isset( $resp->transaction->transaction_lines[0] ) ) { 
  53. $transaction = $resp->transaction->transaction_lines[0]; 
  54. $Info = (object) array( 
  55. 'country' => $resp->transaction->tax_country_code,  
  56. 'rate' => $transaction->tax_rate,  
  57. 'name' => $transaction->tax_name,  
  58. 'amount' => 0,  
  59. ); 
  60. catch ( Exception $ex ) { 
  61. MS_Helper_Debug::log( 'Taxamo error: ' . $ex->getMessage() ); 
  62.  
  63. if ( ! is_object( $Info ) ) { $Info = (object) array(); } 
  64. if ( ! isset( $Info->name ) ) { $Info->name = __( 'No Tax', 'membership2' ); } 
  65. if ( ! isset( $Info->rate ) ) { $Info->rate = 0; } 
  66. if ( ! isset( $Info->amount ) ) { $Info->amount = 0; } 
  67. if ( ! isset( $Info->country ) ) { $Info->country = 'US'; } 
  68.  
  69. $Info->amount = $amount / 100 * $Info->rate; 
  70.  
  71. return $Info; 
  72.  
  73. /** 
  74. * Creates a confirmed transaction in Taxamo. 
  75. * @since 1.0.0 
  76. * @param numeric $amount Transaction amount 
  77. */ 
  78. static public function register_payment( $amount, $label, $tax_rate, $invoice_id, $name, $email, $gateway, $currency, $ip_addr ) { 
  79. try { 
  80. $profile = self::get_tax_profile(); 
  81.  
  82. if ( empty( $ip_addr ) ) { 
  83. $ip_addr = lib3()->net->current_ip()->ip; 
  84.  
  85. // Register the transaction with Taxamo. 
  86. $transaction = self::prepare_transaction( 
  87. $amount,  
  88. $label,  
  89. $tax_rate,  
  90. $invoice_id,  
  91. $name,  
  92. $email,  
  93. $currency,  
  94. $ip_addr 
  95. ); 
  96. $payload = array( 'transaction' => $transaction ); 
  97. $resp = self::taxamo()->createTransaction( $payload ); 
  98.  
  99. if ( ! empty( $resp->transaction->key ) ) { 
  100. $tr_key = $resp->transaction->key; 
  101. // Confirm the Transaction. 
  102. $resp = self::taxamo()->confirmTransaction( $tr_key, null ); 
  103.  
  104. // Register the payment. 
  105. $information = sprintf( 
  106. __( 'Invoice %1$s paid via %2$s.', 'membership2' ),  
  107. $invoice_id,  
  108. $gateway 
  109. ); 
  110. $payment = array( 
  111. 'amount' => $amount,  
  112. 'payment_information' => $information,  
  113. ); 
  114. $resp = self::taxamo()->createPayment( $tr_key, $payment ); 
  115. catch ( Exception $ex ) { 
  116. MS_Helper_Debug::log( 'Taxamo error: ' . $ex->getMessage() ); 
  117.  
  118. /** 
  119. * Returns an object containing all tax-related user profile details. 
  120. * @since 1.0.0 
  121. * @api 
  122. * @return object { 
  123. * Local Taxamo user profile settings 
  124. * $tax_country {@see fetch_country()} 
  125. * $detected_country {@see fetch_country()} 
  126. * $declared_country {@see fetch_country()} 
  127. * $vat_country {@see fetch_country()} 
  128. * $country_choice string [auto|declared|vat] 
  129. * $vat_number string 
  130. * $vat_valid bool 
  131. * $use_vat_number bool 
  132. * } 
  133. */ 
  134. static public function get_tax_profile() { 
  135. static $Profile = null; 
  136.  
  137. if ( null === $Profile ) { 
  138. $member = MS_Model_Member::get_current_member(); 
  139.  
  140. $Profile = (object) array(); 
  141. $Profile->detected_ip = lib3()->net->current_ip()->ip; 
  142. $Profile->detected_country = self::fetch_country( 'auto' ); 
  143. $Profile->declared_country = self::fetch_country( 'declared' ); 
  144. $Profile->vat_country = self::fetch_country( 'vat' ); 
  145. $Profile->card_country = self::fetch_country( 'card' ); 
  146. $Profile->country_choice = self::get_tax_profile_value( $member, 'tax_country_choice' ); 
  147.  
  148. $valid_choices = array( 'auto', 'vat', 'declared' ); 
  149. if ( ! in_array( $Profile->country_choice, $valid_choices ) ) { 
  150. $Profile->country_choice = 'auto'; 
  151.  
  152. $Profile->card_info = self::get_tax_profile_value( $member, 'tax_card_info' ); 
  153. $Profile->vat_number = self::get_tax_profile_value( $member, 'tax_vat_number' ); 
  154. $Profile->vat_valid = self::get_tax_profile_value( $member, 'tax_vat_valid' ); 
  155. $Profile->use_vat_number = 'vat' == $Profile->country_choice && $Profile->vat_valid; 
  156.  
  157. // Decide which country to use for tax calculation. Default is auto-detected. 
  158. $Profile->tax_country = $Profile->detected_country; 
  159.  
  160. switch ( $Profile->country_choice ) { 
  161. case 'declared': 
  162. $Profile->tax_country = $Profile->declared_country; 
  163. break; 
  164.  
  165. case 'vat': 
  166. if ( $Profile->vat_valid ) { 
  167. $Profile->tax_country = $Profile->vat_country; 
  168. } else { 
  169. $Profile->tax_country = $Profile->detected_country; 
  170. break; 
  171.  
  172. // For users without a VAT number the vat_valid field is missing. 
  173. if ( empty( $Profile->vat_country->vat_valid ) ) { 
  174. $Profile->vat_country->vat_valid = false; 
  175.  
  176. $Profile = apply_filters( 
  177. 'ms_addon_taxamo_get_tax_profile',  
  178. $Profile 
  179. ); 
  180.  
  181. return $Profile; 
  182.  
  183. /** 
  184. * Saves a single field to the user tax profile. 
  185. * All fields that are included in the get_tax_profile() response are 
  186. * valid field names. Exception: 'detected_country' cannot be changed. 
  187. * @since 1.0.0 
  188. * @api 
  189. * @param string $field The field key. 
  190. * @param mixed $value The new value. 
  191. * @return bool True on success. 
  192. */ 
  193. static public function set_tax_profile( $field, $value ) { 
  194. $member = MS_Model_Member::get_current_member(); 
  195.  
  196. $valid_keys = array( 
  197. 'country_choice',  
  198. 'declared_country',  
  199. 'card_country',  
  200. 'card_info',  
  201. 'vat_number',  
  202. ); 
  203.  
  204. $valid_keys = apply_filters( 
  205. 'ms_addon_taxamo_set_tax_profile_valid_keys',  
  206. $valid_keys,  
  207. $this 
  208. ); 
  209.  
  210. if ( ! in_array( $field, $valid_keys ) ) { 
  211. return false; 
  212.  
  213. // Special case: When VAT Number is changed also refresh VAT country. 
  214. if ( 'vat_number' == $field ) { 
  215. $valid_code = MS_Addon_Taxamo_Api::country_from_vat( $value ); 
  216. $value = str_replace( ' ', '', $value ); 
  217.  
  218. if ( $valid_code ) { 
  219. $vat_country = (object) array( 
  220. 'code' => $valid_code,  
  221. ); 
  222. self::set_tax_profile_value( $member, 'tax_vat_valid', true ); 
  223. } else { 
  224. $vat_country = (object) array( 
  225. 'code' => '',  
  226. ); 
  227. self::set_tax_profile_value( $member, 'tax_vat_valid', false ); 
  228.  
  229. self::set_tax_profile_value( $member, 'tax_vat_country', $vat_country ); 
  230.  
  231. $key = 'tax_' . $field; 
  232. if ( self::set_tax_profile_value( $member, $key, $value ) ) { 
  233. $member->save(); 
  234.  
  235. return true; 
  236.  
  237. /** 
  238. * Validates the given VAT number and returns the country code if the number 
  239. * is valid. Otherwise an empty string is returned. 
  240. * @since 1.0.0 
  241. * @param string $vat_number The VAT number to validate. 
  242. * @return string Country code. 
  243. */ 
  244. static public function country_from_vat( $vat_number ) { 
  245. if ( strlen( $vat_number ) < 5 ) { return ''; } 
  246.  
  247. $prefix = substr( $vat_number, 0, 2 ); 
  248. $codes = self::get_country_codes( 'vat' ); 
  249. if ( ! isset( $codes[$prefix] ) ) { return ''; } 
  250.  
  251. $country_code = $codes[$prefix]; 
  252. $resp = self::taxamo()->validateTaxNumber( null, $vat_number ); 
  253.  
  254. $result = $resp->billing_country_code; 
  255. return $result; 
  256.  
  257. /** 
  258. * Returns a list of all taxamo relevant EU countries. 
  259. * @since 1.0.0 
  260. * @api 
  261. * @param string $type [prefix|name|vat] 
  262. * name .. code => "name" 
  263. * prefix .. code => "prefix - name" 
  264. * vat .. vat-prefix => code 
  265. * @return array 
  266. */ 
  267. static public function get_country_codes( $type = 'prefix' ) { 
  268. if ( null === self::$Countries ) { 
  269. $country_names = MS_Gateway::get_country_codes(); // Country names in current language. 
  270.  
  271. $list = get_site_transient( 'ms_taxamo_countries' ); 
  272. $list = false; 
  273. if ( ! $list || ! is_array( $list ) ) { 
  274. $resp = self::taxamo()->getCountriesDict( 'true' ); 
  275. $list = array(); 
  276. foreach ( $resp->dictionary as $item ) { 
  277. $list[$item->code] = array( 
  278. 'name' => $item->name,  
  279. 'vat' => $item->tax_number_country_code,  
  280. ); 
  281. set_site_transient( 'ms_taxamo_countries', $list, WEEK_IN_SECONDS ); 
  282.  
  283. self::$Countries = array(); 
  284. self::$Countries_Prefix = array(); 
  285. self::$Countries_Vat = array(); 
  286. foreach ( $list as $code => $item ) { 
  287. if ( isset( $country_names[$code] ) ) { 
  288. $item['name'] = $country_names[$code]; 
  289.  
  290. self::$Countries[$code] = $item['name']; 
  291. self::$Countries_Prefix[$code] = $code . ' - ' . $item['name']; 
  292. self::$Countries_Vat[$item['vat']] = $code; 
  293. self::$Countries['XX'] = '- ' . __( 'Outside the EU', 'membership2' ) . ' -'; 
  294. self::$Countries_Prefix['XX'] = '- ' . __( 'Outside the EU', 'membership2' ) . ' -'; 
  295.  
  296. switch ( $type ) { 
  297. case 'prefix': 
  298. return self::$Countries_Prefix; 
  299.  
  300. case 'vat': 
  301. return self::$Countries_Vat; 
  302.  
  303. case 'name': 
  304. default: 
  305. return self::$Countries; 
  306.  
  307. // ------------------------------------------------------- PRIVATE FUNCTIONS 
  308.  
  309. /** 
  310. * Prepares a transaction object before it is sent to taxamo. 
  311. * @since 1.0.0 
  312. */ 
  313. static protected function prepare_transaction( $amount, $label, $tax_rate, $invoice_num, $name, $email, $currency, $ip_addr ) { 
  314. self::taxamo(); 
  315. $profile = self::get_tax_profile(); 
  316.  
  317. $amount = max( 0, floatval( $amount ) ); 
  318. $tax_rate = max( 0, floatval( $tax_rate ) ); 
  319.  
  320. $tax_number = null; 
  321. $amount_key = 'total_amount'; 
  322. if ( $profile->use_vat_number ) { 
  323. $tax_number = $profile->vat_number; 
  324. $tax_rate = 0; 
  325. if ( ! $tax_rate ) { 
  326. $amount_key = 'amount'; 
  327.  
  328. $tr_line = array( 
  329. 'product_type' => 'e-service',  
  330. $amount_key => $amount,  
  331. 'tax_rate' => $tax_rate,  
  332. 'quantity' => 1,  
  333. 'custom_id' => (string) $invoice_num,  
  334. 'description' => (string) $label,  
  335. ); 
  336.  
  337. // Assemble the available evidence. 
  338. $tr_evidence = array( 
  339. 'by_ip' => array( 
  340. 'resolved_country_code' => $profile->detected_country->code,  
  341. 'evidence_value' => $profile->detected_ip,  
  342. ),  
  343. ); 
  344.  
  345. if ( 'XX' != $profile->declared_country->code ) { 
  346. $tr_evidence['self_declaration'] = array( 
  347. 'resolved_country_code' => $profile->declared_country->code,  
  348. 'evidence_value' => 'Self declared country',  
  349. ); 
  350.  
  351. if ( $profile->use_vat_number ) { 
  352. $tr_evidence['by_tax_number'] = array( 
  353. 'resolved_country_code' => $profile->vat_country->code,  
  354. 'evidence_value' => $profile->vat_number,  
  355. ); 
  356.  
  357. $used_code = $profile->tax_country->code; 
  358. switch ( $profile->country_choice ) { 
  359. case 'declared': 
  360. $tr_evidence['forced'] = array( 
  361. 'resolved_country_code' => $used_code,  
  362. 'evidence_value' => 'Self declared country',  
  363. ); 
  364. break; 
  365.  
  366. case 'vat': 
  367. case 'auto': 
  368. default: 
  369. $tr_evidence['by_billing'] = array( 
  370. 'resolved_country_code' => $used_code,  
  371. 'evidence_value' => $profile->tax_country->code,  
  372. ); 
  373. break; 
  374.  
  375. foreach ( $tr_evidence as $key => $item ) { 
  376. $tr_evidence[$key]['used'] = ($used_code == $item['resolved_country_code']); 
  377.  
  378. $force_country = null; 
  379. if ( 'declared' == $profile->country_choice ) { 
  380. $force_country = $profile->tax_country->code; 
  381.  
  382. $transaction = array( 
  383. 'currency_code' => $currency,  
  384. 'billing_country_code' => $profile->tax_country->code,  
  385. 'tax_country_code' => $profile->tax_country->code,  
  386. 'force_country_code' => $profile->tax_country->code,  
  387. 'buyer_ip' => $ip_addr,  
  388. 'custom_id' => (string) $invoice_num,  
  389. 'buyer_name' => $name,  
  390. 'buyer_email' => $email,  
  391. 'buyer_tax_number' => $tax_number,  
  392. 'tax_deducted' => $profile->use_vat_number,  
  393. 'transaction_lines' => array( $tr_line ),  
  394. 'evidence' => $tr_evidence,  
  395. ); 
  396.  
  397. return $transaction; 
  398.  
  399. /** 
  400. * Determines the users country based on his IP address. 
  401. * @since 1.0.0 
  402. * @internal 
  403. * @param string $mode [declared|vat|card|auto] Either the country from user 
  404. * settings or the auto-detected country. 
  405. */ 
  406. static protected function fetch_country( $mode = 'declared' ) { 
  407. $member = MS_Model_Member::get_current_member(); 
  408. $country = false; 
  409. $store_it = false; 
  410.  
  411. $non_auto_countries = array( 'declared', 'vat', 'card' ); 
  412. if ( ! in_array( $mode, $non_auto_countries ) ) { $mode = 'auto'; } 
  413.  
  414. $auto_detect = ('auto' == $mode); 
  415. $key = 'tax_' . $mode . '_country'; 
  416.  
  417. // If no country is stored use the API to determine it. 
  418. if ( $auto_detect ) { 
  419. try { 
  420. $ip_info = lib3()->net->current_ip(); 
  421. $data = (object)(array) self::taxamo()->locateGivenIP( $ip_info->ip ); 
  422. $country = (object) array( 
  423. 'code' => $data->country_code,  
  424. ); 
  425.  
  426. // Store result in Session, not in DB. 
  427. $store_it = true; 
  428. $member = null; 
  429. catch ( Exception $ex ) { 
  430. MS_Helper_Debug::log( 'Taxamo error: ' . $ex->getMessage() ); 
  431. } else { 
  432. // Try to get the stored country from user-meta or session (for guest) 
  433. $country = self::get_tax_profile_value( $member, $key ); 
  434.  
  435. // API did not return a valid resonse, use a dummy value. 
  436. if ( ! $country ) { 
  437. $country = (object) array( 
  438. 'code' => '',  
  439. ); 
  440.  
  441. // Store result in user-deta or session. 
  442. if ( $store_it && self::set_tax_profile_value( $member, $key, $country ) ) { 
  443. $member->save(); 
  444.  
  445. $country_names = self::get_country_codes( 'name' ); 
  446.  
  447. if ( $country->code && isset( $country_names[ $country->code ] ) ) { 
  448. $country->name = $country_names[ $country->code ]; 
  449. } else { 
  450. $country->name = $country_names['XX']; 
  451.  
  452. return $country; 
  453.  
  454. /** 
  455. * Internal helper function that returns a user profile value either from 
  456. * DB or from the session (depending if the user is logged in or not). 
  457. * @since 1.0.0 
  458. * @internal 
  459. * @param MS_Model_Member $member 
  460. * @param string $key The field key. 
  461. * @return mixed The field value. 
  462. */ 
  463. static protected function get_tax_profile_value( $member, $key ) { 
  464. if ( is_object( $member ) && $member->is_valid() ) { 
  465. $result = $member->get_custom_data( $key ); 
  466. } else { 
  467. $result = lib3()->session->get( 'ms_' . $key ); 
  468. if ( is_array( $result ) && count( $result ) ) { 
  469. $result = $result[0]; 
  470.  
  471. return $result; 
  472.  
  473. /** 
  474. * Internal helper function that saves a user profile value either to DB or 
  475. * to the session (depending if the user is logged in or not). 
  476. * @since 1.0.0 
  477. * @internal 
  478. * @param MS_Model_Member $member 
  479. * @param string $key The field key. 
  480. * @param mixed $value The value to save 
  481. * @return bool True means that the value was set in $member, otherwise it 
  482. * was set in the session. 
  483. */ 
  484. static protected function set_tax_profile_value( $member, $key, $value ) { 
  485. if ( is_object( $member ) && $member->is_valid() ) { 
  486. $member->set_custom_data( $key, $value ); 
  487. $need_save = true; 
  488. } else { 
  489. lib3()->session->get_clear( 'ms_' . $key ); 
  490. lib3()->session->add( 'ms_' . $key, $value ); 
  491. $need_save = false; 
  492.  
  493. return $need_save; 
  494.  
  495. /** 
  496. * Returns the Taxamo REST API object. 
  497. * Important: All calls to `taxamo()->` functions must be wrapped in 
  498. * try..catch because an invalid API token will result in a fatal error. 
  499. * @since 1.0.0 
  500. * @return Taxamo 
  501. */ 
  502. static protected function taxamo() { 
  503. static $Taxamo = null; 
  504.  
  505. if ( null === $Taxamo ) { 
  506. if ( ! class_exists( 'Taxamo' ) ) { 
  507. require_once MS_Plugin::instance()->dir . '/lib/taxamo/Taxamo.php'; 
  508.  
  509. // Initialize the Taxamo API connection 
  510. $connection = new APIClient( 
  511. MS_Addon_Taxamo::model()->get( 'private_key' ),  
  512. 'https://api.taxamo.com' 
  513. ); 
  514.  
  515. // Initialize the Taxamo REST API wrapper. 
  516. $Taxamo = new Taxamo( $connection ); 
  517.  
  518. // Initialize the API object. 
  519. self::init(); 
  520.  
  521. return $Taxamo; 
  522.  
  523. /** 
  524. * Initializes the taxamo API object. 
  525. * @since 1.0.0 
  526. */ 
  527. static protected function init() { 
  528. self::taxamo(); 
  529. self::fetch_country( 'auto' );