diff --git a/src/multisafepay/api/paths/orders/order_id/refund/request/components/checkout_data.py b/src/multisafepay/api/paths/orders/order_id/refund/request/components/checkout_data.py index ae6667c..a5421c4 100644 --- a/src/multisafepay/api/paths/orders/order_id/refund/request/components/checkout_data.py +++ b/src/multisafepay/api/paths/orders/order_id/refund/request/components/checkout_data.py @@ -155,7 +155,7 @@ def refund_by_merchant_item_id( refund_item = found_item.clone() refund_item.add_quantity(quantity) - refund_item.add_unit_price(found_item.unit_price * -1.0) + refund_item.add_unit_price(found_item.unit_price * -1) self.add_item(refund_item) diff --git a/src/multisafepay/api/paths/orders/order_manager.py b/src/multisafepay/api/paths/orders/order_manager.py index 38066b4..9ae9092 100644 --- a/src/multisafepay/api/paths/orders/order_manager.py +++ b/src/multisafepay/api/paths/orders/order_manager.py @@ -39,6 +39,7 @@ from multisafepay.api.shared.description import Description from multisafepay.client.client import Client from multisafepay.util.dict_utils import dict_empty +from multisafepay.util.json_encoder import DecimalEncoder from multisafepay.util.message import MessageList, gen_could_not_created_msg from multisafepay.value_object.amount import Amount from multisafepay.value_object.currency import Currency @@ -127,7 +128,7 @@ def create( CustomApiResponse: The custom API response containing the created order data. """ - json_data = json.dumps(request_order.to_dict()) + json_data = json.dumps(request_order.to_dict(), cls=DecimalEncoder) response: ApiResponse = self.client.create_post_request( "json/orders", request_body=json_data, @@ -152,7 +153,7 @@ def update( CustomApiResponse: The custom API response containing the updated order data. """ - json_data = json.dumps(update_request.to_dict()) + json_data = json.dumps(update_request.to_dict(), cls=DecimalEncoder) encoded_order_id = self.encode_path_segment(order_id) response = self.client.create_patch_request( f"json/orders/{encoded_order_id}", @@ -182,7 +183,7 @@ def capture( CustomApiResponse: The custom API response containing the capture data. """ - json_data = json.dumps(capture_request.to_dict()) + json_data = json.dumps(capture_request.to_dict(), cls=DecimalEncoder) encoded_order_id = self.encode_path_segment(order_id) response = self.client.create_post_request( @@ -223,7 +224,7 @@ def refund( CustomApiResponse: The custom API response containing the refund data. """ - json_data = json.dumps(request_refund.to_dict()) + json_data = json.dumps(request_refund.to_dict(), cls=DecimalEncoder) encoded_order_id = self.encode_path_segment(order_id) response = self.client.create_post_request( f"json/orders/{encoded_order_id}/refunds", diff --git a/src/multisafepay/api/shared/cart/cart_item.py b/src/multisafepay/api/shared/cart/cart_item.py index e8d6279..50ec3b4 100644 --- a/src/multisafepay/api/shared/cart/cart_item.py +++ b/src/multisafepay/api/shared/cart/cart_item.py @@ -9,11 +9,15 @@ import copy import math -from typing import Dict, List, Optional +from decimal import Decimal +from typing import TYPE_CHECKING, Optional, Type, Union from multisafepay.exception.invalid_argument import InvalidArgumentException from multisafepay.model.api_model import ApiModel -from multisafepay.value_object.weight import Weight +from pydantic import validator + +if TYPE_CHECKING: + from multisafepay.value_object.weight import Weight class CartItem(ApiModel): @@ -32,7 +36,7 @@ class CartItem(ApiModel): product_url: (Optional[str]) The product URL. quantity: (Optional[int]) The quantity. tax_table_selector: (Optional[str]) The tax table selector. - unit_price: (Optional[float]) The unit price. + unit_price: (Optional[Decimal]) The unit price as a precise Decimal value. weight: (Optional[Weight]) The weight. """ @@ -43,12 +47,36 @@ class CartItem(ApiModel): image: Optional[str] merchant_item_id: Optional[str] name: Optional[str] - options: Optional[List[Dict]] + options: Optional[list] product_url: Optional[str] quantity: Optional[int] tax_table_selector: Optional[str] - unit_price: Optional[float] - weight: Optional[Weight] + unit_price: Optional[Decimal] + + @validator("unit_price", pre=True) + def convert_unit_price_to_decimal( + cls: Type["CartItem"], + value: Union[str, float, Decimal, None], + ) -> Optional[Decimal]: + """ + Convert unit_price to Decimal for precise monetary calculations. + + Parameters + ---------- + value (Union[str, int, float, Decimal, None]): The value to convert. + + Returns + ------- + Optional[Decimal]: The converted Decimal value or None. + + """ + if value is None: + return None + if isinstance(value, Decimal): + return value + return Decimal(str(value)) + + weight: Optional["Weight"] def add_cashback(self: "CartItem", cashback: str) -> "CartItem": """ @@ -149,7 +177,7 @@ def add_name(self: "CartItem", name: str) -> "CartItem": self.name = name return self - def add_options(self: "CartItem", options: List[Dict]) -> "CartItem": + def add_options(self: "CartItem", options: list) -> "CartItem": """ Add options to the cart item. @@ -216,23 +244,29 @@ def add_tax_table_selector( self.tax_table_selector = tax_table_selector return self - def add_unit_price(self: "CartItem", unit_price: float) -> "CartItem": + def add_unit_price( + self: "CartItem", + unit_price: Union[Decimal, float, str], + ) -> "CartItem": """ - Add unit price to the cart item. + Add unit price to the cart item with precise Decimal conversion. Parameters ---------- - unit_price: (float) The unit price to be added. + unit_price: (Union[Decimal, float, int, str]) The unit price to be added. Returns ------- CartItem: The updated CartItem instance. """ - self.unit_price = unit_price + if isinstance(unit_price, Decimal): + self.unit_price = unit_price + else: + self.unit_price = Decimal(str(unit_price)) return self - def add_weight(self: "CartItem", weight: Weight) -> "CartItem": + def add_weight(self: "CartItem", weight: "Weight") -> "CartItem": """ Add weight to the cart item. @@ -250,10 +284,10 @@ def add_weight(self: "CartItem", weight: Weight) -> "CartItem": def add_tax_rate_percentage( self: "CartItem", - tax_rate_percentage: int, + tax_rate_percentage: Union[int, Decimal], ) -> "CartItem": """ - Add tax rate percentage to the cart item. + Add tax rate percentage to the cart item using precise Decimal arithmetic. This method sets the tax rate percentage for the cart item. The tax rate should be a non-negative number. @@ -263,7 +297,7 @@ def add_tax_rate_percentage( Parameters ---------- - tax_rate_percentage: (int) The tax rate percentage to be added. + tax_rate_percentage: (Union[int, Decimal]) The tax rate percentage to be added. Returns ------- @@ -275,13 +309,17 @@ def add_tax_rate_percentage( "Tax rate percentage cannot be negative.", ) - if math.isnan(tax_rate_percentage) or math.isinf(tax_rate_percentage): + if isinstance(tax_rate_percentage, float) and ( + math.isnan(tax_rate_percentage) or math.isinf(tax_rate_percentage) + ): raise InvalidArgumentException( "Tax rate percentage cannot be special floats.", ) try: - rating = tax_rate_percentage / 100 + # Use Decimal for precise division + percentage_decimal = Decimal(str(tax_rate_percentage)) + rating = percentage_decimal / Decimal("100") self.tax_table_selector = str(rating) except (ValueError, TypeError) as e: raise InvalidArgumentException( @@ -290,9 +328,12 @@ def add_tax_rate_percentage( return self - def add_tax_rate(self: "CartItem", tax_rate: float) -> "CartItem": + def add_tax_rate( + self: "CartItem", + tax_rate: Union[Decimal, float], + ) -> "CartItem": """ - Add tax rate to the cart item. + Add tax rate to the cart item using Decimal for precision. This method sets the tax rate for the cart item. The tax rate should be a non-negative number. @@ -302,7 +343,7 @@ def add_tax_rate(self: "CartItem", tax_rate: float) -> "CartItem": Parameters ---------- - tax_rate: (float) The tax rate to be added. + tax_rate: (Union[Decimal, float]) The tax rate to be added. Returns ------- @@ -312,12 +353,17 @@ def add_tax_rate(self: "CartItem", tax_rate: float) -> "CartItem": if tax_rate < 0: raise InvalidArgumentException("Tax rate cannot be negative.") - if math.isnan(tax_rate) or math.isinf(tax_rate): + if isinstance(tax_rate, float) and ( + math.isnan(tax_rate) or math.isinf(tax_rate) + ): raise InvalidArgumentException( "Tax rate cannot be special floats.", ) try: + # Convert to Decimal if not already + if not isinstance(tax_rate, Decimal): + tax_rate = Decimal(str(tax_rate)) self.tax_table_selector = str(tax_rate) except (ValueError, TypeError) as e: raise InvalidArgumentException( @@ -355,3 +401,10 @@ def from_dict(d: Optional[dict]) -> Optional["CartItem"]: return None return CartItem(**d) + + +# Update forward references to resolve Weight +# pylint: disable=wrong-import-position +from multisafepay.value_object.weight import Weight # noqa: E402 + +CartItem.update_forward_refs() diff --git a/src/multisafepay/api/shared/costs.py b/src/multisafepay/api/shared/costs.py index 510546f..0b721b4 100644 --- a/src/multisafepay/api/shared/costs.py +++ b/src/multisafepay/api/shared/costs.py @@ -7,9 +7,11 @@ """Transaction costs model for handling fees and charges in payment processing.""" -from typing import Optional +from decimal import Decimal +from typing import Optional, Type, Union from multisafepay.model.api_model import ApiModel +from pydantic import validator class Costs(ApiModel): @@ -21,7 +23,7 @@ class Costs(ApiModel): transaction_id (Optional[int]): The ID of the transaction. description (Optional[str]): The description of the cost. type (Optional[str]): The type of the cost. - amount (Optional[float]): The amount of the cost. + amount (Optional[Decimal]): The amount of the cost as a precise Decimal value. currency (Optional[str]): The currency of the cost. status (Optional[str]): The status of the cost. @@ -30,10 +32,33 @@ class Costs(ApiModel): transaction_id: Optional[int] description: Optional[str] type: Optional[str] - amount: Optional[float] + amount: Optional[Decimal] currency: Optional[str] status: Optional[str] + @validator("amount", pre=True) + def convert_amount_to_decimal( + cls: Type["Costs"], + value: Union[str, float, Decimal, None], + ) -> Optional[Decimal]: + """ + Convert amount to Decimal for precise monetary calculations. + + Parameters + ---------- + value (Union[str, int, float, Decimal, None]): The value to convert. + + Returns + ------- + Optional[Decimal]: The converted Decimal value or None. + + """ + if value is None: + return None + if isinstance(value, Decimal): + return value + return Decimal(str(value)) + def add_transaction_id(self: "Costs", transaction_id: int) -> "Costs": """ Add a transaction ID to the Costs instance. @@ -82,20 +107,26 @@ def add_type(self: "Costs", type_: str) -> "Costs": self.type = type_ return self - def add_amount(self: "Costs", amount: float) -> "Costs": + def add_amount( + self: "Costs", + amount: Union[Decimal, float, str], + ) -> "Costs": """ - Add an amount to the Costs instance. + Add an amount to the Costs instance with precise Decimal conversion. Parameters ---------- - amount (float): The amount of the cost. + amount (Union[Decimal, float, int, str]): The amount of the cost. Returns ------- Costs: The updated Costs instance. """ - self.amount = amount + if isinstance(amount, Decimal): + self.amount = amount + else: + self.amount = Decimal(str(amount)) return self def add_currency(self: "Costs", currency: str) -> "Costs": diff --git a/src/multisafepay/util/__init__.py b/src/multisafepay/util/__init__.py index f26e44f..7e664d8 100644 --- a/src/multisafepay/util/__init__.py +++ b/src/multisafepay/util/__init__.py @@ -1,7 +1,9 @@ """Utility functions and helpers for MultiSafepay SDK operations.""" +from multisafepay.util.json_encoder import DecimalEncoder from multisafepay.util.webhook import Webhook __all__ = [ + "DecimalEncoder", "Webhook", ] diff --git a/src/multisafepay/util/json_encoder.py b/src/multisafepay/util/json_encoder.py new file mode 100644 index 0000000..9a6edef --- /dev/null +++ b/src/multisafepay/util/json_encoder.py @@ -0,0 +1,42 @@ +# Copyright (c) MultiSafepay, Inc. All rights reserved. + +# This file is licensed under the Open Software License (OSL) version 3.0. +# For a copy of the license, see the LICENSE.txt file in the project root. + +# See the DISCLAIMER.md file for disclaimer details. + +"""JSON encoder utilities for API serialization.""" + +import json +from decimal import Decimal + + +class DecimalEncoder(json.JSONEncoder): + """ + Custom JSON encoder that converts Decimal objects to float for API serialization. + + This encoder ensures that Decimal values used for precise calculations + are properly serialized when sending data to the API. + """ + + def default( + self: "DecimalEncoder", + o: object, + ) -> object: # pylint: disable=invalid-name + """ + Convert Decimal to float, otherwise use default encoder. + + Parameters + ---------- + o : object + The object to serialize. + + Returns + ------- + object + The serialized object (float for Decimal, default for others). + + """ + if isinstance(o, Decimal): + return float(o) + return super().default(o) diff --git a/src/multisafepay/util/total_amount.py b/src/multisafepay/util/total_amount.py index dae80bf..62c9ce2 100644 --- a/src/multisafepay/util/total_amount.py +++ b/src/multisafepay/util/total_amount.py @@ -8,12 +8,43 @@ """Total amount calculation and validation utilities for order processing.""" import json +from decimal import Decimal +from typing import Union from multisafepay.exception.invalid_total_amount import ( InvalidTotalAmountException, ) +def _convert_decimals_to_float( + obj: Union[Decimal, dict, list, object], +) -> Union[float, dict, list, object]: + """ + Recursively convert Decimal objects to float for JSON serialization. + + Parameters + ---------- + obj : Union[Decimal, dict, list, object] + The object to convert (can be dict, list, Decimal, or any other type) + + Returns + ------- + Union[float, dict, list, object] + The converted object with all Decimals replaced by floats + + """ + if isinstance(obj, Decimal): + return float(obj) + if isinstance(obj, dict): + return { + key: _convert_decimals_to_float(value) + for key, value in obj.items() + } + if isinstance(obj, list): + return [_convert_decimals_to_float(item) for item in obj] + return obj + + def validate_total_amount(data: dict) -> bool: """ Validate the total amount in the provided data dictionary. @@ -40,17 +71,22 @@ def validate_total_amount(data: dict) -> bool: amount = data["amount"] total_unit_price = __calculate_totals(data) - if (total_unit_price * 100) != amount: + # Convert total_unit_price to cents (integer) for comparison + total_unit_price_cents = int(total_unit_price * 100) + + if total_unit_price_cents != amount: msg = f"Total of unit_price ({total_unit_price}) does not match amount ({amount})" - msg += "\n" + json.dumps(data, indent=4) + # Create a JSON-serializable copy of data by converting Decimal to float + serializable_data = _convert_decimals_to_float(data) + msg += "\n" + json.dumps(serializable_data, indent=4) raise InvalidTotalAmountException(msg) return True -def __calculate_totals(data: dict) -> float: +def __calculate_totals(data: dict) -> Decimal: """ - Calculate the total unit price of items in the shopping cart. + Calculate the total unit price of items in the shopping cart using precise Decimal arithmetic. Parameters ---------- @@ -58,23 +94,31 @@ def __calculate_totals(data: dict) -> float: Returns ------- - float: The total unit price of all items in the shopping cart. + Decimal: The total unit price of all items in the shopping cart with precise decimal calculation. """ - total_unit_price = 0 + total_unit_price = Decimal("0") for item in data["shopping_cart"]["items"]: tax_rate = __get_tax_rate_by_item(item, data) - item_price = item["unit_price"] * item["quantity"] - item_price += tax_rate * item_price + + # Convert to Decimal for precise calculations + unit_price = Decimal(str(item["unit_price"])) + quantity = Decimal(str(item["quantity"])) + tax_rate_decimal = Decimal(str(tax_rate)) + + # Calculate item price with tax + item_price = unit_price * quantity + item_price += tax_rate_decimal * item_price total_unit_price += item_price - return round(total_unit_price, 2) + # Round to 2 decimal places for currency + return total_unit_price.quantize(Decimal("0.01")) def __get_tax_rate_by_item( item: dict, data: dict, -) -> object: +) -> Union[Decimal, int]: """ Get the tax rate for a specific item in the shopping cart. @@ -85,7 +129,7 @@ def __get_tax_rate_by_item( Returns ------- - object: The tax rate for the item, or 0 if no tax rate is found. + Union[Decimal, int]: The tax rate for the item as Decimal, or 0 if no tax rate is found. """ if "tax_table_selector" not in item or not item["tax_table_selector"]: @@ -103,10 +147,12 @@ def __get_tax_rate_by_item( continue tax_rule = tax_table["rules"][0] - return tax_rule["rate"] - return ( + return Decimal(str(tax_rule["rate"])) + + default_rate = ( data["checkout_options"]["tax_tables"]["default"]["rate"] if "default" in data["checkout_options"]["tax_tables"] and "rate" in data["checkout_options"]["tax_tables"]["default"] else 0 ) + return Decimal(str(default_rate)) if default_rate != 0 else 0 diff --git a/src/multisafepay/value_object/unit_price.py b/src/multisafepay/value_object/unit_price.py index 3d6b67e..85ecb68 100644 --- a/src/multisafepay/value_object/unit_price.py +++ b/src/multisafepay/value_object/unit_price.py @@ -7,7 +7,11 @@ """Unit price value object for item pricing in shopping carts.""" +from decimal import Decimal +from typing import Type, Union + from multisafepay.model.inmutable_model import InmutableModel +from pydantic import validator class UnitPrice(InmutableModel): @@ -16,19 +20,48 @@ class UnitPrice(InmutableModel): Attributes ---------- - unit_price (float): The unit price of the item. + unit_price (Decimal): The unit price of the item as a precise decimal value. """ - unit_price: float + unit_price: Decimal + + @validator("unit_price", pre=True) + def convert_to_decimal( + cls: Type["UnitPrice"], + value: Union[str, float, Decimal], + ) -> Decimal: + """ + Convert the input value to Decimal for precise calculations. + + Parameters + ---------- + value (Union[str, float, Decimal]): The value to convert. + + Returns + ------- + Decimal: The converted Decimal value. + + Raises + ------ + TypeError: If the value cannot be converted to Decimal. + + """ + if isinstance(value, Decimal): + return value + if not isinstance(value, (str, float)): + raise TypeError( + f"Cannot convert {type(value).__name__} to Decimal", + ) + return Decimal(str(value)) - def get(self: "UnitPrice") -> float: + def get(self: "UnitPrice") -> Decimal: """ Get the unit price of the item. Returns ------- - float: The unit price of the item. + Decimal: The unit price of the item. """ return self.unit_price diff --git a/tests/multisafepay/integration/api/shared/cart/test_integration_cart_item.py b/tests/multisafepay/integration/api/shared/cart/test_integration_cart_item.py index 7372bb0..ca39b39 100644 --- a/tests/multisafepay/integration/api/shared/cart/test_integration_cart_item.py +++ b/tests/multisafepay/integration/api/shared/cart/test_integration_cart_item.py @@ -8,6 +8,8 @@ """Shared API models and utilities.""" +from decimal import Decimal + from multisafepay.api.shared.cart.cart_item import CartItem from multisafepay.value_object.weight import Weight @@ -48,7 +50,7 @@ def test_initializes_with_valid_values(): assert item.quantity == 10 assert item.tax_table_selector == "standard" assert item.tax_rate == 0.2 - assert item.unit_price == 19.99 + assert item.unit_price == Decimal("19.99") assert item.weight.value == 100 assert item.weight.unit == "grams" @@ -114,6 +116,6 @@ def test_creates_from_dict(): assert item.quantity == 10 assert item.tax_table_selector == "standard" assert item.tax_rate == 0.2 - assert item.unit_price == 19.99 + assert item.unit_price == Decimal("19.99") assert item.weight.unit == "grams" assert item.weight.value == 100.0 diff --git a/tests/multisafepay/unit/api/shared/cart/test_unit_cart_item.py b/tests/multisafepay/unit/api/shared/cart/test_unit_cart_item.py index 1c91bf5..bb85f41 100644 --- a/tests/multisafepay/unit/api/shared/cart/test_unit_cart_item.py +++ b/tests/multisafepay/unit/api/shared/cart/test_unit_cart_item.py @@ -103,9 +103,11 @@ def test_adds_tax_table_selector(): def test_adds_unit_price(): """Test that unit price can be added to a CartItem.""" + from decimal import Decimal + item = CartItem() item.add_unit_price(19.99) - assert item.unit_price == 19.99 + assert item.unit_price == Decimal("19.99") def test_creates_from_empty_dict(): diff --git a/tests/multisafepay/unit/api/shared/test_unit_costs.py b/tests/multisafepay/unit/api/shared/test_unit_costs.py index 0e1c23e..ac1e91b 100644 --- a/tests/multisafepay/unit/api/shared/test_unit_costs.py +++ b/tests/multisafepay/unit/api/shared/test_unit_costs.py @@ -8,6 +8,7 @@ """Unit tests for the shared costs model.""" +from decimal import Decimal from multisafepay.api.shared.costs import Costs @@ -25,7 +26,7 @@ def test_initializes_with_valid_values(): assert costs.transaction_id == 123 assert costs.description == "Service Fee" assert costs.type == "Fixed" - assert costs.amount == 99.99 + assert costs.amount == Decimal("99.99") assert costs.currency == "USD" assert costs.status == "Pending" @@ -62,7 +63,7 @@ def test_adds_type(): def test_adds_amount(): """Test that an amount is added to a Costs instance.""" costs = Costs().add_amount(99.99) - assert costs.amount == 99.99 + assert costs.amount == Decimal("99.99") def test_adds_currency(): @@ -91,7 +92,7 @@ def test_creates_from_dict_with_all_fields(): assert costs.transaction_id == 123 assert costs.description == "Service Fee" assert costs.type == "Fixed" - assert costs.amount == 99.99 + assert costs.amount == Decimal("99.99") assert costs.currency == "USD" assert costs.status == "Pending" diff --git a/tests/multisafepay/unit/util/test_unit_total_amount.py b/tests/multisafepay/unit/util/test_unit_total_amount.py index a8e572c..371b21b 100644 --- a/tests/multisafepay/unit/util/test_unit_total_amount.py +++ b/tests/multisafepay/unit/util/test_unit_total_amount.py @@ -8,6 +8,8 @@ """Utility functions for test unit total amount.""" +from decimal import Decimal + from multisafepay.util.total_amount import ( validate_total_amount, __get_tax_rate_by_item, @@ -34,7 +36,7 @@ def test_get_tax_rate_by_item(): }, } - assert __get_tax_rate_by_item(item, data) == 0.1 + assert __get_tax_rate_by_item(item, data) == Decimal("0.1") def test_get_tax_rate_by_item_btw21(): @@ -56,7 +58,7 @@ def test_get_tax_rate_by_item_btw21(): }, } - assert __get_tax_rate_by_item(item, data) == 0.21 + assert __get_tax_rate_by_item(item, data) == Decimal("0.21") def test_calculate_totals(): @@ -73,7 +75,7 @@ def test_calculate_totals(): }, } - assert __calculate_totals(data) == 2.42 + assert __calculate_totals(data) == Decimal("2.42") def test_validate_total_amount(): diff --git a/tests/multisafepay/unit/value_object/test_unit_price.py b/tests/multisafepay/unit/value_object/test_unit_price.py index 6eb4943..af41bbd 100644 --- a/tests/multisafepay/unit/value_object/test_unit_price.py +++ b/tests/multisafepay/unit/value_object/test_unit_price.py @@ -8,6 +8,8 @@ """Value object for Test Unit Price data.""" +from decimal import Decimal + from pydantic.error_wrappers import ValidationError import pytest @@ -17,7 +19,7 @@ def test_unit_price_initialization(): """Test the initialization of the UnitPrice object with a valid unit price.""" unit_price = UnitPrice(unit_price=19.99) - assert unit_price.unit_price == 19.99 + assert unit_price.unit_price == Decimal("19.99") def test_empty_unit_price_initialization(): @@ -29,10 +31,10 @@ def test_empty_unit_price_initialization(): def test_valid_cast_unit_price_initialization(): """Test the initialization of the UnitPrice object with a string unit price.""" unit_price = UnitPrice(unit_price="19.99") - assert unit_price.unit_price == 19.99 + assert unit_price.unit_price == Decimal("19.99") def test_unit_price_get_unit_price(): """Test the get method of the UnitPrice object.""" unit_price = UnitPrice(unit_price=19.99) - assert unit_price.get() == 19.99 + assert unit_price.get() == Decimal("19.99")