Sales.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. <?php
  2. /**
  3. *
  4. * PHP Pro Bid $Id$ o7sbe8CfJji4DIa8b0Lloyp/chIXnE+At24rYq22Ha0=
  5. *
  6. * @link https://www.phpprobid.com
  7. * @copyright Copyright (c) 2017 Online Ventures Software & CodeCube SRL
  8. * @license https://www.phpprobid.com/license Commercial License
  9. *
  10. * @version 7.10 [rev.7.10.01]
  11. */
  12. /**
  13. * sales table service class
  14. * creates/edits sales (a sale can include one ore more listings in it)
  15. */
  16. namespace Ppb\Service;
  17. use Ppb\Db\Table,
  18. Cube\Db\Expr,
  19. Ppb\Service,
  20. Ppb\Service\Table\SalesListings as SalesListingsTableService,
  21. Ppb\Model\Shipping as ShippingModel,
  22. Ppb\Db\Table\Row\Sale as SaleModel,
  23. Ppb\Db\Table\Row\Listing as ListingModel,
  24. Ppb\Db\Table\Row\UserAddressBook as UserAddressBookModel;
  25. class Sales extends AbstractService
  26. {
  27. /**
  28. *
  29. * the id of the sale row updated/created by a save operation
  30. *
  31. * @var int
  32. */
  33. protected $_saleId;
  34. /**
  35. *
  36. * sales listings table service
  37. *
  38. * @var \Ppb\Service\Table\SalesListings
  39. */
  40. protected $_salesListings;
  41. /**
  42. *
  43. * listings table service class
  44. *
  45. * @var \Ppb\Service\Listings
  46. */
  47. protected $_listings = null;
  48. /**
  49. *
  50. * reputation table service class
  51. *
  52. * @var \Ppb\Service\Reputation
  53. */
  54. protected $_reputation = null;
  55. /**
  56. *
  57. * class constructor
  58. */
  59. public function __construct()
  60. {
  61. parent::__construct();
  62. $this->setTable(
  63. new Table\Sales());
  64. }
  65. /**
  66. *
  67. * set sale id
  68. *
  69. * @param int $saleId
  70. *
  71. * @return $this
  72. */
  73. public function setSaleId($saleId)
  74. {
  75. $this->_saleId = $saleId;
  76. return $this;
  77. }
  78. /**
  79. *
  80. * get sale id
  81. *
  82. * @return int
  83. */
  84. public function getSaleId()
  85. {
  86. return $this->_saleId;
  87. }
  88. /**
  89. *
  90. * get sales listings table service
  91. *
  92. * @return \Ppb\Service\Table\SalesListings
  93. */
  94. public function getSalesListings()
  95. {
  96. if (!$this->_salesListings instanceof SalesListingsTableService) {
  97. $this->setSalesListings(
  98. new SalesListingsTableService());
  99. }
  100. return $this->_salesListings;
  101. }
  102. /**
  103. *
  104. * set sales listings table
  105. *
  106. * @param \Ppb\Service\Table\SalesListings $salesListings
  107. *
  108. * @return \Ppb\Service\Sales
  109. */
  110. public function setSalesListings(SalesListingsTableService $salesListings)
  111. {
  112. $this->_salesListings = $salesListings;
  113. return $this;
  114. }
  115. /**
  116. *
  117. * get listings table service class
  118. *
  119. * @return \Ppb\Service\Listings
  120. */
  121. public function getListings()
  122. {
  123. if (!$this->_listings instanceof Service\Listings) {
  124. $this->setListings(
  125. new Service\Listings());
  126. }
  127. return $this->_listings;
  128. }
  129. /**
  130. *
  131. * set listings table service class
  132. *
  133. * @param \Ppb\Service\Listings $listings
  134. *
  135. * @return \Ppb\Service\Sales
  136. */
  137. public function setListings(Service\Listings $listings)
  138. {
  139. $this->_listings = $listings;
  140. return $this;
  141. }
  142. /**
  143. *
  144. * get reputation table service class
  145. *
  146. * @return \Ppb\Service\Reputation
  147. */
  148. public function getReputation()
  149. {
  150. if (!$this->_reputation instanceof Service\Reputation) {
  151. $this->setReputation(
  152. new Service\Reputation());
  153. }
  154. return $this->_reputation;
  155. }
  156. /**
  157. *
  158. * set reputation table service class
  159. *
  160. * @param \Ppb\Service\Reputation $reputation
  161. *
  162. * @return \Ppb\Service\Sales
  163. */
  164. public function setReputation(Service\Reputation $reputation)
  165. {
  166. $this->_reputation = $reputation;
  167. return $this;
  168. }
  169. /**
  170. *
  171. * create or edit a sale (invoice). when creating a sale, also add rows in
  172. * the sales listings table. when updating a sale, modify the sale id of the involved sales
  173. * listings, and then remove any sales that do not contain any listings.
  174. *
  175. * calculates the postage when creating or editing the invoice
  176. * the postage will be calculated based on the "postage_id" key, or the first option in the postage array will
  177. * be used if the "postage_id" key doesnt correspond to a key in the results array
  178. *
  179. * will also add the calculated or edited insurance amount if "apply_insurance" is checked.
  180. *
  181. * an element from the listings array must include:
  182. * 'listing' => object of type listing
  183. * 'price' => the price the listing has been sold for
  184. * 'quantity' => the quantity sold
  185. *
  186. * @param array $post
  187. *
  188. * @return \Ppb\Service\Sales
  189. * @throws \InvalidArgumentException
  190. */
  191. public function save($post)
  192. {
  193. $sale = null;
  194. $pending = (!empty($post['pending'])) ? $post['pending'] : 0;
  195. if ((empty($post['buyer_id']) || empty($post['user_token'])) && empty($post['seller_id'])) {
  196. throw new \InvalidArgumentException("The 'buyer_id'/'user_token' and 'seller_id' keys need to be specified when creating/editing a sale/shopping cart");
  197. }
  198. $data = $this->_prepareSaveData($post);
  199. if (array_key_exists('id', $data)) {
  200. $sale = $this->findBy('id', $data['id']);
  201. unset($data['id']);
  202. }
  203. $newSale = false;
  204. if (count($sale) > 0) {
  205. $data['updated_at'] = new Expr('now()');
  206. $sale->save($data);
  207. $id = $sale['id'];
  208. }
  209. else {
  210. $data['created_at'] = $data['updated_at'] = new Expr('now()');
  211. $this->_table->insert($data);
  212. $id = $this->_table->getAdapter()->lastInsertId();
  213. $listing = $this->getListings()->findBy('id', $post['listings'][0]['listing_id']);
  214. /** @var \Ppb\Db\Table\Row\Sale $sale */
  215. $sale = $this->findBy('id', $id);
  216. $sale->saveSaleData(array(
  217. 'currency' => $listing['currency'],
  218. 'country' => $listing['country'],
  219. 'state' => $listing['state'],
  220. 'address' => $listing['address'],
  221. 'pickup_options' => $listing['pickup_options'],
  222. 'apply_tax' => $listing['apply_tax'],
  223. ));
  224. $newSale = true;
  225. }
  226. $this->setSaleId($id);
  227. if (isset($post['listings'])) {
  228. foreach ($post['listings'] as $data) {
  229. $data['sale_id'] = $id;
  230. // for shopping carts, don't allow an item to be added to a cart more than once.
  231. if ($pending) {
  232. $salesListingsTable = $this->getSalesListings()->getTable();
  233. $select = $salesListingsTable->select('id')
  234. ->where("listing_id = ?", $data['listing_id'])
  235. ->where("sale_id = ?", $data['sale_id']);
  236. if (!empty($data['product_attributes'])) {
  237. $select->where("product_attributes = ?", $data['product_attributes']);
  238. }
  239. $saleListing = $salesListingsTable->fetchRow($select);
  240. if ($saleListing) {
  241. $data['id'] = $saleListing->getData('id');
  242. }
  243. }
  244. $this->getSalesListings()->save($data);
  245. }
  246. }
  247. if (!$pending) {
  248. $postageId = (!empty($post['postage_id'])) ? $post['postage_id'] : 0;
  249. $applyInsurance = (!empty($post['apply_insurance'])) ? $post['apply_insurance'] : 0;
  250. $this->_processPostageFields($sale, $post, $postageId, $applyInsurance);
  251. if ($newSale || isset($post['checkout'])) {
  252. $this->_processPostSaleActions();
  253. }
  254. }
  255. return $this;
  256. }
  257. /**
  258. *
  259. * delete a sale (and all sale listings attached)
  260. *
  261. * @param int $id sale id
  262. *
  263. * @return int the number of affected rows
  264. */
  265. public function delete($id)
  266. {
  267. return $this->_table->delete(
  268. $this->_table->getAdapter()->quoteInto('id = ?', $id));
  269. }
  270. /**
  271. *
  272. * if a sale is complete, do the following actions:
  273. *
  274. * - save sale transaction to accounting table
  275. * - update the payer's balance if in account mode
  276. * - prepare reputation rows for each listing in the sale
  277. * - close any listings from the sale which had their quantity expired and update the quantity field in the listings table
  278. * - add the tax rate to the sale if tax applies
  279. * - set 'expires_at' field if force payment is enabled
  280. * - email seller and buyer
  281. * - V7.5: if sale total is 0.00 then flag_payment is set to 1
  282. * - V7.8: if auto relist if sold is enabled: relist
  283. *
  284. * @return $this
  285. */
  286. protected function _processPostSaleActions()
  287. {
  288. /** @var \Ppb\Db\Table\Row\Sale $sale */
  289. $sale = $this->findBy('id', $this->getSaleId());
  290. $settings = $this->getSettings();
  291. /** @var \Ppb\Db\Table\Row\User $seller */
  292. $seller = $sale->findParentRow('\Ppb\Db\Table\Users', 'Seller');
  293. /** @var \Ppb\Db\Table\Row\User $buyer */
  294. $buyer = $sale->findParentRow('\Ppb\Db\Table\Users', 'Buyer');
  295. /** @var \Ppb\Db\Table\Row\User $payer */
  296. $payer = ($settings['sale_fee_payer'] == 'buyer') ? $buyer : $seller;
  297. $saleTransactionService = new Service\Fees\SaleTransaction(
  298. $sale, $payer
  299. );
  300. $saleTransactionFees = $saleTransactionService->calculate();
  301. $totalAmount = $saleTransactionService->getTotalAmount();
  302. $accountingService = new Service\Accounting();
  303. if ($payer->userPaymentMode() == 'account') {
  304. $payer->updateBalance(
  305. $totalAmount);
  306. $sale->updateActive();
  307. $accountingService->setRefundFlag(\Ppb\Db\Table\Row\Accounting::REFUND_ALLOWED);
  308. }
  309. else if ($totalAmount <= 0) {
  310. $sale->updateActive();
  311. }
  312. $accountingService->setUserId($payer['id'])
  313. ->setSaleId($this->getSaleId())
  314. ->saveMultiple($saleTransactionFees);
  315. $reputation = $this->getReputation();
  316. $voucher = $sale->getVoucher();
  317. if ($voucher !== null) {
  318. $voucher->updateUses();
  319. }
  320. $salesListings = $sale->findDependentRowset('\Ppb\Db\Table\SalesListings');
  321. $listingIds = array();
  322. $taxCalculated = false;
  323. /** @var \Ppb\Db\Table\Row\SaleListing $saleListing */
  324. foreach ($salesListings as $saleListing) {
  325. /** @var \Ppb\Db\Table\Row\Listing $listing */
  326. $listing = $saleListing->findParentRow('\Ppb\Db\Table\Listings');
  327. // get tax rate - retrieved based on the first listing in the sale
  328. if ($taxCalculated !== true) {
  329. if (($taxType = $listing->getTaxType($buyer, $sale['billing_address_id'])) !== false) {
  330. $sale->save(array(
  331. 'tax_rate' => $taxType->getData('amount')
  332. ));
  333. }
  334. $taxCalculated = true;
  335. }
  336. $quantity = $listing->updateQuantity($saleListing['quantity'], $saleListing['product_attributes'], ListingModel::SUBTRACT);
  337. if ($quantity == 0) {
  338. $listing->close();
  339. }
  340. if ($voucher !== null) {
  341. $price = $voucher->apply($saleListing->price(), $sale['currency'], $listing['id']);
  342. $saleListing->save(array(
  343. 'price' => $price,
  344. ));
  345. }
  346. // prepare reputation row: seller => buyer
  347. $reputation->save(array(
  348. 'user_id' => $sale['buyer_id'],
  349. 'poster_id' => $sale['seller_id'],
  350. 'sale_listing_id' => $saleListing['id'],
  351. 'listing_name' => $listing['name'],
  352. 'reputation_type' => Reputation::PURCHASE,
  353. ));
  354. // prepare reputation row: buyer => seller
  355. $reputation->save(array(
  356. 'user_id' => $sale['seller_id'],
  357. 'poster_id' => $sale['buyer_id'],
  358. 'sale_listing_id' => $saleListing['id'],
  359. 'listing_name' => $listing['name'],
  360. 'reputation_type' => Reputation::SALE,
  361. ));
  362. // relist only if the listing is closed
  363. if ($quantity == 0) {
  364. $listingIds[] = $listing['id'];
  365. }
  366. }
  367. if (count($listingIds) > 0) {
  368. $select = $this->getListings()->getTable()->select()
  369. ->forUpdate()
  370. ->where('id IN (?)', $listingIds);
  371. $this->getListings()->fetchAll($select)
  372. ->setAutomatic(true)
  373. ->relist();
  374. }
  375. if ($seller->isForcePayment()) {
  376. $sale->setExpiresFlag();
  377. }
  378. $sale->clearSalesListings();
  379. if ($sale->calculateTotal() <= 0) {
  380. $sale->save(array(
  381. 'flag_payment' => 1,
  382. ));
  383. $sale->setExpiresFlag(true);
  384. }
  385. $mail = new \Members\Model\Mail\User();
  386. $mail->saleBuyerNotification($sale, $buyer)->send();
  387. $mail->saleSellerNotification($sale, $seller)->send();
  388. return $this;
  389. }
  390. /**
  391. *
  392. * process postage fields - calculate and save postage costs
  393. * the shipping_address_id field will always need to be set in the sale row
  394. * shipping needs to be enabled for this
  395. *
  396. *
  397. * @param \Ppb\Db\Table\Row\Sale $sale
  398. * @param array $post
  399. * @param int $postageId
  400. * @param int $applyInsurance
  401. *
  402. * @return $this
  403. */
  404. protected function _processPostageFields(SaleModel $sale, $post, $postageId, $applyInsurance)
  405. {
  406. /** @var \Ppb\Db\Table\Row\User $seller */
  407. $seller = $sale->findParentRow('\Ppb\Db\Table\Users', 'Seller');
  408. /** @var \Ppb\Db\Table\Row\User $buyer */
  409. $buyer = $sale->findParentRow('\Ppb\Db\Table\Users', 'Buyer');
  410. $shippingAddress = $buyer->getAddress($sale['shipping_address_id']);
  411. $shippingModel = new ShippingModel($seller);
  412. $shippingModel->setLocationId($shippingAddress['country'])
  413. ->setPostCode($shippingAddress['zip_code']);
  414. $salesListings = $sale->findDependentRowset('\Ppb\Db\Table\SalesListings');
  415. /** @var \Ppb\Db\Table\Row\SaleListing $saleListing */
  416. foreach ($salesListings as $saleListing) {
  417. $shippingModel->addData(
  418. $saleListing->findParentRow('\Ppb\Db\Table\Listings'), $saleListing['quantity']);
  419. }
  420. $result = array();
  421. try {
  422. $result = $shippingModel->calculatePostage();
  423. } catch (\RuntimeException $e) {
  424. }
  425. $shippingDetails = (!empty($result[$postageId])) ? $result[$postageId] : reset($result);
  426. // TODO: only the seller or admin can enter a custom amount for the postage and insurance amounts
  427. $insuranceAmount = (isset($post['insurance_amount'])) ? $post['insurance_amount'] : $shippingModel->calculateInsurance();
  428. $postageAmount = (isset($post['postage_amount'])) ? $post['postage_amount'] : $shippingDetails['price'];
  429. $sale->saveSaleData(array(
  430. 'postage_id' => $postageId,
  431. 'apply_insurance' => $applyInsurance,
  432. 'postage' => $shippingDetails,
  433. ));
  434. $postageData = array(
  435. 'postage_amount' => (double)$postageAmount,
  436. 'insurance_amount' => (double)$insuranceAmount
  437. );
  438. if ($shippingAddress instanceof UserAddressBookModel) {
  439. if ($shippingAddressId = $shippingAddress->getData('id')) {
  440. $postageData['shipping_address_id'] = $shippingAddressId;
  441. }
  442. }
  443. $sale->save($postageData);
  444. return $this;
  445. }
  446. /**
  447. *
  448. * get all carts of a certain user based on his token
  449. *
  450. * @var string $userToken
  451. * @return array
  452. */
  453. public function getMultiOptions($userToken)
  454. {
  455. $data = array();
  456. $select = $this->getTable()->select()
  457. ->where('pending = ?', 1)
  458. ->where('user_token = ?', $userToken)
  459. ->order(array('updated_at DESC', 'created_at DESC'));
  460. $rowset = $this->fetchAll($select);
  461. /** @var \Ppb\Db\Table\Row\Sale $row */
  462. foreach ($rowset as $row) {
  463. /** @var \Ppb\Db\Table\Row\User $seller */
  464. $seller = $row->findParentRow('\Ppb\Db\Table\Users', 'Seller');
  465. $data[(string)$row['id']] = '[ ' . $row['id'] . ' ] ' .
  466. (($seller->storeStatus(true) == true) ?
  467. $seller->getData('store_name') : $seller->getData('username'));
  468. }
  469. return $data;
  470. }
  471. }