Skip to content
Closed
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
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.2.1-beta] - 2025-02-19

### Added
- **Cart Tax Calculation**: `CalculateCart()` function for calculating sales tax on shopping carts
- Accepts a `CalculateCartRequest` with customer info, addresses, currency, and line items
- Sends cart to `POST /calculate/cart` on the ZipTax API
- Returns per-item tax rate and amount via `CalculateCartResponse`
- **Origin/Destination Sourcing**: Automatic tax sourcing resolution in `CalculateCart()`
- Looks up both origin and destination addresses via `GetSalesTaxByAddress`
- Extracts state from V60 normalized addresses to determine intrastate vs interstate
- Interstate transactions: uses destination address (sourcing skipped)
- Intrastate transactions: checks `sourcingRules.value` — uses origin address for "O", destination for "D"
- Overrides both addresses in the cart payload with the resolved address before sending to the API
- **9 New Pydantic Models** for cart tax calculation:
- Request models: `CalculateCartRequest`, `CartItem`, `CartAddress`, `CartCurrency`, `CartLineItem`
- Response models: `CalculateCartResponse`, `CartItemResponse`, `CartLineItemResponse`, `CartTax`
- **Pydantic Validation** on cart models:
- `CartLineItem.price` and `quantity`: must be greater than 0 (`gt=0`)
- `CalculateCartRequest.items`: exactly 1 element (`min_length=1, max_length=1`)
- `CartItem.line_items`: 1-250 elements (`min_length=1, max_length=250`)
- `CartCurrency.currency_code`: must be `"USD"` (`Literal["USD"]`)
- **Internal Helpers**:
- `_extract_state_from_normalized_address()`: parses state from V60 normalized address format
- `_resolve_sourcing_address()`: resolves which address to use for tax calculation
- **Test Coverage**: 122 tests with 96% code coverage
- `TestExtractStateFromNormalizedAddress`: 5 tests for state parsing
- `TestResolveSourcingAddress`: 4 tests for sourcing resolution
- `TestCalculateCart`: 16 tests for cart calculation, sourcing integration, and validation
- **Documentation**:
- CalculateCart usage guide with code examples in README.md
- Origin/destination sourcing explanation in README.md
- Cart endpoint specification in `docs/spec.yaml`
- Actual API request/response examples in spec

### Changed
- Version bumped from `0.2.0-beta` to `0.2.1-beta`

### Technical Details
- Cart calculation uses the ZipTax HTTP client (not TaxCloud) with `X-API-Key` authentication
- Sourcing resolution always calls the V60 API for both addresses (no heuristic parsing)
- Request bodies serialized with `model_dump(by_alias=True, exclude_none=True)` for camelCase API fields
- `taxabilityCode` is optional and excluded from the payload when not set
- All quality checks pass: black, ruff, mypy, pytest with 96% coverage

## [0.2.0-beta] - 2025-02-16

### Added
Expand Down
18 changes: 15 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,9 @@ def has_taxcloud_config(self) -> bool:
- Uses decorator pattern for retry logic
- TaxCloud methods check credentials before executing

**Critical Pattern**:
**Critical Patterns**:
```python
# TaxCloud methods check credentials first
def _check_taxcloud_config(self) -> None:
"""Check if TaxCloud credentials are configured."""
if not self.config.has_taxcloud_config or self.taxcloud_http_client is None:
Expand All @@ -131,6 +132,15 @@ def _check_taxcloud_config(self) -> None:
def CreateOrder(self, request: CreateOrderRequest, ...) -> OrderResponse:
self._check_taxcloud_config() # Guards against missing credentials
# ... implementation

# CalculateCart resolves origin/destination sourcing before calling the API
def CalculateCart(self, request: CalculateCartRequest) -> CalculateCartResponse:
cart = request.items[0]
resolved_address = self._resolve_sourcing_address(
origin_address=cart.origin.address,
destination_address=cart.destination.address,
)
# Override both addresses with the resolved address, then POST to /calculate/cart
```

#### 4. **HTTP Client (`utils/http.py`)**
Expand All @@ -155,6 +165,7 @@ self.session.headers.update({"X-API-Key": api_key})
- **Purpose**: Pydantic models for request/response validation
- **Structure**:
- V60 models for ZipTax API responses
- Cart models for cart tax calculation (with Pydantic field constraints)
- TaxCloud models for order management
- Uses `Field` with `alias` for camelCase ↔ snake_case mapping

Expand Down Expand Up @@ -545,6 +556,7 @@ This file is used as a reference for code generation and documentation.
**Endpoints**:
- `GET /request/v60/` - Tax rate lookup by address or geolocation
- `GET /account/v60/metrics` - Account usage metrics
- `POST /calculate/cart` - Cart tax calculation with per-item rates

**Response Format**: JSON with nested structure

Expand Down Expand Up @@ -773,6 +785,6 @@ For API-specific questions:

---

**Last Updated**: 2025-02-16
**SDK Version**: 0.2.0-beta
**Last Updated**: 2025-02-19
**SDK Version**: 0.2.1-beta
**Maintained By**: ZipTax Team
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Official Python SDK for the [Ziptax API](https://zip-tax.com) - Get accurate sal

### Core Features (ZipTax API)
- 🚀 Simple and intuitive API
- 🛒 Cart tax calculation with automatic origin/destination sourcing
- 🔄 Automatic retry logic with exponential backoff
- ✅ Input validation
- 🔍 Type hints for better IDE support
Expand Down Expand Up @@ -148,6 +149,86 @@ print(f"Account Active: {metrics.is_active}")
print(f"Message: {metrics.message}")
```

### Calculate Cart Tax

Calculate sales tax for a shopping cart with multiple line items. The SDK automatically resolves origin/destination sourcing rules before sending the request to the API.

```python
from ziptax.models import (
CalculateCartRequest,
CartItem,
CartAddress,
CartCurrency,
CartLineItem,
)

# Build the cart request
request = CalculateCartRequest(
items=[
CartItem(
customer_id="customer-453",
currency=CartCurrency(currency_code="USD"),
destination=CartAddress(
address="200 Spectrum Center Dr, Irvine, CA 92618"
),
origin=CartAddress(
address="323 Washington Ave N, Minneapolis, MN 55401"
),
line_items=[
CartLineItem(
item_id="item-1",
price=10.75,
quantity=1.5,
),
CartLineItem(
item_id="item-2",
price=25.00,
quantity=2.0,
taxability_code=0,
),
],
)
]
)

# Calculate tax
result = client.request.CalculateCart(request)

# Access results
cart = result.items[0]
print(f"Cart ID: {cart.cart_id}")
for item in cart.line_items:
print(f" {item.item_id}: rate={item.tax.rate}, amount=${item.tax.amount:.2f}")
```

#### Origin/Destination Sourcing

The SDK automatically determines whether to use origin-based or destination-based tax rates:

- **Interstate** (different states): Uses the destination address
- **Intrastate, destination-based** (e.g., CA, NY): Uses the destination address
- **Intrastate, origin-based** (e.g., TX, OH): Uses the origin address

This is handled transparently -- the SDK looks up both addresses via `GetSalesTaxByAddress`, checks the `sourcingRules.value` field, and sends the correct address to the cart API.

#### Validation

The cart models enforce constraints at construction time via Pydantic:

- `items` must contain exactly 1 cart
- `line_items` must contain 1-250 items
- `price` and `quantity` must be greater than 0
- `currency_code` must be `"USD"`

```python
from pydantic import ValidationError

try:
CartLineItem(item_id="item-1", price=-5.00, quantity=1.0)
except ValidationError as e:
print(e) # price must be greater than 0
```

## TaxCloud Order Management

The SDK includes optional support for TaxCloud order management features. To use these features, you need both a ZipTax API key and TaxCloud credentials (Connection ID and API Key).
Expand Down Expand Up @@ -522,6 +603,7 @@ API endpoint functions accessible via `client.request`.
- `GetSalesTaxByGeoLocation(lat, lng, **kwargs)` - Get tax rates by coordinates
- `GetRatesByPostalCode(postal_code, **kwargs)` - Get tax rates by US postal code
- `GetAccountMetrics(**kwargs)` - Get account usage metrics
- `CalculateCart(request)` - Calculate sales tax for a shopping cart with origin/destination sourcing

#### TaxCloud API Methods (Optional)

Expand Down
Loading
Loading