Skip to content

Comments

ZIP-596: adds taxcloud support for CalculateCart feature#15

Open
ericlakich wants to merge 9 commits intomainfrom
ZIP-596/v0.2.3-beta
Open

ZIP-596: adds taxcloud support for CalculateCart feature#15
ericlakich wants to merge 9 commits intomainfrom
ZIP-596/v0.2.3-beta

Conversation

@ericlakich
Copy link
Contributor

This pull request introduces a major update (v0.2.3-beta) to the ZipTax SDK, focusing on comprehensive support for shopping cart tax calculation with dual routing between the ZipTax and TaxCloud APIs. The update adds new models, utilities, and tests, and refactors the main cart calculation method to support both backends transparently. Documentation and versioning are also updated to reflect these changes.

Cart Tax Calculation & Routing Enhancements:

  • Added automatic routing in CalculateCart() to TaxCloud's /tax/connections/{connectionId}/carts endpoint when TaxCloud credentials are configured, maintaining the same input contract (CalculateCartRequest) and transforming requests as needed (address parsing, taxabilityCode mapping, line item indexing). The return type is now a union of CalculateCartResponse and TaxCloudCalculateCartResponse.
  • Introduced a static method _transform_cart_for_taxcloud() for request transformation, and split cart calculation into two private helpers for ZipTax and TaxCloud backends.

Model and Utility Additions:

  • Added address parsing utility parse_address_string() in utils/validation.py to convert single-string addresses to structured components, with robust error handling.
  • Introduced three new Pydantic models for TaxCloud cart responses: TaxCloudCalculateCartResponse, TaxCloudCartItemResponse, and TaxCloudCartLineItemResponse, and exported them in __init__.py and models/__init__.py. [1] [2] [3]

Testing and Validation:

  • Added 17 new tests in TestCalculateCartTaxCloudRouting to cover routing logic, request/response transformation, and error handling for cart calculation.
  • Enforced and documented Pydantic validation constraints for cart models (e.g., item and line item counts, positive price/quantity, currency code). [1] [2]

Documentation and Versioning:

  • Updated README.md and CLAUDE.md to document the new cart tax calculation feature, dual-routing behavior, usage examples, and model constraints. [1] [2] [3]
  • Bumped version from 0.2.0-beta to 0.2.3-beta in all relevant files. [1] [2] [3] [4]

Technical and Quality Improvements:

  • Ensured all quality checks pass (black, ruff, mypy, pytest with 96% coverage), and clarified retry logic applies to both ZipTax and TaxCloud routes.
  • Updated exports in __init__.py and models/__init__.py to include new cart and TaxCloud models. [1] [2] [3]

These changes significantly expand the SDK's tax calculation capabilities, improve developer ergonomics, and ensure robust validation and testing.

  • Cart Tax Calculation & Routing Enhancements:

    • Automatic routing of CalculateCart() to TaxCloud or ZipTax based on credentials, with request/response transformation and unified input contract.
    • Refactored cart calculation logic into backend-specific helpers and added static method for TaxCloud request transformation.
  • Model and Utility Additions:

    • Added address parsing utility and three new Pydantic models for TaxCloud cart responses, with exports updated across modules. [1] [2] [3]
  • Testing and Validation:

    • Introduced 17 new tests for cart routing and validation, and enforced/documented Pydantic constraints on cart models. [1] [2]
  • Documentation and Versioning:

    • Updated documentation for new features, usage, and constraints; bumped version to 0.2.3-beta in all relevant files. [1] [2] [3] [4] [5]
  • Technical and Quality Improvements:

    • Maintained high code quality and coverage, clarified retry logic, and updated module exports for new models. [1] [2] [3] [4]

Copilot AI review requested due to automatic review settings February 20, 2026 23:59
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds comprehensive support for shopping cart tax calculation with intelligent dual routing between the ZipTax and TaxCloud APIs. The implementation maintains a unified input contract (CalculateCartRequest) while automatically selecting the appropriate backend based on client configuration. When TaxCloud credentials are configured, requests are transformed and routed to TaxCloud's /tax/connections/{connectionId}/carts endpoint with proper address parsing and field mapping.

Changes:

  • Added automatic backend routing in CalculateCart() that transparently handles ZipTax and TaxCloud APIs with unified input but distinct response types
  • Introduced address parsing utility (parse_address_string()) and three new TaxCloud response models (TaxCloudCalculateCartResponse, TaxCloudCartItemResponse, TaxCloudCartLineItemResponse)
  • Added 17 new tests covering routing logic, request transformation, response parsing, and error handling scenarios

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/ziptax/resources/functions.py Implements dual-routing CalculateCart() with private helpers for ZipTax and TaxCloud backends, plus static transformation method
src/ziptax/utils/validation.py Adds parse_address_string() utility to convert single-string addresses to structured TaxCloud format
src/ziptax/models/responses.py Defines 9 ZipTax cart models and 3 TaxCloud cart response models with Pydantic validation constraints
src/ziptax/__init__.py Exports new cart and TaxCloud models, updates version to 0.2.3-beta
src/ziptax/models/__init__.py Exports cart calculation and TaxCloud response models
tests/test_functions.py Adds 17 tests for TaxCloud routing and 9 tests for basic cart functionality with Pydantic validation
tests/conftest.py Adds fixtures for ZipTax and TaxCloud cart responses
pyproject.toml Updates version to 0.2.3-beta
docs/spec.yaml Documents cart endpoint specification with dual-routing behavior and transformation rules
README.md Adds cart tax calculation usage guide with dual-routing explanation and validation examples
CLAUDE.md Updates developer documentation with cart calculation patterns and SDK version
CHANGELOG.md Documents changes for 0.2.3-beta and 0.2.1-beta releases

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Error handling (unparseable addresses, invalid state/zip)

### Changed
- Version bumped from `0.2.1-beta` to `0.2.3-beta`
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version 0.2.2-beta appears to have been skipped (jumping from 0.2.1-beta to 0.2.3-beta). While this is not necessarily incorrect, it's unusual and could cause confusion. Consider adding a note explaining why 0.2.2 was skipped, or if this was unintentional, consider using 0.2.2-beta instead.

Suggested change
- Version bumped from `0.2.1-beta` to `0.2.3-beta`
- Version bumped from `0.2.1-beta` to `0.2.3-beta`
- Version `0.2.2-beta` was reserved for an internal build and was never released publicly; the version number intentionally skips from `0.2.1-beta` to `0.2.3-beta`.

Copilot uses AI. Check for mistakes.
Comment on lines +188 to +250
def parse_address_string(address: str) -> Dict[str, str]:
"""Parse a single address string into structured TaxCloud address components.

Parses addresses in the format:
"line1, city, state zip" or "line1, city, state zip-plus4"

Examples:
"200 Spectrum Center Dr, Irvine, CA 92618"
"323 Washington Ave N, Minneapolis, MN 55401-2427"

Args:
address: Full address string to parse

Returns:
Dictionary with keys: line1, city, state, zip, countryCode

Raises:
ZipTaxValidationError: If the address cannot be parsed into
the required components. The address must contain at least
3 comma-separated segments, and the last segment must contain
a valid state abbreviation and ZIP code.
"""
if not address or not address.strip():
raise ZipTaxValidationError(
"Address string cannot be empty. "
"Expected format: 'street, city, state zip'"
)

# Split by comma and strip whitespace
parts = [p.strip() for p in address.split(",")]

if len(parts) < 3:
raise ZipTaxValidationError(
f"Cannot parse address into structured components. "
f"Expected at least 3 comma-separated parts "
f"(street, city, state zip), got {len(parts)}: {address!r}"
)

# line1 is everything before the last two segments
# city is the second-to-last segment
# state + zip is the last segment
line1 = ", ".join(parts[:-2])
city = parts[-2]
state_zip = parts[-1]

# Parse state and zip from the last segment (e.g., "CA 92618" or "CA 92618-1905")
state_zip_match = re.match(r"^([A-Za-z]{2})\s+(\d{5}(?:-\d{4})?)$", state_zip)
if not state_zip_match:
raise ZipTaxValidationError(
f"Cannot parse state and ZIP from address segment: {state_zip!r}. "
f"Expected format: 'ST 12345' or 'ST 12345-6789'"
)

state = state_zip_match.group(1).upper()
zip_code = state_zip_match.group(2)

return {
"line1": line1,
"city": city,
"state": state,
"zip": zip_code,
"countryCode": "US",
}
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding test coverage for addresses with more than 3 comma-separated segments (e.g., addresses with apartment numbers or suite numbers). The current implementation should handle these correctly by joining all parts before the last two, but explicit test coverage would ensure this behavior is maintained. Example: "123 Main St, Suite 100, Los Angeles, CA 90001"

Copilot uses AI. Check for mistakes.
Comment on lines +226 to +243
# line1 is everything before the last two segments
# city is the second-to-last segment
# state + zip is the last segment
line1 = ", ".join(parts[:-2])
city = parts[-2]
state_zip = parts[-1]

# Parse state and zip from the last segment (e.g., "CA 92618" or "CA 92618-1905")
state_zip_match = re.match(r"^([A-Za-z]{2})\s+(\d{5}(?:-\d{4})?)$", state_zip)
if not state_zip_match:
raise ZipTaxValidationError(
f"Cannot parse state and ZIP from address segment: {state_zip!r}. "
f"Expected format: 'ST 12345' or 'ST 12345-6789'"
)

state = state_zip_match.group(1).upper()
zip_code = state_zip_match.group(2)

Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The address parsing function doesn't validate that the parsed city, line1, or state components are non-empty strings. After splitting and stripping, it's possible (though unlikely) to have empty segments if the input contains patterns like "street, , state zip" or ", city, state zip". Consider adding validation to ensure city and line1 are non-empty after parsing to provide clearer error messages for malformed addresses.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant