Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
9 changes: 5 additions & 4 deletions src/multisafepay/api/paths/orders/order_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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}",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand Down
95 changes: 74 additions & 21 deletions src/multisafepay/api/shared/cart/cart_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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.

"""
Expand All @@ -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":
"""
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand All @@ -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.

Expand All @@ -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
-------
Expand All @@ -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(
Expand All @@ -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.

Expand All @@ -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
-------
Expand All @@ -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(
Expand Down Expand Up @@ -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()
45 changes: 38 additions & 7 deletions src/multisafepay/api/shared/costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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.

Expand All @@ -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.
Expand Down Expand Up @@ -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":
Expand Down
2 changes: 2 additions & 0 deletions src/multisafepay/util/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
Loading