ZIP-596: adds taxcloud support for CalculateCart feature#15
ZIP-596: adds taxcloud support for CalculateCart feature#15ericlakich wants to merge 9 commits intomainfrom
Conversation
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
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.
| - 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`. |
| 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", | ||
| } |
There was a problem hiding this comment.
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"
| # 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) | ||
|
|
There was a problem hiding this comment.
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.
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:
CalculateCart()to TaxCloud's/tax/connections/{connectionId}/cartsendpoint when TaxCloud credentials are configured, maintaining the same input contract (CalculateCartRequest) and transforming requests as needed (address parsing,taxabilityCodemapping, line item indexing). The return type is now a union ofCalculateCartResponseandTaxCloudCalculateCartResponse._transform_cart_for_taxcloud()for request transformation, and split cart calculation into two private helpers for ZipTax and TaxCloud backends.Model and Utility Additions:
parse_address_string()inutils/validation.pyto convert single-string addresses to structured components, with robust error handling.TaxCloudCalculateCartResponse,TaxCloudCartItemResponse, andTaxCloudCartLineItemResponse, and exported them in__init__.pyandmodels/__init__.py. [1] [2] [3]Testing and Validation:
TestCalculateCartTaxCloudRoutingto cover routing logic, request/response transformation, and error handling for cart calculation.Documentation and Versioning:
README.mdandCLAUDE.mdto document the new cart tax calculation feature, dual-routing behavior, usage examples, and model constraints. [1] [2] [3]0.2.0-betato0.2.3-betain all relevant files. [1] [2] [3] [4]Technical and Quality Improvements:
__init__.pyandmodels/__init__.pyto 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:
CalculateCart()to TaxCloud or ZipTax based on credentials, with request/response transformation and unified input contract.Model and Utility Additions:
Testing and Validation:
Documentation and Versioning:
0.2.3-betain all relevant files. [1] [2] [3] [4] [5]Technical and Quality Improvements: