php 59 lines · 7 steps

Calculating a cart total with a Money type

A cart calculator sums line items, applies discounts, shipping rules, and tax using precise integer-cent Money objects instead of floats.

Explained by highlit
1<?php
2 
3namespace App\Services;
4 
5use App\Models\Cart;
6use Money\Money;
7 
8final class CartTotalCalculator
9{
10 private const FREE_SHIPPING_THRESHOLD = 7500;
11 private const FLAT_SHIPPING = 995;
12 private const TAX_RATE = 0.0825;
13 
14 public function calculate(Cart $cart): array
15 {
16 $subtotal = Money::USD(0);
17 $discountTotal = Money::USD(0);
18 
19 foreach ($cart->lines as $line) {
20 $gross = Money::USD($line->unit_price)->multiply($line->quantity);
21 $discount = $this->lineDiscount($gross, $line->discount_percent);
22 
23 $subtotal = $subtotal->add($gross);
24 $discountTotal = $discountTotal->add($discount);
25 }
26 
27 $discountedSubtotal = $subtotal->subtract($discountTotal);
28 $shipping = $this->shipping($discountedSubtotal, $cart->requires_shipping);
29 $tax = $discountedSubtotal->multiply((string) self::TAX_RATE, Money::ROUND_HALF_UP);
30 
31 return [
32 'subtotal' => $subtotal,
33 'discount' => $discountTotal,
34 'shipping' => $shipping,
35 'tax' => $tax,
36 'total' => $discountedSubtotal->add($shipping)->add($tax),
37 ];
38 }
39 
40 private function lineDiscount(Money $gross, float $percent): Money
41 {
42 if ($percent <= 0) {
43 return Money::USD(0);
44 }
45 
46 return $gross->multiply((string) ($percent / 100), Money::ROUND_HALF_UP);
47 }
48 
49 private function shipping(Money $discountedSubtotal, bool $requiresShipping): Money
50 {
51 if (! $requiresShipping) {
52 return Money::USD(0);
53 }
54 
55 return $discountedSubtotal->greaterThanOrEqual(Money::USD(self::FREE_SHIPPING_THRESHOLD))
56 ? Money::USD(0)
57 : Money::USD(self::FLAT_SHIPPING);
58 }
59}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Representing money as integer cents in a Money value object avoids floating-point errors that plague price arithmetic.
  2. 2Encoding business rules like free-shipping thresholds and tax rates as constants keeps pricing logic centralized and readable.
  3. 3Immutable value objects return new instances on every operation, so accumulation happens by reassignment rather than mutation.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Calculating a cart total with a Money type — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code