├── .gitignore
├── .travis.yml
├── README.md
├── composer.json
├── phpunit.xml.dist
├── ruleset.xml
├── src
├── Collection
│ └── ItemPriceCollection.php
├── Description
│ ├── AbstractPriceDescription.php
│ └── PriceDescriptionInterface.php
├── Modifier
│ ├── AbstractPriceModifier.php
│ ├── DiscountPrice.php
│ ├── ItemComparatorInterface.php
│ ├── PriceModifierInterface.php
│ └── TaxPrice.php
├── PricingFactory.php
├── Total
│ └── PriceTotalInterface.php
└── Type
│ ├── ItemPrice.php
│ ├── PriceInterface.php
│ └── UnitPrice.php
└── tests
└── Unit
├── Collection
└── ItemPriceCollectionTest.php
├── Description
└── AbstractPriceDescriptionTest.php
├── Modifier
├── AbstractPriceModifierTest.php
├── DiscountPriceTest.php
└── TaxPriceTest.php
├── PricingFactoryTest.php
└── Type
├── ItemPriceTest.php
└── UnitPriceTest.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /build/
2 | /vendor/
3 | /composer.lock
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - 5.4
4 | - 5.5
5 | - 5.6
6 | - 7.0
7 | - 7.1
8 | - 7.2
9 | - hhvm
10 | before_script:
11 | - composer install
12 | script:
13 | - ./vendor/bin/phpunit --coverage-text --coverage-clover ./build/logs/clover.xml --verbose
14 | - ./vendor/bin/phpcs --extensions=php --standard=ruleset.xml ./src ./tests
15 | after_script:
16 | - php ./vendor/bin/coveralls -v
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # blesta/pricing
2 |
3 | [](https://travis-ci.org/blesta/pricing) [](https://coveralls.io/github/blesta/pricing?branch=master)
4 |
5 | A library for handling pricing. Supports:
6 |
7 | - Unit Prices
8 | - Item Prices
9 | - Unit Price that may include discounts and taxes
10 | - Discounts
11 | - Percentages
12 | - Fixed amounts
13 | - Taxes (inclusive_calculated, inclusive, exclusive)
14 | - Inclusive and Exclusive
15 | - Applied in sequence or compounded
16 | - Inclusive calculated is meant to be subtracted from the item price
17 | - Item Collection
18 | - Iterate over Item Prices
19 | - Aggregate totals over Item Prices
20 |
21 | ## Installation
22 |
23 | Install via composer:
24 |
25 | ```sh
26 | composer require blesta/pricing
27 | ```
28 |
29 | ## Basic Usage
30 |
31 | ### UnitPrice
32 |
33 | ```php
34 | use Blesta\Pricing\Type\UnitPrice;
35 |
36 | $price = new UnitPrice(5.00, 2, "id");
37 | $price->setDescription("2 X 5.00");
38 | $unit_price = $price->price(); // 5.00
39 | $qty = $price->qty(); // 2
40 | $total = $price->total(); // 10.00
41 | $key = $price->key(); // id
42 |
43 | // Update the unit price, quantity, and key
44 | $price->setPrice(10.00);
45 | $price->setQty(3);
46 | $price->setKey('id2');
47 | ```
48 |
49 | ### DiscountPrice
50 |
51 | ```php
52 | use Blesta\Pricing\Modifier\DiscountPrice;
53 |
54 | $discount = new DiscountPrice(25.00, "percent");
55 | $discount->setDescription("25% off");
56 | $price_after_discount = $discount->off(100.00); // 75.00
57 | $discount_price = $discount->on(100.00); // 25.00
58 | ```
59 |
60 | ### TaxPrice
61 |
62 | Exclusive tax (price does not include tax):
63 |
64 | ```php
65 | use Blesta\Pricing\Modifier\TaxPrice;
66 |
67 | $tax = new TaxPrice(10.00, TaxPrice::EXCLUSIVE);
68 | $tax->setDescription("10 % tax");
69 | $tax->on(100.00); // 10.00
70 | $tax->off(100.00); // 100.00 (price on exclusive tax doesn't include tax, so nothing to take off)
71 | $tax->including(100.00); // 110.00
72 | ```
73 |
74 | Inclusive tax (price already includes tax):
75 |
76 | ```php
77 | use Blesta\Pricing\Modifier\TaxPrice;
78 |
79 | $tax = new TaxPrice(25.00, TaxPrice::INCLUSIVE);
80 | $tax->setDescription("25 % tax");
81 | $tax->on(100.00); // 25.00
82 | $tax->off(100.00); // 75.00
83 | $tax->including(100.00); // 100.00
84 | ```
85 |
86 | Inclusive tax (price already includes tax) calculated based on the price minus tax:
87 |
88 | ```php
89 | use Blesta\Pricing\Modifier\TaxPrice;
90 |
91 | $tax = new TaxPrice(25.00, TaxPrice::INCLUSIVE_CALCULATED);
92 | $tax->setDescription("25 % tax");
93 | $tax->on(100.00); // 20.00
94 | $tax->off(100.00); // 80.00
95 | $tax->including(100.00); // 100.00
96 | ```
97 |
98 | Cascading tax (tax on a tax):
99 |
100 | ```php
101 | use Blesta\Pricing\Modifier\TaxPrice;
102 | use Blesta\Pricing\Type\UnitPrice;
103 |
104 | $price = new UnitPrice(10.00);
105 | $tax1 = new TaxPrice(10.00, TaxPrice::EXCLUSIVE);
106 | $tax2 = new TaxPrice(5.00, TaxPrice::EXCLUSIVE);
107 | $tax2->on(
108 | $tax1->on(
109 | $price->total()
110 | )
111 | + $price->total()
112 | ); // 0.55 = [((10.00 * 0.10) + 10.00) * 0.05]
113 | ```
114 |
115 | ### ItemPrice
116 |
117 | ```php
118 | use Blesta\Pricing\Type\ItemPrice;
119 |
120 | $item_price = new ItemPrice(10.00, 3);
121 | $item_price->total(); // 30.00
122 | ```
123 |
124 | With discount applied:
125 |
126 | ```php
127 | use Blesta\Pricing\Modifier\DiscountPrice;
128 |
129 | $discount = new DiscountPrice(5.00, "percent");
130 |
131 | // call setDiscount() as many times as needed to apply discounts
132 | $item_price->setDiscount($discount);
133 | $item_price->totalAfterDiscount(); // 28.50
134 | ```
135 |
136 | Amount applied for a specific discount:
137 |
138 | ```php
139 | use Blesta\Pricing\Modifier\DiscountPrice;
140 |
141 | $item_price = new ItemPrice(10.00, 3);
142 |
143 | $discount1 = new DiscountPrice(5.00, "percent");
144 | $discount2 = new DiscountPrice(25.00, "percent");
145 |
146 | // NOTE: Order matters here
147 | $item_price->setDiscount($discount1);
148 | $item_price->setDiscount($discount2);
149 |
150 | $item_price->discountAmount($discount1); // 1.50
151 | $item_price->discountAmount($discount2); // 7.125 ((30.00 - 1.50) * 0.25)
152 | ```
153 |
154 | With tax applied:
155 |
156 | ```php
157 | use Blesta\Pricing\Modifier\TaxPrice;
158 |
159 | $tax = new TaxPrice(10.00, TaxPrice::EXCLUSIVE);
160 |
161 | // call setTax() as many times as needed to apply multiple levels of taxes
162 | $item_price->setTax($tax);
163 | // pass as many TaxPrice objects to setTax as you want to compound tax
164 | // ex. $item_price->setTax($tax1, $tax2, ...);
165 | $item_price->totalAfterTax(); // 32.1375 = (subtotal + ([subtotal - discounts] * taxes)) = (30 + [30 - (1.50 + 7.125)] * 0.10)
166 | ```
167 |
168 | With tax and discount:
169 |
170 | ```php
171 | $item_price->total(); // 23.5125 = (subtotal - discounts + ([subtotal - discounts] * taxes)) = (30 - (1.50 + 7.125) + [30 - (1.50 + 7.125)] * 0.10)
172 | ```
173 |
174 | With tax and discount where the discount does *not* apply to the taxes:
175 |
176 | ```php
177 | $item_price->setDiscountTaxes(false);
178 | $item_price->total(); // 24.375 = (subtotal - discounts + ([subtotal] * taxes)) = (30 - (1.50 + 7.125) + ([30] * 0.10))
179 | ```
180 |
181 | Without taxes of the 'exclusive' type:
182 |
183 | ```php
184 | $item_price->setDiscountTaxes(true);
185 | $item_price->excludeTax(TaxPrice::EXCLUSIVE)->totalAfterTax(); // 30.00 = (30 + [30 - (1.50 + 7.125)] * 0)
186 | $item_price->total(); // 21.375 = (30 - (1.50 + 7.125) + [30 - (1.50 + 7.125)] * 0)
187 |
188 | // Be sure to reset the excluded taxes before attempting to fetch totals that should include them again!
189 | $item_price->resetTaxes();
190 | $item_price->total(); // 23.5125 = (subtotal - discounts + ([subtotal - discounts] * taxes)) = (30 - (1.50 + 7.125) + [30 - (1.50 + 7.125)] * 0.10)
191 | $item_price->excludeTax(TaxPrice::EXCLUSIVE)->total(); // 21.375 = (30 - (1.50 + 7.125) + [30 - (1.50 + 7.125)] * 0)
192 | $item_price->resetTaxes();
193 | ```
194 |
195 | Amount applied for a specific tax:
196 |
197 | ```php
198 | use Blesta\Pricing\Modifier\TaxPrice;
199 |
200 | $tax1 = new TaxPrice(10.00, TaxPrice::EXCLUSIVE);
201 | $tax2 = new TaxPrice(5.00, TaxPrice::INCLUSIVE);
202 |
203 | // NOTE: order *DOES NOT* matter
204 | $item_price->setTax($tax1);
205 | $item_price->setTax($tax2);
206 |
207 | $item_price->taxAmount($tax1); // 2.1375 = ([subtotal - discounts] * taxes) = ([30 - (1.50 + 7.125)] * 0.10)
208 | $item_price->taxAmount($tax2); // 1.06875 = ([subtotal - discounts] * taxes) = ([30 - (1.50 + 7.125)] * 0.05)
209 | ```
210 |
211 | Without taxes of the 'exclusive' type:
212 |
213 | ```php
214 | $item_price->excludeTax(TaxPrice::EXCLUSIVE)->totalAfterTax(); // 31.06875 = (subtotal + ([subtotal - discounts] * taxes)) = (30 + [30 - (1.50 + 7.125)] * 0.05)
215 | $item_price->resetTaxes();
216 | ```
217 |
218 | Cascading tax:
219 |
220 | ```php
221 | use Blesta\Pricing\Modifier\TaxPrice;
222 | use Blesta\Pricing\Type\ItemPrice;
223 |
224 | $item_price = new ItemPrice(10.00, 3);
225 |
226 | $tax1 = new TaxPrice(10.00, TaxPrice::EXCLUSIVE);
227 | $tax2 = new TaxPrice(5.00, TaxPrice::INCLUSIVE);
228 | $tax3 = new TaxPrice(2.50, TaxPrice::EXCLUSIVE);
229 |
230 | $item_price->setTax($tax1, $tax2, $tax3);
231 | $item_price->taxAmount($tax1); // 3.00 = ([subtotal - discounts] * taxes) = ([30 - 0] * 0.10)
232 | $item_price->taxAmount($tax2); // 1.65 = ([subtotal - discounts + previous-taxes] * 0.05) = ([30.00 - 0 + 3.00] * 0.05)
233 | $item_price->taxAmount($tax3); // 0.86625 = ([subtotal - discounts + previous-taxes] * 0.025) = ([30.00 - 0 + 3.00 + 1.65] * 0.025)
234 | $item_price->taxAmount(); // 5.51625
235 |
236 | // Exclude taxes of the 'inclusive' type
237 | $item_price->excludeTax(TaxPrice::INCLUSIVE);
238 | $item_price->taxAmount($tax1); // 3.00 = ([subtotal - discounts] * taxes) = ([30 - 0] * 0.10)
239 | $item_price->taxAmount($tax2); // 0 = ([subtotal - discounts + previous-taxes] * 0) = ([30.00 - 0 + 3.00] * 0)
240 | $item_price->taxAmount($tax3); // 0.86625 = ([subtotal - discounts + previous-taxes] * 0.025) = ([30.00 - 0 + 3.00 + 1.65] * 0.025)
241 | $item_price->taxAmount(); // 3.86625
242 | $item_price->resetTaxes();
243 | ```
244 |
245 | ### ItemPriceCollection
246 |
247 | ```php
248 | use Blesta\Pricing\Collection\ItemPriceCollection;
249 | use Blesta\Pricing\Type\ItemPrice;
250 |
251 | $item_collection = new ItemPriceCollection();
252 |
253 | $item1 = new ItemPrice(10.00, 3);
254 | $item2 = new ItemPrice(25.00, 2);
255 | $item_collection->append($item1)->append($item2);
256 |
257 | $item_collection->total(); // 80.00
258 |
259 | foreach ($item_collection as $item) {
260 | $item->total(); // 30.00, 50.00
261 | }
262 | ```
263 |
264 | ### PricingFactory
265 |
266 | Using the PricingFactory can streamline usage. Assume you have the following:
267 |
268 | ```php
269 | $products = array(
270 | array('desc' => 'Apples', 'amount' => 0.5, 'qty' => 3),
271 | array('desc' => 'Oranges', 'amount' => 0.75, 'qty' => 10)
272 | );
273 | ```
274 |
275 | So we initialize our PricingFactory, and let it create our DiscountPrice and TaxPrice objects for use.
276 |
277 | ```php
278 | use Blesta\Pricing\PricingFactory;
279 |
280 | $pricing_factory = new PricingFactory();
281 |
282 | // Some coupon
283 | $discount = $pricing_factory->discountPrice(50.00, "percent");
284 | $discount->setDescription('Super-Saver Coupon');
285 |
286 | // Typical local sales tax
287 | $tax = $pricing_factory->taxPrice(10.00, TaxPrice::EXCLUSIVE);
288 | $tax->setDescription("Sales tax");
289 | ```
290 |
291 | Then we let the PricingFactory initialize our ItemPriceCollection, and each ItemPrice over our data set.
292 |
293 | ```php
294 | $item_collection = $pricing_factory->itemPriceCollection();
295 |
296 | foreach ($products as $product) {
297 | $item = $pricing_factory->itemPrice($product['amount'], $product['qty']);
298 | $item->setDescription($product['desc']);
299 | $item->setTax($tax);
300 |
301 | if ('Apples' === $product['desc']) {
302 | $item->setDiscount($discount);
303 | }
304 | $item_collection->append($item);
305 | }
306 |
307 | $item_collection->discountAmount($discount); // 0.75
308 | $item_collection->taxAmount($tax); // 0.825
309 | $item_collection->subtotal(); // 9.00
310 | $item_collection->totalAfterTax(); // 9.825
311 | $item_collection->totalAfterDiscount(); // 8.25
312 | $item_collection->total(); // 9.075
313 | ```
314 |
315 | You may also exclude specific taxes by their type when calculating totals:
316 |
317 | ```php
318 | $item_collection->excludeTax(TaxPrice::EXCLUSIVE)->taxAmount($tax); // 0.00
319 | $item_collection->excludeTax(TaxPrice::EXCLUSIVE)->totalAfterTax(); // 9.00
320 | $item_collection->excludeTax(TaxPrice::EXCLUSIVE)->total(); // 8.25
321 | $item_collection->total(); // 9.075 (item tax exclusions in the collection are reset after each call to a ::total..., the ::taxAmount, or ::discountAmount)
322 | ```
323 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blesta/pricing",
3 | "description": "A library for handling pricing and pricing modifiers",
4 | "keywords": ["pricing", "tax", "discount", "totals"],
5 | "homepage": "http://github.com/blesta/pricing",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Cody Phillips",
10 | "email": "therealclphillips@gmail.com"
11 | },
12 | {
13 | "name": "Tyson Phillips"
14 | }
15 | ],
16 | "require": {
17 | "php": ">=5.4.0"
18 | },
19 | "require-dev": {
20 | "phpunit/phpunit": "~4.6",
21 | "squizlabs/php_codesniffer": "~2.3",
22 | "satooshi/php-coveralls": "^1.0"
23 | },
24 | "autoload": {
25 | "psr-4": {
26 | "Blesta\\Pricing\\": "src/"
27 | }
28 | },
29 | "autoload-dev": {
30 | "psr-4": {
31 | "Blesta\\Pricing\\Tests\\": "tests/"
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | tests/
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 | src/
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ruleset.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | PSR2 without namespace enforcement.
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/Collection/ItemPriceCollection.php:
--------------------------------------------------------------------------------
1 | collection[] = $price;
36 | return $this;
37 | }
38 |
39 | /**
40 | * Removes an ItemPrice from the collection
41 | *
42 | * @param ItemPrice $price An item to remove from the collection
43 | * @return ItemPriceCollection reference to this
44 | */
45 | #[\ReturnTypeWillChange]
46 | public function remove(ItemPrice $price)
47 | {
48 | // Remove all instances of the price from the collection
49 | foreach ($this->collection as $index => $item) {
50 | if ($item === $price) {
51 | unset($this->collection[$index]);
52 | }
53 | }
54 |
55 | return $this;
56 | }
57 |
58 | /**
59 | * Retrieves the count of all ItemPrice objects in the collection
60 | *
61 | * @return int The number of ItemPrice objects in the collection
62 | */
63 | #[\ReturnTypeWillChange]
64 | public function count()
65 | {
66 | return count($this->collection);
67 | }
68 |
69 | /**
70 | * Retrieves the total price of all items within the collection including taxes without discounts
71 | *
72 | * @return float The total price including taxes without including discounts
73 | */
74 | #[\ReturnTypeWillChange]
75 | public function totalAfterTax()
76 | {
77 | $total = 0;
78 | foreach ($this->collection as $item) {
79 | $total += $item->totalAfterTax();
80 | }
81 |
82 | // Reset any discount amounts or excluded tax types back
83 | $this->resetDiscounts();
84 | $this->resetTaxes();
85 |
86 | return $total;
87 | }
88 |
89 | /**
90 | * Retrieves the total price of all items within the collection including discounts without taxes
91 | *
92 | * @return float The total price including discounts without including taxes
93 | */
94 | #[\ReturnTypeWillChange]
95 | public function totalAfterDiscount()
96 | {
97 | $total = 0;
98 | foreach ($this->collection as $item) {
99 | $total += $item->totalAfterDiscount();
100 | }
101 |
102 | // Reset any discount amounts or excluded tax types back
103 | $this->resetDiscounts();
104 | $this->resetTaxes();
105 |
106 | return $total;
107 | }
108 |
109 | /**
110 | * Retrieves the subtotal of all items within the collection
111 | *
112 | * @return float The subtotal of all items in the collection
113 | */
114 | #[\ReturnTypeWillChange]
115 | public function subtotal()
116 | {
117 | // Sum the subtotals of each ItemPrice
118 | $total = 0;
119 | foreach ($this->collection as $item_price) {
120 | $total += $item_price->subtotal();
121 | }
122 | $this->resetDiscounts();
123 |
124 | return $total;
125 | }
126 |
127 | /**
128 | * Retrieves the total of all items within the collection
129 | *
130 | * @return float The total of all items in the collection
131 | */
132 | #[\ReturnTypeWillChange]
133 | public function total()
134 | {
135 | // Sum the totals of each ItemPrice
136 | $total = 0;
137 | foreach ($this->collection as $item_price) {
138 | $total += $item_price->total();
139 | }
140 |
141 | // Reset any discount amounts or excluded tax types back
142 | $this->resetDiscounts();
143 | $this->resetTaxes();
144 |
145 | return $total;
146 | }
147 |
148 | /**
149 | * Retrieves the total tax amount for all ItemPrice's within the collection
150 | *
151 | * @param TaxPrice $tax A TaxPrice to apply to all ItemPrice's in the collection, ignoring
152 | * any TaxPrice's that may already be set on the items within the collection (optional)
153 | * @param string $type The type of tax for which to retrieve amounts (optional)
154 | */
155 | #[\ReturnTypeWillChange]
156 | public function taxAmount(TaxPrice $tax = null, $type = null)
157 | {
158 | $total = 0;
159 | foreach ($this->collection as $item_price) {
160 | $total += $item_price->taxAmount($tax, $type);
161 | }
162 |
163 | // Reset any discount amounts or excluded tax types back
164 | $this->resetDiscounts();
165 | $this->resetTaxes();
166 |
167 | return $total;
168 | }
169 |
170 | /**
171 | * Retrieves the total discount amount for all items within the collection
172 | *
173 | * @param DiscountPrice $discount A DiscountPrice to apply to all ItemPrice's in the
174 | * collection, ignoring any DiscountPrice's that may already be set on the items within
175 | * the collection (optional)
176 | */
177 | #[\ReturnTypeWillChange]
178 | public function discountAmount(DiscountPrice $discount = null)
179 | {
180 | // Apply the given discount to all items
181 | $total = 0;
182 | // Calculate the discount amount from each item's own discounts
183 | foreach ($this->collection as $item_price) {
184 | $total += $item_price->discountAmount($discount);
185 | }
186 |
187 | // Reset any discount amounts or excluded tax types back
188 | $this->resetDiscounts();
189 | $this->resetTaxes();
190 |
191 | return $total;
192 | }
193 |
194 | /**
195 | * Retrieves a list of all unique TaxPrice objects apart of this collection
196 | *
197 | * @return array An array of TaxPrice objects
198 | */
199 | #[\ReturnTypeWillChange]
200 | public function taxes()
201 | {
202 | // Include unique instances of TaxPrice
203 | $taxes = [];
204 | foreach ($this->collection as $item_price) {
205 | foreach ($item_price->taxes() as $tax_price) {
206 | if (!in_array($tax_price, $taxes, true)) {
207 | $taxes[] = $tax_price;
208 | }
209 | }
210 | }
211 |
212 | return $taxes;
213 | }
214 |
215 | /**
216 | * Retrieves a list of all unique DiscountPrice objects apart of this collection
217 | *
218 | * @return array An array of DiscountPrice objects
219 | */
220 | #[\ReturnTypeWillChange]
221 | public function discounts()
222 | {
223 | // Include unique instances of DiscountPrice
224 | $discounts = [];
225 | foreach ($this->collection as $item_price) {
226 | foreach ($item_price->discounts() as $discount_price) {
227 | if (!in_array($discount_price, $discounts, true)) {
228 | $discounts[] = $discount_price;
229 | }
230 | }
231 | }
232 |
233 | return $discounts;
234 | }
235 |
236 | /**
237 | * Resets the applied discount amounts for all ItemPrice's in the collection
238 | */
239 | #[\ReturnTypeWillChange]
240 | public function resetDiscounts()
241 | {
242 | foreach ($this->collection as $item_price) {
243 | $item_price->resetDiscounts();
244 | }
245 | }
246 |
247 | /**
248 | * Marks the given tax type as not shown in totals returned by all ItemPrices in the collection
249 | *
250 | * @param string $tax_type The type of tax to exclude
251 | * @return ItemPriceCollection A reference to this object
252 | */
253 | #[\ReturnTypeWillChange]
254 | public function excludeTax($tax_type)
255 | {
256 | foreach ($this->collection as $item_price) {
257 | $item_price->excludeTax($tax_type);
258 | }
259 |
260 | return $this;
261 | }
262 |
263 | /**
264 | * Resets the list of tax types for all ItemPrices in the collection
265 | */
266 | #[\ReturnTypeWillChange]
267 | public function resetTaxes()
268 | {
269 | foreach ($this->collection as $item_price) {
270 | $item_price->resetTaxes();
271 | }
272 | }
273 |
274 | /**
275 | * Merges this ItemPriceCollection with the given ItemPriceCollection to produce
276 | * a new ItemPriceCollection for ItemPrices that share a key.
277 | *
278 | * The resulting ItemPriceCollection is composed of ItemPrices as constructed by
279 | * the given comparator.
280 | *
281 | * Multiple items sharing the same key from the same collection are subject to
282 | * being merged multiple times in the order in which they appear in the collection.
283 | *
284 | * @param ItemPriceCollection $collection The collection to be merged
285 | * @param ItemComparatorInterface $comparator The comparator used to merge item prices
286 | */
287 | #[\ReturnTypeWillChange]
288 | public function merge(ItemPriceCollection $collection, ItemComparatorInterface $comparator)
289 | {
290 | // Set a new collection for the merged results
291 | $price_collection = new self();
292 |
293 | foreach ($collection as $new_item) {
294 | foreach ($this as $current_item) {
295 | // Only items with matching non-null keys may be merged
296 | if ($current_item->key() !== null
297 | && $current_item->key() === $new_item->key()
298 | && ($item = $comparator->merge($current_item, $new_item))
299 | ) {
300 | $price_collection->append($item);
301 | }
302 | }
303 | }
304 |
305 | return $price_collection;
306 | }
307 |
308 | /**
309 | * Retrieves the item in the collection at the current pointer
310 | *
311 | * @return mixed The ItemPrice in the collection at the current position, otherwise null
312 | */
313 | #[\ReturnTypeWillChange]
314 | public function current()
315 | {
316 | return (
317 | $this->valid()
318 | ? $this->collection[$this->position]
319 | : null
320 | );
321 | }
322 |
323 | /**
324 | * Retrieves the index currently being pointed at in the collection
325 | *
326 | * @return int The index of the position in the collection
327 | */
328 | #[\ReturnTypeWillChange]
329 | public function key()
330 | {
331 | return $this->position;
332 | }
333 |
334 | /**
335 | * Moves the pointer to the next item in the collection
336 | */
337 | #[\ReturnTypeWillChange]
338 | public function next()
339 | {
340 | // Set the next position to the position of the next item in the collection
341 | $position = $this->position;
342 | foreach ($this->collection as $index => $item) {
343 | if ($index > $position) {
344 | $this->position = $index;
345 | break;
346 | }
347 | }
348 |
349 | // If there is no next item in the collection, increment the position instead
350 | if ($position == $this->position) {
351 | ++$this->position;
352 | }
353 | }
354 |
355 | /**
356 | * Moves the pointer to the first item in the collection
357 | */
358 | #[\ReturnTypeWillChange]
359 | public function rewind()
360 | {
361 | // Reset the array pointer to the first entry in the collection
362 | reset($this->collection);
363 |
364 | // Set the position to the first entry in the collection if there is one
365 | $first_index = key($this->collection);
366 | $this->position = $first_index === null
367 | ? 0
368 | : $first_index;
369 | }
370 |
371 | /**
372 | * Determines whether the current pointer references a valid item in the collection
373 | *
374 | * @return bool True if the pointer references a valid item in the collection, false otherwise
375 | */
376 | #[\ReturnTypeWillChange]
377 | public function valid()
378 | {
379 | return array_key_exists($this->position, $this->collection);
380 | }
381 | }
382 |
--------------------------------------------------------------------------------
/src/Description/AbstractPriceDescription.php:
--------------------------------------------------------------------------------
1 | description = $description;
22 | }
23 |
24 | /**
25 | * Retrieves the price description
26 | *
27 | * @return string The price description
28 | */
29 | public function getDescription()
30 | {
31 | return $this->description;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Description/PriceDescriptionInterface.php:
--------------------------------------------------------------------------------
1 | amount = $amount;
30 | $this->type = $type;
31 | }
32 |
33 | /**
34 | * Retrieves the price amount
35 | *
36 | * @return float The price amount
37 | */
38 | public function amount()
39 | {
40 | return $this->amount;
41 | }
42 |
43 | /**
44 | * Retrieves the price type
45 | *
46 | * @return string The price type
47 | */
48 | public function type()
49 | {
50 | return $this->type;
51 | }
52 |
53 | /**
54 | * {@inheritdoc}
55 | */
56 | public function reset()
57 | {
58 | // Nothing to do
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Modifier/DiscountPrice.php:
--------------------------------------------------------------------------------
1 | discount_remaining = 0;
39 | if ('percent' !== $this->type) {
40 | $this->discount_remaining = $amount;
41 | }
42 | }
43 |
44 | /**
45 | * Determines the price remaining after discount.
46 | * If the discount is an amount type, the discount off will be determined from
47 | * the discount amount remaining rather than the full discount amount.
48 | *
49 | * @param float $price The base price before discount
50 | * @return float The $price after discount
51 | */
52 | public function off($price)
53 | {
54 | $discount = $this->on($price);
55 |
56 | // Update the running total of the discount amount remaining
57 | if ('percent' !== $this->type) {
58 | // The usable discount amount must consider the total discount remaining
59 | $applied_discount = min($this->discount_remaining, abs($discount));
60 |
61 | // Update the total discount remaining and set the discount off
62 | $this->discount_remaining -= $applied_discount;
63 | $discount = $applied_discount;
64 | }
65 |
66 | return $price - abs($discount);
67 | }
68 |
69 | /**
70 | * Determines the discount amount from the given price
71 | *
72 | * @param float $price The base price before discount
73 | * @return float The discount amount
74 | */
75 | public function on($price)
76 | {
77 | // Percent discount may cover at most the entire price
78 | if ('percent' === $this->type) {
79 | return ($this->amount > 100 ? $price : $price * $this->amount / 100);
80 | } else {
81 | $discount = min(abs($price), $this->discount_remaining);
82 | return ($price >= 0 ? $discount : -$discount);
83 | }
84 | }
85 |
86 | /**
87 | * {@inheritdoc}
88 | */
89 | public function reset()
90 | {
91 | $this->discount_remaining = $this->amount;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/Modifier/ItemComparatorInterface.php:
--------------------------------------------------------------------------------
1 | subtract = $subtract;
42 |
43 | parent::__construct($amount, $type);
44 | }
45 |
46 | /**
47 | * Determines the price after removing tax (from inclusive tax)
48 | *
49 | * @param float $price The price
50 | * @return float The $price without tax
51 | */
52 | public function off($price)
53 | {
54 | if (TaxPrice::INCLUSIVE == $this->type || TaxPrice::INCLUSIVE_CALCULATED == $this->type) {
55 | return $price - $this->on($price);
56 | }
57 | return $price;
58 | }
59 |
60 | /**
61 | * Determines the amount of tax for the given price
62 | *
63 | * @param float $price The price
64 | * @return float The tax amount
65 | */
66 | public function on($price)
67 | {
68 | if (TaxPrice::INCLUSIVE_CALCULATED == $this->type) {
69 | return ( $price / ( 100 + $this->amount ) ) * $this->amount;
70 | } else {
71 | return max(0, $this->amount / 100) * $price;
72 | }
73 | }
74 |
75 | /**
76 | * Determines the price including tax
77 | *
78 | * @param float $price The price before tax
79 | * @return float The price including tax
80 | */
81 | public function including($price)
82 | {
83 | if (TaxPrice::EXCLUSIVE == $this->type) {
84 | return $price + $this->on($price);
85 | }
86 | return $price;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/PricingFactory.php:
--------------------------------------------------------------------------------
1 | true,
40 | TaxPrice::EXCLUSIVE => true,
41 | TaxPrice::INCLUSIVE_CALCULATED => true,
42 | ];
43 | /**
44 | * @var bool Whether to apply discounts before calculating tax
45 | */
46 | protected $discount_taxes = true;
47 |
48 | /**
49 | * Initialize the item price
50 | *
51 | * @param float $price The unit price
52 | * @param int $qty The quantity of unit prices (optional, default 1)
53 | * @param string $key A unique identifier (optional, default null)
54 | */
55 | public function __construct($price, $qty = 1, $key = null)
56 | {
57 | parent::__construct($price, $qty, $key);
58 |
59 | // Reset the internal discount subtotal
60 | $this->resetDiscountSubtotal();
61 | }
62 |
63 | /**
64 | * Assigns a discount to the item
65 | *
66 | * @param DiscountPrice $discount A discount object to set for the item
67 | */
68 | public function setDiscount(DiscountPrice $discount)
69 | {
70 | // Disallow duplicates from being set
71 | if (!in_array($discount, $this->discounts, true)) {
72 | $this->discounts[] = $discount;
73 | }
74 | }
75 |
76 | /**
77 | * Assigns a TaxPrice to the item
78 | * Passing multiple TaxPrice arguments will set them to be compounded
79 | *
80 | * @throws InvalidArgumentException If something other than a TaxPrice was given
81 | */
82 | public function setTax()
83 | {
84 | $taxes = func_get_args();
85 | foreach ($taxes as $tax) {
86 | // Only a TaxPrice instance is accepted
87 | if (!($tax instanceof TaxPrice)) {
88 | throw new InvalidArgumentException(sprintf(
89 | '%s requires an instance of %s, %s given.',
90 | 'setTax',
91 | 'TaxPrice',
92 | gettype($tax)
93 | ));
94 | }
95 | }
96 |
97 | // Check for duplicate TaxPrice instances from the given arguments
98 | foreach ($taxes as $i => $tax_price_i) {
99 | foreach ($taxes as $j => $tax_price_j) {
100 | // Throw exception if the same instance of a TaxPrice was given multiple times
101 | if ($j > $i && $tax_price_i === $tax_price_j) {
102 | throw new InvalidArgumentException(sprintf(
103 | '%s requires unique instances of %s, but identical instances were given.',
104 | 'setTax',
105 | 'TaxPrice'
106 | ));
107 | }
108 | }
109 | }
110 |
111 | // Remove duplicate TaxPrice's that already exist
112 | foreach ($taxes as $index => $tax_price) {
113 | foreach ($this->taxes as $tax_row) {
114 | if (in_array($tax_price, $tax_row, true)) {
115 | unset($taxes[$index]);
116 | }
117 | }
118 | }
119 |
120 | $this->taxes[] = array_values($taxes);
121 | }
122 |
123 | /**
124 | * Sets whether to calculate tax before or after discounts are applied
125 | *
126 | * @param bool $discount_taxes True to calculate taxes after discounts are applied, false otherwise
127 | */
128 | public function setDiscountTaxes($discount_taxes)
129 | {
130 | $this->discount_taxes = $discount_taxes;
131 | }
132 |
133 | /**
134 | * Retrieves the total item price amount considering all taxes without discounts
135 | */
136 | public function totalAfterTax()
137 | {
138 | return parent::total() + $this->taxAmount();
139 | }
140 |
141 | /**
142 | * Retrieves the total item price amount considering all discounts without taxes
143 | */
144 | public function totalAfterDiscount()
145 | {
146 | return parent::total() - $this->discountAmount();
147 | }
148 |
149 | /**
150 | * Retrieves the total item price amount not considering discounts or taxes
151 | *
152 | * @return float The item subtotal
153 | */
154 | public function subtotal()
155 | {
156 | // inclusive_calculated taxes should be subtracted from the subtotal
157 | if (parent::total() < 0) {
158 | return parent::total() + abs($this->taxAmount(null, TaxPrice::INCLUSIVE_CALCULATED));
159 | } else {
160 | return parent::total() - abs($this->taxAmount(null, TaxPrice::INCLUSIVE_CALCULATED));
161 | }
162 | }
163 |
164 | /**
165 | * Retrieves the total item price amount considering all discounts and taxes
166 | *
167 | * @return float The total item price
168 | */
169 | public function total()
170 | {
171 | // discountAmount() is called twice: once by totalAfterDiscount, and once by taxAmount
172 | // The discount must be removed only once, so flag it to be ignored the second time
173 | $this->discount_amounts = [];
174 | $this->cache_discount_amounts = true;
175 | $total = $this->totalAfterDiscount();
176 |
177 | // Include tax without taking the discount off again, and reset the flag
178 | $this->cache_discount_amounts = false;
179 | $total += $this->taxAmount();
180 | $this->discount_amounts = [];
181 |
182 | return $total;
183 | }
184 |
185 | /**
186 | * Retrieves the total tax amount considering all item taxes, or just the given tax
187 | *
188 | * @param TaxPrice $tax A specific tax price whose tax to calculate for this item (optional, default null)
189 | * @param string $type The type of tax for which to retrieve amounts (optional) (see $tax_types for options)
190 | * @return float The total tax amount for all taxes set for this item, or the total tax amount
191 | * for the given tax price if given
192 | */
193 | public function taxAmount(TaxPrice $tax = null, $type = null)
194 | {
195 | // Determine the tax set on this item's price
196 | if ($tax) {
197 | $tax_amount = $this->amountTax($tax);
198 | } else {
199 | $tax_amount = $this->amountTaxAll($type);
200 | }
201 |
202 | return $tax_amount;
203 | }
204 |
205 | /**
206 | * Retrieves the tax amount for the given TaxPrice
207 | *
208 | * @param TaxPrice $tax A specific tax price whose tax to calculate for this item
209 | * @return float The total tax amount for the given TaxPrice
210 | */
211 | private function amountTax(TaxPrice $tax)
212 | {
213 | // Apply tax either before or after the discount
214 | $taxable_price = $this->discount_taxes ? $this->totalAfterDiscount() : parent::total();
215 | $tax_amount = 0;
216 |
217 | foreach ($this->taxes as $tax_group) {
218 | // Only calculate the tax amount if the tax exists in a tax group
219 | if (in_array($tax, $tax_group, true)) {
220 | $tax_amount = $this->compoundTaxAmount($tax_group, $taxable_price, $tax);
221 | }
222 | }
223 |
224 | return $tax_amount;
225 | }
226 |
227 | /**
228 | * Retrieves the total tax amount considering all item taxes
229 | *
230 | * @param string $type The type of tax for which to retrieve amounts (optional) (see $tax_types for options)
231 | * @return float The total tax amount for all taxes set for this item
232 | */
233 | private function amountTaxAll($type = null)
234 | {
235 | // Apply tax either before or after the discount
236 | $taxable_price = $this->discount_taxes ? $this->totalAfterDiscount() : parent::total();
237 | $tax_amount = 0;
238 |
239 | // Determine all taxes set on this item's price, compounded accordingly
240 | foreach ($this->taxes as $tax_group) {
241 | // Sum all taxes
242 | $tax_amount += $this->compoundTaxAmount($tax_group, $taxable_price, null, $type);
243 | }
244 |
245 | return $tax_amount;
246 | }
247 |
248 | /**
249 | * Retrieves the tax amount for a specific tax group
250 | *
251 | * @param array $tax_group A subset of the taxes array
252 | * @param float $taxable_price The total amount from which to calculate tax
253 | * @param TaxPrice $tax A specific tax from the group whose tax amount to retrieve (optional)
254 | * @param string $type The type of tax for which to retrieve amounts (optional) (see $tax_types for options)
255 | * @return float The total tax amount for all taxes set for this item in this group, or
256 | * the tax amount for the given TaxPrice
257 | */
258 | private function compoundTaxAmount(array $tax_group, $taxable_price, TaxPrice $tax = null, $type = null)
259 | {
260 | $compound_tax = 0;
261 | $tax_total = 0;
262 |
263 | foreach ($tax_group as $tax_price) {
264 | // If a tax type is specified, skip taxes that don't match it
265 | $tax_type = $tax_price->type();
266 |
267 | // Calculate the compound tax
268 | $tax_amount = $tax_price->on($taxable_price + $compound_tax);
269 |
270 | // Subtracted or inclusive_calculated taxes should be deducted from the compound tax
271 | if ($tax_price->subtract || $tax_type === TaxPrice::INCLUSIVE_CALCULATED) {
272 | $compound_tax -= $tax_amount;
273 | } else {
274 | $compound_tax += $tax_amount;
275 | }
276 |
277 | if ($type && $tax_type !== $type) {
278 | continue;
279 | }
280 |
281 | if (isset($this->tax_types[$tax_type]) && $this->tax_types[$tax_type]) {
282 | if ($tax_price->subtract) {
283 | // Subtract the tax amount instead of adding
284 | $tax_total -= $tax_amount;
285 | } elseif ($tax_type === TaxPrice::INCLUSIVE_CALCULATED && $type !== TaxPrice::INCLUSIVE_CALCULATED) {
286 | // Don't add inclusive_calculated taxes to the total unless specifically
287 | // fetching the total for that tax type
288 | $tax_total += 0;
289 | } else {
290 | // Add tax normally
291 | $tax_total += $tax_amount;
292 | }
293 | } elseif ($tax && $tax === $tax_price) {
294 | // Return a total of zero if we were given a tax, but it is of an excluded tax type
295 | return 0;
296 | }
297 |
298 | // Ignore any other group taxes, and only return the tax amount for the given TaxPrice
299 | if ($tax && $tax === $tax_price) {
300 | return $tax_amount;
301 | }
302 | }
303 |
304 | return $tax_total;
305 | }
306 |
307 | /**
308 | * Retrieves the total discount amount considering all item discounts, or just the given discount
309 | *
310 | * @param DiscountPrice $discount A specific discount price whose discount to calculate
311 | * for this item (optional, default null)
312 | * @return float The total discount amount for all discounts set for this item, or the
313 | * total discount amount for the given discount price if given
314 | */
315 | public function discountAmount(DiscountPrice $discount = null)
316 | {
317 | $total_discount = 0;
318 | $subtotal = parent::total();
319 |
320 | // Determine the discount set on this item's price
321 | if ($discount) {
322 | $total_discount = $this->amountDiscount($discount);
323 | } else {
324 | $total_discount = $this->amountDiscountAll();
325 | }
326 |
327 | // Total discount not to exceed the subtotal amount, neither positive nor negative
328 | return (
329 | $subtotal >= 0
330 | ? min($subtotal, $total_discount)
331 | : max($subtotal, $total_discount)
332 | );
333 | }
334 |
335 | /**
336 | * Retrieves the total discount amount considering the given discount
337 | *
338 | * @param DiscountPrice $discount A specific discount price whose discount to calculate
339 | * for this item
340 | * @return float The total discount amount for the given discount price
341 | */
342 | private function amountDiscount(DiscountPrice $discount)
343 | {
344 | $total_discount = 0;
345 |
346 | // Only calculate the discount amount if the discount is set for this item
347 | if (in_array($discount, $this->discounts, true)) {
348 | // Get the discount on the discounted subtotal remaining
349 | $total_discount = $discount->on($this->discounted_subtotal);
350 |
351 | // Update the discounted subtotal for this item by removing the amount discounted
352 | $this->discounted_subtotal = $discount->off($this->discounted_subtotal);
353 | }
354 |
355 | return $total_discount;
356 | }
357 |
358 | /**
359 | * Retrieves the total discount amount considering all item discounts
360 | *
361 | * @return float The total discount amount for all discounts set for this item
362 | */
363 | private function amountDiscountAll()
364 | {
365 | $subtotal = parent::total();
366 | $total_discount = 0;
367 |
368 | // Determine all the discounts set on this item's price
369 | foreach ($this->discounts as $key => $discount) {
370 | // Fetch the discount amount and remove it from the DiscountPrice,
371 | // or use the values previously cached
372 | if ($this->cache_discount_amounts || empty($this->discount_amounts)) {
373 | // Get the discount on the subtotal
374 | $discount_amount = $discount->on($subtotal);
375 | $total_discount += $discount_amount;
376 |
377 | // Cache the discount set for this DiscountPrice
378 | if ($this->cache_discount_amounts) {
379 | $this->discount_amounts[$key] = $discount_amount;
380 | }
381 |
382 | // Update the subtotal for this item to remove the amount discounted
383 | $subtotal = $discount->off($subtotal);
384 | } else {
385 | // Use the cached discount amount for this DiscountPrice
386 | $total_discount += $this->discount_amounts[$key];
387 | }
388 | }
389 |
390 | return $total_discount;
391 | }
392 |
393 | /**
394 | * Fetch all unique taxes set
395 | *
396 | * @param bool $unique True to fetch all unique taxes for the item,
397 | * or false to fetch all groups of taxes (default true)
398 | * @return array An array of TaxPrice objects when $unique is true,
399 | * or an array containing arrays of grouped TaxPrice objects
400 | */
401 | public function taxes($unique = true)
402 | {
403 | // Retrieve all taxes within their respective groups
404 | if (!$unique) {
405 | return $this->taxes;
406 | }
407 |
408 | // Retrieve all unique taxes
409 | $all_taxes = [];
410 | foreach ($this->taxes as $taxes) {
411 | $all_taxes = array_merge($all_taxes, array_values($taxes));
412 | }
413 | return $all_taxes;
414 | }
415 |
416 | /**
417 | * Fetch all unique discounts set
418 | *
419 | * @return array An array of DiscountPrice objects
420 | */
421 | public function discounts()
422 | {
423 | return $this->discounts;
424 | }
425 |
426 | /**
427 | * Resets the applied discount amounts for all assigned DiscountPrice's
428 | */
429 | public function resetDiscounts()
430 | {
431 | // Reset the internal discounted subtotal
432 | $this->resetDiscountSubtotal();
433 |
434 | // Reset each discount
435 | foreach ($this->discounts as $discount) {
436 | $discount->reset();
437 | }
438 | }
439 |
440 | /**
441 | * Resets the discounted subtotal used internally
442 | */
443 | private function resetDiscountSubtotal()
444 | {
445 | $this->discounted_subtotal = parent::total();
446 | }
447 |
448 | /**
449 | * Marks the given tax type as not shown in totals returned by this object
450 | *
451 | * @param string $tax_type The type of tax to exclude
452 | * @return A reference to this object
453 | */
454 | public function excludeTax($tax_type)
455 | {
456 | if (array_key_exists($tax_type, $this->tax_types)) {
457 | $this->tax_types[$tax_type] = false;
458 | }
459 |
460 | return $this;
461 | }
462 |
463 | /**
464 | * Resets the list of tax types to show in totals returned by this object
465 | */
466 | public function resetTaxes()
467 | {
468 | foreach ($this->tax_types as $type => $value) {
469 | $this->tax_types[$type] = true;
470 | }
471 | }
472 | }
473 |
--------------------------------------------------------------------------------
/src/Type/PriceInterface.php:
--------------------------------------------------------------------------------
1 | setPrice($price);
36 | $this->setQty($qty);
37 | $this->setKey($key);
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | public function price()
44 | {
45 | return $this->price;
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | public function setPrice($price)
52 | {
53 | $this->price = $price;
54 | }
55 |
56 | /**
57 | * {@inheritdoc}
58 | */
59 | public function qty()
60 | {
61 | return $this->qty;
62 | }
63 |
64 | /**
65 | * {@inheritdoc}
66 | */
67 | public function setQty($qty)
68 | {
69 | $this->qty = $qty;
70 | }
71 |
72 | /**
73 | * {@inheritdoc}
74 | */
75 | public function key()
76 | {
77 | return $this->key;
78 | }
79 |
80 | /**
81 | * {@inheritdoc}
82 | */
83 | public function setKey($key)
84 | {
85 | $this->key = $key;
86 | }
87 |
88 | /**
89 | * Retrieves the total price
90 | *
91 | * @return float The total price considering quantity
92 | */
93 | public function total()
94 | {
95 | return $this->qty * $this->price;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/tests/Unit/Collection/ItemPriceCollectionTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder('Blesta\Pricing\Type\ItemPrice')
23 | ->disableOriginalConstructor()
24 | ->getMock();
25 | $itemMock[] = $this->getMockBuilder('Blesta\Pricing\Type\ItemPrice')
26 | ->disableOriginalConstructor()
27 | ->getMock();
28 |
29 | $collection = new ItemPriceCollection();
30 |
31 | // Add 1 item
32 | $collection->append($itemMock[0]);
33 | $this->assertEquals(1, $collection->count());
34 |
35 | // Add a second item
36 | $collection->append($itemMock[1]);
37 | $this->assertEquals(2, $collection->count());
38 | }
39 |
40 | /**
41 | * @covers ::remove
42 | * @covers ::count
43 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::count
44 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append
45 | */
46 | public function testRemove()
47 | {
48 | $itemMock[] = $this->getMockBuilder('Blesta\Pricing\Type\ItemPrice')
49 | ->disableOriginalConstructor()
50 | ->getMock();
51 | $itemMock[] = $this->getMockBuilder('Blesta\Pricing\Type\ItemPrice')
52 | ->disableOriginalConstructor()
53 | ->getMock();
54 |
55 | $collection = new ItemPriceCollection();
56 | $this->assertEquals(0, $collection->count());
57 |
58 | foreach ($itemMock as $item) {
59 | $collection->append($item);
60 | }
61 |
62 | $this->assertEquals(count($itemMock), $collection->count());
63 | $collection->remove($itemMock[0]);
64 | $this->assertEquals(count($itemMock)-1, $collection->count());
65 | }
66 |
67 | /**
68 | * @covers ::totalAfterTax
69 | * @uses Blesta\Pricing\Collection\ItemPriceCollection
70 | * @uses Blesta\Pricing\Type\ItemPrice
71 | * @uses Blesta\Pricing\Modifier\DiscountPrice
72 | * @uses Blesta\Pricing\Modifier\TaxPrice
73 | * @uses Blesta\Pricing\Type\UnitPrice
74 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type
75 | * @dataProvider totalProvider
76 | */
77 | public function testTotalAfterTax(ItemPriceCollection $collection, array $expected_totals)
78 | {
79 | $this->assertEquals($expected_totals['total_with_tax'], $collection->totalAfterTax());
80 | }
81 |
82 | /**
83 | * @covers ::totalAfterDiscount
84 | * @uses Blesta\Pricing\Collection\ItemPriceCollection
85 | * @uses Blesta\Pricing\Type\ItemPrice
86 | * @uses Blesta\Pricing\Modifier\DiscountPrice
87 | * @uses Blesta\Pricing\Modifier\TaxPrice
88 | * @uses Blesta\Pricing\Type\UnitPrice
89 | * @dataProvider totalProvider
90 | */
91 | public function testTotalAfterDiscount(ItemPriceCollection $collection, array $expected_totals)
92 | {
93 | $this->assertEquals($expected_totals['total_with_discount'], $collection->totalAfterDiscount());
94 | }
95 |
96 | /**
97 | * @covers ::subtotal
98 | * @uses Blesta\Pricing\Collection\ItemPriceCollection
99 | * @uses Blesta\Pricing\Type\ItemPrice
100 | * @uses Blesta\Pricing\Modifier\DiscountPrice
101 | * @uses Blesta\Pricing\Modifier\TaxPrice
102 | * @uses Blesta\Pricing\Type\UnitPrice
103 | * @dataProvider totalProvider
104 | */
105 | public function testSubtotal(ItemPriceCollection $collection, array $expected_totals)
106 | {
107 | $this->assertEquals($expected_totals['subtotal'], $collection->subtotal());
108 | }
109 |
110 | /**
111 | * @covers ::total
112 | * @covers ::discountAmount
113 | * @uses Blesta\Pricing\Collection\ItemPriceCollection
114 | * @uses Blesta\Pricing\Type\ItemPrice
115 | * @uses Blesta\Pricing\Modifier\DiscountPrice
116 | * @uses Blesta\Pricing\Modifier\TaxPrice
117 | * @uses Blesta\Pricing\Type\UnitPrice
118 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type
119 | * @dataProvider totalProvider
120 | */
121 | public function testTotal(ItemPriceCollection $collection, array $expected_totals)
122 | {
123 | $this->assertEquals($expected_totals['total'], $collection->total());
124 | }
125 |
126 | /**
127 | *
128 | * @covers ::taxAmount
129 | * @uses Blesta\Pricing\Collection\ItemPriceCollection
130 | * @uses Blesta\Pricing\Type\ItemPrice
131 | * @uses Blesta\Pricing\Modifier\DiscountPrice
132 | * @uses Blesta\Pricing\Modifier\TaxPrice
133 | * @uses Blesta\Pricing\Type\UnitPrice
134 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type
135 | * @dataProvider totalProvider
136 | */
137 | public function testTaxAmount(ItemPriceCollection $collection, array $expected_totals)
138 | {
139 | $this->assertEquals($expected_totals['tax'], $collection->taxAmount());
140 | }
141 |
142 | /**
143 | * @covers ::discountAmount
144 | * @uses Blesta\Pricing\Collection\ItemPriceCollection
145 | * @uses Blesta\Pricing\Type\ItemPrice
146 | * @uses Blesta\Pricing\Modifier\DiscountPrice
147 | * @uses Blesta\Pricing\Modifier\TaxPrice
148 | * @uses Blesta\Pricing\Type\UnitPrice
149 | * @dataProvider totalProvider
150 | */
151 | public function testDiscountAmount(ItemPriceCollection $collection, array $expected_totals)
152 | {
153 | $this->assertEquals($expected_totals['discount'], $collection->discountAmount());
154 | }
155 |
156 | /**
157 | * @covers ::taxes
158 | * @uses Blesta\Pricing\Collection\ItemPriceCollection
159 | * @uses Blesta\Pricing\Type\ItemPrice
160 | * @uses Blesta\Pricing\Modifier\DiscountPrice
161 | * @uses Blesta\Pricing\Modifier\TaxPrice
162 | * @uses Blesta\Pricing\Type\UnitPrice
163 | * @dataProvider totalProvider
164 | */
165 | public function testTaxes(ItemPriceCollection $collection, array $expected_totals)
166 | {
167 | $this->assertContainsOnlyInstancesOf('Blesta\Pricing\Modifier\TaxPrice', $collection->taxes());
168 |
169 | // Exactly each expected tax should exist
170 | foreach ($expected_totals['taxes'] as $tax_price) {
171 | $this->assertContains($tax_price, $collection->taxes());
172 | }
173 | $this->assertCount(count($expected_totals['taxes']), $collection->taxes());
174 | }
175 |
176 | /**
177 | * @covers ::discounts
178 | * @uses Blesta\Pricing\Collection\ItemPriceCollection
179 | * @uses Blesta\Pricing\Type\ItemPrice
180 | * @uses Blesta\Pricing\Modifier\DiscountPrice
181 | * @uses Blesta\Pricing\Modifier\TaxPrice
182 | * @uses Blesta\Pricing\Type\UnitPrice
183 | * @dataProvider totalProvider
184 | */
185 | public function testDiscounts(ItemPriceCollection $collection, array $expected_totals)
186 | {
187 | $this->assertContainsOnlyInstancesOf('Blesta\Pricing\Modifier\DiscountPrice', $collection->discounts());
188 |
189 | // Exactly each expected discount should exist
190 | foreach ($expected_totals['discounts'] as $discount_price) {
191 | $this->assertContains($discount_price, $collection->discounts());
192 | }
193 | $this->assertCount(count($expected_totals['discounts']), $collection->discounts());
194 | }
195 |
196 | /**
197 | * @covers ::resetDiscounts
198 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append
199 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::discounts
200 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::discountAmount
201 | * @uses Blesta\Pricing\Type\ItemPrice
202 | * @uses Blesta\Pricing\Type\UnitPrice
203 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct
204 | * @uses Blesta\Pricing\Modifier\DiscountPrice
205 | */
206 | public function testResetDiscounts()
207 | {
208 | $item = new ItemPrice(10, 1);
209 | $discount = new DiscountPrice(1, 'amount');
210 | $item->setDiscount($discount);
211 |
212 | $collection = new ItemPriceCollection();
213 | $collection->append($item);
214 |
215 | // 1 discount on 10 is 1
216 | $this->assertEquals(1, $item->discountAmount());
217 | // Discount already applied. No discount again
218 | $this->assertEquals(0, $item->discountAmount());
219 |
220 | $collection->resetDiscounts();
221 |
222 | // 1 discount on 10 is 1
223 | $this->assertEquals(1, $item->discountAmount());
224 | }
225 |
226 | /**
227 | * Data provider for subtotal/total
228 | *
229 | * DO NOT SET DISCOUNT AMOUNTS THAT APPLY TO MULTIPLE ITEMS
230 | * DO NET SET AN ITEM TO MULTIPLE COLLECTIONS
231 | * Results will be incorrect without resetting item values appropriately
232 | *
233 | * @return array
234 | */
235 | public function totalProvider()
236 | {
237 | $testCases = [];
238 |
239 | for ($i = 0; $i < 2; $i++) {
240 | // Items with discounts and tax
241 | $tax_price = new TaxPrice(10, TaxPrice::EXCLUSIVE);
242 | $item1 = new ItemPrice(10, 2);
243 | $item1->setDiscount(new DiscountPrice(20, 'percent'));
244 | $item1->setDiscount(new DiscountPrice(1, 'amount'));
245 | $item1->setTax($tax_price);
246 |
247 | $item4 = new ItemPrice(10, 2);
248 | $item4->setDiscount(new DiscountPrice(10, 'percent'));
249 | $item4->setTax($tax_price);
250 |
251 | // Item with tax
252 | $item2 = new ItemPrice(6, 4);
253 | $item2->setTax(new TaxPrice(5, TaxPrice::EXCLUSIVE));
254 | $item3 = new ItemPrice(5, 5);
255 |
256 | $item5 = new ItemPrice(5.25, 3);
257 | $item5->setTax($tax_price);
258 |
259 | // Item with compound tax and discount
260 | $item6 = new ItemPrice(100.00, 1);
261 | $item6->setDiscount(new DiscountPrice(1.50, 'amount'));
262 | $item6->setTax(new TaxPrice(8, TaxPrice::EXCLUSIVE), new TaxPrice(5, TaxPrice::EXCLUSIVE));
263 |
264 | // For the second test case, test discounts that do not apply to taxes
265 | if ($i === 1) {
266 | $item1->setDiscountTaxes(false);
267 | $item2->setDiscountTaxes(false);
268 | $item3->setDiscountTaxes(false);
269 | $item4->setDiscountTaxes(false);
270 | $item5->setDiscountTaxes(false);
271 | $item6->setDiscountTaxes(false);
272 | }
273 |
274 | // Set collections of the items
275 | $collection1 = new ItemPriceCollection();
276 | $collection1->append($item1);
277 |
278 | $collection2 = new ItemPriceCollection();
279 | $collection2->append($item2)->append($item3);
280 |
281 | $collection3 = new ItemPriceCollection();
282 | $collection3->append($item4)->append($item5)->append($item6);
283 |
284 | $testCases[] = [$collection1, $this->getItemTotals($item1)];
285 | $testCases[] = [$collection2, $this->getItemTotals($item2, $item3)];
286 | $testCases[] = [$collection3, $this->getItemTotals($item4, $item5, $item6)];
287 | }
288 |
289 | return $testCases;
290 | }
291 |
292 | /**
293 | * Retrieves total information for a set of items
294 | *
295 | * @param ItemPrice An ItemPrice object
296 | * @param ...
297 | * @return array An array of totals combining each item price
298 | */
299 | private function getItemTotals()
300 | {
301 | // NOTE: 'total', 'total_with_discount', and 'discount' may be INCORRECT
302 | // if a DiscountPrice of type 'amount' applies to multiple items!
303 | $totals = [
304 | 'subtotal' => 0,
305 | 'total' => 0,
306 | 'total_with_tax' => 0,
307 | 'total_with_discount' => 0,
308 | 'tax' => 0,
309 | 'discount' => 0,
310 | 'taxes' => [],
311 | 'discounts' => []
312 | ];
313 |
314 | $args = func_get_args();
315 | foreach ($args as $item) {
316 | $totals['subtotal'] += $item->subtotal();
317 | $item->resetDiscounts();
318 | $totals['total'] += $item->total();
319 | $item->resetDiscounts();
320 | $totals['total_with_tax'] += $item->totalAfterTax();
321 | $item->resetDiscounts();
322 | $totals['total_with_discount'] += $item->totalAfterDiscount();
323 | $item->resetDiscounts();
324 | $totals['tax'] += $item->taxAmount();
325 | $item->resetDiscounts();
326 | $totals['discount'] += $item->discountAmount();
327 | $item->resetDiscounts();
328 | $totals['taxes'] = $this->getUnique($totals['taxes'], $item->taxes());
329 | $totals['discounts'] = $this->getUnique($totals['discounts'], $item->discounts());
330 | }
331 |
332 | return $totals;
333 | }
334 |
335 | /**
336 | * Includes unique items from $arr2 into $arr1
337 | *
338 | * @param array $arr1 An array of objects
339 | * @param array $arr2 An array of objects to include
340 | * @return array An array of unique objects
341 | */
342 | private function getUnique($arr1, $arr2)
343 | {
344 | foreach ($arr2 as $obj) {
345 | if (!in_array($obj, $arr1, true)) {
346 | $arr1 = array_merge($arr1, [$obj]);
347 | }
348 | }
349 |
350 | return $arr1;
351 | }
352 |
353 | /**
354 | * Tests totals of items that share amount discounts
355 | *
356 | * @covers ::discountAmount
357 | * @covers ::taxAmount
358 | * @covers ::total
359 | * @covers ::totalAfterTax
360 | * @covers ::totalAfterDiscount
361 | * @uses Blesta\Pricing\Collection\ItemPriceCollection
362 | * @uses Blesta\Pricing\Type\ItemPrice
363 | * @uses Blesta\Pricing\Modifier\DiscountPrice
364 | * @uses Blesta\Pricing\Modifier\TaxPrice
365 | * @uses Blesta\Pricing\Type\UnitPrice
366 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct
367 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type
368 | */
369 | public function testMultipleDiscountTotals()
370 | {
371 | // Test two items with the same discount amounts
372 | $collection = new ItemPriceCollection();
373 | $discount1 = new DiscountPrice(5, 'amount');
374 | $discount2 = new DiscountPrice(10, 'amount');
375 |
376 | $item1 = new ItemPrice(10, 2);
377 | $item1->setDiscount($discount1);
378 | $item1->setDiscount($discount2);
379 |
380 | $item2 = new ItemPrice(100, 1);
381 | $item1->setDiscount($discount1);
382 | $item1->setDiscount($discount2);
383 |
384 | $collection->append($item1)->append($item2);
385 |
386 | $this->assertEquals(0, $collection->taxAmount());
387 | $this->assertEquals(15, $collection->discountAmount());
388 | $this->assertEquals(105, $collection->totalAfterDiscount());
389 | $this->assertEquals(120, $collection->totalAfterTax());
390 | $this->assertEquals(105, $collection->total());
391 |
392 |
393 | // Test multiple items with varying taxes/discounts
394 | $collection->remove($item1)->remove($item2);
395 | $this->assertEquals(0, $collection->count());
396 |
397 | $discount3 = new DiscountPrice(50, 'amount');
398 | $tax = new TaxPrice(20, TaxPrice::EXCLUSIVE);
399 |
400 | $item3 = new ItemPrice(10, 1);
401 | $item3->setDiscount(new DiscountPrice(10, 'percent'));
402 | $item3->setDiscount($discount3);
403 | $item3->setTax(new TaxPrice(10, TaxPrice::EXCLUSIVE));
404 | $item3->setTax($tax);
405 |
406 | $item4 = new ItemPrice(1000, 2);
407 | $item4->setDiscount($discount3);
408 | $item4->setTax($tax);
409 |
410 | $collection->append($item3)->append($item4);
411 |
412 | $this->assertEquals(391.8, $collection->taxAmount());
413 | $this->assertEquals(51, $collection->discountAmount());
414 | $this->assertEquals(1959, $collection->totalAfterDiscount());
415 | $this->assertEquals(2401.8, $collection->totalAfterTax());
416 | $this->assertEquals(2350.8, $collection->total());
417 | }
418 |
419 | /**
420 | * @covers ::resetTaxes
421 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::excludeTax
422 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append
423 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::valid
424 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::current
425 | * @uses Blesta\Pricing\Type\ItemPrice
426 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
427 | * @uses Blesta\Pricing\Type\ItemPrice::excludeTax
428 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
429 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
430 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
431 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
432 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
433 | * @uses Blesta\Pricing\Type\UnitPrice::total
434 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type
435 | */
436 | public function testResetTaxes()
437 | {
438 | $item1 = new ItemPrice(10);
439 | $item2 = new ItemPrice(10);
440 |
441 | $collection = new ItemPriceCollection();
442 | $temp_collection = clone $collection;
443 | $collection->append($item1);
444 | $temp_collection->append($item2);
445 |
446 | $collection->excludeTax(TaxPrice::EXCLUSIVE);
447 | $this->assertNotEquals($temp_collection, $collection);
448 |
449 | $collection->resetTaxes();
450 | $this->assertEquals($collection, $temp_collection);
451 | }
452 |
453 | /**
454 | * @covers ::excludeTax
455 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append
456 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::valid
457 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::current
458 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
459 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
460 | * @uses Blesta\Pricing\Type\ItemPrice::excludeTax
461 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
462 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
463 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
464 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
465 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
466 | * @uses Blesta\Pricing\Type\UnitPrice::total
467 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type
468 | */
469 | public function testExcludeTax()
470 | {
471 | $item1 = new ItemPrice(10);
472 | $item2 = new ItemPrice(10);
473 |
474 | $collection = new ItemPriceCollection();
475 | $temp_collection = clone $collection;
476 | $collection->append($item1);
477 | $temp_collection->append($item2);
478 |
479 | $collection->excludeTax('invalid_tax_type');
480 | $this->assertEquals($temp_collection, $collection);
481 |
482 | $collection->excludeTax(TaxPrice::EXCLUSIVE);
483 | $this->assertNotEquals($temp_collection, $collection);
484 |
485 | $this->assertInstanceOf(
486 | 'Blesta\Pricing\Collection\ItemPriceCollection',
487 | $collection->excludeTax(TaxPrice::INCLUSIVE)
488 | );
489 | }
490 |
491 |
492 | /**
493 | * @covers ::merge
494 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append
495 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::count
496 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::current
497 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::next
498 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::rewind
499 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::valid
500 | * @uses Blesta\Pricing\Type\UnitPrice::key
501 | * @dataProvider mergeProvider
502 | */
503 | public function testMerge(ItemPriceCollection $collection1, ItemPriceCollection $collection2, $expected_items)
504 | {
505 | // Assume the merge will return the second ItemPrice back to us
506 | $comparator = $this->getMockBuilder('Blesta\Pricing\Modifier\ItemComparatorInterface')->getMock();
507 | $comparator->method('merge')
508 | ->will($this->returnArgument(1));
509 |
510 | $collection = $collection1->merge($collection2, $comparator);
511 | $this->assertInstanceOf('Blesta\Pricing\Collection\ItemPriceCollection', $collection);
512 |
513 | $this->assertEquals($expected_items, $collection->count());
514 | }
515 |
516 | /**
517 | * Data provider for mergeing item prices
518 | */
519 | public function mergeProvider()
520 | {
521 | $collection1 = new ItemPriceCollection();
522 | $collection2 = new ItemPriceCollection();
523 | $collection3 = new ItemPriceCollection();
524 | $collection4 = new ItemPriceCollection();
525 |
526 | $item1 = new ItemPrice(10, 1);
527 | $item1->setKey('id');
528 |
529 | $item2 = new ItemPrice(20, 2);
530 | $item2->setKey('test');
531 |
532 | $item3 = new ItemPrice(15, 1);
533 | $item3->setKey('id');
534 |
535 | $collection1->append($item1)->append($item2);
536 | $collection2->append($item2)->append($item3);
537 | $collection3->append($item3);
538 | $collection4->append($item2);
539 |
540 | return [
541 | [$collection1, $collection2, 2],
542 | [$collection1, $collection3, 1],
543 | [$collection2, $collection3, 1],
544 | [$collection3, $collection1, 1],
545 | [$collection3, $collection4, 0]
546 | ];
547 | }
548 |
549 | /**
550 | * @covers ::current
551 | * @covers ::valid
552 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
553 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
554 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
555 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
556 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
557 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
558 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
559 | * @uses Blesta\Pricing\Type\UnitPrice::total
560 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append
561 | */
562 | public function testCurrent()
563 | {
564 | $collection = new ItemPriceCollection();
565 |
566 | // No items exist, there is no current item
567 | $this->assertNull($collection->current());
568 |
569 | // One item
570 | $item = new ItemPrice(10, 1);
571 | $collection->append($item);
572 | $this->assertSame($item, $collection->current());
573 |
574 | // First item is still the current item
575 | $collection->append(new ItemPrice(30, 2));
576 | $this->assertSame($item, $collection->current());
577 | }
578 |
579 | /**
580 | * @covers ::key
581 | * @covers ::next
582 | */
583 | public function testKey()
584 | {
585 | $collection = new ItemPriceCollection();
586 |
587 | // No items exist, but the key should be at the first index
588 | $this->assertEquals(0, $collection->key());
589 |
590 | // Key should point at the next index
591 | $collection->next();
592 | $this->assertEquals(1, $collection->key());
593 | }
594 |
595 | /**
596 | * @covers ::next
597 | * @covers ::rewind
598 | * @covers ::current
599 | * @covers ::key
600 | * @covers ::valid
601 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
602 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
603 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
604 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
605 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
606 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
607 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
608 | * @uses Blesta\Pricing\Type\UnitPrice::total
609 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append
610 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::remove
611 | */
612 | public function testNext()
613 | {
614 | $collection = new ItemPriceCollection();
615 |
616 | // Position starts at 0, increments each time
617 | $collection->next();
618 | $collection->next();
619 | $collection->next();
620 | $this->assertEquals(3, $collection->key());
621 |
622 | $collection->rewind();
623 | $this->assertEquals(0, $collection->key());
624 |
625 | // Add items, ensure next() iterates to the next item, not just the next index
626 | $item1 = new ItemPrice(1, 1);
627 | $item2 = new ItemPrice(2, 1);
628 | $item3 = new ItemPrice(3, 1);
629 | $collection->append($item1)->append($item2)->append($item3);
630 | $collection->remove($item2);
631 | $this->assertSame($item1, $collection->current());
632 |
633 | $collection->next();
634 | $this->assertSame($item3, $collection->current());
635 |
636 | // Remove the current item, and then get the next item
637 | $collection->rewind();
638 | $this->assertSame($item1, $collection->current());
639 | $collection->remove($item1);
640 | $collection->next();
641 | $this->assertSame($item3, $collection->current());
642 |
643 | // The next item is outside the collection and should be null
644 | $collection->next();
645 | $this->assertNull($collection->current());
646 | }
647 |
648 | /**
649 | * @covers ::rewind
650 | * @covers ::key
651 | * @covers ::next
652 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append
653 | */
654 | public function testRewind()
655 | {
656 | $collection = new ItemPriceCollection();
657 |
658 | // No items exist
659 | $this->assertEquals(0, $collection->key());
660 |
661 | $collection->rewind();
662 | $this->assertEquals(0, $collection->key());
663 |
664 | // Increase the position
665 | $collection->next();
666 | $collection->next();
667 | $this->assertEquals(2, $collection->key());
668 |
669 | // Rewind the position back
670 | $collection->rewind();
671 | $this->assertEquals(0, $collection->key());
672 | }
673 |
674 | /**
675 | * @covers ::valid
676 | * @covers ::next
677 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append
678 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
679 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
680 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
681 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
682 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
683 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
684 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
685 | * @uses Blesta\Pricing\Type\UnitPrice::total
686 | */
687 | public function testValid()
688 | {
689 | $collection = new ItemPriceCollection();
690 |
691 | // No items exist, position is not valid
692 | $this->assertFalse($collection->valid());
693 |
694 | // Item takes first position
695 | $collection->append(new ItemPrice(10, 1));
696 | $this->assertTrue($collection->valid());
697 |
698 | // No item exists in the next position
699 | $collection->next();
700 | $this->assertFalse($collection->valid());
701 | }
702 | }
703 |
--------------------------------------------------------------------------------
/tests/Unit/Description/AbstractPriceDescriptionTest.php:
--------------------------------------------------------------------------------
1 | getMockForAbstractClass('Blesta\Pricing\Description\AbstractPriceDescription');
20 | $this->assertSame($description, $stub->getDescription());
21 |
22 | // Set my own description
23 | $description = '100x Product 1 - Limited Time Offer';
24 | $stub = $this->getMockForAbstractClass('Blesta\Pricing\Description\AbstractPriceDescription');
25 | $stub->setDescription($description);
26 | $this->assertSame($description, $stub->getDescription());
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/Unit/Modifier/AbstractPriceModifierTest.php:
--------------------------------------------------------------------------------
1 | getMockForAbstractClass('Blesta\Pricing\Modifier\AbstractPriceModifier', [$price, 'inclusive']);
19 | $this->assertSame($price, $stub->amount());
20 | }
21 |
22 | /**
23 | * @covers ::__construct
24 | * @covers ::type
25 | */
26 | public function testType()
27 | {
28 | $type = 'inclusive';
29 | $stub = $this->getMockForAbstractClass('Blesta\Pricing\Modifier\AbstractPriceModifier', [10.00, $type]);
30 | $this->assertSame($type, $stub->type());
31 | }
32 |
33 | /**
34 | * @covers ::__construct
35 | * @covers ::reset
36 | */
37 | public function testReset()
38 | {
39 | $stub = $this->getMockForAbstractClass('Blesta\Pricing\Modifier\AbstractPriceModifier', [10.00, 'inclusive']);
40 | $this->assertNull($stub->reset());
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/Unit/Modifier/DiscountPriceTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf('Blesta\Pricing\Modifier\DiscountPrice', new DiscountPrice(5.00, 'percent'));
19 | $this->assertInstanceOf('Blesta\Pricing\Modifier\DiscountPrice', new DiscountPrice(5.00, 'amount'));
20 | }
21 |
22 | /**
23 | * Test InvalidArgumentException is thrown
24 | *
25 | * @covers ::__construct
26 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct
27 | * @expectedException InvalidArgumentException
28 | */
29 | public function testConstructException()
30 | {
31 | // Amount must be non-negative
32 | $discount = new DiscountPrice(-1, 'amount');
33 | }
34 |
35 | /**
36 | * @covers ::off
37 | * @uses Blesta\Pricing\Modifier\DiscountPrice::on
38 | * @dataProvider offProvider
39 | */
40 | public function testOff($discount, $price, $price_after)
41 | {
42 | $this->assertEquals($price_after, $discount->off($price));
43 | }
44 |
45 | /**
46 | * Data provider
47 | *
48 | * @return array
49 | */
50 | public function offProvider()
51 | {
52 | return [
53 | [new DiscountPrice(0, 'percent'), 10.00, 10.00],
54 | [new DiscountPrice(10, 'percent'), 10.00, 9.00],
55 | [new DiscountPrice(50, 'percent'), 10.00, 5.00],
56 | [new DiscountPrice(100, 'percent'), 10.00, 0.00],
57 | [new DiscountPrice(200, 'percent'), 10.00, 0.00],
58 |
59 | [new DiscountPrice(0, 'percent'), -10.00, -10.00],
60 | [new DiscountPrice(10, 'percent'), -10.00, -11.00],
61 | [new DiscountPrice(50, 'percent'), -10.00, -15.00],
62 | [new DiscountPrice(100, 'percent'), -10.00, -20.00],
63 | [new DiscountPrice(200, 'percent'), -10.00, -20.00],
64 |
65 | [new DiscountPrice(0, 'amount'), 10.00, 10.00],
66 | [new DiscountPrice(3, 'amount'), 10.00, 7.00],
67 | [new DiscountPrice(50, 'amount'), 10.00, 0.00],
68 | [new DiscountPrice(100, 'amount'), 10.00, 0.00],
69 | [new DiscountPrice(3, 'amount'), -10.00, -13.00],
70 | [new DiscountPrice(50, 'amount'), -10.00, -20.00],
71 | [new DiscountPrice(100, 'amount'), -10.00, -20.00],
72 | ];
73 | }
74 |
75 | /**
76 | * Test amount discounts for multiple prices, as the discount remaining should
77 | * change with each price the discount is applied to
78 | *
79 | * @covers ::off
80 | * @uses Blesta\Pricing\Modifier\DiscountPrice::on
81 | * @dataProvider offMultipleProvider
82 | */
83 | public function testOffMultiple($discount, $prices, $price_after_all)
84 | {
85 | $price_remaining = 0;
86 | foreach ($prices as $price) {
87 | $price_remaining += $discount->off($price);
88 | }
89 |
90 | $this->assertEquals($price_after_all, $price_remaining);
91 | }
92 |
93 | /**
94 | * Data provider for testOffMultiple
95 | * @return array
96 | */
97 | public function offMultipleProvider()
98 | {
99 | return [
100 | [new DiscountPrice(0, 'amount'), [4, 10], 14],
101 | [new DiscountPrice(10, 'amount'), [4, 10], 4],
102 | [new DiscountPrice(20, 'amount'), [4, 10], 0],
103 | [new DiscountPrice(100, 'amount'), [4, 10], 0],
104 | [new DiscountPrice(10, 'amount'), [-4, -10], -24],
105 | [new DiscountPrice(20, 'amount'), [-4, -10], -28],
106 | [new DiscountPrice(100, 'amount'), [-4, -10], -28],
107 | [new DiscountPrice(5, 'amount'), [-4, 10], 1],
108 |
109 | [new DiscountPrice(10, 'amount'), [9, 5, 4], 8],
110 | ];
111 | }
112 |
113 | /**
114 | * @covers ::on
115 | * @dataProvider onProvider
116 | */
117 | public function testOn($discount, $price, $discount_price)
118 | {
119 | $this->assertEquals($discount_price, $discount->on($price));
120 | }
121 |
122 | /**
123 | * Data provider
124 | *
125 | * @return array
126 | */
127 | public function onProvider()
128 | {
129 | return [
130 | [new DiscountPrice(0, 'percent'), 10.00, 0.00],
131 | [new DiscountPrice(50, 'percent'), 10.00, 5.00],
132 | [new DiscountPrice(100, 'percent'), 10.00, 10.00],
133 | [new DiscountPrice(200, 'percent'), 10.00, 10.00],
134 | [new DiscountPrice(0, 'percent'), -10.00, 0.00],
135 | [new DiscountPrice(50, 'percent'), -10.00, -5.00],
136 | [new DiscountPrice(100, 'percent'), -10.00, -10.00],
137 | [new DiscountPrice(200, 'percent'), -10.00, -10.00],
138 |
139 | [new DiscountPrice(0, 'amount'), 10.00, 0.00],
140 | [new DiscountPrice(3, 'amount'), 10.00, 3.00],
141 | [new DiscountPrice(10, 'amount'), 10.00, 10.00],
142 | [new DiscountPrice(20, 'amount'), 10.00, 10.00],
143 | [new DiscountPrice(0, 'amount'), -10.00, 0.00],
144 | [new DiscountPrice(3, 'amount'), -10.00, -3.00],
145 | [new DiscountPrice(10, 'amount'), -10.00, -10.00],
146 | [new DiscountPrice(20, 'amount'), -10.00, -10.00],
147 | ];
148 | }
149 |
150 | /**
151 | * @covers ::reset
152 | * @uses Blesta\Pricing\Modifier\DiscountPrice::__construct
153 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct
154 | * @uses Blesta\Pricing\Modifier\DiscountPrice::off
155 | * @uses Blesta\Pricing\Modifier\DiscountPrice::on
156 | */
157 | public function testReset()
158 | {
159 | $discount = new DiscountPrice(10, 'amount');
160 | $this->assertEquals(0, $discount->off(10));
161 | $this->assertEquals(10, $discount->off(10));
162 |
163 | $discount->reset();
164 | $this->assertEquals(0, $discount->off(10));
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/tests/Unit/Modifier/TaxPriceTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf('Blesta\Pricing\Modifier\TaxPrice', new TaxPrice(10.00, TaxPrice::EXCLUSIVE));
19 | }
20 |
21 | /**
22 | * @covers ::__construct
23 | * @expectedException InvalidArgumentException
24 | */
25 | public function testConstructException()
26 | {
27 | // Amount must be non-negative
28 | $tax = new TaxPrice(-10, TaxPrice::EXCLUSIVE);
29 | }
30 |
31 | /**
32 | * @covers ::off
33 | * @uses Blesta\Pricing\Modifier\TaxPrice::on
34 | * @dataProvider offProvider
35 | */
36 | public function testOff($tax, $price, $price_after)
37 | {
38 | $this->assertEquals($price_after, round($tax->off($price), 2));
39 | }
40 |
41 | /**
42 | * Data provider
43 | *
44 | * @return array
45 | */
46 | public function offProvider()
47 | {
48 | return [
49 | [new TaxPrice(0, TaxPrice::EXCLUSIVE), 10.00, 10.00],
50 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), 10.00, 10.00],
51 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), 10.00, 10.00],
52 | [new TaxPrice(0, TaxPrice::EXCLUSIVE), -10.00, -10.00],
53 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), -10.00, -10.00],
54 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), -10.00, -10.00],
55 |
56 | [new TaxPrice(0, TaxPrice::INCLUSIVE), 10.00, 10.00],
57 | [new TaxPrice(50, TaxPrice::INCLUSIVE), 10.00, 5.00],
58 | [new TaxPrice(100, TaxPrice::INCLUSIVE), 10.00, 0.00],
59 | [new TaxPrice(0, TaxPrice::INCLUSIVE), -10.00, -10.00],
60 | [new TaxPrice(50, TaxPrice::INCLUSIVE), -10.00, -5.00],
61 | [new TaxPrice(100, TaxPrice::INCLUSIVE), -10.00, 0.00],
62 |
63 | [new TaxPrice(0, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 10.00],
64 | [new TaxPrice(50, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 6.67],
65 | [new TaxPrice(100, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 5.00],
66 | [new TaxPrice(0, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -10.00],
67 | [new TaxPrice(50, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -6.67],
68 | [new TaxPrice(100, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -5.00],
69 | ];
70 | }
71 |
72 | /**
73 | * @covers ::on
74 | * @dataProvider onProvider
75 | */
76 | public function testOn($tax, $price, $tax_amount)
77 | {
78 | $this->assertEquals($tax_amount, round($tax->on($price), 2));
79 | }
80 |
81 | /**
82 | * Data provider
83 | *
84 | * @return array
85 | */
86 | public function onProvider()
87 | {
88 | return [
89 | [new TaxPrice(0, TaxPrice::EXCLUSIVE), 10.00, 0.00],
90 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), 10.00, 5.00],
91 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), 10.00, 10.00],
92 | [new TaxPrice(0, TaxPrice::EXCLUSIVE), -10.00, 0.00],
93 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), -10.00, -5.00],
94 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), -10.00, -10.00],
95 |
96 | [new TaxPrice(0, TaxPrice::INCLUSIVE), 10.00, 0.00],
97 | [new TaxPrice(50, TaxPrice::INCLUSIVE), 10.00, 5.00],
98 | [new TaxPrice(100, TaxPrice::INCLUSIVE), 10.00, 10.00],
99 | [new TaxPrice(0, TaxPrice::INCLUSIVE), -10.00, 0.00],
100 | [new TaxPrice(50, TaxPrice::INCLUSIVE), -10.00, -5.00],
101 | [new TaxPrice(100, TaxPrice::INCLUSIVE), -10.00, -10.00],
102 |
103 | [new TaxPrice(0, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 0.00],
104 | [new TaxPrice(50, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 3.33],
105 | [new TaxPrice(100, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 5.00],
106 | [new TaxPrice(0, TaxPrice::INCLUSIVE_CALCULATED), -10.00, 0.00],
107 | [new TaxPrice(50, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -3.33],
108 | [new TaxPrice(100, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -5.00],
109 | ];
110 | }
111 |
112 | /**
113 | * @covers ::including
114 | * @uses Blesta\Pricing\Modifier\TaxPrice::on
115 | * @dataProvider includingProvider
116 | */
117 | public function testIncluding($tax, $price, $result)
118 | {
119 | $this->assertEquals($result, $tax->including($price));
120 | }
121 |
122 | /**
123 | * Data provider
124 | *
125 | * @return array
126 | */
127 | public function includingProvider()
128 | {
129 | return [
130 | [new TaxPrice(0, TaxPrice::EXCLUSIVE), 10.00, 10.00],
131 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), 10.00, 15.00],
132 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), 10.00, 20.00],
133 | [new TaxPrice(0, TaxPrice::EXCLUSIVE), -10.00, -10.00],
134 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), -10.00, -15.00],
135 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), -10.00, -20.00],
136 |
137 | [new TaxPrice(0, TaxPrice::INCLUSIVE), 10.00, 10.00],
138 | [new TaxPrice(50, TaxPrice::INCLUSIVE), 10.00, 10.00],
139 | [new TaxPrice(100, TaxPrice::INCLUSIVE), 10.00, 10.00],
140 | [new TaxPrice(0, TaxPrice::INCLUSIVE), -10.00, -10.00],
141 | [new TaxPrice(50, TaxPrice::INCLUSIVE), -10.00, -10.00],
142 | [new TaxPrice(100, TaxPrice::INCLUSIVE), -10.00, -10.00],
143 |
144 | [new TaxPrice(0, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 10.00],
145 | [new TaxPrice(50, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 10.00],
146 | [new TaxPrice(100, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 10.00],
147 | [new TaxPrice(0, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -10.00],
148 | [new TaxPrice(50, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -10.00],
149 | [new TaxPrice(100, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -10.00],
150 | ];
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/tests/Unit/PricingFactoryTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf('Blesta\Pricing\Type\UnitPrice', $pricing_factory->unitPrice(5.00, 2));
23 | }
24 |
25 | /**
26 | * @covers ::itemPrice
27 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
28 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
29 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
30 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
31 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
32 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
33 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
34 | * @uses Blesta\Pricing\Type\UnitPrice::total
35 | */
36 | public function testItemPrice()
37 | {
38 | $pricing_factory = new PricingFactory();
39 | $this->assertInstanceOf('Blesta\Pricing\Type\ItemPrice', $pricing_factory->itemPrice(5.00, 2));
40 | }
41 |
42 | /**
43 | * @covers ::discountPrice
44 | * @uses Blesta\Pricing\Modifier\DiscountPrice::__construct
45 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct
46 | */
47 | public function testDiscountPrice()
48 | {
49 | $pricing_factory = new PricingFactory();
50 | $this->assertInstanceOf(
51 | 'Blesta\Pricing\Modifier\DiscountPrice',
52 | $pricing_factory->discountPrice(20.00, 'percent')
53 | );
54 | }
55 |
56 | /**
57 | * @covers ::taxPrice
58 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct
59 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct
60 | */
61 | public function testTaxPrice()
62 | {
63 | $pricing_factory = new PricingFactory();
64 | $this->assertInstanceOf(
65 | 'Blesta\Pricing\Modifier\TaxPrice',
66 | $pricing_factory->taxPrice(7.75, 'exclusive')
67 | );
68 | }
69 |
70 | /**
71 | * @covers ::itemPriceCollection
72 | */
73 | public function testItemPriceCollection()
74 | {
75 | $pricing_factory = new PricingFactory();
76 | $this->assertInstanceOf(
77 | 'Blesta\Pricing\Collection\ItemPriceCollection',
78 | $pricing_factory->itemPriceCollection()
79 | );
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tests/Unit/Type/ItemPriceTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf('Blesta\Pricing\Type\ItemPrice', new ItemPrice(5.00, 1, 'id'));
28 | }
29 |
30 | /**
31 | * @covers ::setDiscount
32 | * @covers ::discounts
33 | * @dataProvider discountProvider
34 | */
35 | public function testSetDiscount($item, $discount)
36 | {
37 | $item->setDiscount($discount);
38 | $this->assertContains($discount, $item->discounts());
39 |
40 | $item->setDiscount($discount);
41 | $this->assertCount(
42 | 1,
43 | $item->discounts(),
44 | 'Only one instance of each discount may exist.'
45 | );
46 | }
47 |
48 | /**
49 | * Discount data provider
50 | *
51 | * @return array
52 | */
53 | public function discountProvider()
54 | {
55 | return [
56 | [new ItemPrice(10, 0), new DiscountPrice(10, 'percent')],
57 | [new ItemPrice(10), new DiscountPrice(10, 'percent')]
58 | ];
59 | }
60 |
61 |
62 | /**
63 | * @covers ::setTax
64 | * @covers ::taxes
65 | * @dataProvider taxProvider
66 | */
67 | public function testSetTax($item, array $taxes)
68 | {
69 | // Add all given taxes
70 | call_user_func_array([$item, 'setTax'], $taxes);
71 | foreach ($taxes as $tax) {
72 | $this->assertContains($tax, $item->taxes());
73 | }
74 |
75 | // At most, each tax should be added, but may be less if duplicates set
76 | $num_set_taxes = count($item->taxes());
77 | $this->assertLessThanOrEqual(count($taxes), $num_set_taxes);
78 |
79 | // The same tax should not be added again
80 | $item->setTax($taxes[0]);
81 | $this->assertCount(
82 | $num_set_taxes,
83 | $item->taxes(),
84 | 'Only one instance of each tax may exist.'
85 | );
86 | }
87 |
88 | /**
89 | * @covers ::setTax
90 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
91 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
92 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
93 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
94 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
95 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
96 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
97 | * @uses Blesta\Pricing\Type\UnitPrice::total
98 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct
99 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct
100 | * @expectedException InvalidArgumentException
101 | */
102 | public function testSetTaxInvalidException()
103 | {
104 | // Invalid argument: not TaxPrice
105 | $item = new ItemPrice(10);
106 | $item->setTax(new stdClass());
107 | }
108 |
109 | /**
110 | * @covers ::setTax
111 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
112 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
113 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
114 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
115 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
116 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
117 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
118 | * @uses Blesta\Pricing\Type\UnitPrice::total
119 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct
120 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct
121 | * @expectedException InvalidArgumentException
122 | */
123 | public function testSetTaxMultipleException()
124 | {
125 | // Invalid argument: Multiple TaxPrice's of the same instance
126 | $item = new ItemPrice(10);
127 | $tax_price = new TaxPrice(10, TaxPrice::EXCLUSIVE);
128 | $item->setTax($tax_price, $tax_price);
129 | }
130 |
131 | /**
132 | * Tax data provider
133 | *
134 | * @return array
135 | */
136 | public function taxProvider()
137 | {
138 | return [
139 | [new ItemPrice(10, 0), [new TaxPrice(10, TaxPrice::EXCLUSIVE)]],
140 | [new ItemPrice(10), [new TaxPrice(10, TaxPrice::EXCLUSIVE)]],
141 | [new ItemPrice(10, 1), [new TaxPrice(10, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::EXCLUSIVE)]],
142 | ];
143 | }
144 |
145 | /**
146 | * @covers ::setDiscountTaxes
147 | * @covers ::discountAmount
148 | * @covers ::taxAmount
149 | * @covers ::amountTax
150 | * @covers ::amountTaxAll
151 | * @covers ::totalAfterDiscount
152 | * @covers ::totalAfterTax
153 | * @covers ::total
154 | * @uses Blesta\Pricing\Type\ItemPrice
155 | * @uses Blesta\Pricing\Type\UnitPrice
156 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier
157 | * @uses Blesta\Pricing\Modifier\DiscountPrice
158 | * @uses Blesta\Pricing\Modifier\TaxPrice
159 | * @dataProvider taxingDiscountsProvider
160 | */
161 | public function testTaxingDiscounts(
162 | $discount_taxes,
163 | $item,
164 | array $discounts,
165 | array $taxes,
166 | $discount_amount,
167 | $tax_amount,
168 | $total_after_discount,
169 | $total_after_tax,
170 | $total
171 | ) {
172 | // Set whether taxes may be discounted
173 | $item->setDiscountTaxes($discount_taxes);
174 |
175 | // Apply the taxes and discounts to the item
176 | foreach ($discounts as $discount) {
177 | $item->setDiscount($discount);
178 | }
179 |
180 | foreach ($taxes as $tax) {
181 | $item->setTax($tax);
182 | }
183 |
184 | $this->assertEquals($discount_amount, round($item->discountAmount(), 2));
185 |
186 | // Reset discount amounts that were applied so that we can use them again to calculate the next total
187 | $item->resetDiscounts();
188 | $this->assertEquals($tax_amount, round($item->taxAmount(), 2));
189 |
190 | // Reset discount amounts that were applied so that we can use them again to calculate the next total
191 | $item->resetDiscounts();
192 | $this->assertEquals($total_after_discount, round($item->totalAfterDiscount(), 2));
193 |
194 | // Reset discount amounts that were applied so that we can use them again to calculate the next total
195 | $item->resetDiscounts();
196 | $this->assertEquals($total_after_tax, round($item->totalAfterTax(), 2));
197 |
198 | // Reset discount amounts that were applied so that we can use them again to calculate the next total
199 | $item->resetDiscounts();
200 | $this->assertEquals($total, round($item->total(), 2));
201 | }
202 |
203 | /**
204 | * Data provider for testing whether discounts apply before or after tax
205 | *
206 | * @return array
207 | */
208 | public function taxingDiscountsProvider()
209 | {
210 | return [
211 | [
212 | true, // discount taxes
213 | new ItemPrice(10, 1),
214 | [new DiscountPrice(10, 'percent')],
215 | [new TaxPrice(50, TaxPrice::EXCLUSIVE)],
216 | 1.00, // discount amount (10 * 0.1)
217 | 4.50, // tax amount [10 * (1 - 0.1)] * (0.5)
218 | 9.00, // total after discount [10 - (10 * 0.1)]
219 | 14.50, // total after tax ([)10 + 4.50)
220 | 13.50 // grand total (9 + 4.50)
221 | ],
222 | [
223 | false, // do not discount taxes
224 | new ItemPrice(10, 1),
225 | [new DiscountPrice(10, 'percent')],
226 | [new TaxPrice(50, TaxPrice::EXCLUSIVE)],
227 | 1.00, // discount amount (10 * 0.1)
228 | 5.00, // tax amount (10 * 0.5)
229 | 9.00, // total after discount [10 - (10 * 0.1)]
230 | 15.00, // total after tax (10 + 5)
231 | 14.00 // grand total (9 + 5)
232 | ],
233 | [
234 | true, // discount taxes
235 | new ItemPrice(10, 1),
236 | [new DiscountPrice(100, 'percent')],
237 | [new TaxPrice(50, TaxPrice::EXCLUSIVE)],
238 | 10.00, // discount amount (10 * 1)
239 | 0.00, // tax amount [10 * (1 - 1)] * (0.5)
240 | 0.00, // total after discount [10 - (10 * 1)]
241 | 10.00, // total after tax (10 + 0)
242 | 0.00 // grand total (0 + 0)
243 | ],
244 | [
245 | false, // do not discount taxes
246 | new ItemPrice(10, 1),
247 | [new DiscountPrice(100, 'percent')],
248 | [new TaxPrice(50, TaxPrice::EXCLUSIVE)],
249 | 10.00, // discount amount (10 * 1)
250 | 5.00, // tax amount (10 * 0.5)
251 | 0.00, // total after discount [10 - (10 * 1)]
252 | 15.00, // total after tax (10 + 5)
253 | 5.00 // grand total (0 + 5)
254 | ],
255 | [
256 | true, // discount taxes
257 | new ItemPrice(10, 1),
258 | [new DiscountPrice(10, 'percent'), new DiscountPrice(100, 'amount')],
259 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::INCLUSIVE)],
260 | 10.00, // discount amount (10)
261 | 0.00, // tax amount (0 * 0.5) + (0 * 0.1)
262 | 0.00, // total after discount (10 - 10)
263 | 10.00, // total after tax (10 + 0)
264 | 0.00 // grand total (0 + 0)
265 | ],
266 | [
267 | false, // discount taxes
268 | new ItemPrice(10, 1),
269 | [new DiscountPrice(10, 'percent'), new DiscountPrice(100, 'amount')],
270 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::INCLUSIVE)],
271 | 10.00, // discount amount (10)
272 | 6.00, // tax amount (10 * 0.5) + (10 * 0.1)
273 | 0.00, // total after discount (10 - 10)
274 | 16.00, // total after tax (10 + 6)
275 | 6.00 // grand total (0 + 6)
276 | ],
277 | [
278 | true, // discount taxes
279 | new ItemPrice(10, 1),
280 | [new DiscountPrice(10, 'percent'), new DiscountPrice(5, 'amount')],
281 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::INCLUSIVE)],
282 | 6.00, // discount amount (10 * 0.1) + 5
283 | 2.40, // tax amount [(10 - 6) * 0.5)] + [(10 - 6) * 0.1)]
284 | 4.00, // total after discount (10 - 6)
285 | 12.40, // total after tax (10 + 2.40)
286 | 6.40 // grand total (4 + 2.40)
287 | ],
288 | [
289 | false, // discount taxes
290 | new ItemPrice(10, 1),
291 | [new DiscountPrice(10, 'percent'), new DiscountPrice(5, 'amount')],
292 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::INCLUSIVE)],
293 | 6.00, // discount amount (10 * 0.1) + 5
294 | 6.00, // tax amount (10 * 0.5) + (10 * 0.1)
295 | 4.00, // total after discount (10 - 6)
296 | 16.00, // total after tax (10 + 6)
297 | 10.00 // grand total (4 + 6)
298 | ],
299 | [
300 | true, // discount taxes
301 | new ItemPrice(10, 1),
302 | [new DiscountPrice(10, 'percent'), new DiscountPrice(5, 'amount')],
303 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::INCLUSIVE_CALCULATED)],
304 | 6.00, // discount amount (10 * 0.1) + 5
305 | 2.00, // tax amount [(10 - 6) * 0.5)]
306 | 4.00, // total after discount (10 - 6)
307 | 12.00, // total after tax (10 + 2.00)
308 | 6.00 // grand total (4 + 2.00)
309 | ],
310 | [
311 | false, // discount taxes
312 | new ItemPrice(10, 1),
313 | [new DiscountPrice(10, 'percent'), new DiscountPrice(5, 'amount')],
314 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::INCLUSIVE_CALCULATED)],
315 | 6.00, // discount amount (10 * 0.1) + 5
316 | 5.00, // tax amount (10 * 0.5)
317 | 4.00, // total after discount (10 - 6)
318 | 15.00, // total after tax (10 + 5.00)
319 | 9.00 // grand total (4 + 5.00)
320 | ]
321 | ];
322 | }
323 |
324 | /**
325 | * @covers ::totalAfterTax
326 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
327 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
328 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
329 | * @uses Blesta\Pricing\Type\ItemPrice::setTax
330 | * @uses Blesta\Pricing\Type\ItemPrice::taxAmount
331 | * @uses Blesta\Pricing\Type\ItemPrice::amountTax
332 | * @uses Blesta\Pricing\Type\ItemPrice::amountTaxAll
333 | * @uses Blesta\Pricing\Type\ItemPrice::compoundTaxAmount
334 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterDiscount
335 | * @uses Blesta\Pricing\Type\ItemPrice::discountAmount
336 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscount
337 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscountAll
338 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
339 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
340 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
341 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
342 | * @uses Blesta\Pricing\Type\UnitPrice::total
343 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type
344 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct
345 | * @uses Blesta\Pricing\Modifier\TaxPrice::on
346 | * @dataProvider totalAfterTaxProvider
347 | */
348 | public function testTotalAfterTax($item, $taxes)
349 | {
350 | // No taxes set. Subtotal is the total after tax
351 | $this->assertEquals($item->subtotal(), $item->totalAfterTax());
352 |
353 | // Set taxes
354 | call_user_func_array([$item, 'setTax'], $taxes);
355 |
356 | // Total will be larger or smaller than the subtotal if it's positive or negative
357 | if ($item->subtotal() > 0) {
358 | $this->assertGreaterThan($item->subtotal(), $item->totalAfterTax());
359 | } elseif ($item->subtotal() < 0) {
360 | $this->assertLessThan($item->subtotal(), $item->totalAfterTax());
361 | } else {
362 | $this->assertEquals(0, $item->totalAfterTax());
363 | }
364 | }
365 |
366 | /**
367 | * Total After Tax data provider
368 | *
369 | * @return array
370 | */
371 | public function totalAfterTaxProvider()
372 | {
373 | return [
374 | [new ItemPrice(100.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE)]],
375 | [new ItemPrice(0.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE)]],
376 | [new ItemPrice(-100.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE)]],
377 |
378 | [new ItemPrice(100.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::EXCLUSIVE)]],
379 | [new ItemPrice(-100.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE), new TaxPrice(20, TaxPrice::EXCLUSIVE)]],
380 |
381 | [new ItemPrice(100.00, 2), [new TaxPrice(20, TaxPrice::INCLUSIVE_CALCULATED)]],
382 | [new ItemPrice(-100.00, 2), [new TaxPrice(20, TaxPrice::INCLUSIVE_CALCULATED)]],
383 |
384 | [new ItemPrice(100.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE), new TaxPrice(20, TaxPrice::INCLUSIVE_CALCULATED)]],
385 | [new ItemPrice(-100.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE), new TaxPrice(20, TaxPrice::INCLUSIVE_CALCULATED)]],
386 | ];
387 | }
388 |
389 | /**
390 | * @covers ::totalAfterDiscount
391 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
392 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
393 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
394 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount
395 | * @uses Blesta\Pricing\Type\ItemPrice::discountAmount
396 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscount
397 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscountAll
398 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
399 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
400 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
401 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
402 | * @uses Blesta\Pricing\Type\UnitPrice::total
403 | * @uses Blesta\Pricing\Modifier\DiscountPrice::__construct
404 | * @uses Blesta\Pricing\Modifier\DiscountPrice::on
405 | * @uses Blesta\Pricing\Modifier\DiscountPrice::off
406 | * @dataProvider totalAfterDiscountProvider
407 | */
408 | public function testTotalAfterDiscount($item, $discounts)
409 | {
410 | // No discounts set. Subtotal is the total after discount
411 | $this->assertEquals($item->subtotal(), $item->totalAfterDiscount());
412 |
413 | foreach ($discounts as $discount) {
414 | $item->setDiscount($discount);
415 | }
416 |
417 | // Total will be larger or smaller than the subtotal if it's positive or negative
418 | if ($item->subtotal() > 0) {
419 | $this->assertLessThanOrEqual($item->subtotal(), $item->totalAfterDiscount());
420 | } else {
421 | $this->assertGreaterThanOrEqual($item->subtotal(), $item->totalAfterDiscount());
422 | }
423 | }
424 |
425 | /**
426 | * Total After Discount data provider
427 | *
428 | * @return array
429 | */
430 | public function totalAfterDiscountProvider()
431 | {
432 | return [
433 | [new ItemPrice(10, 1), [new DiscountPrice(10, 'percent')]],
434 | [new ItemPrice(0, 1), [new DiscountPrice(10, 'percent')]],
435 | [new ItemPrice(10, 2), [new DiscountPrice(10, 'percent'), new DiscountPrice(10, 'percent')]],
436 | [new ItemPrice(-10, 2), [new DiscountPrice(10, 'percent')]],
437 | [new ItemPrice(10, 2), [new DiscountPrice(3, 'amount')]],
438 | [new ItemPrice(-10, 2), [new DiscountPrice(5, 'amount')]],
439 | ];
440 | }
441 |
442 | /**
443 | * @covers ::subtotal
444 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
445 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
446 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
447 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
448 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
449 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
450 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
451 | * @uses Blesta\Pricing\Type\UnitPrice::total
452 | * @dataProvider subtotalProvider
453 | */
454 | public function testSubtotal($price, $qty)
455 | {
456 | $item = new ItemPrice($price, $qty);
457 | $this->assertEquals($price*$qty, $item->subtotal());
458 | }
459 |
460 | /**
461 | * Subtotal provider
462 | *
463 | * @return array
464 | */
465 | public function subtotalProvider()
466 | {
467 | return [
468 | [10.00, 2],
469 | [10.00, 1],
470 | [10.00, 0],
471 | [0, 5],
472 | [-10.00, 1],
473 | [-10.00, 2],
474 | ];
475 | }
476 |
477 | /**
478 | * @covers ::total
479 | * @covers ::discountAmount
480 | * @covers ::amountDiscount
481 | * @covers ::amountDiscountAll
482 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
483 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
484 | * @uses Blesta\Pricing\Type\ItemPrice::setTax
485 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount
486 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscountTaxes
487 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterTax
488 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterDiscount
489 | * @uses Blesta\Pricing\Type\ItemPrice::taxAmount
490 | * @uses Blesta\Pricing\Type\ItemPrice::amountTax
491 | * @uses Blesta\Pricing\Type\ItemPrice::amountTaxAll
492 | * @uses Blesta\Pricing\Type\ItemPrice::compoundTaxAmount
493 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
494 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct
495 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type
496 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct
497 | * @uses Blesta\Pricing\Modifier\TaxPrice::on
498 | * @uses Blesta\Pricing\Modifier\DiscountPrice::__construct
499 | * @uses Blesta\Pricing\Modifier\DiscountPrice::on
500 | * @uses Blesta\Pricing\Modifier\DiscountPrice::off
501 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
502 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
503 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
504 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
505 | * @uses Blesta\Pricing\Type\UnitPrice::total
506 | */
507 | public function testTotal()
508 | {
509 | $item = new ItemPrice(10, 2);
510 |
511 | // Total is the subtotal when no taxes or discounts exist
512 | $this->assertEquals($item->subtotal(), $item->total());
513 |
514 | // Total is the total after tax when no discount exists
515 | $item->setTax(new TaxPrice(5.25, TaxPrice::EXCLUSIVE));
516 | $this->assertEquals($item->totalAfterTax(), $item->total());
517 |
518 | // Total is the total after discount and tax
519 | $item->setDiscount(new DiscountPrice(50, 'percent'));
520 | $this->assertEquals($item->totalAfterDiscount() + $item->taxAmount(), $item->total());
521 |
522 | // Total is the total after discount and tax even when not discounting the tax
523 | $item->setDiscountTaxes(false);
524 | $this->assertEquals($item->totalAfterDiscount() + $item->taxAmount(), $item->total());
525 | }
526 |
527 | /**
528 | * @covers ::discounts
529 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
530 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
531 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
532 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount
533 | * @uses Blesta\Pricing\Modifier\DiscountPrice::__construct
534 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
535 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
536 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
537 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
538 | * @uses Blesta\Pricing\Type\UnitPrice::total
539 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct
540 | */
541 | public function testDiscounts()
542 | {
543 | // No discounts set
544 | $item = new ItemPrice(10, 1);
545 | $this->assertEmpty($item->discounts());
546 |
547 | $discounts = [
548 | new DiscountPrice(10, TaxPrice::EXCLUSIVE),
549 | new DiscountPrice(5.00, TaxPrice::EXCLUSIVE)
550 | ];
551 |
552 | foreach ($discounts as $discount) {
553 | // Check the discount is set
554 | $item->setDiscount($discount);
555 | $this->assertContains($discount, $item->discounts());
556 | }
557 |
558 | // Check all discounts are set
559 | $this->assertCount(count($discounts), $item->discounts());
560 | }
561 |
562 | /**
563 | * @covers ::taxAmount
564 | * @covers ::amountTax
565 | * @covers ::amountTaxALl
566 | * @covers ::compoundTaxAmount
567 | * @uses Blesta\Pricing\Type\ItemPrice::setTax
568 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount
569 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscountTaxes
570 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterTax
571 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterDiscount
572 | * @uses Blesta\Pricing\Type\ItemPrice::discountAmount
573 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscount
574 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscountAll
575 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
576 | * @uses Blesta\Pricing\Type\ItemPrice::excludeTax
577 | * @uses Blesta\Pricing\Modifier\DiscountPrice
578 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type
579 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct
580 | * @uses Blesta\Pricing\Modifier\TaxPrice::on
581 | * @uses Blesta\Pricing\Modifier\TaxPrice::type
582 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
583 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
584 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
585 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
586 | * @uses Blesta\Pricing\Type\UnitPrice::total
587 | * @dataProvider taxAmountProvider
588 | */
589 | public function testTaxAmount(
590 | $item,
591 | $tax,
592 | $expected_amount,
593 | array $excluded_tax_types,
594 | $discount = null,
595 | $discount_amount = 0
596 | ) {
597 | // No taxes set. No tax amount
598 | $subtotal = $item->subtotal();
599 | $this->assertEquals(0, $item->taxAmount());
600 |
601 | // Set tax price
602 | $item->setTax($tax);
603 |
604 | // Exclude the given tax types from calculation
605 | foreach ($excluded_tax_types as $excluded_tax_type) {
606 | $item->excludeTax($excluded_tax_type);
607 | }
608 |
609 | $tax_price = in_array($tax->type(), $excluded_tax_types) ? 0 : $tax->on($subtotal);
610 | // Test the tax amount
611 | $this->assertEquals($tax_price, $item->taxAmount($tax));
612 |
613 | // Test with all taxes applied
614 | $tax_amount = $item->taxAmount();
615 | if ($subtotal >= 0) {
616 | $this->assertGreaterThanOrEqual(0, $tax_amount);
617 | } else {
618 | $this->assertLessThanOrEqual(0, $tax_amount);
619 | }
620 |
621 | // The given expected amount should be the end result with all taxes applied
622 | $this->assertEquals($expected_amount, $item->taxAmount());
623 | $this->assertEquals($tax_price, $item->taxAmount($tax));
624 |
625 | // Test that discounts are properly applied to taxes
626 | if ($discount) {
627 | $item->setDiscount($discount);
628 |
629 | // The item tax should be equal to the tax applied to the discounted amount
630 | $this->assertEquals($discount_amount, $item->taxAmount());
631 |
632 | // When discounts do not apply to taxes, the tax amount should be the tax applied to the
633 | // subtotal before discount
634 | $item->setDiscountTaxes(false);
635 | $this->assertEquals($tax_price, $item->taxAmount($tax));
636 | }
637 | }
638 |
639 | /**
640 | * Tax Amount provider
641 | *
642 | * @return array
643 | */
644 | public function taxAmountProvider()
645 | {
646 | return [
647 | [new ItemPrice(100, 2), new TaxPrice(10, TaxPrice::EXCLUSIVE), 20, []],
648 | [new ItemPrice(0, 2), new TaxPrice(10, TaxPrice::EXCLUSIVE), 0, []],
649 | [new ItemPrice(-100, 2), new TaxPrice(10, TaxPrice::EXCLUSIVE), -20, []],
650 | [new ItemPrice(100, 2), new TaxPrice(10, TaxPrice::INCLUSIVE), 20, []],
651 | [new ItemPrice(110, 2), new TaxPrice(10, TaxPrice::INCLUSIVE_CALCULATED), 0, []],
652 | [new ItemPrice(100, 2), new TaxPrice(10, TaxPrice::EXCLUSIVE), 0, [TaxPrice::EXCLUSIVE]],
653 | [new ItemPrice(100, 2), new TaxPrice(10, TaxPrice::EXCLUSIVE), 20, [TaxPrice::INCLUSIVE]],
654 | [new ItemPrice(100, 2), new TaxPrice(10, TaxPrice::INCLUSIVE), 0, [TaxPrice::INCLUSIVE]],
655 | [new ItemPrice(110, 2), new TaxPrice(10, TaxPrice::INCLUSIVE_CALCULATED), 0, [TaxPrice::INCLUSIVE_CALCULATED]],
656 | [
657 | new ItemPrice(100, 2),
658 | new TaxPrice(10, TaxPrice::EXCLUSIVE),
659 | 20,
660 | [TaxPrice::INCLUSIVE],
661 | new DiscountPrice(10, 'percent'),
662 | 18 // [(100 * 2) * 0.1] * (1 - 0.1)
663 | ],
664 | [
665 | new ItemPrice(100, 2),
666 | new TaxPrice(10, TaxPrice::EXCLUSIVE),
667 | 20,
668 | [TaxPrice::INCLUSIVE],
669 | new DiscountPrice(100, 'percent'),
670 | 0 // [(100 * 2) * 1] * (1 - 1)
671 | ],
672 | [
673 | new ItemPrice(110, 2),
674 | new TaxPrice(10, TaxPrice::INCLUSIVE_CALCULATED),
675 | 0,
676 | [TaxPrice::INCLUSIVE],
677 | new DiscountPrice(100, 'percent'),
678 | 0 // [(100 * 2) * 1] * (1 - 1)
679 | ]
680 | ];
681 | }
682 |
683 | /**
684 | * @covers ::taxAmount
685 | * @covers ::amountTax
686 | * @covers ::amountTaxALl
687 | * @covers ::compoundTaxAmount
688 | * @uses Blesta\Pricing\Type\ItemPrice::setTax
689 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount
690 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscountTaxes
691 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterTax
692 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterDiscount
693 | * @uses Blesta\Pricing\Type\ItemPrice::discountAmount
694 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscount
695 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscountAll
696 | * @uses Blesta\Pricing\Type\ItemPrice::excludeTax
697 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
698 | * @uses Blesta\Pricing\Modifier\DiscountPrice
699 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier
700 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct
701 | * @uses Blesta\Pricing\Modifier\TaxPrice::on
702 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
703 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
704 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
705 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
706 | * @uses Blesta\Pricing\Type\UnitPrice::total
707 | * @dataProvider taxAmountCompoundProvider
708 | */
709 | public function testTaxAmountCompound(
710 | $item,
711 | array $taxes,
712 | array $expected_tax_amounts,
713 | array $excluded_tax_types,
714 | $discount = null
715 | ) {
716 | // Set all taxes
717 | call_user_func_array([$item, 'setTax'], $taxes);
718 |
719 | // Exclude the given tax types from calculation
720 | foreach ($excluded_tax_types as $excluded_tax_type) {
721 | $item->excludeTax($excluded_tax_type);
722 | }
723 |
724 | // The tax amounts should be compounded, and only return the componud amount for that tax
725 | foreach ($taxes as $index => $tax) {
726 | $tax_amount = $item->taxAmount($tax);
727 | $this->assertEquals($expected_tax_amounts[$index], $tax_amount);
728 | }
729 |
730 | // Total tax amount is the sum of all expected amounts
731 | $expected_amount = 0;
732 | foreach ($expected_tax_amounts as $index => $amount) {
733 | $expected_amount += ($taxes[$index]->type() == TaxPrice::INCLUSIVE_CALCULATED ? 0 : $amount);
734 | }
735 | $this->assertEquals($expected_amount, $item->taxAmount());
736 |
737 | // Test that discounts are properly applied to taxes
738 | if ($discount) {
739 | $item->setDiscount($discount);
740 |
741 | $discount_ratio = 1 - ($discount->amount() / 100);
742 | foreach ($taxes as $index => $tax) {
743 | // Discount the tax
744 | $this->assertEquals($expected_tax_amounts[$index] * $discount_ratio, $item->taxAmount($tax));
745 | }
746 | $this->assertEquals($expected_amount * $discount_ratio, $item->taxAmount());
747 |
748 | // Now set it so that discounts are not applied to taxes, now the tax amount should be the tax
749 | // applied to the subtotal before discount
750 | $item->setDiscountTaxes(false);
751 | foreach ($taxes as $index => $tax) {
752 | $this->assertEquals($expected_tax_amounts[$index], $item->taxAmount($tax));
753 | }
754 | $this->assertEquals($expected_amount, $item->taxAmount());
755 | }
756 | }
757 |
758 | /**
759 | * Compound Tax Amount provider
760 | *
761 | * @return array
762 | */
763 | public function taxAmountCompoundProvider()
764 | {
765 | return [
766 | [
767 | new ItemPrice(100, 2),
768 | [
769 | new TaxPrice(10, TaxPrice::EXCLUSIVE),
770 | new TaxPrice(7.75, TaxPrice::EXCLUSIVE)
771 | ],
772 | [
773 | 20,
774 | 17.05
775 | ],
776 | []
777 | ],
778 | [
779 | new ItemPrice(100, 2),
780 | [
781 | new TaxPrice(10, TaxPrice::EXCLUSIVE),
782 | new TaxPrice(7.75, TaxPrice::EXCLUSIVE)
783 | ],
784 | [
785 | 20,
786 | 17.05
787 | ],
788 | [],
789 | new DiscountPrice(10, 'percent')
790 | ],
791 | [
792 | new ItemPrice(10, 3),
793 | [
794 | new TaxPrice(10, TaxPrice::EXCLUSIVE),
795 | new TaxPrice(5, TaxPrice::EXCLUSIVE),
796 | new TaxPrice(2.5, TaxPrice::EXCLUSIVE)
797 | ],
798 | [
799 | 3,
800 | 1.65,
801 | 0.86625
802 | ],
803 | []
804 | ],
805 | [
806 | new ItemPrice(10, 3),
807 | [
808 | new TaxPrice(10, TaxPrice::EXCLUSIVE),
809 | new TaxPrice(5, TaxPrice::EXCLUSIVE),
810 | new TaxPrice(2.5, TaxPrice::EXCLUSIVE)
811 | ],
812 | [
813 | 0,
814 | 0,
815 | 0
816 | ],
817 | [TaxPrice::EXCLUSIVE]
818 | ],
819 | [
820 | new ItemPrice(10, 3),
821 | [
822 | new TaxPrice(10, TaxPrice::INCLUSIVE),
823 | new TaxPrice(5, TaxPrice::INCLUSIVE),
824 | new TaxPrice(2.5, TaxPrice::EXCLUSIVE)
825 | ],
826 | [
827 | 3,
828 | 1.65,
829 | 0
830 | ],
831 | [TaxPrice::EXCLUSIVE]
832 | ],
833 | [
834 | new ItemPrice(10, 3),
835 | [
836 | new TaxPrice(10, TaxPrice::INCLUSIVE),
837 | new TaxPrice(5, TaxPrice::INCLUSIVE),
838 | new TaxPrice(2.5, TaxPrice::EXCLUSIVE)
839 | ],
840 | [
841 | 0,
842 | 0,
843 | 0
844 | ],
845 | [TaxPrice::INCLUSIVE, TaxPrice::EXCLUSIVE]
846 | ],
847 | [
848 | new ItemPrice(10, 3),
849 | [
850 | new TaxPrice(10, TaxPrice::INCLUSIVE),
851 | new TaxPrice(5, TaxPrice::INCLUSIVE),
852 | new TaxPrice(2.5, TaxPrice::EXCLUSIVE)
853 | ],
854 | [
855 | 0,
856 | 0,
857 | 0.86625
858 | ],
859 | [TaxPrice::INCLUSIVE]
860 | ],
861 | [
862 | new ItemPrice(10, 3),
863 | [
864 | new TaxPrice(2.5, TaxPrice::EXCLUSIVE)
865 | ],
866 | [
867 | 0.75
868 | ],
869 | [TaxPrice::INCLUSIVE]
870 | ],
871 | [
872 | new ItemPrice(100, 2),
873 | [
874 | new TaxPrice(10, TaxPrice::EXCLUSIVE),
875 | new TaxPrice(10, TaxPrice::INCLUSIVE_CALCULATED)
876 | ],
877 | [
878 | 20,
879 | 20
880 | ],
881 | [],
882 | new DiscountPrice(10, 'percent')
883 | ],
884 | ];
885 | }
886 |
887 | /**
888 | * @covers ::discountAmount
889 | * @covers ::amountDiscount
890 | * @covers ::amountDiscountAll
891 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount
892 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
893 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
894 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
895 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
896 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
897 | * @uses Blesta\Pricing\Type\UnitPrice::total
898 | * @dataProvider discountAmountProvider
899 | */
900 | public function testDiscountAmount($item, array $discounts, $expected_amount)
901 | {
902 | // No discount set
903 | $subtotal = $item->subtotal();
904 | $this->assertEquals(0, $item->discountAmount());
905 |
906 | foreach ($discounts as $discount) {
907 | $item->setDiscount($discount);
908 |
909 | // Test discount amount just for this discount
910 | $this->assertEquals($discount->on($subtotal), $item->discountAmount($discount));
911 | }
912 |
913 | // Test with all discounts applied
914 | if ($subtotal >= 0) {
915 | $this->assertLessThanOrEqual($subtotal, $item->discountAmount());
916 | } else {
917 | $this->assertGreaterThanOrEqual($subtotal, $item->discountAmount());
918 | }
919 |
920 | // The given expected amount should be the end result with all discounts applied
921 | $this->assertEquals($expected_amount, $item->discountAmount());
922 | }
923 |
924 | /**
925 | * Creates a stub of DiscountPrice
926 | *
927 | * @param mixed $value The value to mock from DiscountPrice::on
928 | * @return stub
929 | */
930 | protected function discountPriceMock($value)
931 | {
932 | $dp = $this->getMockBuilder('Blesta\Pricing\Modifier\DiscountPrice')
933 | ->disableOriginalConstructor()
934 | ->getMock();
935 | $dp->method('on')
936 | ->willReturn($value);
937 |
938 | return $dp;
939 | }
940 |
941 | /**
942 | * Discount amount provider
943 | *
944 | * @return array
945 | */
946 | public function discountAmountProvider()
947 | {
948 | return [
949 | [new ItemPrice(100, 2), [], 0],
950 | [new ItemPrice(100, 2), [$this->discountPriceMock(20)], 20],
951 | [
952 | new ItemPrice(100, 2),
953 | [
954 | $this->discountPriceMock(20),
955 | $this->discountPriceMock(40)
956 | ],
957 | 60
958 | ],
959 | [new ItemPrice(100, 2), [$this->discountPriceMock(200)], 200],
960 | [
961 | new ItemPrice(100, 2),
962 | [
963 | $this->discountPriceMock(2),
964 | $this->discountPriceMock(3.75)
965 | ],
966 | 5.75
967 | ],
968 | [
969 | new ItemPrice(100, 2),
970 | [
971 | $this->discountPriceMock(40),
972 | $this->discountPriceMock(2)
973 | ],
974 | 42
975 | ],
976 |
977 | [new ItemPrice(-100, 2), [$this->discountPriceMock(-20)], -20],
978 | [
979 | new ItemPrice(-100, 2),
980 | [
981 | $this->discountPriceMock(-20),
982 | $this->discountPriceMock(-40)
983 | ],
984 | -60
985 | ],
986 | [new ItemPrice(-100, 2), [$this->discountPriceMock(-200)], -200],
987 | [
988 | new ItemPrice(-100, 2),
989 | [
990 | $this->discountPriceMock(-2),
991 | $this->discountPriceMock(-3.75)
992 | ],
993 | -5.75
994 | ],
995 | [
996 | new ItemPrice(-100, 2),
997 | [
998 | $this->discountPriceMock(-40),
999 | $this->discountPriceMock(-2)
1000 | ],
1001 | -42
1002 | ],
1003 | ];
1004 | }
1005 |
1006 | /**
1007 | * @covers ::discountAmount
1008 | * @covers ::amountDiscount
1009 | * @covers ::amountDiscountAll
1010 | * @covers ::resetDiscounts
1011 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
1012 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
1013 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
1014 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount
1015 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
1016 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
1017 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
1018 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
1019 | * @uses Blesta\Pricing\Type\UnitPrice::total
1020 | * @uses Blesta\Pricing\Modifier\DiscountPrice::__construct
1021 | * @uses Blesta\Pricing\Modifier\DiscountPrice::on
1022 | * @uses Blesta\Pricing\Modifier\DiscountPrice::off
1023 | * @uses Blesta\Pricing\Modifier\DiscountPrice::reset
1024 | * @dataProvider discountAmountsProvider
1025 | */
1026 | public function testDiscountAmounts($item, array $discounts, array $expected_amounts)
1027 | {
1028 | // No discounts set
1029 | foreach ($discounts as $discount) {
1030 | $this->assertEquals(0, $item->discountAmount($discount));
1031 |
1032 | // Set the discount
1033 | $item->setDiscount($discount);
1034 | }
1035 |
1036 | for ($i=0; $i<2; $i++) {
1037 | // The index of the expected amounts coincide with the index of the discounts
1038 | foreach ($discounts as $index => $discount) {
1039 | $this->assertEquals($expected_amounts[$index], $item->discountAmount($discount));
1040 | }
1041 |
1042 | // The discounts must be reset before they can be tested again
1043 | foreach ($discounts as $index => $discount) {
1044 | // Discounts of zero will be equal, otherwise they should be different
1045 | if ($expected_amounts[$index] == 0) {
1046 | $this->assertEquals($expected_amounts[$index], $item->discountAmount($discount));
1047 | } else {
1048 | $this->assertNotEquals($expected_amounts[$index], $item->discountAmount($discount));
1049 | }
1050 | }
1051 | $item->resetDiscounts();
1052 | }
1053 |
1054 | $expected_amount = 0;
1055 | foreach ($expected_amounts as $amount) {
1056 | $expected_amount += $amount;
1057 | }
1058 | $this->assertEquals($expected_amount, $item->discountAmount());
1059 | }
1060 |
1061 | /**
1062 | * Provider for testDiscountAmounts
1063 | *
1064 | * @return array
1065 | */
1066 | public function discountAmountsProvider()
1067 | {
1068 | return [
1069 | [
1070 | new ItemPrice(10, 3),
1071 | [
1072 | new DiscountPrice(5.00, 'percent'),
1073 | new DiscountPrice(25.00, 'percent')
1074 | ],
1075 | [
1076 | 1.50,
1077 | 7.125
1078 | ]
1079 | ],
1080 | [
1081 | new ItemPrice(50, 1),
1082 | [
1083 | new DiscountPrice(10.00, 'percent'),
1084 | new DiscountPrice(10.00, 'amount'),
1085 | new DiscountPrice(50.00, 'percent'),
1086 | new DiscountPrice(3.00, 'amount'),
1087 | new DiscountPrice(2.5, 'amount'),
1088 | new DiscountPrice(50.5, 'percent'),
1089 | new DiscountPrice(6.25, 'amount'),
1090 | new DiscountPrice(10, 'percent'),
1091 | new DiscountPrice(1, 'amount'),
1092 | ],
1093 | [
1094 | 5,
1095 | 10,
1096 | 17.5,
1097 | 3,
1098 | 2.5,
1099 | 6.06,
1100 | 5.94,
1101 | 0,
1102 | 0
1103 | ]
1104 | ]
1105 | ];
1106 | }
1107 |
1108 | /**
1109 | * @covers ::resetDiscounts
1110 | * @covers ::resetDiscountSubtotal
1111 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
1112 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
1113 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
1114 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount
1115 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
1116 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
1117 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
1118 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
1119 | * @uses Blesta\Pricing\Type\UnitPrice::total
1120 | */
1121 | public function testResetDiscounts()
1122 | {
1123 | $discountMock = $this->getMockBuilder('Blesta\Pricing\Modifier\DiscountPrice')
1124 | ->disableOriginalConstructor()
1125 | ->getMock();
1126 | $discountMock->expects($this->once())
1127 | ->method('reset');
1128 |
1129 | $item = new ItemPrice(10);
1130 | $item->setDiscount($discountMock);
1131 | $item->resetDiscounts();
1132 | }
1133 |
1134 | /**
1135 | * @covers ::taxes
1136 | * @uses Blesta\Pricing\Type\ItemPrice::setTax
1137 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
1138 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
1139 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
1140 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
1141 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
1142 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
1143 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
1144 | * @uses Blesta\Pricing\Type\UnitPrice::total
1145 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct
1146 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct
1147 | * @dataProvider taxesProvider
1148 | */
1149 | public function testTaxes($unique, $taxes, $expected_count, $expected_total)
1150 | {
1151 | $item = new ItemPrice(10);
1152 |
1153 | foreach ($taxes as $tax_group) {
1154 | call_user_func_array([$item, 'setTax'], $tax_group);
1155 | }
1156 |
1157 | // Determine the total tax count based on $unique
1158 | $this->assertCount($expected_count, $item->taxes($unique));
1159 |
1160 | // Determine the total count of all taxes
1161 | $total = 0;
1162 | foreach ($item->taxes() as $group) {
1163 | $total++;
1164 | }
1165 | $this->assertEquals($expected_total, $total);
1166 | }
1167 |
1168 | /**
1169 | * Data provider for taxes
1170 | */
1171 | public function taxesProvider()
1172 | {
1173 | $tax = new TaxPrice(50, TaxPrice::EXCLUSIVE);
1174 |
1175 | return [
1176 | [
1177 | true,
1178 | [[]],
1179 | 0,
1180 | 0
1181 | ],
1182 | [
1183 | true,
1184 | [
1185 | [new TaxPrice(10, TaxPrice::EXCLUSIVE)]
1186 | ],
1187 | 1,
1188 | 1
1189 | ],
1190 | [
1191 | true,
1192 | [
1193 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), new TaxPrice(20, TaxPrice::EXCLUSIVE)],
1194 | [new TaxPrice(10, TaxPrice::EXCLUSIVE)]
1195 | ],
1196 | 3,
1197 | 3
1198 | ],
1199 | [
1200 | true,
1201 | [
1202 | [$tax, new TaxPrice(15, TaxPrice::EXCLUSIVE)],
1203 | [$tax]
1204 | ],
1205 | 2,
1206 | 2
1207 | ],
1208 | [
1209 | false,
1210 | [
1211 | [$tax]
1212 | ],
1213 | 1,
1214 | 1
1215 | ],
1216 | [
1217 | false,
1218 | [
1219 | [new TaxPrice(10, TaxPrice::EXCLUSIVE), new TaxPrice(15, TaxPrice::EXCLUSIVE)],
1220 | [new TaxPrice(25, TaxPrice::EXCLUSIVE)]
1221 | ],
1222 | 2,
1223 | 3
1224 | ]
1225 | ];
1226 | }
1227 |
1228 | /**
1229 | * @covers ::resetTaxes
1230 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
1231 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
1232 | * @uses Blesta\Pricing\Type\ItemPrice::excludeTax
1233 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
1234 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
1235 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
1236 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
1237 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
1238 | * @uses Blesta\Pricing\Type\UnitPrice::total
1239 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type
1240 | */
1241 | public function testResetTaxes()
1242 | {
1243 | $item = new ItemPrice(10);
1244 |
1245 | $item->excludeTax(TaxPrice::EXCLUSIVE);
1246 | $this->assertAttributeEquals(
1247 | [TaxPrice::INCLUSIVE => true, TaxPrice::EXCLUSIVE => false, TaxPrice::INCLUSIVE_CALCULATED => true],
1248 | 'tax_types',
1249 | $item
1250 | );
1251 |
1252 | $item->resetTaxes();
1253 | $this->assertAttributeEquals(
1254 | [TaxPrice::INCLUSIVE => true, TaxPrice::EXCLUSIVE => true, TaxPrice::INCLUSIVE_CALCULATED => true],
1255 | 'tax_types',
1256 | $item
1257 | );
1258 | }
1259 |
1260 | /**
1261 | * @covers ::excludeTax
1262 | * @uses Blesta\Pricing\Type\ItemPrice::__construct
1263 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal
1264 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal
1265 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
1266 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
1267 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
1268 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
1269 | * @uses Blesta\Pricing\Type\UnitPrice::total
1270 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type
1271 | */
1272 | public function testExcludeTax()
1273 | {
1274 | $item = new ItemPrice(10);
1275 |
1276 | $item->excludeTax('invalid_tax_type');
1277 | $this->assertAttributeEquals(
1278 | [TaxPrice::INCLUSIVE => true, TaxPrice::EXCLUSIVE => true, TaxPrice::INCLUSIVE_CALCULATED => true],
1279 | 'tax_types',
1280 | $item
1281 | );
1282 |
1283 | $item->excludeTax(TaxPrice::EXCLUSIVE);
1284 | $this->assertAttributeEquals(
1285 | [TaxPrice::INCLUSIVE => true, TaxPrice::EXCLUSIVE => false, TaxPrice::INCLUSIVE_CALCULATED => true],
1286 | 'tax_types',
1287 | $item
1288 | );
1289 |
1290 | $item->excludeTax(TaxPrice::INCLUSIVE);
1291 | $this->assertAttributeEquals(
1292 | [TaxPrice::INCLUSIVE => false, TaxPrice::EXCLUSIVE => false, TaxPrice::INCLUSIVE_CALCULATED => true],
1293 | 'tax_types',
1294 | $item
1295 | );
1296 |
1297 | $this->assertInstanceOf('Blesta\Pricing\Type\ItemPrice', $item->excludeTax(TaxPrice::INCLUSIVE));
1298 | }
1299 | }
1300 |
--------------------------------------------------------------------------------
/tests/Unit/Type/UnitPriceTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf('Blesta\Pricing\Type\UnitPrice', new UnitPrice(5.00, 1, 'id'));
21 | }
22 |
23 | /**
24 | * @covers ::price
25 | * @covers ::setPrice
26 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
27 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
28 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
29 | */
30 | public function testPrice()
31 | {
32 | $price = 5.00;
33 | $qty = 2;
34 | $unit_price = new UnitPrice($price, $qty);
35 | $this->assertEquals($price, $unit_price->price());
36 |
37 | $price = 15.00;
38 | $unit_price->setPrice($price);
39 | $this->assertEquals($price, $unit_price->price());
40 | }
41 |
42 | /**
43 | * @covers ::qty
44 | * @covers ::setQty
45 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
46 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
47 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
48 | */
49 | public function testQty()
50 | {
51 | // Test default quantity
52 | $price = 5.00;
53 | $unit_price = new UnitPrice($price);
54 | $this->assertEquals(1, $unit_price->qty());
55 |
56 | $qty = 5;
57 | $unit_price->setQty($qty);
58 | $this->assertEquals($qty, $unit_price->qty());
59 | }
60 |
61 | /**
62 | * @covers ::key
63 | * @covers ::setKey
64 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
65 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
66 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
67 | */
68 | public function testKey()
69 | {
70 | // No key is null
71 | $price = 5.00;
72 | $unit_price = new UnitPrice($price);
73 | $this->assertNull($unit_price->key());
74 |
75 | // Set a key
76 | $key = 'id';
77 | $unit_price->setKey($key);
78 | $this->assertEquals($key, $unit_price->key());
79 | }
80 |
81 | /**
82 | * @covers ::total
83 | * @uses Blesta\Pricing\Type\UnitPrice::__construct
84 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice
85 | * @uses Blesta\Pricing\Type\UnitPrice::setQty
86 | * @uses Blesta\Pricing\Type\UnitPrice::setKey
87 | */
88 | public function testTotal()
89 | {
90 | $price = 5.00;
91 | $qty = 2;
92 | $unit_price = new UnitPrice($price, $qty);
93 | $this->assertEquals($qty * $price, $unit_price->total());
94 | }
95 | }
96 |
--------------------------------------------------------------------------------