Cart.php 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187
  1. <?php
  2. namespace Webkul\Checkout;
  3. use Illuminate\Support\Facades\Event;
  4. use Webkul\Checkout\Contracts\CartAddress as CartAddressContract;
  5. use Webkul\Checkout\Exceptions\BillingAddressNotFoundException;
  6. use Webkul\Checkout\Models\CartAddress;
  7. use Webkul\Checkout\Models\CartPayment;
  8. use Webkul\Checkout\Repositories\CartAddressRepository;
  9. use Webkul\Checkout\Repositories\CartItemRepository;
  10. use Webkul\Checkout\Repositories\CartRepository;
  11. use Webkul\Customer\Contracts\Customer as CustomerContract;
  12. use Webkul\Customer\Contracts\Wishlist as WishlistContract;
  13. use Webkul\Customer\Repositories\CustomerAddressRepository;
  14. use Webkul\Customer\Repositories\WishlistRepository;
  15. use Webkul\Product\Contracts\Product as ProductContract;
  16. use Webkul\Product\Repositories\ProductRepository;
  17. use Webkul\Shipping\Facades\Shipping;
  18. use Webkul\Tax\Facades\Tax;
  19. use Webkul\Tax\Repositories\TaxCategoryRepository;
  20. class Cart
  21. {
  22. /**
  23. * The cart instance.
  24. *
  25. * @var \Webkul\Checkout\Contracts\Cart
  26. */
  27. private $cart;
  28. /**
  29. * Constant for tax calculation based on shipping origin.
  30. */
  31. const TAX_CALCULATION_BASED_ON_SHIPPING_ORIGIN = 'shipping_origin';
  32. /**
  33. * Constant for tax calculation based on billing address.
  34. */
  35. const TAX_CALCULATION_BASED_ON_BILLING_ADDRESS = 'billing_address';
  36. /**
  37. * Constant for tax calculation based on shipping address.
  38. */
  39. const TAX_CALCULATION_BASED_ON_SHIPPING_ADDRESS = 'shipping_address';
  40. /**
  41. * Create a new class instance.
  42. *
  43. * @return void
  44. */
  45. public function __construct(
  46. protected CartRepository $cartRepository,
  47. protected CartItemRepository $cartItemRepository,
  48. protected CartAddressRepository $cartAddressRepository,
  49. protected ProductRepository $productRepository,
  50. protected TaxCategoryRepository $taxCategoryRepository,
  51. protected WishlistRepository $wishlistRepository,
  52. protected CustomerAddressRepository $customerAddressRepository
  53. ) {
  54. $this->initCart();
  55. }
  56. /**
  57. * Initialize cart
  58. */
  59. public function initCart(?CustomerContract $customer = null): void
  60. {
  61. if (! $customer) {
  62. $customer = auth()->guard()->user();
  63. }
  64. if ($customer) {
  65. $this->cart = $this->cartRepository->findOneWhere([
  66. 'customer_id' => $customer->id,
  67. 'is_active' => 1,
  68. ]);
  69. } elseif (session()->has('cart')) {
  70. $this->cart = $this->cartRepository->find(session()->get('cart')->id);
  71. }
  72. }
  73. /**
  74. * Returns cart
  75. */
  76. public function refreshCart(): void
  77. {
  78. if (! $this->cart) {
  79. return;
  80. }
  81. $this->cart = $this->cartRepository->find($this->cart->id);
  82. }
  83. /**
  84. * Set cart
  85. */
  86. public function setCart(Contracts\Cart $cart): void
  87. {
  88. $this->cart = $cart;
  89. if ($this->cart->customer) {
  90. return;
  91. }
  92. $cartTemp = new \stdClass;
  93. $cartTemp->id = $this->cart->id;
  94. session()->put('cart', $cartTemp);
  95. }
  96. /**
  97. * Returns cart.
  98. */
  99. public function getCart(): ?Contracts\Cart
  100. {
  101. return $this->cart;
  102. }
  103. /**
  104. * Create new cart instance.
  105. */
  106. public function createCart(array $data): ?Contracts\Cart
  107. {
  108. $data = array_merge([
  109. 'is_guest' => 1,
  110. 'channel_id' => core()->getCurrentChannel()->id,
  111. 'global_currency_code' => $baseCurrencyCode = core()->getBaseCurrencyCode(),
  112. 'base_currency_code' => $baseCurrencyCode,
  113. 'channel_currency_code' => core()->getChannelBaseCurrencyCode(),
  114. 'cart_currency_code' => core()->getCurrentCurrencyCode(),
  115. ], $data);
  116. $customer = $data['customer'] ?? auth()->guard()->user();
  117. if ($customer) {
  118. $data = array_merge($data, [
  119. 'is_guest' => 0,
  120. 'customer_id' => $customer->id,
  121. 'customer_first_name' => $customer->first_name,
  122. 'customer_last_name' => $customer->last_name,
  123. 'customer_email' => $customer->email,
  124. ]);
  125. }
  126. $cart = $this->cartRepository->create($data);
  127. $this->setCart($cart);
  128. return $cart;
  129. }
  130. /**
  131. * Remove cart and destroy the session
  132. */
  133. public function removeCart(Contracts\Cart $cart): void
  134. {
  135. $this->cartRepository->delete($cart->id);
  136. if (session()->has('cart')) {
  137. session()->forget('cart');
  138. }
  139. $this->resetCart();
  140. }
  141. /**
  142. * Reset cart
  143. */
  144. public function resetCart(): void
  145. {
  146. $this->cart = null;
  147. }
  148. /**
  149. * Activate the cart by id.
  150. */
  151. public function activateCart(int $cartId): void
  152. {
  153. $cart = $this->cartRepository->update([
  154. 'is_active' => true,
  155. ], $cartId);
  156. $this->setCart($cart);
  157. }
  158. /**
  159. * Deactivates current cart.
  160. */
  161. public function deActivateCart(): void
  162. {
  163. if (! $this->cart) {
  164. return;
  165. }
  166. $this->cartRepository->update(['is_active' => false], $this->cart->id);
  167. $this->resetCart();
  168. if (session()->has('cart')) {
  169. session()->forget('cart');
  170. }
  171. }
  172. /**
  173. * This method handles when guest has some of cart products and then logs in.
  174. */
  175. public function mergeCart(CustomerContract $customer): void
  176. {
  177. if (! session()->has('cart')) {
  178. return;
  179. }
  180. $cart = $this->cartRepository->findOneWhere([
  181. 'customer_id' => $customer->id,
  182. 'is_active' => 1,
  183. ]);
  184. $guestCart = $this->cartRepository->find(session()->get('cart')->id);
  185. /**
  186. * When the logged in customer is not having any of the cart instance previously and are active.
  187. */
  188. if (! $cart) {
  189. $this->cartRepository->update([
  190. 'customer_id' => $customer->id,
  191. 'is_guest' => 0,
  192. 'customer_first_name' => $customer->first_name,
  193. 'customer_last_name' => $customer->last_name,
  194. 'customer_email' => $customer->email,
  195. ], $guestCart->id);
  196. session()->forget('cart');
  197. return;
  198. }
  199. $this->setCart($cart);
  200. foreach ($guestCart->items as $guestCartItem) {
  201. try {
  202. $this->addProduct($guestCartItem->product, $guestCartItem->additional);
  203. } catch (\Exception $e) {
  204. // Ignore exception
  205. }
  206. }
  207. $this->collectTotals();
  208. $this->removeCart($guestCart);
  209. }
  210. /**
  211. * Add items in a cart with some cart and item details.
  212. */
  213. public function addProduct(ProductContract $product, array $data): Contracts\Cart|\Exception
  214. {
  215. Event::dispatch('checkout.cart.add.before', $product->id);
  216. if (! $this->cart) {
  217. $this->createCart([]);
  218. }
  219. $cartProducts = $product->getTypeInstance()->prepareForCart(array_merge([
  220. 'cart_id' => $this->cart->id,
  221. ], $data));
  222. if (is_string($cartProducts)) {
  223. if (! $this->cart->all_items->count()) {
  224. $this->removeCart($this->cart);
  225. } else {
  226. $this->collectTotals();
  227. }
  228. throw new \Exception($cartProducts);
  229. } else {
  230. $parentCartItem = null;
  231. foreach ($cartProducts as $cartProduct) {
  232. $cartItem = $this->getItemByProduct($cartProduct, $data);
  233. if (isset($cartProduct['parent_id'])) {
  234. $cartProduct['parent_id'] = $parentCartItem->id;
  235. }
  236. if (! $cartItem) {
  237. $cartItem = $this->cartItemRepository->create(array_merge($cartProduct, ['cart_id' => $this->cart->id]));
  238. } else {
  239. if (
  240. isset($cartProduct['parent_id'])
  241. && $cartItem->parent_id !== $parentCartItem->id
  242. ) {
  243. $cartItem = $this->cartItemRepository->create(array_merge($cartProduct, [
  244. 'cart_id' => $this->cart->id,
  245. ]));
  246. } else {
  247. $cartItem = $this->cartItemRepository->update($cartProduct, $cartItem->id);
  248. }
  249. }
  250. if (! $parentCartItem) {
  251. $parentCartItem = $cartItem;
  252. }
  253. }
  254. }
  255. $this->collectTotals();
  256. Event::dispatch('checkout.cart.add.after', $this->cart);
  257. return $this->cart;
  258. }
  259. /**
  260. * Remove the item from the cart.
  261. */
  262. public function removeItem(int $itemId): bool
  263. {
  264. if (! $this->cart) {
  265. return false;
  266. }
  267. if (! $this->cart->items->pluck('id')->contains($itemId)) {
  268. return false;
  269. }
  270. Event::dispatch('checkout.cart.delete.before', $itemId);
  271. Shipping::removeAllShippingRates();
  272. $result = $this->cartItemRepository->delete($itemId);
  273. Event::dispatch('checkout.cart.delete.after', $itemId);
  274. return $result;
  275. }
  276. /**
  277. * Update cart items information.
  278. */
  279. public function updateItems(array $data): bool|\Exception
  280. {
  281. foreach ($data['qty'] as $itemId => $quantity) {
  282. $item = $this->cartItemRepository->find($itemId);
  283. if (! $item) {
  284. continue;
  285. }
  286. if ($item->cart_id !== $this->cart->id) {
  287. continue;
  288. }
  289. if (! $item->product->status) {
  290. throw new \Exception(__('shop::app.checkout.cart.inactive'));
  291. }
  292. if ($quantity <= 0) {
  293. $this->removeItem($itemId);
  294. throw new \Exception(__('shop::app.checkout.cart.illegal'));
  295. }
  296. $item->quantity = $quantity;
  297. if (! $this->isItemHaveQuantity($item)) {
  298. throw new \Exception(__('shop::app.checkout.cart.inventory-warning'));
  299. }
  300. Event::dispatch('checkout.cart.update.before', $item);
  301. $this->cartItemRepository->update([
  302. 'quantity' => $quantity,
  303. 'total' => core()->convertPrice($item->base_price * $quantity),
  304. 'total_incl_tax' => core()->convertPrice($item->base_price_incl_tax * $quantity),
  305. 'base_total' => $item->base_price * $quantity,
  306. 'base_total_incl_tax' => $item->base_price_incl_tax * $quantity,
  307. 'total_weight' => $item->weight * $quantity,
  308. 'base_total_weight' => $item->weight * $quantity,
  309. 'additional' => [
  310. ...$item->additional,
  311. 'quantity' => $quantity,
  312. ],
  313. ], $itemId);
  314. Event::dispatch('checkout.cart.update.after', $item);
  315. }
  316. $this->collectTotals();
  317. return true;
  318. }
  319. /**
  320. * Get cart item by product.
  321. */
  322. public function getItemByProduct(array $data, ?array $parentData = null): ?Contracts\CartItem
  323. {
  324. $items = $this->cart->all_items;
  325. foreach ($items as $item) {
  326. if ($item->getTypeInstance()->compareOptions($item->additional, $data['additional'])) {
  327. if (
  328. ! isset($data['additional']['parent_id'])
  329. && ! $item->parent_id
  330. ) {
  331. return $item;
  332. }
  333. if ($item->parent?->getTypeInstance()->compareOptions($item->parent->additional, $parentData ?: request()->all())) {
  334. return $item;
  335. }
  336. }
  337. }
  338. return null;
  339. }
  340. /**
  341. * Update or create billing address.
  342. */
  343. public function saveAddresses(array $params): void
  344. {
  345. $this->updateOrCreateBillingAddress($params['billing']);
  346. $this->updateOrCreateShippingAddress($params['shipping'] ?? []);
  347. $this->setCustomerPersonnelDetails();
  348. $this->resetShippingMethod();
  349. }
  350. /**
  351. * Update or create billing address.
  352. */
  353. public function updateOrCreateBillingAddress(array $params): CartAddressContract
  354. {
  355. $params = collect($params)
  356. ->only([
  357. 'use_for_shipping',
  358. 'default_address',
  359. 'company_name',
  360. 'first_name',
  361. 'last_name',
  362. 'vat_id',
  363. 'email',
  364. 'address',
  365. 'country',
  366. 'state',
  367. 'city',
  368. 'postcode',
  369. 'phone',
  370. ])
  371. ->merge([
  372. 'address_type' => CartAddress::ADDRESS_TYPE_BILLING,
  373. 'parent_address_id' => ($params['address_type'] ?? '') == 'customer' ? $params['id'] : null,
  374. 'cart_id' => $this->cart->id,
  375. 'customer_id' => $this->cart->customer_id,
  376. 'address' => implode(PHP_EOL, $params['address']),
  377. 'use_for_shipping' => (bool) ($params['use_for_shipping'] ?? false),
  378. ])
  379. ->toArray();
  380. if ($this->cart->billing_address) {
  381. $address = $this->cartAddressRepository->update($params, $this->cart->billing_address->id);
  382. } else {
  383. $address = $this->cartAddressRepository->create($params);
  384. }
  385. $this->cart->setRelation('billing_address', $address);
  386. return $address;
  387. }
  388. /**
  389. * Update or create shipping address.
  390. */
  391. public function updateOrCreateShippingAddress(array $params): ?CartAddressContract
  392. {
  393. /**
  394. * If cart is not having any stockable items then no need to save shipping address.
  395. */
  396. if (! $this->cart->haveStockableItems()) {
  397. return null;
  398. }
  399. if (! $this->cart->billing_address) {
  400. throw new BillingAddressNotFoundException;
  401. }
  402. $fillableFields = [
  403. 'default_address',
  404. 'company_name',
  405. 'first_name',
  406. 'last_name',
  407. 'email',
  408. 'address',
  409. 'country',
  410. 'state',
  411. 'city',
  412. 'postcode',
  413. 'phone',
  414. ];
  415. if ($this->cart->billing_address->use_for_shipping) {
  416. $params = $this->cart->billing_address->only($fillableFields);
  417. $params = array_merge($params, [
  418. 'address_type' => CartAddress::ADDRESS_TYPE_SHIPPING,
  419. 'parent_address_id' => $this->cart->billing_address->parent_address_id,
  420. 'cart_id' => $this->cart->id,
  421. 'customer_id' => $this->cart->customer_id,
  422. ]);
  423. } else {
  424. if (empty($params)) {
  425. return null;
  426. }
  427. $params = collect($params)
  428. ->only($fillableFields)
  429. ->merge([
  430. 'address_type' => CartAddress::ADDRESS_TYPE_SHIPPING,
  431. 'parent_address_id' => ($params['address_type'] ?? '') == 'customer' ? $params['id'] : null,
  432. 'cart_id' => $this->cart->id,
  433. 'customer_id' => $this->cart->customer_id,
  434. 'address' => implode(PHP_EOL, $params['address']),
  435. ])
  436. ->toArray();
  437. }
  438. if ($this->cart->shipping_address) {
  439. $address = $this->cartAddressRepository->update($params, $this->cart->shipping_address->id);
  440. } else {
  441. $params['default_address'] = 0;
  442. $address = $this->cartAddressRepository->create($params);
  443. }
  444. $this->cart->setRelation('shipping_address', $address);
  445. return $address;
  446. }
  447. /**
  448. * Save customer details.
  449. */
  450. public function setCustomerPersonnelDetails(): void
  451. {
  452. $this->cart->customer_email = $this->cart->customer?->email ?? $this->cart->billing_address->email;
  453. $this->cart->customer_first_name = $this->cart->customer?->first_name ?? $this->cart->billing_address->first_name;
  454. $this->cart->customer_last_name = $this->cart->customer?->last_name ?? $this->cart->billing_address->last_name;
  455. $this->cart->save();
  456. }
  457. /**
  458. * Save shipping method for cart.
  459. */
  460. public function saveShippingMethod(string $shippingMethodCode): bool
  461. {
  462. if (! $this->cart) {
  463. return false;
  464. }
  465. if (! Shipping::isMethodCodeExists($shippingMethodCode)) {
  466. return false;
  467. }
  468. $this->cart->shipping_method = $shippingMethodCode;
  469. $this->cart->save();
  470. return true;
  471. }
  472. /**
  473. * Save shipping method for cart.
  474. */
  475. public function resetShippingMethod(): bool
  476. {
  477. if (! $this->cart) {
  478. return false;
  479. }
  480. Shipping::removeAllShippingRates();
  481. $this->cart->shipping_method = null;
  482. $this->cart->save();
  483. return true;
  484. }
  485. /**
  486. * Save payment method for cart.
  487. */
  488. public function savePaymentMethod(array $params): bool|Contracts\CartPayment
  489. {
  490. if (! $this->cart) {
  491. return false;
  492. }
  493. if ($cartPayment = $this->cart->payment) {
  494. $cartPayment->delete();
  495. }
  496. $cartPayment = new CartPayment;
  497. $cartPayment->method = $params['method'];
  498. $cartPayment->method_title = core()->getConfigData('sales.payment_methods.'.$params['method'].'.title');
  499. $cartPayment->cart_id = $this->cart->id;
  500. $cartPayment->save();
  501. return $cartPayment;
  502. }
  503. /**
  504. * Set coupon code to the cart.
  505. */
  506. public function setCouponCode(?string $code): self
  507. {
  508. $this->cart->coupon_code = $code;
  509. $this->cart->save();
  510. return $this;
  511. }
  512. /**
  513. * Set coupon code to the cart.
  514. */
  515. public function removeCouponCode(): self
  516. {
  517. return $this->setCouponCode(null);
  518. }
  519. /**
  520. * Move a wishlist item to cart.
  521. */
  522. public function moveToCart(WishlistContract $wishlistItem, ?int $quantity = 1): bool
  523. {
  524. if (! $wishlistItem->product->getTypeInstance()->canBeMovedFromWishlistToCart($wishlistItem)) {
  525. return false;
  526. }
  527. if (! $wishlistItem->additional) {
  528. $wishlistItem->additional = ['product_id' => $wishlistItem->product_id];
  529. }
  530. $additional = [
  531. ...$wishlistItem->additional,
  532. 'quantity' => $quantity,
  533. ];
  534. $result = $this->addProduct($wishlistItem->product, $additional);
  535. if ($result) {
  536. Event::dispatch('customer.wishlist.delete.before', $wishlistItem->id);
  537. $this->wishlistRepository->delete($wishlistItem->id);
  538. Event::dispatch('customer.wishlist.delete.after', $wishlistItem->id);
  539. return true;
  540. }
  541. return false;
  542. }
  543. /**
  544. * Move to wishlist items.
  545. */
  546. public function moveToWishlist(int $itemId, int $quantity = 1): bool
  547. {
  548. $cartItem = $this->cart->items()->find($itemId);
  549. if (! $cartItem) {
  550. return false;
  551. }
  552. $wishlistItems = $this->wishlistRepository->findWhere([
  553. 'customer_id' => $this->cart->customer_id,
  554. 'product_id' => $cartItem->product_id,
  555. ]);
  556. $found = false;
  557. foreach ($wishlistItems as $wishlistItem) {
  558. $options = $wishlistItem->item_options;
  559. if (! $options) {
  560. $options = ['product_id' => $wishlistItem->product_id];
  561. }
  562. if ($cartItem->getTypeInstance()->compareOptions($cartItem->additional, $options)) {
  563. $found = true;
  564. }
  565. }
  566. if (! $found) {
  567. Event::dispatch('customer.wishlist.create.before', $cartItem->product_id);
  568. $wishlist = $this->wishlistRepository->create([
  569. 'channel_id' => $this->cart->channel_id,
  570. 'customer_id' => $this->cart->customer_id,
  571. 'product_id' => $cartItem->product_id,
  572. 'additional' => [
  573. ...$cartItem->additional,
  574. 'quantity' => $quantity,
  575. ],
  576. ]);
  577. Event::dispatch('customer.wishlist.create.after', $wishlist);
  578. }
  579. if (! $this->cart->items->count()) {
  580. $this->cartRepository->delete($this->cart->id);
  581. $this->refreshCart();
  582. } else {
  583. $this->cartItemRepository->delete($itemId);
  584. $this->refreshCart();
  585. $this->collectTotals();
  586. }
  587. return true;
  588. }
  589. /**
  590. * Checks if cart has any error.
  591. */
  592. public function hasError(): bool
  593. {
  594. return ! empty($this->getErrors());
  595. }
  596. /**
  597. * Get Cart Errors.
  598. */
  599. public function getErrors()
  600. {
  601. if (! $this->cart) {
  602. return [
  603. 'error_code' => 'CART_NOT_FOUND',
  604. 'message' => trans('shop::app.checkout.cart.index.empty-product'),
  605. ];
  606. }
  607. if (! $this->isItemsHaveSufficientQuantity()) {
  608. return [
  609. 'error_code' => 'INSUFFICIENT_QUANTITY',
  610. 'message' => trans('shop::app.checkout.cart.inventory-warning'),
  611. ];
  612. }
  613. if (! $this->haveMinimumOrderAmount()) {
  614. $minimumOrderDescription = core()->getConfigData('sales.order_settings.minimum_order.description');
  615. return [
  616. 'error_code' => 'MINIMUM_ORDER_AMOUNT',
  617. 'message' => $minimumOrderDescription ?: trans('shop::app.checkout.cart.minimum-order-message'),
  618. 'amount' => core()->formatPrice((int) core()->getConfigData('sales.order_settings.minimum_order.minimum_order_amount') ?: $this->getOrderAmount()),
  619. ];
  620. }
  621. return [];
  622. }
  623. /**
  624. * Check minimum Order Amount of cart.
  625. */
  626. public function getOrderAmount(): int
  627. {
  628. $minimumOrderAmount = $this->cart->sub_total;
  629. if (core()->getConfigData('sales.order_settings.minimum_order.include_tax_to_amount')) {
  630. $minimumOrderAmount += $this->cart->tax_total;
  631. }
  632. if (core()->getConfigData('sales.order_settings.minimum_order.include_discount_amount')) {
  633. $minimumOrderAmount -= $this->cart->tax_total;
  634. if ($this->cart->discount_amount) {
  635. $minimumOrderAmount -= $this->cart->discount_amount;
  636. }
  637. }
  638. return $minimumOrderAmount;
  639. }
  640. /**
  641. * Check minimum order.
  642. */
  643. public function haveMinimumOrderAmount(): bool
  644. {
  645. if (! core()->getConfigData('sales.order_settings.minimum_order.enable')) {
  646. return true;
  647. }
  648. return $this->getOrderAmount() >= ((int) core()->getConfigData('sales.order_settings.minimum_order.minimum_order_amount') ?: 0);
  649. }
  650. /**
  651. * Checks if all cart items have sufficient quantity.
  652. */
  653. public function isItemsHaveSufficientQuantity(): bool
  654. {
  655. if (! $this->cart) {
  656. return false;
  657. }
  658. foreach ($this->cart->items as $item) {
  659. if (! $this->isItemHaveQuantity($item)) {
  660. return false;
  661. }
  662. }
  663. return true;
  664. }
  665. /**
  666. * Checks if all cart items have sufficient quantity.
  667. */
  668. public function isItemHaveQuantity(Contracts\CartItem $item): bool
  669. {
  670. return $item->getTypeInstance()->isItemHaveQuantity($item);
  671. }
  672. /**
  673. * Updates cart totals.
  674. */
  675. public function collectTotals(): self
  676. {
  677. if (! $this->validateItems()) {
  678. /**
  679. * Reset the cart so that fresh copy of cart can be created.
  680. */
  681. $this->refreshCart();
  682. }
  683. if (! $this->cart) {
  684. return $this;
  685. }
  686. Event::dispatch('checkout.cart.collect.totals.before', $this->cart);
  687. $this->calculateItemsTax();
  688. $this->calculateShippingTax();
  689. $this->refreshCart();
  690. $this->cart->sub_total = $this->cart->base_sub_total = 0;
  691. $this->cart->sub_total_incl_tax = $this->cart->base_sub_total_incl_tax = 0;
  692. $this->cart->grand_total = $this->cart->base_grand_total = 0;
  693. $this->cart->tax_total = $this->cart->base_tax_total = 0;
  694. $this->cart->discount_amount = $this->cart->base_discount_amount = 0;
  695. $this->cart->shipping_amount = $this->cart->base_shipping_amount = 0;
  696. $this->cart->shipping_amount_incl_tax = $this->cart->base_shipping_amount_incl_tax = 0;
  697. $quantities = 0;
  698. foreach ($this->cart->items as $item) {
  699. $this->cart->discount_amount += $item->discount_amount;
  700. $this->cart->base_discount_amount += $item->base_discount_amount;
  701. $this->cart->tax_total += $item->tax_amount;
  702. $this->cart->base_tax_total += $item->base_tax_amount;
  703. $this->cart->sub_total = (float) $this->cart->sub_total + $item->total;
  704. $this->cart->base_sub_total = (float) $this->cart->base_sub_total + $item->base_total;
  705. $this->cart->sub_total_incl_tax = (float) $this->cart->sub_total_incl_tax + $item->total_incl_tax;
  706. $this->cart->base_sub_total_incl_tax = (float) $this->cart->base_sub_total_incl_tax + $item->base_total_incl_tax;
  707. $quantities += $item->quantity;
  708. }
  709. $this->cart->items_qty = $quantities;
  710. $this->cart->items_count = $this->cart->items->count();
  711. $this->cart->grand_total = $this->cart->sub_total + $this->cart->tax_total - $this->cart->discount_amount;
  712. $this->cart->base_grand_total = $this->cart->base_sub_total + $this->cart->base_tax_total - $this->cart->base_discount_amount;
  713. if ($shipping = $this->cart->selected_shipping_rate) {
  714. $this->cart->tax_total += $shipping->tax_amount;
  715. $this->cart->base_tax_total += $shipping->base_tax_amount;
  716. $this->cart->shipping_amount = $shipping->price;
  717. $this->cart->base_shipping_amount = $shipping->base_price;
  718. $this->cart->shipping_amount_incl_tax = $shipping->price_incl_tax;
  719. $this->cart->base_shipping_amount_incl_tax = $shipping->base_price_incl_tax;
  720. $this->cart->grand_total = (float) $this->cart->grand_total + $shipping->tax_amount + $shipping->price - $shipping->discount_amount;
  721. $this->cart->base_grand_total = (float) $this->cart->base_grand_total + $shipping->base_tax_amount + $shipping->base_price - $shipping->base_discount_amount;
  722. $this->cart->discount_amount += $shipping->discount_amount;
  723. $this->cart->base_discount_amount += $shipping->base_discount_amount;
  724. }
  725. $this->cart->discount_amount = round($this->cart->discount_amount, 2);
  726. $this->cart->base_discount_amount = round($this->cart->base_discount_amount, 2);
  727. $this->cart->sub_total = round($this->cart->sub_total, 2);
  728. $this->cart->base_sub_total = round($this->cart->base_sub_total, 2);
  729. $this->cart->sub_total_incl_tax = round($this->cart->sub_total_incl_tax, 2);
  730. $this->cart->base_sub_total_incl_tax = round($this->cart->base_sub_total_incl_tax, 2);
  731. $this->cart->grand_total = round($this->cart->grand_total, 2);
  732. $this->cart->base_grand_total = round($this->cart->base_grand_total, 2);
  733. $this->cart->cart_currency_code = core()->getCurrentCurrencyCode();
  734. $this->cart->save();
  735. Event::dispatch('checkout.cart.collect.totals.after', $this->cart);
  736. return $this;
  737. }
  738. /**
  739. * To validate if the product information is changed by admin and the items have been added to the cart before it.
  740. */
  741. public function validateItems(): bool
  742. {
  743. if (! $this->cart) {
  744. return false;
  745. }
  746. if (! $this->cart->items->count()) {
  747. $this->removeCart($this->cart);
  748. return false;
  749. }
  750. $isInvalid = false;
  751. foreach ($this->cart->items as $key => $item) {
  752. $validationResult = $item->getTypeInstance()->validateCartItem($item);
  753. if ($validationResult->isItemInactive()) {
  754. $this->removeItem($item->id);
  755. $isInvalid = true;
  756. session()->flash('info', __('shop::app.checkout.cart.inactive'));
  757. } else {
  758. if (Tax::isInclusiveTaxProductPrices()) {
  759. $itemBasePrice = $item->base_price_incl_tax;
  760. } else {
  761. $itemBasePrice = $item->base_price;
  762. }
  763. $basePrice = ! is_null($item->custom_price) ? $item->custom_price : $itemBasePrice;
  764. $price = core()->convertPrice($basePrice);
  765. /**
  766. * Reset the item price every time to initial price if the inclusive price is enabled.
  767. * Update the item price if exchange rates changes with exclusive price is enabled.
  768. */
  769. if ($price != $item->price) {
  770. $item = $this->cartItemRepository->update([
  771. 'price' => $price,
  772. 'price_incl_tax' => $price,
  773. 'base_price' => $basePrice,
  774. 'base_price_incl_tax' => $basePrice,
  775. 'total' => $total = core()->convertPrice($basePrice * $item->quantity),
  776. 'total_incl_tax' => $total,
  777. 'base_total' => ($baseTotal = $basePrice * $item->quantity),
  778. 'base_total_incl_tax' => $baseTotal,
  779. ], $item->id);
  780. $this->cart->items->put($key, $item);
  781. }
  782. }
  783. $isInvalid |= $validationResult->isCartInvalid();
  784. }
  785. return ! $isInvalid;
  786. }
  787. /**
  788. * Calculates cart items tax.
  789. */
  790. public function calculateItemsTax(): void
  791. {
  792. if (! $this->cart) {
  793. return;
  794. }
  795. Event::dispatch('checkout.cart.calculate.items.tax.before', $this->cart);
  796. $taxCategories = [];
  797. foreach ($this->cart->items as $key => $item) {
  798. $taxCategoryId = $item->tax_category_id;
  799. if (empty($taxCategoryId)) {
  800. $taxCategoryId = $item->product->tax_category_id;
  801. }
  802. if (empty($taxCategoryId)) {
  803. $taxCategoryId = core()->getConfigData('sales.taxes.categories.product');
  804. }
  805. if (empty($taxCategoryId)) {
  806. continue;
  807. }
  808. if (! isset($taxCategories[$taxCategoryId])) {
  809. $taxCategories[$taxCategoryId] = $this->taxCategoryRepository->find($taxCategoryId);
  810. }
  811. if (! $taxCategories[$taxCategoryId]) {
  812. continue;
  813. }
  814. $calculationBasedOn = core()->getConfigData('sales.taxes.calculation.based_on');
  815. $address = null;
  816. if ($calculationBasedOn == self::TAX_CALCULATION_BASED_ON_SHIPPING_ORIGIN) {
  817. $address = Tax::getShippingOriginAddress();
  818. } elseif ($calculationBasedOn == self::TAX_CALCULATION_BASED_ON_SHIPPING_ADDRESS) {
  819. if ($item->getTypeInstance()->isStockable()) {
  820. $address = $this->cart->shipping_address;
  821. } else {
  822. $address = $this->cart->billing_address;
  823. }
  824. } elseif ($calculationBasedOn == self::TAX_CALCULATION_BASED_ON_BILLING_ADDRESS) {
  825. $address = $this->cart->billing_address;
  826. }
  827. if ($address === null && $this->cart->customer) {
  828. $address = $this->cart->customer->addresses()
  829. ->where('default_address', 1)->first();
  830. }
  831. if ($address === null) {
  832. $address = Tax::getDefaultAddress();
  833. }
  834. $item->applied_tax_rate = null;
  835. $item->tax_percent = $item->tax_amount = $item->base_tax_amount = 0;
  836. Tax::isTaxApplicableInCurrentAddress($taxCategories[$taxCategoryId], $address, function ($rate) use ($item, $taxCategoryId) {
  837. $item->applied_tax_rate = $rate->identifier;
  838. $item->tax_category_id = $taxCategoryId;
  839. $item->tax_percent = $rate->tax_rate;
  840. if (Tax::isInclusiveTaxProductPrices()) {
  841. $item->tax_amount = round(($item->total_incl_tax * $rate->tax_rate) / (100 + $rate->tax_rate), 4);
  842. $item->base_tax_amount = round(($item->base_total_incl_tax * $rate->tax_rate) / (100 + $rate->tax_rate), 4);
  843. $item->total = $item->total_incl_tax - $item->tax_amount;
  844. $item->base_total = $item->base_total_incl_tax - $item->base_tax_amount;
  845. $item->price = $item->total / $item->quantity;
  846. $item->base_price = $item->base_total / $item->quantity;
  847. } else {
  848. $item->tax_amount = round(($item->total * $rate->tax_rate) / 100, 4);
  849. $item->base_tax_amount = round(($item->base_total * $rate->tax_rate) / 100, 4);
  850. $item->total_incl_tax = $item->total + $item->tax_amount;
  851. $item->base_total_incl_tax = $item->base_total + $item->base_tax_amount;
  852. $item->price_incl_tax = $item->price + ($item->tax_amount / $item->quantity);
  853. $item->base_price_incl_tax = $item->base_price + ($item->base_tax_amount / $item->quantity);
  854. }
  855. });
  856. if (empty($item->applied_tax_rate)) {
  857. $item->price_incl_tax = $item->price;
  858. $item->base_price_incl_tax = $item->base_price;
  859. $item->total_incl_tax = $item->total;
  860. $item->base_total_incl_tax = $item->base_total;
  861. }
  862. $item->save();
  863. $this->cart->items->put($key, $item);
  864. }
  865. Event::dispatch('checkout.cart.calculate.items.tax.after', $this->cart);
  866. }
  867. /**
  868. * Calculates cart shipping tax.
  869. */
  870. public function calculateShippingTax(): void
  871. {
  872. if (! $this->cart) {
  873. return;
  874. }
  875. $shippingRate = $this->cart->selected_shipping_rate;
  876. if (! $shippingRate) {
  877. return;
  878. }
  879. if (! $taxCategoryId = core()->getConfigData('sales.taxes.categories.shipping')) {
  880. return;
  881. }
  882. $taxCategory = $this->taxCategoryRepository->find($taxCategoryId);
  883. $calculationBasedOn = core()->getConfigData('sales.taxes.calculation.based_on');
  884. $address = null;
  885. if ($calculationBasedOn == self::TAX_CALCULATION_BASED_ON_SHIPPING_ORIGIN) {
  886. $address = Tax::getShippingOriginAddress();
  887. } elseif (
  888. $this->cart->haveStockableItems()
  889. && $calculationBasedOn == self::TAX_CALCULATION_BASED_ON_SHIPPING_ADDRESS
  890. ) {
  891. $address = $this->cart->shipping_address;
  892. } elseif ($calculationBasedOn == self::TAX_CALCULATION_BASED_ON_BILLING_ADDRESS) {
  893. $address = $this->cart->billing_address;
  894. }
  895. if ($address === null && $this->cart->customer) {
  896. $address = $this->cart->customer->addresses()
  897. ->where('default_address', 1)->first();
  898. }
  899. if ($address === null) {
  900. $address = Tax::getDefaultAddress();
  901. }
  902. Event::dispatch('checkout.cart.calculate.shipping.tax.before', $this->cart);
  903. Tax::isTaxApplicableInCurrentAddress($taxCategory, $address, function ($rate) use ($shippingRate) {
  904. $shippingRate->applied_tax_rate = $rate->identifier;
  905. $shippingRate->tax_percent = $rate->tax_rate;
  906. if (Tax::isInclusiveTaxShippingPrices()) {
  907. $shippingRate->tax_amount = round(($shippingRate->price_incl_tax * $rate->tax_rate) / (100 + $rate->tax_rate), 4);
  908. $shippingRate->base_tax_amount = round(($shippingRate->base_price_incl_tax * $rate->tax_rate) / (100 + $rate->tax_rate), 4);
  909. $shippingRate->price = $shippingRate->price_incl_tax - $shippingRate->tax_amount;
  910. $shippingRate->base_price = $shippingRate->base_price_incl_tax - $shippingRate->base_tax_amount;
  911. } else {
  912. $shippingRate->tax_amount = round(($shippingRate->price * $rate->tax_rate) / 100, 4);
  913. $shippingRate->base_tax_amount = round(($shippingRate->base_price * $rate->tax_rate) / 100, 4);
  914. $shippingRate->price_incl_tax = $shippingRate->price + $shippingRate->tax_amount;
  915. $shippingRate->base_price_incl_tax = $shippingRate->base_price + $shippingRate->base_tax_amount;
  916. }
  917. });
  918. if (empty($shippingRate->applied_tax_rate)) {
  919. $shippingRate->price_incl_tax = $shippingRate->price;
  920. $shippingRate->base_price_incl_tax = $shippingRate->base_price;
  921. }
  922. $shippingRate->save();
  923. Event::dispatch('checkout.cart.calculate.shipping.tax.after', $this->cart);
  924. }
  925. }