Shipping.php 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228
  1. <?php
  2. /**
  3. *
  4. * PHP Pro Bid $Id$ 94C7KNN/BdTtx4IqxR5vsTzCIGduH+ERfjL0ZUax41U=
  5. *
  6. * @link http://www.phpprobid.com
  7. * @copyright Copyright (c) 2017 Online Ventures Software & CodeCube SRL
  8. * @license http://www.phpprobid.com/license Commercial License
  9. *
  10. * @version 7.9 [rev.7.9.02]
  11. */
  12. /**
  13. * shipping model
  14. *
  15. * the model will require a user object in order to be initialized, and to be able
  16. * to calculate postage, it will require either a listing owned by the user, or
  17. * a sale made by the user
  18. *
  19. * Important: the calculation will always take the location of the item(s) as the source location,
  20. * and not the location of the owner
  21. */
  22. namespace Ppb\Model;
  23. use Cube\Controller\Front,
  24. Cube\Translate,
  25. Cube\Translate\Adapter\AbstractAdapter as TranslateAdapter,
  26. Ppb\Db\Table\Row\User as UserModel,
  27. Ppb\Db\Table\Row\Listing as ListingModel,
  28. Ppb\Form\Element\FlatRatesLocationGroups,
  29. Ppb\Service;
  30. class Shipping
  31. {
  32. /**
  33. * weight uom
  34. */
  35. const UOM_LBS = 'lbs';
  36. const UOM_KG = 'kg';
  37. /**
  38. * dimensions uom
  39. */
  40. const UOM_INCHES = 'inches';
  41. const UOM_CM = 'cm';
  42. /**
  43. * dimension coordinates
  44. */
  45. const DIMENSION_LENGTH = 'length';
  46. const DIMENSION_WIDTH = 'width';
  47. const DIMENSION_HEIGHT = 'height';
  48. /**
  49. * min weight value
  50. */
  51. const MIN_WEIGHT = 0.1;
  52. /**
  53. * pickup options
  54. */
  55. const NO_PICKUPS = 'no_pickups';
  56. const CAN_PICKUP = 'can_pickup';
  57. const MUST_PICKUP = 'must_pickup';
  58. /**
  59. * key and value for drop downs for the pick-up option
  60. */
  61. const KEY_PICK_UP = -1;
  62. const VALUE_PICK_UP = 'Pick-up';
  63. /**
  64. * seller postage setup page fields
  65. */
  66. const SETUP_FREE_POSTAGE = 'free_postage';
  67. const SETUP_FREE_POSTAGE_AMOUNT = 'free_postage_amount';
  68. const SETUP_POSTAGE_TYPE = 'postage_type';
  69. const SETUP_POSTAGE_FLAT_FIRST = 'postage_flat_first';
  70. const SETUP_POSTAGE_FLAT_ADDL = 'postage_flat_addl';
  71. const SETUP_SHIPPING_CARRIERS = 'shipping_carriers';
  72. const SETUP_WEIGHT_UOM = 'weight_uom';
  73. const SETUP_SHIPPING_LOCATIONS = 'shipping_locations';
  74. const SETUP_LOCATION_GROUPS = 'location_groups';
  75. const SETUP_DIMENSIONS_UOM = 'dimensions_uom';
  76. /**
  77. * postage calculation types
  78. */
  79. const POSTAGE_TYPE_ITEM = 'item';
  80. const POSTAGE_TYPE_FLAT = 'flat';
  81. const POSTAGE_TYPE_CARRIERS = 'carriers';
  82. /**
  83. * shipping locations options
  84. */
  85. const POSTAGE_LOCATION_DOMESTIC = 'domestic';
  86. const POSTAGE_LOCATION_WORLDWIDE = 'worldwide';
  87. const POSTAGE_LOCATION_CUSTOM = 'custom';
  88. /**
  89. * listing setup shipping related fields
  90. */
  91. const FLD_ACCEPT_RETURNS = 'accept_returns';
  92. const FLD_RETURNS_POLICY = 'returns_policy';
  93. const FLD_PICKUP_OPTIONS = 'pickup_options';
  94. const FLD_SHIPPING_DETAILS = 'shipping_details';
  95. const FLD_POSTAGE = 'postage';
  96. const FLD_ITEM_WEIGHT = 'item_weight';
  97. const FLD_DIMENSIONS = 'dimensions';
  98. const FLD_INSURANCE = 'insurance';
  99. /**
  100. * listings data array keys
  101. */
  102. const DATA_LISTING = 'listing';
  103. const DATA_QUANTITY = 'quantity';
  104. /**
  105. * standard shipping method desc.
  106. */
  107. const MSG_STANDARD_SHIPPING = 'Standard Shipping';
  108. /**
  109. *
  110. * weight uom list
  111. *
  112. * @var array
  113. */
  114. public static $weightUom = array(
  115. self::UOM_LBS => 'Lbs',
  116. self::UOM_KG => 'Kg'
  117. );
  118. /**
  119. *
  120. * dimensions uom list
  121. *
  122. * @var array
  123. */
  124. public static $dimensionsUom = array(
  125. self::UOM_INCHES => 'inches',
  126. self::UOM_CM => 'cm',
  127. );
  128. public static $dimensionsCoordinates = array(
  129. self::DIMENSION_LENGTH => 'Length',
  130. self::DIMENSION_WIDTH => 'Width',
  131. self::DIMENSION_HEIGHT => 'Height',
  132. );
  133. /**
  134. *
  135. * pick-up options array
  136. *
  137. * @var array
  138. */
  139. public static $pickupOptions = array(
  140. self::NO_PICKUPS => 'No pick-ups',
  141. self::CAN_PICKUP => 'Buyer can pick-up',
  142. self::MUST_PICKUP => 'Buyer must pick-up',
  143. );
  144. /**
  145. *
  146. * seller postage setup page fields
  147. *
  148. * @var array
  149. */
  150. public static $postageSetupFields = array(
  151. self::SETUP_FREE_POSTAGE => 'Offer Free Postage',
  152. self::SETUP_FREE_POSTAGE_AMOUNT => 'If amount exceeds',
  153. self::SETUP_POSTAGE_TYPE => 'Postage Calculation Type',
  154. self::SETUP_POSTAGE_FLAT_FIRST => 'First Item',
  155. self::SETUP_POSTAGE_FLAT_ADDL => 'Additional Items',
  156. self::SETUP_SHIPPING_CARRIERS => 'Select Shipping Carriers',
  157. self::SETUP_WEIGHT_UOM => 'Weight UOM',
  158. self::SETUP_SHIPPING_LOCATIONS => 'Shipping Locations',
  159. self::SETUP_LOCATION_GROUPS => 'Location Groups',
  160. self::SETUP_DIMENSIONS_UOM => 'Dimensions UOM',
  161. );
  162. /**
  163. *
  164. * listing postage related fields array
  165. *
  166. * @var array
  167. */
  168. public static $postageFields = array(
  169. self::FLD_ACCEPT_RETURNS => 'Accept Returns',
  170. self::FLD_RETURNS_POLICY => 'Return Policy Details',
  171. self::FLD_PICKUP_OPTIONS => 'Pick-ups',
  172. self::FLD_POSTAGE => 'Postage',
  173. self::FLD_ITEM_WEIGHT => 'Item Weight',
  174. self::FLD_DIMENSIONS => 'Dimensions',
  175. self::FLD_INSURANCE => 'Insurance',
  176. self::FLD_SHIPPING_DETAILS => 'Shipping Instructions',
  177. );
  178. /**
  179. *
  180. * default dimensions - standard rectangular box
  181. *
  182. * @var array
  183. */
  184. protected static $_defaultDimensions = array(
  185. self::DIMENSION_LENGTH => 12,
  186. self::DIMENSION_WIDTH => 9,
  187. self::DIMENSION_HEIGHT => 6,
  188. );
  189. /**
  190. *
  191. * seller user model
  192. *
  193. * @var \Ppb\Db\Table\Row\User
  194. */
  195. protected $_user;
  196. /**
  197. *
  198. * the postage settings that the seller has set up
  199. *
  200. * @var array
  201. */
  202. protected $_postageSettings = array();
  203. /**
  204. *
  205. * array of data of listing object - quantity combinations for which the postage is to be calculated
  206. *
  207. * @var array
  208. */
  209. protected $_data = array();
  210. /**
  211. *
  212. * locations table service
  213. *
  214. * @var \Ppb\Service\Table\Relational\Locations
  215. */
  216. protected $_locations;
  217. /**
  218. *
  219. * currencies table service
  220. *
  221. * @var \Ppb\Service\Table\Currencies
  222. */
  223. protected $_currencies;
  224. /**
  225. *
  226. * translate adapter
  227. *
  228. * @var \Cube\Translate\Adapter\AbstractAdapter
  229. */
  230. protected $_translate;
  231. /**
  232. *
  233. * the location id of the destination (from the locations table)
  234. *
  235. * @var int
  236. */
  237. protected $_locationId;
  238. /**
  239. *
  240. * the zip/post code of the destination
  241. *
  242. * @var string
  243. */
  244. protected $_postCode;
  245. /**
  246. *
  247. * the name of the shipping method that will be used to send the items.
  248. * (valid for shipping carriers too)
  249. *
  250. * @var string
  251. */
  252. protected $_postageMethod;
  253. /**
  254. *
  255. * this flag is set from the addData() method
  256. *
  257. * @var bool
  258. */
  259. protected $_canPickUp = false;
  260. /**
  261. *
  262. * the currency of the listings inserted in the model
  263. *
  264. * @var string
  265. */
  266. protected $_currency;
  267. /**
  268. *
  269. * class constructor
  270. *
  271. * @param \Ppb\Db\Table\Row\User $user
  272. */
  273. public function __construct(UserModel $user)
  274. {
  275. $this->_user = $user;
  276. $this->setPostageSettings($user['postage_settings']);
  277. }
  278. /**
  279. *
  280. * get pick-up option description
  281. *
  282. * @param string $key pick-up option key
  283. *
  284. * @return string|null
  285. */
  286. public static function getPickupOptions($key)
  287. {
  288. if (isset(self::$pickupOptions[$key])) {
  289. return self::$pickupOptions[$key];
  290. }
  291. return null;
  292. }
  293. /**
  294. *
  295. * get postage settings
  296. *
  297. * @param string|null $key
  298. *
  299. * @return mixed
  300. */
  301. public function getPostageSettings($key = null)
  302. {
  303. if ($key !== null) {
  304. return (isset($this->_postageSettings[$key])) ? $this->_postageSettings[$key] : null;
  305. }
  306. return $this->_postageSettings;
  307. }
  308. /**
  309. *
  310. * set postage settings (accepted an array or a serialized string)
  311. *
  312. * @param array|string $postageSettings
  313. *
  314. * @return \Ppb\Model\Shipping
  315. */
  316. public function setPostageSettings($postageSettings)
  317. {
  318. if (!is_array($postageSettings)) {
  319. $postageSettings = \Ppb\Utility::unserialize($postageSettings, array());
  320. }
  321. $this->_postageSettings = $postageSettings;
  322. return $this;
  323. }
  324. /**
  325. *
  326. * get currencies table service
  327. *
  328. * @return \Ppb\Service\Table\Currencies
  329. */
  330. public function getCurrencies()
  331. {
  332. if (!$this->_currencies instanceof Service\Table\Currencies) {
  333. $this->setCurrencies(
  334. new Service\Table\Currencies());
  335. }
  336. return $this->_currencies;
  337. }
  338. /**
  339. *
  340. * set currencies service
  341. *
  342. * @param \Ppb\Service\Table\Currencies $currencies
  343. *
  344. * @return $this
  345. */
  346. public function setCurrencies(Service\Table\Currencies $currencies)
  347. {
  348. $this->_currencies = $currencies;
  349. return $this;
  350. }
  351. /**
  352. *
  353. * set translate adapter
  354. *
  355. * @param \Cube\Translate\Adapter\AbstractAdapter $translate
  356. *
  357. * @return $this
  358. */
  359. public function setTranslate(TranslateAdapter $translate)
  360. {
  361. $this->_translate = $translate;
  362. return $this;
  363. }
  364. /**
  365. *
  366. * get translate adapter
  367. *
  368. * @return \Cube\Translate\Adapter\AbstractAdapter
  369. */
  370. public function getTranslate()
  371. {
  372. if (!$this->_translate instanceof TranslateAdapter) {
  373. $translate = Front::getInstance()->getBootstrap()->getResource('translate');
  374. if ($translate instanceof Translate) {
  375. $this->setTranslate(
  376. $translate->getAdapter());
  377. }
  378. }
  379. return $this->_translate;
  380. }
  381. /**
  382. *
  383. * get data array
  384. *
  385. * @return array
  386. */
  387. public function getData()
  388. {
  389. return $this->_data;
  390. }
  391. /**
  392. *
  393. * set data array
  394. *
  395. * @param array $data
  396. *
  397. * @return \Ppb\Model\Shipping
  398. */
  399. public function setData(array $data)
  400. {
  401. $this->_data = array();
  402. $this->_canPickUp = false;
  403. foreach ($data as $row) {
  404. $this->addData($row[self::DATA_LISTING], $row[self::DATA_QUANTITY]);
  405. }
  406. return $this;
  407. }
  408. /**
  409. *
  410. * add a data row in the array
  411. * all items that are added must have the same location
  412. * ** and currency (for now)
  413. * ** and pickup options as well
  414. *
  415. * @param \Ppb\Db\Table\Row\Listing $listing
  416. * @param int $quantity
  417. *
  418. * @throws \RuntimeException
  419. * @return \Ppb\Model\Shipping
  420. */
  421. public function addData(ListingModel $listing, $quantity = 1)
  422. {
  423. foreach ($this->_data as $data) {
  424. if (
  425. $data[self::DATA_LISTING]['country'] != $listing['country'] ||
  426. $data[self::DATA_LISTING]['state'] != $listing['state'] ||
  427. $data[self::DATA_LISTING]['address'] != $listing['address'] ||
  428. $data[self::DATA_LISTING]['currency'] != $listing['currency'] ||
  429. $data[self::DATA_LISTING][self::FLD_PICKUP_OPTIONS] != $listing[self::FLD_PICKUP_OPTIONS]
  430. ) {
  431. $translate = $this->getTranslate();
  432. throw new \RuntimeException($translate->_("All the listings added in the shipping model must have the same location "
  433. . "(country, state, address), use the same currency and have the same pick-up options selected."));
  434. }
  435. }
  436. if ($listing[self::FLD_PICKUP_OPTIONS] !== self::NO_PICKUPS) {
  437. $this->_canPickUp = true;
  438. }
  439. $this->_data[] = array(
  440. self::DATA_LISTING => $listing,
  441. self::DATA_QUANTITY => ($quantity > 0) ? (int)$quantity : 1
  442. );
  443. $this->_currency = $listing['currency'];
  444. return $this;
  445. }
  446. /**
  447. *
  448. * get locations service
  449. *
  450. * @return \Ppb\Service\Table\Relational\Locations
  451. */
  452. public function getLocations()
  453. {
  454. if (!$this->_locations instanceof Service\Table\Relational\Locations) {
  455. $this->setLocations(
  456. new Service\Table\Relational\Locations());
  457. }
  458. return $this->_locations;
  459. }
  460. /**
  461. *
  462. * set locations service
  463. *
  464. * @param \Ppb\Service\Table\Relational\Locations $locations
  465. *
  466. * @return \Ppb\Model\Shipping
  467. */
  468. public function setLocations(Service\Table\Relational\Locations $locations)
  469. {
  470. $this->_locations = $locations;
  471. return $this;
  472. }
  473. /**
  474. *
  475. * get destination location id
  476. *
  477. * @return int
  478. */
  479. public function getLocationId()
  480. {
  481. return $this->_locationId;
  482. }
  483. /**
  484. *
  485. * set destination location id
  486. *
  487. * @param int $locationId
  488. *
  489. * @return \Ppb\Model\Shipping
  490. */
  491. public function setLocationId($locationId)
  492. {
  493. $this->_locationId = (int)$locationId;
  494. return $this;
  495. }
  496. /**
  497. *
  498. * get destination location post code
  499. *
  500. * @return string
  501. */
  502. public function getPostCode()
  503. {
  504. return $this->_postCode;
  505. }
  506. /**
  507. *
  508. * set destination location post code
  509. *
  510. * @param string $postCode
  511. *
  512. * @return \Ppb\Model\Shipping
  513. */
  514. public function setPostCode($postCode)
  515. {
  516. $this->_postCode = (string)$postCode;
  517. return $this;
  518. }
  519. public function getPostageMethod()
  520. {
  521. $translate = $this->getTranslate();
  522. if (null !== $translate) {
  523. return $translate->_($this->_postageMethod);
  524. }
  525. return $this->_postageMethod;
  526. }
  527. public function setPostageMethod($postageMethod)
  528. {
  529. $this->_postageMethod = (string)$postageMethod;
  530. return $this;
  531. }
  532. /**
  533. *
  534. * get weight uom set by the user
  535. *
  536. * @param bool $sentence
  537. *
  538. * @return string
  539. */
  540. public function getWeightUom($sentence = true)
  541. {
  542. $weightUom = $this->getPostageSettings(self::SETUP_WEIGHT_UOM);
  543. if ($weightUom !== null) {
  544. if ($sentence !== true) {
  545. return $weightUom;
  546. }
  547. $translate = $this->getTranslate();
  548. $sentence = self::$weightUom[$weightUom];
  549. if (null !== $translate) {
  550. return $translate->_($sentence);
  551. }
  552. return $sentence;
  553. }
  554. return null;
  555. }
  556. /**
  557. *
  558. * get dimensions uom set by the user
  559. *
  560. * @param bool $sentence
  561. *
  562. * @return string
  563. */
  564. public function getDimensionsUom($sentence = true)
  565. {
  566. $dimensionsUom = $this->getPostageSettings(self::SETUP_DIMENSIONS_UOM);
  567. if ($dimensionsUom !== null) {
  568. if ($sentence !== true) {
  569. return $dimensionsUom;
  570. }
  571. $translate = $this->getTranslate();
  572. $sentence = self::$dimensionsUom[$dimensionsUom];
  573. if (null !== $translate) {
  574. return $translate->_($sentence);
  575. }
  576. return $sentence;
  577. }
  578. return null;
  579. }
  580. /**
  581. *
  582. * get location groups array
  583. *
  584. * @return array|bool will return false if there are no locations or if the location based calculation is not set to custom
  585. */
  586. public function getLocationGroups()
  587. {
  588. if ($this->getPostageSettings(self::SETUP_SHIPPING_LOCATIONS) == self::POSTAGE_LOCATION_CUSTOM) {
  589. return $this->_postageSettings[self::SETUP_LOCATION_GROUPS][FlatRatesLocationGroups::FIELD_NAME];
  590. }
  591. return false;
  592. }
  593. /**
  594. *
  595. * get the postage calculation type set by the user
  596. *
  597. * @return string
  598. */
  599. public function getPostageType()
  600. {
  601. return $this->getPostageSettings(self::SETUP_POSTAGE_TYPE);
  602. }
  603. /**
  604. *
  605. * return the postage options available based on a set of input values
  606. *
  607. * required values:
  608. * - one or more listings from the same seller
  609. * - a destination location (id from the locations table)
  610. * - a destination zip/post code
  611. *
  612. * outputs:
  613. * - an array of all available postage methods
  614. * OR
  615. * - a runtime error if any error has occurred
  616. *
  617. * [OBSOLETE]
  618. * currencies output:
  619. * - item based : item currency
  620. * - flat rates : site's default currency
  621. * - carriers : carrier currency
  622. *
  623. * [ACTUAL]
  624. * currency output will always be in the item's currency
  625. *
  626. * @return array
  627. * @throws \RuntimeException
  628. */
  629. public function calculatePostage()
  630. {
  631. $result = array();
  632. $translate = $this->getTranslate();
  633. if (!$this->_locationId) {
  634. throw new \RuntimeException($translate->_("No destination location has been set."));
  635. }
  636. else if (!$this->_postCode) {
  637. throw new \RuntimeException($translate->_("No destination zip/post code has been set."));
  638. }
  639. else if (empty($this->_data)) {
  640. throw new \RuntimeException($translate->_("At least one item needs to be set in order to calculate the postage."));
  641. }
  642. $shippableLocations = $this->getShippableLocations();
  643. $shippingLocations = $this->getPostageSettings(self::SETUP_SHIPPING_LOCATIONS);
  644. if (in_array($this->_locationId, $shippableLocations)) {
  645. if ($this->_isFreePostage()) {
  646. $result[] = array(
  647. 'currency' => $this->_currency,
  648. 'price' => 0,
  649. 'carrier' => $translate->_('N/A'),
  650. 'method' => self::MSG_STANDARD_SHIPPING,
  651. );
  652. }
  653. else {
  654. switch ($this->getPostageType()) {
  655. case self::POSTAGE_TYPE_ITEM:
  656. $postageMethods = null;
  657. $postageData = array();
  658. foreach ($this->_data as $row) {
  659. $fldPostage = $row[self::DATA_LISTING][self::FLD_POSTAGE];
  660. if (isset($fldPostage['locations'])) {
  661. if (in_array($shippingLocations,
  662. array(self::POSTAGE_LOCATION_DOMESTIC, self::POSTAGE_LOCATION_WORLDWIDE))
  663. ) {
  664. // unset locations field if we have domestic or worldwide postage
  665. unset($fldPostage['locations']);
  666. }
  667. else {
  668. // set locations as countries
  669. $fldPostage['locations'] = array_filter($fldPostage['locations']);
  670. foreach ($fldPostage['locations'] as $key => $loc) {
  671. foreach ($loc as $k => $v) {
  672. $fldPostage['locations'][$key][$k] = $this->_postageSettings[self::SETUP_LOCATION_GROUPS][FlatRatesLocationGroups::FIELD_LOCATIONS][$v];
  673. }
  674. $fldPostage['locations'][$key] = call_user_func_array('array_merge',
  675. $fldPostage['locations'][$key]);
  676. }
  677. }
  678. }
  679. // modify the price to be calculated on the quantity requested
  680. if (!empty($fldPostage['price'])) {
  681. foreach ($fldPostage['price'] as $key => $value) {
  682. $fldPostage['price'][$key] = $fldPostage['price'][$key] * $row[self::DATA_QUANTITY];
  683. }
  684. // flip postage array
  685. $postageData[] = $this->_flipArray($fldPostage);
  686. }
  687. // unset price field to compare data
  688. if (isset($fldPostage['price'])) {
  689. unset($fldPostage['price']);
  690. }
  691. // the locations and postage methods must be the same for the postage to be calculated (for multiple items)
  692. if ($postageMethods !== null && $postageMethods != $fldPostage) {
  693. throw new \RuntimeException($translate->_("All listings included must have the same postage methods."));
  694. }
  695. $postageMethods = $fldPostage;
  696. }
  697. $listing = $this->_data[0][self::DATA_LISTING];
  698. switch ($shippingLocations) {
  699. case self::POSTAGE_LOCATION_DOMESTIC:
  700. case self::POSTAGE_LOCATION_WORLDWIDE:
  701. if (!empty($postageMethods['method'])) {
  702. foreach ($postageMethods['method'] as $key => $method) {
  703. if (!empty($method)) {
  704. foreach ($postageData as $data) {
  705. $price = (!empty($result[$key]['price'])) ? $result[$key]['price'] : 0;
  706. $result[$key] = array(
  707. 'currency' => $listing['currency'],
  708. 'price' => ($price + $data[$key]['price']),
  709. 'method' => $method,
  710. );
  711. }
  712. }
  713. }
  714. }
  715. break;
  716. case self::POSTAGE_LOCATION_CUSTOM:
  717. foreach ($postageMethods['method'] as $key => $method) {
  718. if (!empty($method)) {
  719. foreach ($postageData as $data) {
  720. if (in_array($this->_locationId, (array)$data[$key]['locations'])) {
  721. $price = (!empty($result[$key]['price'])) ? $result[$key]['price'] : 0;
  722. $result[$key] = array(
  723. 'currency' => $listing['currency'],
  724. 'price' => ($price + $data[$key]['price']),
  725. 'method' => $method,
  726. );
  727. }
  728. }
  729. }
  730. }
  731. break;
  732. }
  733. break;
  734. case self::POSTAGE_TYPE_FLAT:
  735. $quantity = 0;
  736. foreach ($this->_data as $row) {
  737. $quantity += $row[self::DATA_QUANTITY];
  738. }
  739. $settings = Front::getInstance()->getBootstrap()->getResource('settings');
  740. switch ($shippingLocations) {
  741. case self::POSTAGE_LOCATION_DOMESTIC:
  742. case self::POSTAGE_LOCATION_WORLDWIDE:
  743. $price = $this->getPostageSettings(self::SETUP_POSTAGE_FLAT_FIRST) +
  744. ($this->getPostageSettings(self::SETUP_POSTAGE_FLAT_ADDL) * ($quantity - 1));
  745. $result[] = array(
  746. 'currency' => $settings['currency'],
  747. 'price' => doubleval($price),
  748. 'method' => self::MSG_STANDARD_SHIPPING,
  749. );
  750. break;
  751. case self::POSTAGE_LOCATION_CUSTOM:
  752. $locationGroups = $this->getPostageSettings(self::SETUP_LOCATION_GROUPS);
  753. foreach ($locationGroups[FlatRatesLocationGroups::FIELD_LOCATIONS] as $key => $val) {
  754. if (in_array($this->_locationId, array_values((array)$val))) {
  755. $price = $locationGroups[FlatRatesLocationGroups::FIELD_FIRST][$key] +
  756. ($locationGroups[FlatRatesLocationGroups::FIELD_ADDL][$key] * ($quantity - 1));
  757. $result[] = array(
  758. 'currency' => $settings['currency'],
  759. 'price' => doubleval($price),
  760. 'method' => $locationGroups[FlatRatesLocationGroups::FIELD_NAME][$key],
  761. );
  762. }
  763. }
  764. break;
  765. }
  766. break;
  767. case self::POSTAGE_TYPE_CARRIERS:
  768. $shippingCarriers = (array)$this->getPostageSettings(self::SETUP_SHIPPING_CARRIERS);
  769. foreach ($shippingCarriers as $rowCarrier) {
  770. $className = '\\Ppb\\Model\\Shipping\\Carrier\\' . $rowCarrier;
  771. if (class_exists($className)) {
  772. /** @var \Ppb\Model\Shipping\Carrier\AbstractCarrier $carrier */
  773. $carrier = new $className();
  774. if (!empty($this->_data[0][self::DATA_LISTING]['country'])) {
  775. $sourceCountry = $this->getLocations()->findBy('id',
  776. $this->_data[0][self::DATA_LISTING]['country']);
  777. $carrier->setSourceCountry($sourceCountry['iso_code']);
  778. }
  779. $destCountry = $this->getLocations()->findBy('id', $this->getLocationId());
  780. $carrier->setSourceZip($this->_data[0][self::DATA_LISTING]['address'])
  781. ->setDestCountry($destCountry['iso_code'])
  782. ->setDestZip($this->_postCode)
  783. ->setWeightUom(
  784. $this->getPostageSettings(self::SETUP_WEIGHT_UOM))
  785. ->setWeight(
  786. $this->_calculateTotalWeight())
  787. ->setDimensionsUom(
  788. $this->getPostageSettings(self::SETUP_DIMENSIONS_UOM))
  789. ->setDimensions(
  790. $this->_calculateTotalDimensions());
  791. if (($carrierResult = $carrier->getPrice()) !== false) {
  792. foreach ($carrierResult as $val) {
  793. $result[] = array(
  794. 'currency' => $val['currency'],
  795. 'price' => $val['price'],
  796. 'method' => $translate->_($rowCarrier) . ' ' . $val['name'],
  797. 'code' => $val['code'],
  798. 'carrier' => $rowCarrier,
  799. 'class' => $className,
  800. );
  801. }
  802. }
  803. }
  804. }
  805. break;
  806. }
  807. }
  808. }
  809. if ($this->_canPickUp === true) {
  810. $result[self::KEY_PICK_UP] = array(
  811. 'currency' => $this->_currency,
  812. 'price' => 0,
  813. 'method' => $translate->_(self::VALUE_PICK_UP),
  814. 'carrier' => '-',
  815. );
  816. }
  817. if (empty($result)) {
  818. throw new \RuntimeException($translate->_("The item(s) cannot be shipped to your selected destination."));
  819. }
  820. // convert currencies
  821. $result = $this->_convertCurrency($result);
  822. return $result;
  823. }
  824. /**
  825. *
  826. * calculate insurance amount for the items in a sale
  827. *
  828. * @return float
  829. */
  830. public function calculateInsurance()
  831. {
  832. $insuranceAmount = 0;
  833. foreach ($this->_data as $row) {
  834. $insuranceAmount += $row[self::DATA_LISTING][self::FLD_INSURANCE];
  835. }
  836. return $insuranceAmount;
  837. }
  838. /**
  839. *
  840. * get all locations the item(s) can be posted to
  841. *
  842. * @param bool $dropDown
  843. *
  844. * @return array
  845. */
  846. public function getShippableLocations($dropDown = false)
  847. {
  848. $regions = false;
  849. if ($this->getPostageType() == self::POSTAGE_TYPE_ITEM) {
  850. foreach ($this->_data as $row) {
  851. $listingRegions = array();
  852. $listingPostageRegions = isset($row[self::DATA_LISTING][self::FLD_POSTAGE][FlatRatesLocationGroups::FIELD_LOCATIONS]) ?
  853. $row[self::DATA_LISTING][self::FLD_POSTAGE][FlatRatesLocationGroups::FIELD_LOCATIONS] : array();
  854. $listingPostageRegions = array_filter($listingPostageRegions);
  855. if (count($listingPostageRegions) > 0) {
  856. $listingRegions = array_unique(
  857. call_user_func_array('array_merge', $listingPostageRegions));
  858. }
  859. $regions = (is_array($regions)) ? array_intersect($regions, $listingRegions) : $listingRegions;
  860. }
  861. }
  862. $locations = $this->_getShippingLocations($regions);
  863. // if (empty($locations)) {
  864. // throw new \RuntimeException($translate->_("No shipping available for the selected items."));
  865. // }
  866. if ($dropDown === true) {
  867. $locations = $this->getLocations()->getMultiOptions((array)$locations);
  868. }
  869. return $locations;
  870. }
  871. /**
  872. *
  873. * get an array containing the ids of the shipping locations from the user's postage settings
  874. * and optionally a set of regions
  875. *
  876. * by default users will be set to ship domestically, based on the location of the item(s) and not the one of the seller
  877. *
  878. * @param array|bool $regions
  879. *
  880. * @return array
  881. */
  882. protected function _getShippingLocations($regions = false)
  883. {
  884. $locations = array();
  885. $shippingLocations = (isset($this->_postageSettings[self::SETUP_SHIPPING_LOCATIONS])) ?
  886. $this->_postageSettings[self::SETUP_SHIPPING_LOCATIONS] : self::POSTAGE_LOCATION_DOMESTIC;
  887. switch ($shippingLocations) {
  888. case self::POSTAGE_LOCATION_DOMESTIC:
  889. $locations = array($this->_data[0][self::DATA_LISTING]['country']);
  890. // $locations = array($this->_store['country']); // changed to the item's location.
  891. break;
  892. case self::POSTAGE_LOCATION_WORLDWIDE:
  893. $locationsService = new Service\Table\Relational\Locations();
  894. $locations = array_keys($locationsService->getMultiOptions());
  895. break;
  896. case self::POSTAGE_LOCATION_CUSTOM:
  897. $postageLocations = array_filter($this->_postageSettings[self::SETUP_LOCATION_GROUPS][FlatRatesLocationGroups::FIELD_LOCATIONS]);
  898. if ($regions !== false) {
  899. foreach ($postageLocations as $key => $val) {
  900. if (!in_array($key, $regions)) {
  901. unset($postageLocations[$key]);
  902. }
  903. }
  904. if (count($postageLocations) > 0) {
  905. $locations = $this->_mergeLocations($postageLocations);
  906. }
  907. }
  908. else {
  909. $locations = $this->_mergeLocations($postageLocations);
  910. }
  911. break;
  912. }
  913. return $locations;
  914. }
  915. /**
  916. *
  917. * rebuild locations array and remove duplicate locations
  918. *
  919. * @param $locations
  920. *
  921. * @return array
  922. */
  923. protected function _mergeLocations(array $locations)
  924. {
  925. if (count($locations) > 0) {
  926. $locations = array_unique(
  927. call_user_func_array('array_merge', $locations));
  928. }
  929. return $locations;
  930. }
  931. protected function _flipArray(array $array)
  932. {
  933. $output = array();
  934. foreach ($array['method'] as $key => $value) {
  935. if (!empty($value)) {
  936. if (isset($array['price'])) {
  937. $output[$key]['price'] = $array['price'][$key];
  938. }
  939. $output[$key]['method'] = $array['method'][$key];
  940. if (isset($array['locations'])) {
  941. $output[$key]['locations'] = (isset($array['locations'][$key])) ? $array['locations'][$key] : null;
  942. }
  943. }
  944. }
  945. return $output;
  946. }
  947. /**
  948. *
  949. * accepts an array returned by the calculatePostage() method and converts the amounts and currencies
  950. * to the currency of the listings in the model
  951. *
  952. * @param array $data
  953. *
  954. * @throws \RuntimeException
  955. * @return array
  956. */
  957. protected function _convertCurrency($data)
  958. {
  959. $translate = $this->getTranslate();
  960. foreach ($data as $key => $value) {
  961. if (!array_key_exists('currency', $value)
  962. && !array_key_exists('price', $value)
  963. ) {
  964. throw new \RuntimeException($translate->_("Invalid array input in the _convertCurrency() method."));
  965. }
  966. if ($value['currency'] != $this->_currency) {
  967. $data[$key]['currency'] = $this->_currency;
  968. $data[$key]['price'] = $this->getCurrencies()->convertAmount($value['price'], $value['currency'],
  969. $this->_currency);
  970. }
  971. }
  972. return $data;
  973. }
  974. /**
  975. *
  976. * this method will calculate the total invoice amount and if the total is over the minimum
  977. * amount required for the postage to be free, it will return true; otherwise it will return false
  978. *
  979. * @return bool
  980. */
  981. protected function _isFreePostage()
  982. {
  983. if ($this->_postageSettings[self::SETUP_FREE_POSTAGE]) {
  984. $total = 0;
  985. foreach ($this->_data as $data) {
  986. $total += $data[self::DATA_LISTING]['buyout_price'] * $data[self::DATA_QUANTITY];
  987. }
  988. $settings = Front::getInstance()->getBootstrap()->getResource('settings');
  989. $freePostageAmount = $this->getCurrencies()->convertAmount($this->_postageSettings[self::SETUP_FREE_POSTAGE_AMOUNT],
  990. $settings['currency'], $this->_currency);
  991. if ($total >= $freePostageAmount) {
  992. return true;
  993. }
  994. }
  995. return false;
  996. }
  997. /**
  998. *
  999. * calculate the total weight of the items added in the shipping model
  1000. *
  1001. * @return float
  1002. */
  1003. protected function _calculateTotalWeight()
  1004. {
  1005. $weight = 0;
  1006. foreach ($this->_data as $row) {
  1007. $weight += $row[self::DATA_LISTING][self::FLD_ITEM_WEIGHT] * $row[self::DATA_QUANTITY];
  1008. }
  1009. return ($weight > 0) ? $weight : self::MIN_WEIGHT;
  1010. }
  1011. /**
  1012. *
  1013. * we first order the dimensions to have the same orientation for the items
  1014. * the total dimension is calculated as follows:
  1015. * max(length) & max(width) & sum(height)
  1016. *
  1017. *
  1018. * @return array
  1019. */
  1020. protected function _calculateTotalDimensions()
  1021. {
  1022. $dimensions = null;
  1023. $lengths = array();
  1024. $widths = array();
  1025. $heights = array();
  1026. $counter = 0;
  1027. foreach ($this->_data as $row) {
  1028. $listingDimensions = (!empty($row[self::DATA_LISTING][self::FLD_DIMENSIONS])) ?
  1029. $row[self::DATA_LISTING][self::FLD_DIMENSIONS] : null;
  1030. if ($listingDimensions !== null) {
  1031. $sortedDimensions = array_values($listingDimensions);
  1032. rsort($sortedDimensions);
  1033. for ($i = 0; $i < $row[self::DATA_QUANTITY]; $i++) {
  1034. $lengths[] = $sortedDimensions[0];
  1035. $widths[] = $sortedDimensions[1];
  1036. $heights[] = $sortedDimensions[2];
  1037. $counter++;
  1038. }
  1039. }
  1040. }
  1041. if ($counter > 0) {
  1042. return array(
  1043. self::DIMENSION_LENGTH => max($lengths),
  1044. self::DIMENSION_WIDTH => max($widths),
  1045. self::DIMENSION_HEIGHT => array_sum($heights),
  1046. );
  1047. }
  1048. return null;
  1049. }
  1050. }