Skip to content
Merged
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 @@ -33,7 +33,12 @@ class ProcessChargeInput(BaseModel):
booking_reference: str = Field(
..., description="Booking reference number", examples=["BKG-00012345"]
)
charge_amount: Decimal = Field(..., description="Charge amount", examples=["50.00"])
charge_amount: float = Field(
...,
description="Charge amount",
examples=[50.00],
allow_inf_nan=False,
)
reason: str = Field(
..., description="Reason for charge", examples=["modification_fee"]
)
Expand Down Expand Up @@ -87,8 +92,10 @@ async def run(
if not booking:
raise Tool.ExecutionError(f"Booking not found: {request.booking_reference}")

charge_amount = Decimal(str(request.charge_amount))

# Validate charge amount
if request.charge_amount <= 0:
if charge_amount <= 0:
raise Tool.ExecutionError("Invalid charge amount - must be greater than 0")

# Get existing transactions to generate sequential ID
Expand All @@ -102,7 +109,7 @@ async def run(
transaction_id=transaction_id,
booking_reference=request.booking_reference,
customer_id=booking.customer_id,
amount=request.charge_amount.quantize(TWO_PLACES, rounding=ROUND_HALF_UP),
amount=charge_amount.quantize(TWO_PLACES, rounding=ROUND_HALF_UP),
currency="USD",
transaction_type=TransactionType.CHARGE,
payment_status=PaymentStatus.SUCCESSFUL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ class ProcessChargeDisputeInput(BaseModel):
dispute_reason: str = Field(
..., description="Reason for the dispute", examples=["unauthorized_charge"]
)
dispute_amount: Decimal = Field(
..., description="Amount being disputed", examples=["150.00"]
dispute_amount: float = Field(
...,
description="Amount being disputed",
examples=[150.00],
allow_inf_nan=False,
)


Expand Down Expand Up @@ -99,13 +102,15 @@ async def run(
dispute_num = len(dispute_transactions) + 1
dispute_case_id = f"DSP-{dispute_num:08d}"

dispute_amount = Decimal(str(request.dispute_amount))

# Create dispute transaction
dispute_transaction = Transaction(
id=dispute_case_id,
transaction_id=dispute_case_id,
booking_reference=original_transaction.booking_reference,
customer_id=original_transaction.customer_id,
amount=request.dispute_amount.quantize(TWO_PLACES, rounding=ROUND_HALF_UP),
amount=dispute_amount.quantize(TWO_PLACES, rounding=ROUND_HALF_UP),
currency="USD",
transaction_type=TransactionType.DISPUTE,
payment_status=PaymentStatus.PENDING,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ class ProcessRefundInput(BaseModel):
booking_reference: str = Field(
..., description="Booking reference number", examples=["BKG-00012345"]
)
refund_amount: Decimal = Field(
..., description="Refund amount", examples=["250.00"]
refund_amount: float = Field(
...,
description="Refund amount",
examples=[250.00],
allow_inf_nan=False,
)
reason: str = Field(..., description="Reason for refund", examples=["cancellation"])

Expand Down Expand Up @@ -89,6 +92,8 @@ async def run(
if not booking:
raise Tool.ExecutionError(f"Booking not found: {request.booking_reference}")

refund_amount = Decimal(str(request.refund_amount))

# Get existing transactions to generate sequential ID
all_transactions = db.get_all(Transaction)
transaction_num = len(all_transactions) + 1
Expand All @@ -100,7 +105,7 @@ async def run(
transaction_id=transaction_id,
booking_reference=request.booking_reference,
customer_id=booking.customer_id,
amount=request.refund_amount.quantize(TWO_PLACES, rounding=ROUND_HALF_UP),
amount=refund_amount.quantize(TWO_PLACES, rounding=ROUND_HALF_UP),
currency="USD",
transaction_type=TransactionType.REFUND,
payment_status=PaymentStatus.SUCCESSFUL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,29 @@ def db_with_booking():
return db


def test_process_charge_schema_uses_json_number():
"""Test that the tool schema is compatible with the Responses API."""
amount_schema = ProcessCharge().input_schema["properties"]["charge_amount"]

assert amount_schema["type"] == "number"
assert "pattern" not in amount_schema


@pytest.mark.parametrize("charge_amount", [float("nan"), float("inf"), float("-inf")])
@pytest.mark.anyio
async def test_process_charge_rejects_non_finite_amount(db_with_booking, charge_amount):
"""Test that non-finite amounts fail input validation."""
with pytest.raises(Exception, match="finite number"):
await ProcessCharge().run_with_validation(
db_with_booking,
{
"booking_reference": "BKG-00012345",
"charge_amount": charge_amount,
"reason": "test",
},
)


@pytest.mark.anyio
async def test_process_charge_success(db_with_booking):
"""Test successfully processing a charge."""
Expand All @@ -81,14 +104,18 @@ async def test_process_charge_success(db_with_booking):

@pytest.mark.anyio
async def test_process_charge_creates_transaction(db_with_booking):
"""Test that charge creates a transaction record."""
"""Test that charge creates a transaction record, rounding the amount half-up.

2.675 is not exactly representable as a float, so this also pins the
decimal-string conversion that keeps rounding away from the binary value.
"""
tool = ProcessCharge()

await tool.run_with_validation(
db_with_booking,
{
"booking_reference": "BKG-00012345",
"charge_amount": 75.00,
"charge_amount": 2.675,
"reason": "late_checkout_fee",
},
)
Expand All @@ -99,7 +126,9 @@ async def test_process_charge_creates_transaction(db_with_booking):

txn = transactions[0]
assert txn.transaction_type == TransactionType.CHARGE
assert txn.amount == Decimal("75.00")
# Decimal equality ignores scale, so also pin the exact 2-decimal-place repr.
assert txn.amount == Decimal("2.68")
assert str(txn.amount) == "2.68"
assert txn.payment_status == PaymentStatus.SUCCESSFUL
assert txn.reason == "late_checkout_fee"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

"""Tests for process_charge_dispute tool."""

from decimal import Decimal

import pytest
from tb_business_ops_servers_202606.toolslib.external_booking.payment_api.models import (
PaymentStatus,
Expand Down Expand Up @@ -48,6 +50,31 @@ def db_with_transaction():
return db


def test_process_charge_dispute_schema_uses_json_number():
"""Test that the tool schema is compatible with the Responses API."""
amount_schema = ProcessChargeDispute().input_schema["properties"]["dispute_amount"]

assert amount_schema["type"] == "number"
assert "pattern" not in amount_schema


@pytest.mark.parametrize("dispute_amount", [float("nan"), float("inf"), float("-inf")])
@pytest.mark.anyio
async def test_process_charge_dispute_rejects_non_finite_amount(
db_with_transaction, dispute_amount
):
"""Test that non-finite amounts fail input validation."""
with pytest.raises(Exception, match="finite number"):
await ProcessChargeDispute().run_with_validation(
db_with_transaction,
{
"transaction_id": "TXN-00000001",
"dispute_reason": "test",
"dispute_amount": dispute_amount,
},
)


@pytest.mark.anyio
async def test_process_charge_dispute_success(db_with_transaction):
"""Test successfully processing a charge dispute."""
Expand All @@ -69,15 +96,19 @@ async def test_process_charge_dispute_success(db_with_transaction):

@pytest.mark.anyio
async def test_process_charge_dispute_creates_transaction(db_with_transaction):
"""Test that dispute creates a transaction record."""
"""Test that dispute creates a transaction record, rounding the amount half-up.

2.675 is not exactly representable as a float, so this also pins the
decimal-string conversion that keeps rounding away from the binary value.
"""
tool = ProcessChargeDispute()

await tool.run_with_validation(
db_with_transaction,
{
"transaction_id": "TXN-00000001",
"dispute_reason": "unrecognized charge",
"dispute_amount": 150.00,
"dispute_amount": 2.675,
},
)

Expand All @@ -90,7 +121,9 @@ async def test_process_charge_dispute_creates_transaction(db_with_transaction):
assert len(dispute_txns) == 1
dispute = dispute_txns[0]
assert dispute.transaction_type == TransactionType.DISPUTE
assert dispute.amount == 150.00
# Decimal equality ignores scale, so also pin the exact 2-decimal-place repr.
assert dispute.amount == Decimal("2.68")
assert str(dispute.amount) == "2.68"
assert dispute.payment_status == PaymentStatus.PENDING
assert dispute.reason == "unrecognized charge"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

"""Tests for process_refund tool."""

from decimal import Decimal

import pytest
from tb_business_ops_servers_202606.toolslib.external_booking.booking_api.models import (
BoardType,
Expand Down Expand Up @@ -58,6 +60,29 @@ def db_with_booking():
return db


def test_process_refund_schema_uses_json_number():
"""Test that the tool schema is compatible with the Responses API."""
amount_schema = ProcessRefund().input_schema["properties"]["refund_amount"]

assert amount_schema["type"] == "number"
assert "pattern" not in amount_schema


@pytest.mark.parametrize("refund_amount", [float("nan"), float("inf"), float("-inf")])
@pytest.mark.anyio
async def test_process_refund_rejects_non_finite_amount(db_with_booking, refund_amount):
"""Test that non-finite amounts fail input validation."""
with pytest.raises(Exception, match="finite number"):
await ProcessRefund().run_with_validation(
db_with_booking,
{
"booking_reference": "BKG-00012345",
"refund_amount": refund_amount,
"reason": "test",
},
)


@pytest.mark.anyio
async def test_process_refund_success(db_with_booking):
"""Test successfully processing a refund."""
Expand All @@ -80,14 +105,18 @@ async def test_process_refund_success(db_with_booking):

@pytest.mark.anyio
async def test_process_refund_creates_transaction(db_with_booking):
"""Test that refund creates a transaction record."""
"""Test that refund creates a transaction record, rounding the amount half-up.

2.675 is not exactly representable as a float, so this also pins the
decimal-string conversion that keeps rounding away from the binary value.
"""
tool = ProcessRefund()

await tool.run_with_validation(
db_with_booking,
{
"booking_reference": "BKG-00012345",
"refund_amount": 150.00,
"refund_amount": 2.675,
"reason": "service issue",
},
)
Expand All @@ -98,7 +127,9 @@ async def test_process_refund_creates_transaction(db_with_booking):

txn = transactions[0]
assert txn.transaction_type == TransactionType.REFUND
assert txn.amount == 150.00
# Decimal equality ignores scale, so also pin the exact 2-decimal-place repr.
assert txn.amount == Decimal("2.68")
assert str(txn.amount) == "2.68"
assert txn.payment_status == PaymentStatus.SUCCESSFUL
assert txn.reason == "service issue"

Expand Down