From a4ad9f8881bd3e506da303df290e2fa2951385ea Mon Sep 17 00:00:00 2001 From: Eric Lakich Date: Mon, 16 Feb 2026 09:41:22 -0800 Subject: [PATCH 1/9] ZIP-562: adds order management functions for TaxCloud integration --- .github/workflows/README.md | 167 ++++++ .github/workflows/version-check.yml | 286 +++++++++ CHANGELOG.md | 18 + CLAUDE.md | 352 +++++++++++ CONTRIBUTING.md | 331 +++++++++++ README.md | 221 ++++++- docs/spec.yaml | 882 +++++++++++++++++++++++++++- examples/taxcloud-orders.ts | 161 +++++ package.json | 1 + src/client.ts | 122 +++- src/config.ts | 4 + src/models/index.ts | 1 + src/models/taxcloud.ts | 220 +++++++ src/utils/http.ts | 20 + tests/taxcloud-orders.test.ts | 359 +++++++++++ 15 files changed, 3109 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/README.md create mode 100644 .github/workflows/version-check.yml create mode 100644 CLAUDE.md create mode 100644 CONTRIBUTING.md create mode 100644 examples/taxcloud-orders.ts create mode 100644 src/models/taxcloud.ts create mode 100644 tests/taxcloud-orders.test.ts diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..c9b10fa --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,167 @@ +# GitHub Actions Workflows + +This directory contains automated workflows for the ZipTax Node.js SDK. + +## Workflows + +### 1. Test (`test.yml`) + +**Triggers**: Push to `main`, Pull Requests to `main` + +Runs comprehensive quality checks: + +- **Lint**: ESLint validation +- **Type Check**: TypeScript type validation +- **Format Check**: Prettier formatting validation +- **Test**: Unit tests across Node.js 18.x, 20.x, 22.x on Ubuntu, macOS, Windows +- **Coverage**: Test coverage with Codecov integration (requires 80%+) + +### 2. Version Check (`version-check.yml`) + +**Triggers**: Pull Requests to `main` (opened, synchronized, reopened, labeled, unlabeled) + +Enforces semantic versioning: + +- **Version Comparison**: Ensures `package.json` version is bumped from base branch +- **Semantic Validation**: Validates version follows [Semantic Versioning](https://semver.org/) +- **CHANGELOG Check**: Warns if CHANGELOG.md is not updated +- **PR Comments**: Posts detailed feedback with bump type and guidance +- **Skip Option**: Can be bypassed with `skip-version-check` label for docs-only changes + +#### Version Check Details + +The workflow will: +1. โœ… **Pass** if version is properly bumped (major, minor, patch, or prerelease) +2. โŒ **Fail** if version is not bumped or is invalid +3. โš ๏ธ **Warn** if CHANGELOG.md is not updated +4. ๐Ÿท๏ธ **Skip** if PR has `skip-version-check` label + +#### Usage + +**Before creating a PR**, bump the version appropriately: + +```bash +# Breaking changes (1.0.0 โ†’ 2.0.0) +npm version major + +# New features, backward compatible (1.0.0 โ†’ 1.1.0) +npm version minor + +# Bug fixes, backward compatible (1.0.0 โ†’ 1.0.1) +npm version patch + +# Prerelease versions (1.0.0 โ†’ 1.0.1-beta.0) +npm version prerelease --preid=beta +``` + +**Update CHANGELOG.md**: +- Document changes under `[Unreleased]` section +- Follow existing format (Added, Changed, Fixed, etc.) + +**Skip version check** (docs-only changes): +- Add `skip-version-check` label to the PR +- Only use for documentation, CI/CD, or repo maintenance changes + +### 3. Publish (`publish.yml`) + +**Triggers**: +- GitHub Releases (when created) +- Manual dispatch with tag input + +Publishes package to npm: + +- **Quality Checks**: Runs tests, linting, type checking +- **Build**: Creates distribution files +- **Publish**: Publishes to npm with provenance + +#### Manual Publishing + +1. Create a release on GitHub with a version tag (e.g., `v1.2.3`) +2. Workflow automatically publishes to npm +3. Or trigger manually via Actions tab with a specific tag + +**Required Secret**: `NPM_TOKEN` must be configured in repository secrets + +## Status Badges + +Add these badges to your README.md: + +```markdown +[![Test](https://github.com/ziptax/ziptax-node/actions/workflows/test.yml/badge.svg)](https://github.com/ziptax/ziptax-node/actions/workflows/test.yml) +[![codecov](https://codecov.io/gh/ziptax/ziptax-node/branch/main/graph/badge.svg)](https://codecov.io/gh/ziptax/ziptax-node) +``` + +## Troubleshooting + +### Version Check Fails + +**Problem**: PR fails version check even though you bumped the version + +**Solutions**: +1. Ensure you're comparing against the correct base branch (`main`) +2. Verify `package.json` version follows semantic versioning (x.y.z) +3. Check that new version is greater than base branch version +4. For docs-only changes, add `skip-version-check` label + +### Test Coverage Below 80% + +**Problem**: Coverage check fails + +**Solutions**: +1. Add tests for new code +2. Run `npm run test:coverage` locally to identify gaps +3. Ensure all new functions/methods have test coverage + +### Publish Fails + +**Problem**: Publish workflow fails with authentication error + +**Solutions**: +1. Verify `NPM_TOKEN` secret is configured correctly +2. Ensure token has publish permissions +3. Check that package name is available on npm + +## Best Practices + +1. **Always bump version** in PRs that include code changes +2. **Update CHANGELOG.md** with all changes +3. **Run tests locally** before pushing: `npm test` +4. **Check formatting**: `npm run format:check` +5. **Validate types**: `npm run type-check` +6. **Review CI failures** and fix before merging + +## Semantic Versioning Guide + +Following [SemVer](https://semver.org/): + +- **Major (X.0.0)**: Breaking changes, incompatible API changes + - Example: Removing a public method, changing return types + +- **Minor (0.X.0)**: New features, backward compatible + - Example: Adding new methods, new optional parameters + +- **Patch (0.0.X)**: Bug fixes, backward compatible + - Example: Fixing bugs, typos, performance improvements + +- **Prerelease (0.0.0-beta.X)**: Pre-production versions + - Example: Beta releases, release candidates + +## Contributing + +When contributing: + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Bump version appropriately +5. Update CHANGELOG.md +6. Run all checks locally +7. Create a Pull Request +8. Wait for CI to pass +9. Address any feedback + +## Questions? + +- Review workflow files for implementation details +- Check [GitHub Actions documentation](https://docs.github.com/en/actions) +- Open an issue if you encounter problems diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml new file mode 100644 index 0000000..63832d1 --- /dev/null +++ b/.github/workflows/version-check.yml @@ -0,0 +1,286 @@ +name: Version Check + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened, labeled, unlabeled] + +permissions: + contents: read + pull-requests: write + +jobs: + check-version-bump: + name: Check Version Bump + runs-on: ubuntu-latest + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Checkout base branch + run: | + git fetch origin ${{ github.event.pull_request.base.ref }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Get version from PR branch + id: pr-version + run: | + PR_VERSION=$(node -p "require('./package.json').version") + echo "version=$PR_VERSION" >> $GITHUB_OUTPUT + echo "PR branch version: $PR_VERSION" + + - name: Get version from base branch + id: base-version + run: | + git checkout origin/${{ github.event.pull_request.base.ref }} -- package.json + BASE_VERSION=$(node -p "require('./package.json').version") + echo "version=$BASE_VERSION" >> $GITHUB_OUTPUT + echo "Base branch version: $BASE_VERSION" + git checkout HEAD -- package.json + + - name: Check for skip-version-check label + id: check-skip + run: | + if [[ "${{ contains(github.event.pull_request.labels.*.name, 'skip-version-check') }}" == "true" ]]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "โš ๏ธ Version check skipped due to 'skip-version-check' label" + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Validate semantic version bump + if: steps.check-skip.outputs.skip == 'false' + id: validate + run: | + PR_VERSION="${{ steps.pr-version.outputs.version }}" + BASE_VERSION="${{ steps.base-version.outputs.version }}" + + echo "Comparing versions:" + echo " Base: $BASE_VERSION" + echo " PR: $PR_VERSION" + + # Skip check if versions are identical (allow for documentation-only changes with label) + if [[ "$PR_VERSION" == "$BASE_VERSION" ]]; then + echo "โŒ Version not bumped!" + echo "status=failed" >> $GITHUB_OUTPUT + echo "message=Version in package.json ($PR_VERSION) has not been bumped from base branch ($BASE_VERSION). Please update the version following semantic versioning." >> $GITHUB_OUTPUT + exit 1 + fi + + # Use npm to compare versions + npx semver $PR_VERSION -r ">$BASE_VERSION" > /dev/null 2>&1 + if [ $? -eq 0 ]; then + echo "โœ… Version successfully bumped from $BASE_VERSION to $PR_VERSION" + echo "status=success" >> $GITHUB_OUTPUT + + # Determine bump type + MAJOR_BASE=$(echo $BASE_VERSION | cut -d. -f1) + MINOR_BASE=$(echo $BASE_VERSION | cut -d. -f2) + PATCH_BASE=$(echo $BASE_VERSION | cut -d. -f3 | cut -d- -f1) + + MAJOR_PR=$(echo $PR_VERSION | cut -d. -f1) + MINOR_PR=$(echo $PR_VERSION | cut -d. -f2) + PATCH_PR=$(echo $PR_VERSION | cut -d. -f3 | cut -d- -f1) + + if [ "$MAJOR_PR" -gt "$MAJOR_BASE" ]; then + BUMP_TYPE="major" + elif [ "$MINOR_PR" -gt "$MINOR_BASE" ]; then + BUMP_TYPE="minor" + elif [ "$PATCH_PR" -gt "$PATCH_BASE" ]; then + BUMP_TYPE="patch" + else + BUMP_TYPE="prerelease" + fi + + echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT + echo "message=Version bumped from $BASE_VERSION to $PR_VERSION (${BUMP_TYPE} bump)" >> $GITHUB_OUTPUT + else + echo "โŒ Invalid version bump!" + echo "status=failed" >> $GITHUB_OUTPUT + echo "message=Version $PR_VERSION is not greater than base version $BASE_VERSION. Please ensure you're following semantic versioning." >> $GITHUB_OUTPUT + exit 1 + fi + + - name: Check CHANGELOG update + if: steps.check-skip.outputs.skip == 'false' + id: changelog + run: | + git diff origin/${{ github.event.pull_request.base.ref }} HEAD -- CHANGELOG.md > /dev/null 2>&1 + if [ $? -eq 0 ]; then + if git diff origin/${{ github.event.pull_request.base.ref }} HEAD -- CHANGELOG.md | grep -q "^+"; then + echo "โœ… CHANGELOG.md has been updated" + echo "updated=true" >> $GITHUB_OUTPUT + else + echo "โš ๏ธ CHANGELOG.md not updated" + echo "updated=false" >> $GITHUB_OUTPUT + fi + else + echo "โš ๏ธ CHANGELOG.md not updated" + echo "updated=false" >> $GITHUB_OUTPUT + fi + + - name: Post success comment + if: steps.validate.outputs.status == 'success' && steps.check-skip.outputs.skip == 'false' + uses: actions/github-script@v7 + with: + script: | + const bumpType = '${{ steps.validate.outputs.bump_type }}'; + const prVersion = '${{ steps.pr-version.outputs.version }}'; + const baseVersion = '${{ steps.base-version.outputs.version }}'; + const changelogUpdated = '${{ steps.changelog.outputs.updated }}' === 'true'; + + const bumpEmoji = { + major: '๐Ÿš€', + minor: 'โœจ', + patch: '๐Ÿ›', + prerelease: '๐Ÿงช' + }; + + let body = `## ${bumpEmoji[bumpType]} Version Check Passed\n\n`; + body += `โœ… Version successfully bumped from \`${baseVersion}\` to \`${prVersion}\`\n`; + body += `๐Ÿ“ฆ Bump type: **${bumpType}**\n\n`; + + if (changelogUpdated) { + body += `โœ… CHANGELOG.md has been updated\n\n`; + } else { + body += `โš ๏ธ **Warning**: CHANGELOG.md does not appear to be updated. `; + body += `Please ensure you've documented your changes.\n\n`; + } + + body += `### Semantic Versioning Guide\n`; + body += `- **Major** (X.0.0): Breaking changes\n`; + body += `- **Minor** (0.X.0): New features (backward compatible)\n`; + body += `- **Patch** (0.0.X): Bug fixes (backward compatible)\n`; + + // Find existing bot comments + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Version Check') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + + - name: Post failure comment + if: failure() && steps.check-skip.outputs.skip == 'false' + uses: actions/github-script@v7 + with: + script: | + const prVersion = '${{ steps.pr-version.outputs.version }}'; + const baseVersion = '${{ steps.base-version.outputs.version }}'; + + let body = `## โŒ Version Check Failed\n\n`; + body += `The version in \`package.json\` has not been properly updated.\n\n`; + body += `- **Base branch version**: \`${baseVersion}\`\n`; + body += `- **PR branch version**: \`${prVersion}\`\n\n`; + body += `### Required Actions\n\n`; + body += `Please update the version in \`package.json\` following [Semantic Versioning](https://semver.org/):\n\n`; + body += `\`\`\`bash\n`; + body += `# For breaking changes (major)\n`; + body += `npm version major\n\n`; + body += `# For new features (minor)\n`; + body += `npm version minor\n\n`; + body += `# For bug fixes (patch)\n`; + body += `npm version patch\n\n`; + body += `# For prerelease versions\n`; + body += `npm version prerelease --preid=beta\n`; + body += `\`\`\`\n\n`; + body += `### Update CHANGELOG.md\n\n`; + body += `Don't forget to document your changes in \`CHANGELOG.md\` under the \`[Unreleased]\` section.\n\n`; + body += `---\n\n`; + body += `๐Ÿ’ก **Tip**: If this is a documentation-only change or you have a valid reason to skip version checking, `; + body += `add the \`skip-version-check\` label to this PR.\n`; + + // Find existing bot comments + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Version Check') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + + - name: Post skip notice comment + if: steps.check-skip.outputs.skip == 'true' + uses: actions/github-script@v7 + with: + script: | + const body = `## โš ๏ธ Version Check Skipped\n\n` + + `The version check has been skipped due to the \`skip-version-check\` label.\n\n` + + `This should only be used for:\n` + + `- Documentation-only changes\n` + + `- CI/CD configuration updates\n` + + `- Repository maintenance tasks\n\n` + + `If this PR includes code changes, please remove the label and bump the version appropriately.`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Version Check') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f66c1..38af39f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- TaxCloud API integration for order management (optional) +- Support for CreateOrder endpoint - create orders from marketplace transactions +- Support for GetOrder endpoint - retrieve specific orders by ID +- Support for UpdateOrder endpoint - update order completedDate +- Support for RefundOrder endpoint - create partial or full refunds +- Support for GetRatesByPostalCode endpoint - postal code tax rate lookups +- New configuration options: `taxCloudConnectionId` and `taxCloudAPIKey` +- Comprehensive TypeScript types for all TaxCloud API models +- TaxCloud examples and documentation + +### Changed +- Client initialization now supports optional TaxCloud credentials +- Updated README with TaxCloud documentation and examples +- Enhanced HTTP client with PATCH method support for order updates + ## [1.0.0] - 2024-01-15 ### Added diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..054a811 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,352 @@ +# CLAUDE.md - ZipTax Node.js SDK + +This document provides context and guidance for AI assistants (like Claude) working on the ZipTax Node.js SDK project. + +## Project Overview + +The ZipTax Node.js SDK is an official TypeScript/JavaScript client library for interacting with: +1. **ZipTax API** - Sales and use tax rate lookups for US addresses +2. **TaxCloud API** - Order management and tax compliance (optional integration) + +**Repository:** https://github.com/ziptax/ziptax-node +**Package:** `@ziptax/node-sdk` on npm +**License:** MIT + +## Architecture + +### Core Structure + +``` +src/ +โ”œโ”€โ”€ client.ts # Main ZiptaxClient class +โ”œโ”€โ”€ config.ts # Configuration types and interfaces +โ”œโ”€โ”€ exceptions.ts # Custom error classes +โ”œโ”€โ”€ models/ +โ”‚ โ”œโ”€โ”€ index.ts # Model exports +โ”‚ โ”œโ”€โ”€ responses.ts # ZipTax API response types +โ”‚ โ””โ”€โ”€ taxcloud.ts # TaxCloud API types +โ””โ”€โ”€ utils/ + โ”œโ”€โ”€ http.ts # HTTPClient with retry logic + โ”œโ”€โ”€ retry.ts # Retry configuration + โ””โ”€โ”€ validation.ts # Input validation helpers +``` + +### Key Design Decisions + +1. **Dual API Support**: Single client supports both ZipTax (required) and TaxCloud (optional) APIs +2. **TypeScript First**: Full type safety with comprehensive interfaces for all API responses +3. **Optional TaxCloud**: TaxCloud features only available when credentials provided during initialization +4. **Retry Logic**: Built-in exponential backoff for transient failures +5. **Naming Convention**: camelCase for all fields (matching API responses) + +### Client Initialization Patterns + +```typescript +// Basic - Tax rate lookups only +const client = new ZiptaxClient({ apiKey: 'xxx' }); + +// With TaxCloud - Adds order management +const client = new ZiptaxClient({ + apiKey: 'ziptax-key', + taxCloudConnectionId: 'uuid', + taxCloudAPIKey: 'taxcloud-key' +}); +``` + +## API Endpoints + +### ZipTax API (Required) + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| `getSalesTaxByAddress()` | GET /request/v60/ | Tax rates by address | +| `getSalesTaxByGeoLocation()` | GET /request/v60/ | Tax rates by lat/lng | +| `getRatesByPostalCode()` | GET /request/v60/ | Tax rates by postal code | +| `getAccountMetrics()` | GET /account/v60/metrics | Account usage metrics | + +### TaxCloud API (Optional) + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| `createOrder()` | POST /tax/connections/{id}/orders | Create order | +| `getOrder()` | GET /tax/connections/{id}/orders/{orderId} | Retrieve order | +| `updateOrder()` | PATCH /tax/connections/{id}/orders/{orderId} | Update order | +| `refundOrder()` | POST /tax/connections/{id}/orders/refunds/{orderId} | Refund order | + +## Type System + +### Important Type Conventions + +1. **ZipTax Responses**: Use camelCase (e.g., `baseRates`, `taxSummaries`) +2. **Account Metrics**: Use snake_case (e.g., `core_request_count`) +3. **Optional Fields**: Many fields are optional despite API documentation +4. **Jurisdiction Names**: Use actual values like "CA", "ORANGE" (not enums) + +### Key Response Types + +- `V60Response` - Standard tax lookup response +- `V60PostalCodeResponse` - Postal code lookup (different format) +- `V60AccountMetrics` - Account metrics (uses snake_case) +- `OrderResponse` - TaxCloud order response +- `RefundTransactionResponse[]` - Array of refund transactions + +## Development Workflow + +### Build & Test Commands + +```bash +npm run build # Build all formats (CJS, ESM, types) +npm test # Run Jest tests +npm run test:coverage # Generate coverage report (requires 80%+) +npm run lint # ESLint check +npm run format # Prettier format +npm run type-check # TypeScript validation +``` + +### Semantic Versioning Enforcement + +**All PRs to `main` require a version bump in `package.json`.** + +The `version-check` GitHub Action automatically validates: +- โœ… Version has been bumped from base branch +- โœ… New version follows semantic versioning +- โš ๏ธ CHANGELOG.md has been updated (warning if not) + +**Before creating a PR:** + +```bash +# Breaking changes (1.0.0 โ†’ 2.0.0) +npm version major + +# New features, backward compatible (1.0.0 โ†’ 1.1.0) +npm version minor + +# Bug fixes, backward compatible (1.0.0 โ†’ 1.0.1) +npm version patch + +# Prerelease versions (1.0.0 โ†’ 1.0.1-beta.0) +npm version prerelease --preid=beta + +# Then update CHANGELOG.md and commit +git add CHANGELOG.md package.json package-lock.json +git commit -m "chore: bump version to x.y.z" +``` + +**Skip version check** (docs/CI changes only): +- Add `skip-version-check` label to PR +- Use sparingly, only for non-code changes + +### Code Quality Requirements + +- **Test Coverage**: Minimum 80% required +- **TypeScript**: Strict mode enabled, no `any` types +- **Linting**: ESLint with TypeScript rules +- **Formatting**: Prettier with 100-char line length + +### Example Scripts + +```bash +npm run example:basic # Basic ZipTax usage +npm run example:async # Concurrent requests +npm run example:errors # Error handling +npm run example:taxcloud # TaxCloud order management +``` + +## Common Tasks + +### Adding a New API Endpoint + +1. Add types to `src/models/responses.ts` or `src/models/taxcloud.ts` +2. Add method to `src/client.ts` with JSDoc comments +3. Export types from `src/models/index.ts` +4. Add tests to `tests/client.test.ts` +5. Update README.md with usage examples +6. Update CHANGELOG.md + +### Updating Dependencies + +```bash +npm update # Update dependencies +npm audit fix # Fix security issues +npm run test # Verify tests pass +``` + +### Publishing New Version + +**Automated via GitHub Actions:** + +1. Ensure version is bumped and CHANGELOG.md is updated +2. Merge PR to `main` (after passing all checks) +3. Create a GitHub Release with version tag (e.g., `v1.2.3`) +4. Publish workflow automatically runs and publishes to npm + +**Manual publishing (if needed):** + +1. Update version: `npm version [major|minor|patch]` +2. Move "[Unreleased]" changes to new version in `CHANGELOG.md` +3. Run `npm run prepublishOnly` (builds, tests, lints) +4. Create git tag: `git tag v1.x.x` +5. Push with tags: `git push origin main --tags` +6. Publish: `npm publish --access public` + +## Testing Strategy + +### Test Structure + +``` +tests/ +โ”œโ”€โ”€ client.test.ts # Client method tests +โ”œโ”€โ”€ http.test.ts # HTTPClient tests +โ”œโ”€โ”€ retry.test.ts # Retry logic tests +โ””โ”€โ”€ setup.ts # Test configuration +``` + +### Mocking Strategy + +- Mock axios responses for HTTP tests +- Use fixtures for realistic API response data +- Test both success and error paths +- Verify retry logic with transient failures + +### Running Specific Tests + +```bash +npm test -- client.test.ts # Single file +npm test -- --testNamePattern="create" # Match test name +npm run test:coverage # With coverage +``` + +## Error Handling + +### Error Hierarchy + +``` +ZiptaxError (base) +โ”œโ”€โ”€ ZiptaxAPIError (API errors) +โ”‚ โ”œโ”€โ”€ ZiptaxAuthenticationError (401) +โ”‚ โ”œโ”€โ”€ ZiptaxValidationError (400) +โ”‚ โ””โ”€โ”€ ZiptaxRateLimitError (429) +โ””โ”€โ”€ ZiptaxNetworkError (network failures) +``` + +### TaxCloud Credentials Error + +When TaxCloud methods called without credentials: +``` +Error: TaxCloud credentials not configured. Please provide... +``` + +## Important Files + +- **docs/spec.yaml** - Complete API specification (source of truth) +- **README.md** - User documentation +- **CHANGELOG.md** - Version history (must update with each PR) +- **package.json** - Dependencies and scripts (version must be bumped in PRs) +- **tsconfig.json** - TypeScript configuration (strict mode) +- **.eslintrc.json** - ESLint rules +- **.prettierrc** - Prettier configuration +- **.github/workflows/** - CI/CD workflows (test, version-check, publish) + +## API Documentation References + +- ZipTax API: https://www.zip-tax.com/documentation +- TaxCloud Orders: https://docs.taxcloud.com/api-reference/api-reference/sales-tax-api/orders/ +- OpenAPI Spec: https://api.zip-tax.com/openapi.json + +## Debugging Tips + +### Enable Logging + +```typescript +const client = new ZiptaxClient({ + apiKey: 'xxx', + enableLogging: true // Logs all requests/responses +}); +``` + +### Common Issues + +1. **TaxCloud not configured**: Check both `taxCloudConnectionId` and `taxCloudAPIKey` are set +2. **Type errors**: Ensure types match API responses (check docs/spec.yaml) +3. **Rate limiting**: SDK includes automatic retry with backoff +4. **Validation errors**: Check required fields and formats (e.g., postal code is 5-digit) + +## Best Practices + +### When Adding Features + +1. Follow existing patterns in `src/client.ts` +2. Add comprehensive TypeScript types +3. Include JSDoc comments with examples +4. Write tests achieving 80%+ coverage +5. **Bump version** in `package.json` using `npm version [major|minor|patch]` +6. **Update CHANGELOG.md** under `[Unreleased]` section +7. Update all documentation (README, examples) +8. Validate against docs/spec.yaml + +### Code Style + +- Use TypeScript interfaces (not types) for public API +- Export all public types from `src/index.ts` +- Keep line length under 100 characters +- Use single quotes for strings +- Add trailing commas in multi-line objects +- Explicit return types on public methods + +### Git Commit Messages + +Follow conventional commits: +- `feat:` - New features (minor version bump) +- `fix:` - Bug fixes (patch version bump) +- `docs:` - Documentation changes (no version bump with label) +- `test:` - Test changes (patch version bump) +- `refactor:` - Code refactoring (patch/minor version bump) +- `chore:` - Build/tooling changes (no version bump with label) +- `BREAKING CHANGE:` - Breaking changes (major version bump) + +Example: `feat: add support for TaxCloud order refunds` + +**Important**: Commit both version bump and changelog update: +```bash +npm version minor # Bumps version and creates commit +git add CHANGELOG.md +git commit --amend --no-edit # Add CHANGELOG to version commit +``` + +## Notable Implementation Details + +### HTTP Client + +- Two instances: one for ZipTax, one for TaxCloud (if configured) +- Automatic retry with exponential backoff +- Custom error handling based on HTTP status codes +- Optional request/response logging + +### Validation + +- Runtime validation for required fields +- Format validation (e.g., postal codes, UUIDs) +- Helpful error messages with field names + +### Build Output + +Three formats generated: +1. **CommonJS** (`dist/cjs/`) - For Node.js require() +2. **ES Modules** (`dist/esm/`) - For modern import +3. **Type Definitions** (`dist/types/`) - For TypeScript + +## Getting Help + +- **Issues**: https://github.com/ziptax/ziptax-node/issues +- **Email**: support@zip.tax +- **Documentation**: https://www.zip-tax.com/documentation + +## Version History + +- **v1.0.0** (2024-01-15) - Initial release with ZipTax API support +- **Unreleased** - Added TaxCloud integration and postal code lookups + +--- + +**Last Updated**: 2026-02-16 +**Maintained By**: ZipTax Team diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a831f71 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,331 @@ +# Contributing to ZipTax Node.js SDK + +Thank you for your interest in contributing to the ZipTax Node.js SDK! This document provides guidelines and instructions for contributing. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Workflow](#development-workflow) +- [Version Bumping (Required)](#version-bumping-required) +- [Pull Request Process](#pull-request-process) +- [Coding Standards](#coding-standards) +- [Testing Requirements](#testing-requirements) +- [Documentation](#documentation) + +## Code of Conduct + +We expect all contributors to be respectful and constructive. Please: + +- Be welcoming and inclusive +- Be respectful of differing viewpoints +- Accept constructive criticism gracefully +- Focus on what's best for the community + +## Getting Started + +### Prerequisites + +- Node.js >= 18.0.0 +- npm >= 9.0.0 +- Git + +### Setup + +1. Fork the repository on GitHub +2. Clone your fork locally: + ```bash + git clone https://github.com/YOUR_USERNAME/ziptax-node.git + cd ziptax-node + ``` + +3. Install dependencies: + ```bash + npm install + ``` + +4. Create a feature branch: + ```bash + git checkout -b feature/your-feature-name + ``` + +5. Set up upstream remote: + ```bash + git remote add upstream https://github.com/ziptax/ziptax-node.git + ``` + +## Development Workflow + +### Making Changes + +1. Make your changes in your feature branch +2. Write or update tests for your changes +3. Ensure all tests pass: + ```bash + npm test + ``` + +4. Check code quality: + ```bash + npm run lint + npm run format:check + npm run type-check + ``` + +5. Fix any issues: + ```bash + npm run format # Auto-fix formatting + npm run lint:fix # Auto-fix linting issues + ``` + +### Running Tests + +```bash +npm test # Run all tests +npm run test:watch # Run tests in watch mode +npm run test:coverage # Run tests with coverage report +``` + +### Building + +```bash +npm run build # Build all formats +``` + +## Version Bumping (Required) + +**โš ๏ธ IMPORTANT: All PRs with code changes MUST bump the version in `package.json`** + +The project enforces semantic versioning via GitHub Actions. Your PR will fail if the version is not bumped. + +### How to Bump Version + +Use npm's built-in version command: + +```bash +# For breaking changes (1.0.0 โ†’ 2.0.0) +npm version major + +# For new features, backward compatible (1.0.0 โ†’ 1.1.0) +npm version minor + +# For bug fixes, backward compatible (1.0.0 โ†’ 1.0.1) +npm version patch + +# For prerelease versions (1.0.0 โ†’ 1.0.1-beta.0) +npm version prerelease --preid=beta +``` + +### Update CHANGELOG.md + +After bumping the version, update `CHANGELOG.md`: + +1. Add your changes under the `[Unreleased]` section +2. Follow the existing format: + - **Added** - New features + - **Changed** - Changes in existing functionality + - **Deprecated** - Soon-to-be removed features + - **Removed** - Removed features + - **Fixed** - Bug fixes + - **Security** - Security fixes + +Example: +```markdown +## [Unreleased] + +### Added +- New method `getOrder()` for retrieving TaxCloud orders + +### Fixed +- Fixed validation error for postal codes with leading zeros +``` + +### Commit Version Changes + +```bash +# npm version already creates a commit, so amend it to include CHANGELOG +git add CHANGELOG.md +git commit --amend --no-edit +``` + +### When to Skip Version Check + +For documentation-only or CI/CD changes, you can skip the version check by: + +1. Adding the `skip-version-check` label to your PR on GitHub +2. Use this sparingly - only for: + - Documentation updates + - README changes + - CI/CD configuration + - Repository maintenance tasks + +## Pull Request Process + +### Before Submitting + +Checklist: +- [ ] Version bumped in `package.json` +- [ ] CHANGELOG.md updated +- [ ] Tests written and passing (80%+ coverage) +- [ ] Code linted and formatted +- [ ] TypeScript compiles without errors +- [ ] Documentation updated (if applicable) +- [ ] Examples updated (if applicable) + +### Submitting + +1. Push your changes to your fork: + ```bash + git push origin feature/your-feature-name + ``` + +2. Create a Pull Request on GitHub: + - Use a descriptive title + - Reference any related issues + - Describe what changed and why + - Mention any breaking changes + +3. Wait for CI checks to pass: + - โœ… Lint + - โœ… Type Check + - โœ… Format Check + - โœ… Tests (all platforms) + - โœ… Coverage (80%+) + - โœ… Version Check + +4. Address review feedback if requested + +### PR Title Format + +Use conventional commit format: + +- `feat: add support for TaxCloud refunds` +- `fix: correct postal code validation` +- `docs: update README with new examples` +- `test: add tests for order creation` +- `refactor: simplify HTTP client logic` +- `chore: update dependencies` + +### Semantic Versioning Guide + +| Change Type | Version Bump | Example | +|-------------|--------------|---------| +| Breaking changes, incompatible API changes | **Major** (x.0.0) | Removing methods, changing signatures | +| New features, backward compatible | **Minor** (0.x.0) | Adding new methods, optional parameters | +| Bug fixes, backward compatible | **Patch** (0.0.x) | Fixing bugs, performance improvements | +| Pre-release versions | **Prerelease** (0.0.0-beta.x) | Beta/RC versions | + +## Coding Standards + +### TypeScript + +- Use TypeScript strict mode +- No `any` types without explicit justification +- Export all public types from `src/index.ts` +- Add JSDoc comments to all public APIs +- Explicit return types on exported functions + +Example: +```typescript +/** + * Get sales tax rate by address + * @param params - Address lookup parameters + * @returns Tax rate response with jurisdiction breakdown + */ +async getSalesTaxByAddress(params: GetSalesTaxByAddressParams): Promise { + // Implementation +} +``` + +### Code Style + +- **Line Length**: 100 characters max +- **Indentation**: 2 spaces +- **Quotes**: Single quotes for strings +- **Semicolons**: Always use +- **Trailing Commas**: Use in multi-line objects/arrays + +These are enforced by Prettier and ESLint. + +### Naming Conventions + +- **Files**: kebab-case (`http-client.ts`) +- **Classes**: PascalCase (`ZiptaxClient`) +- **Functions/Methods**: camelCase (`getSalesTaxByAddress`) +- **Constants**: UPPER_SNAKE_CASE (`DEFAULT_TIMEOUT`) +- **Types/Interfaces**: PascalCase (`V60Response`) + +## Testing Requirements + +### Coverage + +- **Minimum**: 80% code coverage required +- Focus on critical paths and edge cases +- Test both success and error scenarios + +### Test Structure + +```typescript +describe('ZiptaxClient', () => { + describe('getSalesTaxByAddress', () => { + it('should return tax rates for valid address', async () => { + // Test implementation + }); + + it('should throw validation error for empty address', async () => { + // Test implementation + }); + + it('should retry on transient failures', async () => { + // Test implementation + }); + }); +}); +``` + +### Running Tests + +```bash +npm test # All tests +npm test -- client.test.ts # Specific file +npm test -- --testNamePattern=get # Match pattern +npm run test:coverage # With coverage +``` + +## Documentation + +### What to Document + +- **README.md**: User-facing documentation, usage examples +- **CHANGELOG.md**: Version history, changes +- **CLAUDE.md**: Project context for AI assistants +- **JSDoc Comments**: All public APIs +- **Examples**: Practical usage demonstrations + +### Examples + +When adding new features, create or update examples in `examples/`: + +```typescript +/** + * Example: Using TaxCloud order management + */ +import { ZiptaxClient } from '@ziptax/node-sdk'; + +// Example implementation +``` + +## Questions? + +- ๐Ÿ“– Check [CLAUDE.md](./CLAUDE.md) for project details +- ๐Ÿ› [Open an issue](https://github.com/ziptax/ziptax-node/issues) for bugs +- ๐Ÿ’ฌ [Start a discussion](https://github.com/ziptax/ziptax-node/discussions) for questions +- ๐Ÿ“ง Email: support@zip.tax + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. + +--- + +Thank you for contributing to the ZipTax Node.js SDK! ๐ŸŽ‰ diff --git a/README.md b/README.md index 20210f9..4132e3c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# ZipTax Node.js SDK +# Ziptax Node.js SDK -Official Node.js SDK for the [ZipTax API](https://www.zip-tax.com/) - Get accurate sales and use tax rates for any US address. +Official Node.js SDK for the [Ziptax API](https://www.zip-tax.com/) - Get accurate sales and use tax rates for any US address. [![npm version](https://badge.fury.io/js/%40ziptax%2Fnode-sdk.svg)](https://www.npmjs.com/package/@ziptax/node-sdk) [![Test](https://github.com/ziptax/ziptax-node/actions/workflows/test.yml/badge.svg)](https://github.com/ziptax/ziptax-node/actions/workflows/test.yml) @@ -16,6 +16,7 @@ Official Node.js SDK for the [ZipTax API](https://www.zip-tax.com/) - Get accura - โœ… Support for both CommonJS and ES Modules - โœ… Zero runtime dependencies (except axios) - โœ… 80%+ test coverage +- โœ… TaxCloud order management integration (optional) ## Installation @@ -46,6 +47,8 @@ console.log('Base Rates:', result.baseRates); ### Client Initialization +#### Basic Initialization (Tax Rate Lookups Only) + ```typescript const client = new ZiptaxClient({ apiKey: 'your-api-key-here', @@ -61,6 +64,19 @@ const client = new ZiptaxClient({ }); ``` +#### With TaxCloud Order Management (Optional) + +```typescript +const client = new ZiptaxClient({ + apiKey: 'your-ziptax-api-key-here', + taxCloudConnectionId: '25eb9b97-5acb-492d-b720-c03e79cf715a', // Optional: TaxCloud Connection ID (UUID) + taxCloudAPIKey: 'your-taxcloud-api-key-here', // Optional: TaxCloud API Key + // ... other options +}); +``` + +**Note:** TaxCloud order management features are optional and only available when both `taxCloudConnectionId` and `taxCloudAPIKey` are provided during client initialization. + ### Get Sales Tax by Address Returns sales and use tax rate details from an address input. @@ -89,6 +105,17 @@ const result = await client.getSalesTaxByGeoLocation({ }); ``` +### Get Sales Tax by Postal Code + +Returns sales and use tax rate details from a postal code input. + +```typescript +const result = await client.getRatesByPostalCode({ + postalcode: '92694', // Required: 5-digit US postal code + format?: 'json', // Optional: 'json' or 'xml' (default: 'json') +}); +``` + ### Get Account Metrics Returns account metrics and usage information. @@ -100,10 +127,95 @@ console.log('Requests:', metrics.core_request_count, '/', metrics.core_request_l console.log('Usage:', metrics.core_usage_percent.toFixed(2), '%'); ``` +## TaxCloud Order Management (Optional) + +The SDK includes optional TaxCloud integration for order management operations. These features require TaxCloud credentials to be configured during client initialization. + +### Create Order + +Create orders from marketplace transactions, pre-existing systems, or bulk uploads. + +```typescript +const orderResponse = await client.createOrder({ + orderId: 'my-order-1', + customerId: 'customer-453', + transactionDate: '2024-01-15T09:30:00Z', + completedDate: '2024-01-15T09:30:00Z', + origin: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427', + }, + destination: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427', + }, + lineItems: [ + { + index: 0, + itemId: 'item-1', + price: 10.8, + quantity: 1.5, + tax: { amount: 1.31, rate: 0.0813 }, + tic: 0, + }, + ], + currency: { currencyCode: 'USD' }, +}); +``` + +### Get Order + +Retrieve a specific order by its ID from TaxCloud. + +```typescript +const order = await client.getOrder('my-order-1'); + +console.log('Order ID:', order.orderId); +console.log('Customer:', order.customerId); +console.log('Total Tax:', order.lineItems.reduce((sum, item) => sum + item.tax.amount, 0)); +``` + +### Update Order + +Update an existing order's completedDate in TaxCloud. + +```typescript +const updatedOrder = await client.updateOrder('my-order-1', { + completedDate: '2024-01-16T10:00:00Z', +}); +``` + +### Refund Order + +Create a refund against an order in TaxCloud. An order can only be refunded once, regardless of whether the order is partially or fully refunded. + +```typescript +// Partial refund (specific items) +const refunds = await client.refundOrder('my-order-1', { + items: [ + { + itemId: 'item-1', + quantity: 1.0, + }, + ], +}); + +// Full refund (empty items array) +const fullRefunds = await client.refundOrder('my-order-1', { + items: [], +}); +``` + ## Response Types All methods return fully typed responses: +### ZipTax API Response Types + ```typescript interface V60Response { metadata: V60Metadata; @@ -115,6 +227,13 @@ interface V60Response { addressDetail: V60AddressDetail; } +interface V60PostalCodeResponse { + version: string; + rCode: number; + results: V60PostalCodeResult[]; + addressDetail: V60PostalCodeAddressDetail; +} + interface V60AccountMetrics { core_request_count: number; core_request_limit: number; @@ -128,7 +247,34 @@ interface V60AccountMetrics { } ``` -See the [full type definitions](./src/models/responses.ts) for complete details. +### TaxCloud API Response Types + +```typescript +interface OrderResponse { + orderId: string; + customerId: string; + connectionId: string; + transactionDate: string; + completedDate: string; + origin: TaxCloudAddressResponse; + destination: TaxCloudAddressResponse; + lineItems: CartItemWithTaxResponse[]; + currency: CurrencyResponse; + channel: string; + deliveredBySeller: boolean; + excludeFromFiling: boolean; + exemption: Exemption; +} + +interface RefundTransactionResponse { + connectionId: string; + createdDate: string; + items: CartItemRefundWithTaxResponse[]; + returnedDate?: string; +} +``` + +See the [full type definitions](./src/models/) for complete details. **Note:** Most API responses use camelCase field names (e.g., `baseRates`, `taxSummaries`), but account metrics use snake_case (e.g., `core_request_count`, `geo_enabled`). @@ -169,6 +315,26 @@ try { } ``` +### TaxCloud Error Handling + +When using TaxCloud features, errors will be thrown if the credentials are not configured: + +```typescript +try { + const order = await client.createOrder({ + orderId: 'my-order-1', + // ... order details + }); +} catch (error) { + if (error.message.includes('TaxCloud credentials not configured')) { + console.error('Please provide taxCloudConnectionId and taxCloudAPIKey during client initialization'); + } else { + // Handle other errors + console.error('Error creating order:', error); + } +} +``` + ## Advanced Usage ### Concurrent Requests @@ -185,6 +351,30 @@ const results = await Promise.all( ); ``` +### Working with Multiple Orders + +```typescript +// Create multiple orders concurrently +const orders = [ + { + orderId: 'order-1', + customerId: 'customer-1', + // ... order details + }, + { + orderId: 'order-2', + customerId: 'customer-2', + // ... order details + }, +]; + +const createdOrders = await Promise.all( + orders.map((order) => client.createOrder(order)) +); + +console.log(`Created ${createdOrders.length} orders`); +``` + ### Custom Retry Configuration ```typescript @@ -216,13 +406,14 @@ const client = new ZiptaxClient({ See the [examples](./examples) directory for more usage examples: -- [Basic Usage](./examples/basic-usage.ts) -- [Async Operations](./examples/async-usage.ts) -- [Error Handling](./examples/error-handling.ts) +- [Basic Usage](./examples/basic-usage.ts) - ZipTax tax rate lookups +- [Async Operations](./examples/async-usage.ts) - Concurrent requests +- [Error Handling](./examples/error-handling.ts) - Error handling patterns +- [TaxCloud Orders](./examples/taxcloud-orders.ts) - TaxCloud order management ### Running Examples -All examples require a valid ZipTax API key set as an environment variable: +Basic examples require a valid ZipTax API key: ```bash # Run basic usage example @@ -235,11 +426,23 @@ ZIPTAX_API_KEY=your-api-key npm run example:async ZIPTAX_API_KEY=your-api-key npm run example:errors ``` -Or export the environment variable first: +TaxCloud example requires both ZipTax and TaxCloud credentials: + +```bash +# Run TaxCloud order management example +ZIPTAX_API_KEY=your-api-key \ +TAXCLOUD_CONNECTION_ID=your-connection-id \ +TAXCLOUD_API_KEY=your-taxcloud-key \ +npm run example:taxcloud +``` + +Or export the environment variables first: ```bash export ZIPTAX_API_KEY=your-api-key -npm run example:basic +export TAXCLOUD_CONNECTION_ID=your-connection-id +export TAXCLOUD_API_KEY=your-taxcloud-key +npm run example:taxcloud ``` ## Requirements diff --git a/docs/spec.yaml b/docs/spec.yaml index 2b04522..0ba4665 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -28,27 +28,56 @@ project: # ----------------------------------------------------------------------------- # API Configuration # ----------------------------------------------------------------------------- +# The SDK now supports TWO APIs: +# 1. ZipTax API - For tax rate lookups and account metrics (REQUIRED) +# 2. TaxCloud API - For order management operations (OPTIONAL) + api: - # Base API details - name: "ZipTax API" - version: "v60" - base_url: "https://api.zip-tax.com/" - - # API specification source - spec: - type: "openapi" - version: "3.0.0" - source: "https://api.zip-tax.com/openapi.json" - - # Authentication methods - authentication: - type: "api_key" - location: "header" - parameter_name: "X-API-Key" - - # Additional auth configurations - supports_multiple_methods: false - methods: [] + # Primary API (ZipTax) - REQUIRED for all SDK functionality + ziptax: + name: "ZipTax API" + version: "v60" + base_url: "https://api.zip-tax.com/" + + # API specification source + spec: + type: "openapi" + version: "3.0.0" + source: "https://api.zip-tax.com/openapi.json" + + # Authentication methods + authentication: + type: "api_key" + location: "header" + parameter_name: "X-API-Key" + required: true + + # Secondary API (TaxCloud) - OPTIONAL for order management + taxcloud: + name: "TaxCloud API" + version: "v3" + base_url: "https://api.v3.taxcloud.com/" + documentation: "https://docs.taxcloud.com/api-reference/api-reference/sales-tax-api/orders/create-order" + + # Authentication methods + authentication: + type: "api_key" + location: "header" + parameter_name: "X-API-Key" + required: false # Only required if using TaxCloud features + + # Additional authentication requirements + required_parameters: + - name: "connectionId" + description: "TaxCloud Connection ID (UUID format) - used in API paths" + format: "uuid" + example: "25eb9b97-5acb-492d-b720-c03e79cf715a" + + # Feature availability notes + notes: + - "TaxCloud features are OPTIONAL and only available when both Connection ID and API Key are provided during client initialization" + - "Order management functions will return error if TaxCloud credentials not configured" + - "Uses Header authentication with the X-API-KEY header for both APIs, but TaxCloud also requires the connectionId in the path for order endpoints" # ----------------------------------------------------------------------------- # SDK Configuration @@ -221,7 +250,144 @@ resources: returns: type: "V60AccountMetrics" is_array: false - + + # TaxCloud API Orders Endpoints + - name: "CreateOrder" + api: "taxcloud" # Indicates this uses the TaxCloud API + http_method: "POST" + path: "/tax/connections/{connectionId}/orders" + description: "Create orders from marketplace transactions, pre-existing systems, or bulk uploads. Requires TaxCloud Connection ID and API Key configured during client initialization." + operation_id: "createOrder" + requires_taxcloud: true # This endpoint requires TaxCloud credentials + parameters: + - name: "connectionId" + type: "string" + format: "uuid" + required: true + location: "path" + description: "TaxCloud Connection ID (automatically populated from client configuration)" + example: "25eb9b97-5acb-492d-b720-c03e79cf715a" + - name: "addressAutocomplete" + type: "string" + required: false + location: "query" + description: "The specified addresses will be overridden with first result from address validation search" + default: "none" + enum: ["none", "origin", "destination", "all"] + request_body: + type: "CreateOrderRequest" + description: "Order details including line items, addresses, and tax information" + returns: + type: "OrderResponse" + is_array: false + status_code: 201 + authentication: + type: "header" + header: "X-API-KEY" + format: "{taxCloudAPIKey}" + note: "Uses TaxCloud API Key configured during client initialization" + + - name: "GetOrder" + api: "taxcloud" # Indicates this uses the TaxCloud API + http_method: "GET" + path: "/tax/connections/{connectionId}/orders/{orderId}" + description: "Retrieve a specific order by its ID from TaxCloud. Requires TaxCloud Connection ID and API Key configured during client initialization." + operation_id: "getOrder" + requires_taxcloud: true # This endpoint requires TaxCloud credentials + parameters: + - name: "connectionId" + type: "string" + format: "uuid" + required: true + location: "path" + description: "TaxCloud Connection ID (automatically populated from client configuration)" + example: "25eb9b97-5acb-492d-b720-c03e79cf715a" + - name: "orderId" + type: "string" + required: true + location: "path" + description: "The ID of the order to retrieve (order ID from external system)" + example: "my-order-1" + returns: + type: "OrderResponse" + is_array: false + status_code: 200 + authentication: + type: "header" + header: "X-API-KEY" + format: "{taxCloudAPIKey}" + note: "Uses TaxCloud API Key configured during client initialization" + + - name: "UpdateOrder" + api: "taxcloud" # Indicates this uses the TaxCloud API + http_method: "PATCH" + path: "/tax/connections/{connectionId}/orders/{orderId}" + description: "Update an existing order's completedDate in TaxCloud. Use this endpoint to change when an order was shipped/completed. Requires TaxCloud Connection ID and API Key configured during client initialization." + operation_id: "updateOrder" + requires_taxcloud: true # This endpoint requires TaxCloud credentials + parameters: + - name: "connectionId" + type: "string" + format: "uuid" + required: true + location: "path" + description: "TaxCloud Connection ID (automatically populated from client configuration)" + example: "25eb9b97-5acb-492d-b720-c03e79cf715a" + - name: "orderId" + type: "string" + required: true + location: "path" + description: "The ID of the order to update (order ID from external system)" + example: "my-order-1" + request_body: + type: "UpdateOrderRequest" + description: "Update order details (currently only completedDate can be updated)" + returns: + type: "OrderResponse" + is_array: false + status_code: 200 + authentication: + type: "header" + header: "X-API-KEY" + format: "{taxCloudAPIKey}" + note: "Uses TaxCloud API Key configured during client initialization" + + # TaxCloud API Refunds Endpoint + - name: "RefundOrder" + api: "taxcloud" # Indicates this uses the TaxCloud API + http_method: "POST" + path: "/tax/connections/{connectionId}/orders/refunds/{orderId}" + description: "Create a refund against an order in TaxCloud. An order can only be refunded once, regardless of whether the order is partially or fully refunded. Requires TaxCloud Connection ID and API Key configured during client initialization." + operation_id: "refundOrder" + requires_taxcloud: true # This endpoint requires TaxCloud credentials + parameters: + - name: "connectionId" + type: "string" + format: "uuid" + required: true + location: "path" + description: "TaxCloud Connection ID (automatically populated from client configuration)" + example: "25eb9b97-5acb-492d-b720-c03e79cf715a" + - name: "orderId" + type: "string" + required: true + location: "path" + description: "The ID of the order to refund against" + example: "my-order-1" + request_body: + type: "RefundTransactionRequest" + description: "Refund details including items to refund (empty or omitted items means full refund)" + returns: + type: "array" + items_type: "RefundTransactionResponse" + is_array: true + status_code: 201 + authentication: + type: "header" + header: "X-API-KEY" + format: "{taxCloudAPIKey}" + note: "Uses TaxCloud API Key configured during client initialization" + # ----------------------------------------------------------------------------- # Data Models # ----------------------------------------------------------------------------- @@ -764,6 +930,436 @@ models: required: true description: "Account status or informational message" + # --------------------------------------------------------------------------- + # TaxCloud API Models - Order Management + # --------------------------------------------------------------------------- + - name: "CreateOrderRequest" + description: "Request payload for creating an order in TaxCloud" + api: "taxcloud" + properties: + - name: "orderId" + type: "string" + required: true + description: "Order ID in external system" + example: "my-order-1" + - name: "customerId" + type: "string" + required: true + description: "Customer ID in external system" + example: "customer-453" + - name: "transactionDate" + type: "string" + format: "date-time" + required: true + description: "RFC3339 datetime string when order was purchased" + example: "2024-01-15T09:30:00Z" + - name: "completedDate" + type: "string" + format: "date-time" + required: true + description: "RFC3339 datetime string when order was shipped/completed (created tax liability)" + example: "2024-01-15T09:30:00Z" + - name: "origin" + type: "TaxCloudAddress" + required: true + description: "Origin address of the order" + - name: "destination" + type: "TaxCloudAddress" + required: true + description: "Destination address of the order" + - name: "lineItems" + type: "array" + items_type: "CartItemWithTax" + required: true + description: "Array of line items in the order with tax calculations" + - name: "currency" + type: "Currency" + required: true + description: "Currency information for the order" + - name: "channel" + type: "string" + required: false + description: "Sales channel (e.g., amazon, ebay, walmart) for tax exclusion rules" + example: "amazon" + - name: "deliveredBySeller" + type: "boolean" + required: false + description: "Whether the seller directly delivered the order" + - name: "excludeFromFiling" + type: "boolean" + required: false + description: "Whether to exclude this order from tax filing" + default: false + - name: "exemption" + type: "Exemption" + required: false + description: "Exemption certificate information for the order" + + - name: "OrderResponse" + description: "Response after successfully creating an order in TaxCloud" + api: "taxcloud" + properties: + - name: "orderId" + type: "string" + required: true + description: "Order ID in external system" + - name: "customerId" + type: "string" + required: true + description: "Customer ID in external system" + - name: "connectionId" + type: "string" + required: true + description: "TaxCloud connection ID used for this order" + - name: "transactionDate" + type: "string" + format: "date-time" + required: true + description: "RFC3339 datetime string when order was purchased" + - name: "completedDate" + type: "string" + format: "date-time" + required: true + description: "RFC3339 datetime string when order was shipped/completed" + - name: "origin" + type: "TaxCloudAddressResponse" + required: true + description: "Origin address of the order" + - name: "destination" + type: "TaxCloudAddressResponse" + required: true + description: "Destination address of the order" + - name: "lineItems" + type: "array" + items_type: "CartItemWithTaxResponse" + required: true + description: "Array of line items with tax calculations" + - name: "currency" + type: "CurrencyResponse" + required: true + description: "Currency information" + - name: "channel" + type: "string" + required: true + description: "Sales channel for the order" + - name: "deliveredBySeller" + type: "boolean" + required: true + description: "Whether seller directly delivered the order" + - name: "excludeFromFiling" + type: "boolean" + required: true + default: false + description: "Whether order is excluded from tax filing" + - name: "exemption" + type: "Exemption" + required: true + description: "Exemption information" + + - name: "TaxCloudAddress" + description: "Address structure for TaxCloud orders" + api: "taxcloud" + properties: + - name: "line1" + type: "string" + required: true + description: "First line of address (street, PO Box, or building)" + example: "323 Washington Ave N" + - name: "line2" + type: "string" + required: false + description: "Second line of address (apartment or suite number)" + - name: "city" + type: "string" + required: true + description: "City or post-town" + example: "Minneapolis" + - name: "state" + type: "string" + required: true + description: "State, province, county or large territorial division" + example: "MN" + - name: "zip" + type: "string" + required: true + description: "Postal or ZIP code" + example: "55401-2427" + - name: "countryCode" + type: "string" + required: false + description: "ISO 3166-1 alpha-2 country code" + default: "US" + enum: ["US", "CA"] + + - name: "TaxCloudAddressResponse" + description: "Address response structure from TaxCloud" + api: "taxcloud" + properties: + - name: "line1" + type: "string" + required: true + description: "First line of address" + - name: "line2" + type: "string" + required: false + description: "Second line of address" + - name: "city" + type: "string" + required: true + description: "City or post-town" + - name: "state" + type: "string" + required: true + description: "State abbreviation" + - name: "zip" + type: "string" + required: true + description: "Postal or ZIP code" + - name: "countryCode" + type: "string" + required: true + default: "US" + description: "ISO 3166-1 alpha-2 country code" + + - name: "CartItemWithTax" + description: "Cart line item with tax calculation for order creation" + api: "taxcloud" + properties: + - name: "index" + type: "integer" + format: "int64" + required: true + description: "Position/index of item within the cart" + example: 0 + - name: "itemId" + type: "string" + required: true + description: "Unique identifier for the cart item" + example: "item-1" + - name: "price" + type: "number" + format: "float" + required: true + description: "Unit price of the item" + example: 10.8 + - name: "quantity" + type: "number" + format: "float" + required: true + description: "Quantity of the item" + example: 1.5 + - name: "tax" + type: "Tax" + required: true + description: "Tax information for the item" + - name: "productId" + type: "string" + required: false + description: "Product ID from product catalog (must match existing product)" + - name: "tic" + type: "integer" + format: "int64" + required: false + description: "Taxability Information Code (defaults to 0 if not provided)" + default: 0 + + - name: "CartItemWithTaxResponse" + description: "Cart line item response from TaxCloud" + api: "taxcloud" + properties: + - name: "index" + type: "integer" + format: "int64" + required: true + description: "Position/index of item within the cart" + - name: "itemId" + type: "string" + required: true + description: "Unique identifier for the cart item" + - name: "price" + type: "number" + format: "float" + required: true + description: "Unit price of the item" + - name: "quantity" + type: "number" + format: "float" + required: true + description: "Quantity of the item" + - name: "tax" + type: "Tax" + required: true + description: "Tax information for the item" + - name: "tic" + type: "integer" + format: "int64" + required: true + description: "Taxability Information Code" + + - name: "Tax" + description: "Tax calculation details for a cart item" + api: "taxcloud" + properties: + - name: "amount" + type: "number" + format: "float" + required: true + description: "Tax amount calculated for the item" + example: 1.31 + - name: "rate" + type: "number" + format: "float" + required: true + description: "Tax rate applied (decimal format)" + example: 0.0813 + + - name: "Currency" + description: "Currency information for order" + api: "taxcloud" + properties: + - name: "currencyCode" + type: "string" + required: false + description: "ISO currency code" + default: "USD" + enum: ["USD", "CAD"] + + - name: "CurrencyResponse" + description: "Currency response from TaxCloud" + api: "taxcloud" + properties: + - name: "currencyCode" + type: "string" + required: true + description: "ISO currency code" + + - name: "Exemption" + description: "Tax exemption certificate information" + api: "taxcloud" + properties: + - name: "exemptionId" + type: "string" + required: false + description: "ID of exemption certificate used for customer (if provided, isExempt assumed true)" + - name: "isExempt" + type: "boolean" + required: false + description: "Whether customer is exempt from tax" + + - name: "UpdateOrderRequest" + description: "Request payload for updating an order in TaxCloud (currently only completedDate can be updated)" + api: "taxcloud" + properties: + - name: "completedDate" + type: "string" + format: "date-time" + required: true + description: "RFC3339 datetime string when order was shipped/completed (creates tax liability)" + example: "2024-01-16T10:00:00Z" + + # --------------------------------------------------------------------------- + # TaxCloud API Models - Refunds + # --------------------------------------------------------------------------- + - name: "RefundTransactionRequest" + description: "Request payload for creating a refund against an order in TaxCloud" + api: "taxcloud" + properties: + - name: "items" + type: "array" + items_type: "CartItemRefundWithTaxRequest" + required: false + description: "Items to refund. If empty list or omitted, entire order will be refunded" + - name: "returnedDate" + type: "string" + format: "date-time" + required: false + description: "Include only if this return is a change to a previously filed sales tax return (triggers Amended Sales Tax Return - not typically recommended)" + + - name: "CartItemRefundWithTaxRequest" + description: "Cart line item to be refunded" + api: "taxcloud" + properties: + - name: "itemId" + type: "string" + required: true + description: "Unique identifier for the cart item to refund" + example: "item-1" + - name: "quantity" + type: "number" + format: "float" + required: true + description: "Quantity of the item to refund" + example: 1.0 + + - name: "RefundTransactionResponse" + description: "Response after successfully creating a refund in TaxCloud" + api: "taxcloud" + properties: + - name: "connectionId" + type: "string" + required: true + description: "TaxCloud connection ID used for this refund" + - name: "createdDate" + type: "string" + format: "date-time" + required: true + description: "RFC3339 datetime string when the refund was created" + - name: "items" + type: "array" + items_type: "CartItemRefundWithTaxResponse" + required: true + description: "Array of refunded line items with tax calculations" + - name: "returnedDate" + type: "string" + format: "date-time" + required: false + description: "RFC3339 datetime string when the refund took effect" + + - name: "CartItemRefundWithTaxResponse" + description: "Refunded cart line item response from TaxCloud" + api: "taxcloud" + properties: + - name: "index" + type: "integer" + format: "int64" + required: true + description: "Position/index of item within the cart" + - name: "itemId" + type: "string" + required: true + description: "Unique identifier for the cart item" + - name: "price" + type: "number" + format: "float" + required: true + description: "Price of the refunded item" + - name: "quantity" + type: "number" + format: "float" + required: true + description: "Quantity of the item refunded" + - name: "tax" + type: "RefundTax" + required: true + description: "Tax information for the refunded item" + - name: "tic" + type: "integer" + format: "int64" + required: false + default: 0 + description: "Taxability Information Code" + + - name: "RefundTax" + description: "Tax details for a refunded item" + api: "taxcloud" + properties: + - name: "amount" + type: "number" + format: "float" + required: true + description: "Tax amount refunded for the item" + example: 1.31 + # ----------------------------------------------------------------------------- # Dependencies @@ -1029,24 +1625,80 @@ instructions: # Code patterns to follow patterns: - - pattern: "Config-based client initialization" + - pattern: "Config-based client initialization (ZipTax only)" example: | + // Create a new client with ZipTax API key only (tax rate lookups) const client = new ZiptaxClient({ apiKey: 'xxx' }); const result = await client.getSalesTaxByAddress({ address: '200 Spectrum Center Drive, Irvine, CA 92618' }); + - pattern: "Client initialization with TaxCloud credentials" + example: | + // Create client with TaxCloud credentials for order management + const client = new ZiptaxClient({ + apiKey: 'ziptax-api-key', + taxCloudConnectionId: '25eb9b97-5acb-492d-b720-c03e79cf715a', + taxCloudAPIKey: 'taxcloud-api-key' + }); + + // Now you can use both APIs + const taxRates = await client.getSalesTaxByAddress({ address: '...' }); + const order = await client.createOrder({ orderId: 'my-order-1', ... }); + + - pattern: "TaxCloud order creation" + example: | + const orderResponse = await client.createOrder({ + orderId: 'my-order-1', + customerId: 'customer-453', + transactionDate: '2024-01-15T09:30:00Z', + completedDate: '2024-01-15T09:30:00Z', + origin: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427' + }, + destination: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427' + }, + lineItems: [ + { + index: 0, + itemId: 'item-1', + price: 10.8, + quantity: 1.5, + tax: { amount: 1.31, rate: 0.0813 }, + tic: 0 + } + ], + currency: { currencyCode: 'USD' } + }); + - pattern: "Resource-based method organization" example: "await client.getSalesTaxByAddress({ address })" - pattern: "Consistent error handling" - example: "try/catch blocks with specific exception types" + example: | + try { + const order = await client.createOrder({...}); + } catch (error) { + if (error instanceof TaxCloudNotConfiguredError) { + console.error('TaxCloud credentials not provided'); + } else { + throw error; + } + } # Avoid these patterns anti_patterns: - "Don't use global state for configuration" - "Avoid bare catch clauses without error handling" - "Don't use 'any' type - leverage TypeScript's type system" + - "Don't call TaxCloud methods without configuring TaxCloud credentials" # ----------------------------------------------------------------------------- # Actual API Response Examples @@ -1228,6 +1880,190 @@ actual_api_responses: "message": "Contact support@zip.tax to modify your account" } + taxcloud_create_order: + description: "Actual TaxCloud response for CreateOrder" + endpoint: "POST /tax/connections/{connectionId}/orders" + example: | + { + "orderId": "my-order-1", + "customerId": "customer-453", + "connectionId": "25eb9b97-5acb-492d-b720-c03e79cf715a", + "transactionDate": "2024-01-15T09:30:00Z", + "completedDate": "2024-01-15T09:30:00Z", + "origin": { + "line1": "323 Washington Ave N", + "city": "Minneapolis", + "state": "MN", + "zip": "55401-2427", + "countryCode": "US" + }, + "destination": { + "line1": "323 Washington Ave N", + "city": "Minneapolis", + "state": "MN", + "zip": "55401-2427", + "countryCode": "US" + }, + "lineItems": [ + { + "index": 0, + "itemId": "item-1", + "price": 10.8, + "quantity": 1.5, + "tax": { + "amount": 1.31, + "rate": 0.0813 + }, + "tic": 0 + } + ], + "currency": { + "currencyCode": "USD" + }, + "channel": null, + "deliveredBySeller": false, + "excludeFromFiling": false, + "exemption": { + "exemptionId": null, + "isExempt": null + } + } + + taxcloud_get_order: + description: "Actual TaxCloud response for GetOrder by ID" + endpoint: "GET /tax/connections/{connectionId}/orders/{orderId}" + example: | + { + "orderId": "my-order-1", + "customerId": "customer-453", + "connectionId": "25eb9b97-5acb-492d-b720-c03e79cf715a", + "transactionDate": "2024-01-15T09:30:00Z", + "completedDate": "2024-01-15T09:30:00Z", + "origin": { + "line1": "323 Washington Ave N", + "city": "Minneapolis", + "state": "MN", + "zip": "55401-2427", + "countryCode": "US" + }, + "destination": { + "line1": "323 Washington Ave N", + "city": "Minneapolis", + "state": "MN", + "zip": "55401-2427", + "countryCode": "US" + }, + "lineItems": [ + { + "index": 0, + "itemId": "item-1", + "price": 10.8, + "quantity": 1.5, + "tax": { + "amount": 1.31, + "rate": 0.0813 + }, + "tic": 0 + } + ], + "currency": { + "currencyCode": "USD" + }, + "channel": null, + "deliveredBySeller": false, + "excludeFromFiling": false, + "exemption": { + "exemptionId": null, + "isExempt": null + } + } + + taxcloud_update_order: + description: "Actual TaxCloud response for UpdateOrder (updating completedDate)" + endpoint: "PATCH /tax/connections/{connectionId}/orders/{orderId}" + request_example: | + { + "completedDate": "2024-01-16T10:00:00Z" + } + response_example: | + { + "orderId": "my-order-1", + "customerId": "customer-453", + "connectionId": "25eb9b97-5acb-492d-b720-c03e79cf715a", + "transactionDate": "2024-01-15T09:30:00Z", + "completedDate": "2024-01-16T10:00:00Z", + "origin": { + "line1": "323 Washington Ave N", + "city": "Minneapolis", + "state": "MN", + "zip": "55401-2427", + "countryCode": "US" + }, + "destination": { + "line1": "323 Washington Ave N", + "city": "Minneapolis", + "state": "MN", + "zip": "55401-2427", + "countryCode": "US" + }, + "lineItems": [ + { + "index": 0, + "itemId": "item-1", + "price": 10.8, + "quantity": 1.5, + "tax": { + "amount": 1.31, + "rate": 0.0813 + }, + "tic": 0 + } + ], + "currency": { + "currencyCode": "USD" + }, + "channel": null, + "deliveredBySeller": false, + "excludeFromFiling": false, + "exemption": { + "exemptionId": null, + "isExempt": null + } + } + + taxcloud_refund_order: + description: "Actual TaxCloud response for RefundOrder (array of refund transactions)" + endpoint: "POST /tax/connections/{connectionId}/orders/refunds/{orderId}" + request_example: | + { + "items": [ + { + "itemId": "item-1", + "quantity": 1.0 + } + ] + } + response_example: | + [ + { + "connectionId": "25eb9b97-5acb-492d-b720-c03e79cf715a", + "createdDate": "2024-01-17T14:30:00Z", + "items": [ + { + "index": 0, + "itemId": "item-1", + "price": 10.8, + "quantity": 1.0, + "tax": { + "amount": 0.87 + }, + "tic": 0 + } + ], + "returnedDate": "2024-01-17T14:30:00Z" + } + ] + # ----------------------------------------------------------------------------- # CI/CD Quality Requirements # ----------------------------------------------------------------------------- diff --git a/examples/taxcloud-orders.ts b/examples/taxcloud-orders.ts new file mode 100644 index 0000000..4c87cc6 --- /dev/null +++ b/examples/taxcloud-orders.ts @@ -0,0 +1,161 @@ +/** + * TaxCloud order management example for ZipTax SDK + * + * Usage: + * ZIPTAX_API_KEY=your-api-key \ + * TAXCLOUD_CONNECTION_ID=your-connection-id \ + * TAXCLOUD_API_KEY=your-taxcloud-key \ + * npm run example:taxcloud + */ + +import { ZiptaxClient } from '../src'; + +async function main() { + // Get credentials from environment variables + const apiKey = process.env.ZIPTAX_API_KEY || 'your-api-key-here'; + const taxCloudConnectionId = process.env.TAXCLOUD_CONNECTION_ID || ''; + const taxCloudAPIKey = process.env.TAXCLOUD_API_KEY || ''; + + if (apiKey === 'your-api-key-here' || !taxCloudConnectionId || !taxCloudAPIKey) { + console.error('Error: Please set required environment variables'); + console.error('Usage:'); + console.error(' ZIPTAX_API_KEY=your-api-key \\'); + console.error(' TAXCLOUD_CONNECTION_ID=your-connection-id \\'); + console.error(' TAXCLOUD_API_KEY=your-taxcloud-key \\'); + console.error(' npm run example:taxcloud'); + process.exit(1); + } + + // Initialize the client with TaxCloud credentials + const client = new ZiptaxClient({ + apiKey, + taxCloudConnectionId, + taxCloudAPIKey, + enableLogging: true, + }); + + console.log('ZipTax SDK with TaxCloud Integration Example'); + console.log('==============================================\n'); + + try { + // Example 1: Create a new order + console.log('1. Creating a new order...'); + const orderId = `example-order-${Date.now()}`; + + const orderResponse = await client.createOrder({ + orderId, + customerId: 'customer-123', + transactionDate: new Date().toISOString(), + completedDate: new Date().toISOString(), + origin: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427', + }, + destination: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427', + }, + lineItems: [ + { + index: 0, + itemId: 'item-1', + price: 10.8, + quantity: 1.5, + tax: { amount: 1.31, rate: 0.0813 }, + tic: 0, + }, + { + index: 1, + itemId: 'item-2', + price: 25.0, + quantity: 2.0, + tax: { amount: 4.07, rate: 0.0813 }, + tic: 0, + }, + ], + currency: { currencyCode: 'USD' }, + }); + + console.log('Order created successfully!'); + console.log('Order ID:', orderResponse.orderId); + console.log('Customer ID:', orderResponse.customerId); + console.log('Transaction Date:', orderResponse.transactionDate); + console.log('Line Items:', orderResponse.lineItems.length); + console.log( + 'Total Tax:', + orderResponse.lineItems.reduce((sum, item) => sum + item.tax.amount, 0).toFixed(2) + ); + console.log('---\n'); + + // Example 2: Retrieve the order + console.log('2. Retrieving the order...'); + const retrievedOrder = await client.getOrder(orderId); + + console.log('Order retrieved successfully!'); + console.log('Order ID:', retrievedOrder.orderId); + console.log('Completed Date:', retrievedOrder.completedDate); + console.log('Exclude From Filing:', retrievedOrder.excludeFromFiling); + console.log('---\n'); + + // Example 3: Update the order's completed date + console.log('3. Updating order completed date...'); + const newCompletedDate = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); // Tomorrow + + const updatedOrder = await client.updateOrder(orderId, { + completedDate: newCompletedDate, + }); + + console.log('Order updated successfully!'); + console.log('New Completed Date:', updatedOrder.completedDate); + console.log('---\n'); + + // Example 4: Partial refund (refund one item) + console.log('4. Creating a partial refund...'); + const partialRefunds = await client.refundOrder(orderId, { + items: [ + { + itemId: 'item-1', + quantity: 1.0, + }, + ], + }); + + console.log('Partial refund created successfully!'); + console.log('Number of refund transactions:', partialRefunds.length); + partialRefunds.forEach((refund, index) => { + console.log(`Refund ${index + 1}:`); + console.log(' Created Date:', refund.createdDate); + console.log(' Items refunded:', refund.items.length); + console.log( + ' Total tax refunded:', + refund.items.reduce((sum, item) => sum + item.tax.amount, 0).toFixed(2) + ); + }); + console.log('---\n'); + + // Example 5: Demonstrate error handling + console.log('5. Demonstrating error handling...'); + try { + await client.getOrder('non-existent-order-id'); + } catch (error) { + console.log('Expected error caught:', error instanceof Error ? error.message : error); + } + console.log('---\n'); + + console.log('All examples completed successfully!'); + } catch (error) { + console.error('Error:', error); + if (error instanceof Error) { + console.error('Message:', error.message); + if ('statusCode' in error) { + console.error('Status Code:', (error as any).statusCode); + } + } + } +} + +main(); diff --git a/package.json b/package.json index 29a4f77..38aceda 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "example:basic": "tsx examples/basic-usage.ts", "example:async": "tsx examples/async-usage.ts", "example:errors": "tsx examples/error-handling.ts", + "example:taxcloud": "tsx examples/taxcloud-orders.ts", "prepublishOnly": "npm run build && npm run test && npm run lint && npm run type-check" }, "keywords": [ diff --git a/src/client.ts b/src/client.ts index e136f00..602fb2d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -17,15 +17,25 @@ import { GetRatesByPostalCodeParams, GetAccountMetricsParams, } from './config'; -import { V60Response, V60PostalCodeResponse, V60AccountMetrics } from './models'; +import { + V60Response, + V60PostalCodeResponse, + V60AccountMetrics, + CreateOrderRequest, + OrderResponse, + UpdateOrderRequest, + RefundTransactionRequest, + RefundTransactionResponse, +} from './models'; /** * ZipTax API client */ export class ZiptaxClient { private readonly httpClient: HTTPClient; - private readonly config: Required> & - Pick; + private readonly taxCloudHttpClient?: HTTPClient; + private readonly config: Required> & + Pick; /** * Create a new ZipTax client instance @@ -41,7 +51,7 @@ export class ZiptaxClient { ...config, }; - // Initialize HTTP client + // Initialize ZipTax HTTP client this.httpClient = new HTTPClient({ baseURL: this.config.baseURL, apiKey: this.config.apiKey, @@ -49,6 +59,17 @@ export class ZiptaxClient { retryOptions: this.config.retryOptions, enableLogging: this.config.enableLogging, }); + + // Initialize TaxCloud HTTP client if credentials are provided + if (config.taxCloudConnectionId && config.taxCloudAPIKey) { + this.taxCloudHttpClient = new HTTPClient({ + baseURL: 'https://api.v3.taxcloud.com', + apiKey: config.taxCloudAPIKey, + timeout: this.config.timeout, + retryOptions: this.config.retryOptions, + enableLogging: this.config.enableLogging, + }); + } } /** @@ -142,6 +163,97 @@ export class ZiptaxClient { }); } + /** + * Verify TaxCloud credentials are configured + * @throws Error if TaxCloud credentials are not configured + */ + private verifyTaxCloudCredentials(): void { + if (!this.taxCloudHttpClient || !this.config.taxCloudConnectionId) { + throw new Error( + 'TaxCloud credentials not configured. Please provide taxCloudConnectionId and taxCloudAPIKey in the client configuration.' + ); + } + } + + /** + * Create a new TaxCloud order + * @param request - Order creation request + * @returns OrderResponse with created order details + */ + async createOrder(request: CreateOrderRequest): Promise { + this.verifyTaxCloudCredentials(); + + // Validate required fields + validateRequired(request.orderId, 'orderId'); + validateRequired(request.customerId, 'customerId'); + validateRequired(request.transactionDate, 'transactionDate'); + validateRequired(request.completedDate, 'completedDate'); + + const connectionId = this.config.taxCloudConnectionId!; + const path = `/tax/connections/${connectionId}/orders`; + + return this.taxCloudHttpClient!.post(path, request); + } + + /** + * Get an existing TaxCloud order by ID + * @param orderId - Unique order identifier + * @returns OrderResponse with order details + */ + async getOrder(orderId: string): Promise { + this.verifyTaxCloudCredentials(); + + // Validate required fields + validateRequired(orderId, 'orderId'); + + const connectionId = this.config.taxCloudConnectionId!; + const path = `/tax/connections/${connectionId}/orders/${orderId}`; + + return this.taxCloudHttpClient!.get(path); + } + + /** + * Update an existing TaxCloud order + * @param orderId - Unique order identifier + * @param request - Order update request + * @returns OrderResponse with updated order details + */ + async updateOrder(orderId: string, request: UpdateOrderRequest): Promise { + this.verifyTaxCloudCredentials(); + + // Validate required fields + validateRequired(orderId, 'orderId'); + validateRequired(request.completedDate, 'completedDate'); + + const connectionId = this.config.taxCloudConnectionId!; + const path = `/tax/connections/${connectionId}/orders/${orderId}`; + + return this.taxCloudHttpClient!.patch(path, request); + } + + /** + * Refund a TaxCloud order + * @param orderId - Unique order identifier + * @param request - Refund request with items to refund + * @returns Array of RefundTransactionResponse + */ + async refundOrder(orderId: string, request: RefundTransactionRequest): Promise { + this.verifyTaxCloudCredentials(); + + // Validate required fields + validateRequired(orderId, 'orderId'); + validateRequired(request.items, 'items'); + + if (!Array.isArray(request.items) || request.items.length === 0) { + throw new Error('Refund request must include at least one item'); + } + + const connectionId = this.config.taxCloudConnectionId!; + const path = `/tax/connections/${connectionId}/orders/refunds/${orderId}`; + + return this.taxCloudHttpClient!.post(path, request); + } + /** * Get the current configuration */ @@ -152,6 +264,8 @@ export class ZiptaxClient { timeout: this.config.timeout, retryOptions: this.config.retryOptions, enableLogging: this.config.enableLogging, + taxCloudConnectionId: this.config.taxCloudConnectionId, + taxCloudAPIKey: this.config.taxCloudAPIKey, }; } } diff --git a/src/config.ts b/src/config.ts index 67c7f73..31e58f2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -18,6 +18,10 @@ export interface ZiptaxConfig { retryOptions?: RetryOptions; /** Enable request/response logging (default: false) */ enableLogging?: boolean; + /** TaxCloud Connection ID (UUID format) - required for TaxCloud order management */ + taxCloudConnectionId?: string; + /** TaxCloud API Key - required for TaxCloud order management */ + taxCloudAPIKey?: string; } /** diff --git a/src/models/index.ts b/src/models/index.ts index f05af8f..c71fb6e 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -2,3 +2,4 @@ * API response models */ export * from './responses'; +export * from './taxcloud'; diff --git a/src/models/taxcloud.ts b/src/models/taxcloud.ts new file mode 100644 index 0000000..415ffb8 --- /dev/null +++ b/src/models/taxcloud.ts @@ -0,0 +1,220 @@ +/** + * TaxCloud API models for order management + * All field names use camelCase to match TaxCloud API conventions + */ + +/** + * TaxCloud address structure + */ +export interface TaxCloudAddress { + /** Street address line 1 */ + line1: string; + /** Street address line 2 (optional) */ + line2?: string; + /** City */ + city: string; + /** State abbreviation (2-letter) */ + state: string; + /** ZIP code */ + zip: string; + /** Country code (US or CA) */ + countryCode?: 'US' | 'CA'; +} + +/** + * TaxCloud address in response format + */ +export interface TaxCloudAddressResponse extends TaxCloudAddress { + /** Country code (always present in responses) */ + countryCode: 'US' | 'CA'; +} + +/** + * Tax details for a line item + */ +export interface Tax { + /** Tax amount */ + amount: number; + /** Tax rate (decimal format) */ + rate: number; +} + +/** + * Refund tax details (amount only) + */ +export interface RefundTax { + /** Tax amount for refund */ + amount: number; +} + +/** + * Currency information + */ +export interface Currency { + /** ISO currency code */ + currencyCode: string; +} + +/** + * Currency response from API + */ +export interface CurrencyResponse { + /** ISO currency code */ + currencyCode: string; +} + +/** + * Exemption information + */ +export interface Exemption { + /** Exemption certificate ID */ + exemptionId: string | null; + /** Whether item is exempt */ + isExempt: boolean | null; +} + +/** + * Cart item with tax information (for creating orders) + */ +export interface CartItemWithTax { + /** Line item index */ + index: number; + /** Item identifier */ + itemId: string; + /** Item price */ + price: number; + /** Item quantity */ + quantity: number; + /** Tax information */ + tax: Tax; + /** TaxCloud TIC (Taxability Information Code) */ + tic: number; +} + +/** + * Cart item response from API + */ +export interface CartItemWithTaxResponse extends CartItemWithTax { + /** Tax information (always present in responses) */ + tax: Tax; +} + +/** + * Cart item for refund request + */ +export interface CartItemRefundWithTaxRequest { + /** Item identifier */ + itemId: string; + /** Quantity to refund */ + quantity: number; +} + +/** + * Cart item refund response from API + */ +export interface CartItemRefundWithTaxResponse { + /** Line item index */ + index: number; + /** Item identifier */ + itemId: string; + /** Item price */ + price: number; + /** Quantity refunded */ + quantity: number; + /** Tax information for refund */ + tax: RefundTax; + /** TaxCloud TIC (Taxability Information Code) */ + tic: number; +} + +/** + * Request to create a new order + */ +export interface CreateOrderRequest { + /** Unique order identifier */ + orderId: string; + /** Customer identifier */ + customerId: string; + /** Transaction date (RFC3339 format) */ + transactionDate: string; + /** Completed date (RFC3339 format) */ + completedDate: string; + /** Origin address */ + origin: TaxCloudAddress; + /** Destination address */ + destination: TaxCloudAddress; + /** Line items with tax */ + lineItems: CartItemWithTax[]; + /** Currency information */ + currency: Currency; + /** Sales channel (optional) */ + channel?: string | null; + /** Whether delivered by seller (optional) */ + deliveredBySeller?: boolean; + /** Whether to exclude from filing (optional) */ + excludeFromFiling?: boolean; + /** Exemption information (optional) */ + exemption?: Exemption; +} + +/** + * Response from creating or retrieving an order + */ +export interface OrderResponse { + /** Unique order identifier */ + orderId: string; + /** Customer identifier */ + customerId: string; + /** TaxCloud connection ID */ + connectionId: string; + /** Transaction date (RFC3339 format) */ + transactionDate: string; + /** Completed date (RFC3339 format) */ + completedDate: string; + /** Origin address */ + origin: TaxCloudAddressResponse; + /** Destination address */ + destination: TaxCloudAddressResponse; + /** Line items with tax */ + lineItems: CartItemWithTaxResponse[]; + /** Currency information */ + currency: CurrencyResponse; + /** Sales channel */ + channel: string | null; + /** Whether delivered by seller */ + deliveredBySeller: boolean; + /** Whether to exclude from filing */ + excludeFromFiling: boolean; + /** Exemption information */ + exemption: Exemption; +} + +/** + * Request to update an existing order + */ +export interface UpdateOrderRequest { + /** Updated completed date (RFC3339 format) */ + completedDate: string; +} + +/** + * Request to refund an order + */ +export interface RefundTransactionRequest { + /** Items to refund */ + items: CartItemRefundWithTaxRequest[]; +} + +/** + * Response from refund operation + */ +export interface RefundTransactionResponse { + /** TaxCloud connection ID */ + connectionId: string; + /** When refund was created (RFC3339 format) */ + createdDate: string; + /** Refunded items with tax details */ + items: CartItemRefundWithTaxResponse[]; + /** When items were returned (RFC3339 format) */ + returnedDate: string; +} \ No newline at end of file diff --git a/src/utils/http.ts b/src/utils/http.ts index 0347e3a..91de5d1 100644 --- a/src/utils/http.ts +++ b/src/utils/http.ts @@ -24,6 +24,19 @@ export interface HTTPClientConfig { enableLogging?: boolean; } +export interface TaxCloudHTTPClientConfig { + /** Base URL for TaxCloud API requests */ + baseURL: string; + /** TaxCloud API key for authentication */ + apiKey: string; + /** Request timeout in milliseconds */ + timeout?: number; + /** Retry configuration */ + retryOptions?: RetryOptions; + /** Enable request/response logging */ + enableLogging?: boolean; +} + /** * HTTP client for making API requests */ @@ -94,6 +107,13 @@ export class HTTPClient { return this.request({ ...config, method: 'POST', url, data }); } + /** + * Make a PATCH request + */ + async patch(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { + return this.request({ ...config, method: 'PATCH', url, data }); + } + /** * Make a request with retry logic */ diff --git a/tests/taxcloud-orders.test.ts b/tests/taxcloud-orders.test.ts new file mode 100644 index 0000000..9ee4cde --- /dev/null +++ b/tests/taxcloud-orders.test.ts @@ -0,0 +1,359 @@ +/** + * Tests for TaxCloud order management functions + */ + +import { ZiptaxClient } from '../src/client'; +import { HTTPClient } from '../src/utils/http'; +import { + CreateOrderRequest, + OrderResponse, + UpdateOrderRequest, + RefundTransactionRequest, + RefundTransactionResponse, +} from '../src/models'; + +// Mock the HTTPClient +jest.mock('../src/utils/http'); + +const mockOrderResponse: OrderResponse = { + orderId: 'test-order-123', + customerId: 'customer-456', + connectionId: '25eb9b97-5acb-492d-b720-c03e79cf715a', + transactionDate: '2024-01-15T09:30:00Z', + completedDate: '2024-01-15T09:30:00Z', + origin: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427', + countryCode: 'US', + }, + destination: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427', + countryCode: 'US', + }, + lineItems: [ + { + index: 0, + itemId: 'item-1', + price: 10.8, + quantity: 1.5, + tax: { + amount: 1.31, + rate: 0.0813, + }, + tic: 0, + }, + ], + currency: { + currencyCode: 'USD', + }, + channel: null, + deliveredBySeller: false, + excludeFromFiling: false, + exemption: { + exemptionId: null, + isExempt: null, + }, +}; + +const mockRefundResponse: RefundTransactionResponse[] = [ + { + connectionId: '25eb9b97-5acb-492d-b720-c03e79cf715a', + createdDate: '2024-01-17T14:30:00Z', + items: [ + { + index: 0, + itemId: 'item-1', + price: 10.8, + quantity: 1.0, + tax: { + amount: 0.87, + }, + tic: 0, + }, + ], + returnedDate: '2024-01-17T14:30:00Z', + }, +]; + +describe('ZiptaxClient - TaxCloud Orders', () => { + let client: ZiptaxClient; + let mockHttpClient: jest.Mocked; + let mockTaxCloudHttpClient: jest.Mocked; + + beforeEach(() => { + // Clear all mocks + jest.clearAllMocks(); + + // Create mock HTTP clients + mockHttpClient = new HTTPClient({ + baseURL: 'https://api.zip-tax.com', + apiKey: 'test-key', + }) as jest.Mocked; + + mockTaxCloudHttpClient = new HTTPClient({ + baseURL: 'https://api.v3.taxcloud.com', + apiKey: 'test-taxcloud-key', + }) as jest.Mocked; + + // Mock the HTTPClient constructor to return our mocks + (HTTPClient as jest.MockedClass).mockImplementation((config) => { + if (config.baseURL.includes('taxcloud')) { + return mockTaxCloudHttpClient; + } + return mockHttpClient; + }); + + // Initialize client with TaxCloud credentials + client = new ZiptaxClient({ + apiKey: 'test-api-key', + taxCloudConnectionId: '25eb9b97-5acb-492d-b720-c03e79cf715a', + taxCloudAPIKey: 'test-taxcloud-key', + }); + }); + + describe('createOrder', () => { + it('should create a new order successfully', async () => { + const createRequest: CreateOrderRequest = { + orderId: 'test-order-123', + customerId: 'customer-456', + transactionDate: '2024-01-15T09:30:00Z', + completedDate: '2024-01-15T09:30:00Z', + origin: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427', + }, + destination: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427', + }, + lineItems: [ + { + index: 0, + itemId: 'item-1', + price: 10.8, + quantity: 1.5, + tax: { + amount: 1.31, + rate: 0.0813, + }, + tic: 0, + }, + ], + currency: { + currencyCode: 'USD', + }, + }; + + mockTaxCloudHttpClient.post = jest.fn().mockResolvedValue(mockOrderResponse); + + const result = await client.createOrder(createRequest); + + expect(result).toEqual(mockOrderResponse); + expect(mockTaxCloudHttpClient.post).toHaveBeenCalledWith( + '/tax/connections/25eb9b97-5acb-492d-b720-c03e79cf715a/orders', + createRequest + ); + }); + + it('should throw error when TaxCloud credentials are not configured', async () => { + const clientWithoutTaxCloud = new ZiptaxClient({ + apiKey: 'test-api-key', + }); + + const createRequest: CreateOrderRequest = { + orderId: 'test-order-123', + customerId: 'customer-456', + transactionDate: '2024-01-15T09:30:00Z', + completedDate: '2024-01-15T09:30:00Z', + origin: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427', + }, + destination: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427', + }, + lineItems: [], + currency: { currencyCode: 'USD' }, + }; + + await expect(clientWithoutTaxCloud.createOrder(createRequest)).rejects.toThrow( + 'TaxCloud credentials not configured' + ); + }); + + it('should validate required fields', async () => { + const invalidRequest = { + orderId: '', + customerId: 'customer-456', + transactionDate: '2024-01-15T09:30:00Z', + completedDate: '2024-01-15T09:30:00Z', + origin: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427', + }, + destination: { + line1: '323 Washington Ave N', + city: 'Minneapolis', + state: 'MN', + zip: '55401-2427', + }, + lineItems: [], + currency: { currencyCode: 'USD' }, + } as CreateOrderRequest; + + await expect(client.createOrder(invalidRequest)).rejects.toThrow(); + }); + }); + + describe('getOrder', () => { + it('should retrieve an order by ID', async () => { + mockTaxCloudHttpClient.get = jest.fn().mockResolvedValue(mockOrderResponse); + + const result = await client.getOrder('test-order-123'); + + expect(result).toEqual(mockOrderResponse); + expect(mockTaxCloudHttpClient.get).toHaveBeenCalledWith( + '/tax/connections/25eb9b97-5acb-492d-b720-c03e79cf715a/orders/test-order-123' + ); + }); + + it('should throw error when TaxCloud credentials are not configured', async () => { + const clientWithoutTaxCloud = new ZiptaxClient({ + apiKey: 'test-api-key', + }); + + await expect(clientWithoutTaxCloud.getOrder('test-order-123')).rejects.toThrow( + 'TaxCloud credentials not configured' + ); + }); + + it('should validate orderId is provided', async () => { + await expect(client.getOrder('')).rejects.toThrow(); + }); + }); + + describe('updateOrder', () => { + it('should update an order successfully', async () => { + const updateRequest: UpdateOrderRequest = { + completedDate: '2024-01-16T10:00:00Z', + }; + + const updatedResponse = { + ...mockOrderResponse, + completedDate: '2024-01-16T10:00:00Z', + }; + + mockTaxCloudHttpClient.patch = jest.fn().mockResolvedValue(updatedResponse); + + const result = await client.updateOrder('test-order-123', updateRequest); + + expect(result).toEqual(updatedResponse); + expect(mockTaxCloudHttpClient.patch).toHaveBeenCalledWith( + '/tax/connections/25eb9b97-5acb-492d-b720-c03e79cf715a/orders/test-order-123', + updateRequest + ); + }); + + it('should throw error when TaxCloud credentials are not configured', async () => { + const clientWithoutTaxCloud = new ZiptaxClient({ + apiKey: 'test-api-key', + }); + + const updateRequest: UpdateOrderRequest = { + completedDate: '2024-01-16T10:00:00Z', + }; + + await expect(clientWithoutTaxCloud.updateOrder('test-order-123', updateRequest)).rejects.toThrow( + 'TaxCloud credentials not configured' + ); + }); + + it('should validate required fields', async () => { + await expect(client.updateOrder('', { completedDate: '2024-01-16T10:00:00Z' })).rejects.toThrow(); + await expect(client.updateOrder('test-order-123', { completedDate: '' })).rejects.toThrow(); + }); + }); + + describe('refundOrder', () => { + it('should refund an order successfully', async () => { + const refundRequest: RefundTransactionRequest = { + items: [ + { + itemId: 'item-1', + quantity: 1.0, + }, + ], + }; + + mockTaxCloudHttpClient.post = jest.fn().mockResolvedValue(mockRefundResponse); + + const result = await client.refundOrder('test-order-123', refundRequest); + + expect(result).toEqual(mockRefundResponse); + expect(mockTaxCloudHttpClient.post).toHaveBeenCalledWith( + '/tax/connections/25eb9b97-5acb-492d-b720-c03e79cf715a/orders/refunds/test-order-123', + refundRequest + ); + }); + + it('should throw error when TaxCloud credentials are not configured', async () => { + const clientWithoutTaxCloud = new ZiptaxClient({ + apiKey: 'test-api-key', + }); + + const refundRequest: RefundTransactionRequest = { + items: [{ itemId: 'item-1', quantity: 1.0 }], + }; + + await expect(clientWithoutTaxCloud.refundOrder('test-order-123', refundRequest)).rejects.toThrow( + 'TaxCloud credentials not configured' + ); + }); + + it('should validate items array is not empty', async () => { + const invalidRequest: RefundTransactionRequest = { + items: [], + }; + + await expect(client.refundOrder('test-order-123', invalidRequest)).rejects.toThrow( + 'Refund request must include at least one item' + ); + }); + + it('should validate orderId is provided', async () => { + const refundRequest: RefundTransactionRequest = { + items: [{ itemId: 'item-1', quantity: 1.0 }], + }; + + await expect(client.refundOrder('', refundRequest)).rejects.toThrow(); + }); + }); + + describe('getConfig', () => { + it('should return configuration including TaxCloud credentials', () => { + const config = client.getConfig(); + + expect(config).toMatchObject({ + apiKey: 'test-api-key', + taxCloudConnectionId: '25eb9b97-5acb-492d-b720-c03e79cf715a', + taxCloudAPIKey: 'test-taxcloud-key', + }); + }); + }); +}); \ No newline at end of file From 832e799b592f8134234c30caafe6f09b80bfd0bb Mon Sep 17 00:00:00 2001 From: Eric Lakich Date: Mon, 16 Feb 2026 09:43:54 -0800 Subject: [PATCH 2/9] ZIP-562: version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 38aceda..53adc78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ziptax/node-sdk", - "version": "0.1.4-beta", + "version": "0.2.0-beta", "description": "Official Node.js SDK for the ZipTax API", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", From 34c9742ce7174445ff6dcb6c0e19dee754e53617 Mon Sep 17 00:00:00 2001 From: Eric Lakich Date: Mon, 16 Feb 2026 11:51:36 -0800 Subject: [PATCH 3/9] ZIP-562: resolves QA report from Devin --- .env.example | 7 +++++- CLAUDE.md | 14 ++++++----- README.md | 30 ++++++++++------------- docs/spec.yaml | 43 ++++++++------------------------ examples/basic-usage.ts | 4 +-- package-lock.json | 18 +++++++------- src/client.ts | 22 +++++++++-------- src/index.ts | 26 +++++++++++++++++++- src/models/responses.ts | 20 +++++---------- src/models/taxcloud.ts | 11 ++++++--- src/utils/http.ts | 45 +++++++++++++++++++++++----------- tests/client.test.ts | 10 +++----- tests/http.test.ts | 2 +- tests/taxcloud-orders.test.ts | 46 ++++++++++++++++++++--------------- 14 files changed, 159 insertions(+), 139 deletions(-) diff --git a/.env.example b/.env.example index 6ba0499..31c1455 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,8 @@ # ZipTax API Key # Get your API key from https://www.zip-tax.com/ -ZIPTAX_API_KEY=your-api-key-here \ No newline at end of file +ZIPTAX_API_KEY=your-api-key-here + +# TaxCloud Credentials (optional - required for order management) +# Get your credentials from https://taxcloud.com/ +TAXCLOUD_CONNECTION_ID=your-connection-id-here +TAXCLOUD_API_KEY=your-taxcloud-api-key-here \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 054a811..cb13d72 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,7 +78,7 @@ const client = new ZiptaxClient({ ### Important Type Conventions 1. **ZipTax Responses**: Use camelCase (e.g., `baseRates`, `taxSummaries`) -2. **Account Metrics**: Use snake_case (e.g., `core_request_count`) +2. **Account Metrics**: Use snake_case (e.g., `request_count`, `usage_percent`) 3. **Optional Fields**: Many fields are optional despite API documentation 4. **Jurisdiction Names**: Use actual values like "CA", "ORANGE" (not enums) @@ -224,16 +224,18 @@ npm run test:coverage # With coverage ZiptaxError (base) โ”œโ”€โ”€ ZiptaxAPIError (API errors) โ”‚ โ”œโ”€โ”€ ZiptaxAuthenticationError (401) -โ”‚ โ”œโ”€โ”€ ZiptaxValidationError (400) โ”‚ โ””โ”€โ”€ ZiptaxRateLimitError (429) -โ””โ”€โ”€ ZiptaxNetworkError (network failures) +โ”œโ”€โ”€ ZiptaxValidationError (input validation) +โ”œโ”€โ”€ ZiptaxNetworkError (network failures) +โ”œโ”€โ”€ ZiptaxRetryError (max retries exceeded) +โ””โ”€โ”€ ZiptaxConfigurationError (invalid config) ``` ### TaxCloud Credentials Error When TaxCloud methods called without credentials: ``` -Error: TaxCloud credentials not configured. Please provide... +ZiptaxConfigurationError: TaxCloud credentials not configured. Please provide... ``` ## Important Files @@ -343,8 +345,8 @@ Three formats generated: ## Version History -- **v1.0.0** (2024-01-15) - Initial release with ZipTax API support -- **Unreleased** - Added TaxCloud integration and postal code lookups +- **v0.1.4-beta** - Initial beta release with ZipTax API support +- **v0.2.0-beta** - Added TaxCloud integration and postal code lookups --- diff --git a/README.md b/README.md index 4132e3c..4949884 100644 --- a/README.md +++ b/README.md @@ -123,8 +123,8 @@ Returns account metrics and usage information. ```typescript const metrics = await client.getAccountMetrics(); -console.log('Requests:', metrics.core_request_count, '/', metrics.core_request_limit); -console.log('Usage:', metrics.core_usage_percent.toFixed(2), '%'); +console.log('Requests:', metrics.request_count, '/', metrics.request_limit); +console.log('Usage:', metrics.usage_percent.toFixed(2), '%'); ``` ## TaxCloud Order Management (Optional) @@ -204,10 +204,8 @@ const refunds = await client.refundOrder('my-order-1', { ], }); -// Full refund (empty items array) -const fullRefunds = await client.refundOrder('my-order-1', { - items: [], -}); +// Full refund (omit items or pass empty array) +const fullRefunds = await client.refundOrder('my-order-1'); ``` ## Response Types @@ -235,13 +233,9 @@ interface V60PostalCodeResponse { } interface V60AccountMetrics { - core_request_count: number; - core_request_limit: number; - core_usage_percent: number; - geo_enabled: boolean; - geo_request_count: number; - geo_request_limit: number; - geo_usage_percent: number; + request_count: number; + request_limit: number; + usage_percent: number; is_active: boolean; message: string; } @@ -260,7 +254,7 @@ interface OrderResponse { destination: TaxCloudAddressResponse; lineItems: CartItemWithTaxResponse[]; currency: CurrencyResponse; - channel: string; + channel: string | null; deliveredBySeller: boolean; excludeFromFiling: boolean; exemption: Exemption; @@ -276,7 +270,7 @@ interface RefundTransactionResponse { See the [full type definitions](./src/models/) for complete details. -**Note:** Most API responses use camelCase field names (e.g., `baseRates`, `taxSummaries`), but account metrics use snake_case (e.g., `core_request_count`, `geo_enabled`). +**Note:** Most API responses use camelCase field names (e.g., `baseRates`, `taxSummaries`), but account metrics use snake_case (e.g., `request_count`, `is_active`). ## Error Handling @@ -317,16 +311,18 @@ try { ### TaxCloud Error Handling -When using TaxCloud features, errors will be thrown if the credentials are not configured: +When using TaxCloud features, a `ZiptaxConfigurationError` will be thrown if the credentials are not configured: ```typescript +import { ZiptaxConfigurationError } from '@ziptax/node-sdk'; + try { const order = await client.createOrder({ orderId: 'my-order-1', // ... order details }); } catch (error) { - if (error.message.includes('TaxCloud credentials not configured')) { + if (error instanceof ZiptaxConfigurationError) { console.error('Please provide taxCloudConnectionId and taxCloudAPIKey during client initialization'); } else { // Handle other errors diff --git a/docs/spec.yaml b/docs/spec.yaml index 0ba4665..4ccea85 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -9,7 +9,7 @@ project: name: "@ziptax/node-sdk" language: "typescript" - version: "1.0.0" + version: "0.2.0-beta" description: "Official Node.js SDK for the ZipTax API" # Repository information @@ -887,40 +887,21 @@ models: - name: "V60AccountMetrics" description: "Account metrics by API key" properties: - - name: "core_request_count" + - name: "request_count" type: "integer" format: "int64" required: true - description: "Number of core API requests made" - - name: "core_request_limit" + description: "Number of API requests made" + - name: "request_limit" type: "integer" format: "int64" required: true - description: "Maximum allowed core API requests" - - name: "core_usage_percent" + description: "Maximum allowed API requests" + - name: "usage_percent" type: "number" format: "float" required: true - description: "Percentage of core request limit used" - - name: "geo_enabled" - type: "boolean" - required: true - description: "Whether geolocation features are enabled" - - name: "geo_request_count" - type: "integer" - format: "int64" - required: true - description: "Number of geolocation requests made" - - name: "geo_request_limit" - type: "integer" - format: "int64" - required: true - description: "Maximum allowed geolocation requests" - - name: "geo_usage_percent" - type: "number" - format: "float" - required: true - description: "Percentage of geolocation request limit used" + description: "Percentage of request limit used" - name: "is_active" type: "boolean" required: true @@ -1869,13 +1850,9 @@ actual_api_responses: endpoint: "GET /account/v60/metrics" example: | { - "core_request_count": 15595, - "core_request_limit": 1000000, - "core_usage_percent": 1.5595, - "geo_enabled": true, - "geo_request_count": 43891, - "geo_request_limit": 1000000, - "geo_usage_percent": 4.3891, + "request_count": 15595, + "request_limit": 1000000, + "usage_percent": 1.5595, "is_active": true, "message": "Contact support@zip.tax to modify your account" } diff --git a/examples/basic-usage.ts b/examples/basic-usage.ts index 06abd5e..a83075a 100644 --- a/examples/basic-usage.ts +++ b/examples/basic-usage.ts @@ -58,8 +58,8 @@ async function main() { const metrics = await client.getAccountMetrics(); console.log('Account Metrics:'); - console.log('Core Requests:', metrics.core_request_count, '/', metrics.core_request_limit); - console.log('Usage:', metrics.core_usage_percent.toFixed(2), '%'); + console.log('Requests:', metrics.request_count, '/', metrics.request_limit); + console.log('Usage:', metrics.usage_percent.toFixed(2), '%'); console.log('Active:', metrics.is_active); } catch (error) { console.error('Error:', error); diff --git a/package-lock.json b/package-lock.json index 8ed5d80..157fb52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "ziptax", - "version": "1.0.0", + "name": "@ziptax/node-sdk", + "version": "0.2.0-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "ziptax", - "version": "1.0.0", + "name": "@ziptax/node-sdk", + "version": "0.2.0-beta", "license": "MIT", "dependencies": { "axios": "^1.6.0" @@ -2137,13 +2137,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, diff --git a/src/client.ts b/src/client.ts index 602fb2d..aa04980 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3,6 +3,7 @@ */ import { HTTPClient } from './utils/http'; +import { ZiptaxConfigurationError } from './exceptions'; import { validateApiKey, validateRequired, @@ -34,7 +35,9 @@ import { export class ZiptaxClient { private readonly httpClient: HTTPClient; private readonly taxCloudHttpClient?: HTTPClient; - private readonly config: Required> & + private readonly config: Required< + Omit + > & Pick; /** @@ -165,11 +168,11 @@ export class ZiptaxClient { /** * Verify TaxCloud credentials are configured - * @throws Error if TaxCloud credentials are not configured + * @throws ZiptaxConfigurationError if TaxCloud credentials are not configured */ private verifyTaxCloudCredentials(): void { if (!this.taxCloudHttpClient || !this.config.taxCloudConnectionId) { - throw new Error( + throw new ZiptaxConfigurationError( 'TaxCloud credentials not configured. Please provide taxCloudConnectionId and taxCloudAPIKey in the client configuration.' ); } @@ -237,21 +240,20 @@ export class ZiptaxClient { * @param request - Refund request with items to refund * @returns Array of RefundTransactionResponse */ - async refundOrder(orderId: string, request: RefundTransactionRequest): Promise { + async refundOrder( + orderId: string, + request?: RefundTransactionRequest + ): Promise { this.verifyTaxCloudCredentials(); // Validate required fields validateRequired(orderId, 'orderId'); - validateRequired(request.items, 'items'); - - if (!Array.isArray(request.items) || request.items.length === 0) { - throw new Error('Refund request must include at least one item'); - } const connectionId = this.config.taxCloudConnectionId!; const path = `/tax/connections/${connectionId}/orders/refunds/${orderId}`; - return this.taxCloudHttpClient!.post(path, request); + // Empty or omitted items means full refund per TaxCloud API spec + return this.taxCloudHttpClient!.post(path, request || {}); } /** diff --git a/src/index.ts b/src/index.ts index 4233295..32552b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,10 +11,11 @@ export type { ZiptaxConfig, GetSalesTaxByAddressParams, GetSalesTaxByGeoLocationParams, + GetRatesByPostalCodeParams, GetAccountMetricsParams, } from './config'; -// Export response models +// Export ZipTax response models export type { V60Response, V60Metadata, @@ -26,9 +27,32 @@ export type { V60TaxSummary, V60DisplayRate, V60AddressDetail, + V60PostalCodeResponse, + V60PostalCodeResult, + V60PostalCodeAddressDetail, V60AccountMetrics, } from './models'; +// Export TaxCloud models +export type { + TaxCloudAddress, + TaxCloudAddressResponse, + Tax, + RefundTax, + Currency, + CurrencyResponse, + Exemption, + CartItemWithTax, + CartItemWithTaxResponse, + CartItemRefundWithTaxRequest, + CartItemRefundWithTaxResponse, + CreateOrderRequest, + OrderResponse, + UpdateOrderRequest, + RefundTransactionRequest, + RefundTransactionResponse, +} from './models'; + // Export exceptions export { ZiptaxError, diff --git a/src/models/responses.ts b/src/models/responses.ts index dfd6570..6f47596 100644 --- a/src/models/responses.ts +++ b/src/models/responses.ts @@ -245,20 +245,12 @@ export interface V60PostalCodeResponse { * Account metrics by API key */ export interface V60AccountMetrics { - /** Number of core API requests made */ - core_request_count: number; - /** Maximum allowed core API requests */ - core_request_limit: number; - /** Percentage of core request limit used */ - core_usage_percent: number; - /** Whether geolocation features are enabled */ - geo_enabled: boolean; - /** Number of geolocation requests made */ - geo_request_count: number; - /** Maximum allowed geolocation requests */ - geo_request_limit: number; - /** Percentage of geolocation request limit used */ - geo_usage_percent: number; + /** Number of API requests made */ + request_count: number; + /** Maximum allowed API requests */ + request_limit: number; + /** Percentage of request limit used */ + usage_percent: number; /** Whether the account is currently active */ is_active: boolean; /** Account status or informational message */ diff --git a/src/models/taxcloud.ts b/src/models/taxcloud.ts index 415ffb8..fb044ea 100644 --- a/src/models/taxcloud.ts +++ b/src/models/taxcloud.ts @@ -199,10 +199,13 @@ export interface UpdateOrderRequest { /** * Request to refund an order + * If items is empty or omitted, the entire order will be refunded. */ export interface RefundTransactionRequest { - /** Items to refund */ - items: CartItemRefundWithTaxRequest[]; + /** Items to refund (empty or omitted means full refund) */ + items?: CartItemRefundWithTaxRequest[]; + /** Include only if this return is a change to a previously filed sales tax return */ + returnedDate?: string; } /** @@ -216,5 +219,5 @@ export interface RefundTransactionResponse { /** Refunded items with tax details */ items: CartItemRefundWithTaxResponse[]; /** When items were returned (RFC3339 format) */ - returnedDate: string; -} \ No newline at end of file + returnedDate?: string; +} diff --git a/src/utils/http.ts b/src/utils/http.ts index 91de5d1..2f8554c 100644 --- a/src/utils/http.ts +++ b/src/utils/http.ts @@ -24,19 +24,6 @@ export interface HTTPClientConfig { enableLogging?: boolean; } -export interface TaxCloudHTTPClientConfig { - /** Base URL for TaxCloud API requests */ - baseURL: string; - /** TaxCloud API key for authentication */ - apiKey: string; - /** Request timeout in milliseconds */ - timeout?: number; - /** Retry configuration */ - retryOptions?: RetryOptions; - /** Enable request/response logging */ - enableLogging?: boolean; -} - /** * HTTP client for making API requests */ @@ -55,7 +42,7 @@ export class HTTPClient { headers: { 'X-API-Key': config.apiKey, 'Content-Type': 'application/json', - 'User-Agent': 'ziptax-node/1.0.0', + 'User-Agent': `ziptax-node/${process.env.npm_package_version || '0.2.0-beta'}`, }, }); @@ -121,6 +108,7 @@ export class HTTPClient { const makeRequest = async (): Promise => { try { const response: AxiosResponse = await this.axiosInstance.request(config); + this.checkResponseBody(response.data); return response.data; } catch (error) { throw this.handleError(error); @@ -130,6 +118,35 @@ export class HTTPClient { return retryWithBackoff(makeRequest, this.retryOptions); } + /** + * Check response body for API-level errors (e.g., invalid key returns HTTP 200 with error code) + */ + private checkResponseBody(data: unknown): void { + if (typeof data !== 'object' || data === null) { + return; + } + + const body = data as Record; + + // Check for V60 response metadata errors + if (body.metadata && typeof body.metadata === 'object') { + const metadata = body.metadata as Record; + if (metadata.response && typeof metadata.response === 'object') { + const response = metadata.response as Record; + const code = response.code; + if (typeof code === 'number' && code !== 100) { + const message = + typeof response.message === 'string' ? response.message : 'API request failed'; + // Code 101 = invalid key + if (code === 101) { + throw new ZiptaxAuthenticationError(message); + } + throw new ZiptaxAPIError(message, undefined, data); + } + } + } + } + /** * Handle and transform axios errors into ZipTax errors */ diff --git a/tests/client.test.ts b/tests/client.test.ts index 03e92e8..8e2a07f 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -114,13 +114,9 @@ const mockPostalCodeResponse = { }; const mockAccountMetrics = { - core_request_count: 15595, - core_request_limit: 1000000, - core_usage_percent: 1.5595, - geo_enabled: true, - geo_request_count: 43891, - geo_request_limit: 1000000, - geo_usage_percent: 4.3891, + request_count: 15595, + request_limit: 1000000, + usage_percent: 1.5595, is_active: true, message: 'Contact support@zip.tax to modify your account', }; diff --git a/tests/http.test.ts b/tests/http.test.ts index 8d65785..f638f6b 100644 --- a/tests/http.test.ts +++ b/tests/http.test.ts @@ -42,7 +42,7 @@ describe('HTTPClient', () => { headers: { 'X-API-Key': 'test-api-key', 'Content-Type': 'application/json', - 'User-Agent': 'ziptax-node/1.0.0', + 'User-Agent': 'ziptax-node/0.2.0-beta', }, }); }); diff --git a/tests/taxcloud-orders.test.ts b/tests/taxcloud-orders.test.ts index 9ee4cde..3a410e5 100644 --- a/tests/taxcloud-orders.test.ts +++ b/tests/taxcloud-orders.test.ts @@ -3,6 +3,7 @@ */ import { ZiptaxClient } from '../src/client'; +import { ZiptaxConfigurationError } from '../src/exceptions'; import { HTTPClient } from '../src/utils/http'; import { CreateOrderRequest, @@ -164,7 +165,7 @@ describe('ZiptaxClient - TaxCloud Orders', () => { ); }); - it('should throw error when TaxCloud credentials are not configured', async () => { + it('should throw ZiptaxConfigurationError when TaxCloud credentials are not configured', async () => { const clientWithoutTaxCloud = new ZiptaxClient({ apiKey: 'test-api-key', }); @@ -191,7 +192,7 @@ describe('ZiptaxClient - TaxCloud Orders', () => { }; await expect(clientWithoutTaxCloud.createOrder(createRequest)).rejects.toThrow( - 'TaxCloud credentials not configured' + ZiptaxConfigurationError ); }); @@ -233,13 +234,13 @@ describe('ZiptaxClient - TaxCloud Orders', () => { ); }); - it('should throw error when TaxCloud credentials are not configured', async () => { + it('should throw ZiptaxConfigurationError when TaxCloud credentials are not configured', async () => { const clientWithoutTaxCloud = new ZiptaxClient({ apiKey: 'test-api-key', }); await expect(clientWithoutTaxCloud.getOrder('test-order-123')).rejects.toThrow( - 'TaxCloud credentials not configured' + ZiptaxConfigurationError ); }); @@ -270,7 +271,7 @@ describe('ZiptaxClient - TaxCloud Orders', () => { ); }); - it('should throw error when TaxCloud credentials are not configured', async () => { + it('should throw ZiptaxConfigurationError when TaxCloud credentials are not configured', async () => { const clientWithoutTaxCloud = new ZiptaxClient({ apiKey: 'test-api-key', }); @@ -279,13 +280,15 @@ describe('ZiptaxClient - TaxCloud Orders', () => { completedDate: '2024-01-16T10:00:00Z', }; - await expect(clientWithoutTaxCloud.updateOrder('test-order-123', updateRequest)).rejects.toThrow( - 'TaxCloud credentials not configured' - ); + await expect( + clientWithoutTaxCloud.updateOrder('test-order-123', updateRequest) + ).rejects.toThrow(ZiptaxConfigurationError); }); it('should validate required fields', async () => { - await expect(client.updateOrder('', { completedDate: '2024-01-16T10:00:00Z' })).rejects.toThrow(); + await expect( + client.updateOrder('', { completedDate: '2024-01-16T10:00:00Z' }) + ).rejects.toThrow(); await expect(client.updateOrder('test-order-123', { completedDate: '' })).rejects.toThrow(); }); }); @@ -312,7 +315,7 @@ describe('ZiptaxClient - TaxCloud Orders', () => { ); }); - it('should throw error when TaxCloud credentials are not configured', async () => { + it('should throw ZiptaxConfigurationError when TaxCloud credentials are not configured', async () => { const clientWithoutTaxCloud = new ZiptaxClient({ apiKey: 'test-api-key', }); @@ -321,18 +324,21 @@ describe('ZiptaxClient - TaxCloud Orders', () => { items: [{ itemId: 'item-1', quantity: 1.0 }], }; - await expect(clientWithoutTaxCloud.refundOrder('test-order-123', refundRequest)).rejects.toThrow( - 'TaxCloud credentials not configured' - ); + await expect( + clientWithoutTaxCloud.refundOrder('test-order-123', refundRequest) + ).rejects.toThrow(ZiptaxConfigurationError); }); - it('should validate items array is not empty', async () => { - const invalidRequest: RefundTransactionRequest = { - items: [], - }; + it('should allow full refund without items (empty or omitted)', async () => { + mockTaxCloudHttpClient.post = jest.fn().mockResolvedValue(mockRefundResponse); - await expect(client.refundOrder('test-order-123', invalidRequest)).rejects.toThrow( - 'Refund request must include at least one item' + // Full refund with no request body + const result = await client.refundOrder('test-order-123'); + + expect(result).toEqual(mockRefundResponse); + expect(mockTaxCloudHttpClient.post).toHaveBeenCalledWith( + '/tax/connections/25eb9b97-5acb-492d-b720-c03e79cf715a/orders/refunds/test-order-123', + {} ); }); @@ -356,4 +362,4 @@ describe('ZiptaxClient - TaxCloud Orders', () => { }); }); }); -}); \ No newline at end of file +}); From 591241bcdc0628ec466ac5de55e2c561cf9661c0 Mon Sep 17 00:00:00 2001 From: Eric Lakich Date: Mon, 16 Feb 2026 12:09:45 -0800 Subject: [PATCH 4/9] ZIP-562: resolves version bumping check --- .github/workflows/version-check.yml | 52 +++++++++++++++++------------ 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml index 63832d1..a8119bf 100644 --- a/.github/workflows/version-check.yml +++ b/.github/workflows/version-check.yml @@ -29,6 +29,9 @@ jobs: with: node-version: '20.x' + - name: Install semver + run: npm install -g semver + - name: Get version from PR branch id: pr-version run: | @@ -66,7 +69,7 @@ jobs: echo " Base: $BASE_VERSION" echo " PR: $PR_VERSION" - # Skip check if versions are identical (allow for documentation-only changes with label) + # Skip check if versions are identical if [[ "$PR_VERSION" == "$BASE_VERSION" ]]; then echo "โŒ Version not bumped!" echo "status=failed" >> $GITHUB_OUTPUT @@ -74,30 +77,37 @@ jobs: exit 1 fi - # Use npm to compare versions - npx semver $PR_VERSION -r ">$BASE_VERSION" > /dev/null 2>&1 - if [ $? -eq 0 ]; then + # Compare versions using node-semver gt (handles prerelease correctly) + IS_GREATER=$(node -e " + const semver = require('semver'); + const valid_pr = semver.valid('$PR_VERSION'); + const valid_base = semver.valid('$BASE_VERSION'); + if (!valid_pr || !valid_base) { + console.log('invalid'); + } else if (semver.gt('$PR_VERSION', '$BASE_VERSION')) { + console.log('true'); + } else { + console.log('false'); + } + ") + + if [ "$IS_GREATER" == "invalid" ]; then + echo "โŒ Invalid semver format!" + echo "status=failed" >> $GITHUB_OUTPUT + echo "message=One or both versions are not valid semver: base=$BASE_VERSION, pr=$PR_VERSION" >> $GITHUB_OUTPUT + exit 1 + fi + + if [ "$IS_GREATER" == "true" ]; then echo "โœ… Version successfully bumped from $BASE_VERSION to $PR_VERSION" echo "status=success" >> $GITHUB_OUTPUT # Determine bump type - MAJOR_BASE=$(echo $BASE_VERSION | cut -d. -f1) - MINOR_BASE=$(echo $BASE_VERSION | cut -d. -f2) - PATCH_BASE=$(echo $BASE_VERSION | cut -d. -f3 | cut -d- -f1) - - MAJOR_PR=$(echo $PR_VERSION | cut -d. -f1) - MINOR_PR=$(echo $PR_VERSION | cut -d. -f2) - PATCH_PR=$(echo $PR_VERSION | cut -d. -f3 | cut -d- -f1) - - if [ "$MAJOR_PR" -gt "$MAJOR_BASE" ]; then - BUMP_TYPE="major" - elif [ "$MINOR_PR" -gt "$MINOR_BASE" ]; then - BUMP_TYPE="minor" - elif [ "$PATCH_PR" -gt "$PATCH_BASE" ]; then - BUMP_TYPE="patch" - else - BUMP_TYPE="prerelease" - fi + BUMP_TYPE=$(node -e " + const semver = require('semver'); + const diff = semver.diff('$BASE_VERSION', '$PR_VERSION'); + console.log(diff || 'prerelease'); + ") echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT echo "message=Version bumped from $BASE_VERSION to $PR_VERSION (${BUMP_TYPE} bump)" >> $GITHUB_OUTPUT From e84638682a33066ac88be60b3bd702f9c4d8d9cd Mon Sep 17 00:00:00 2001 From: Eric Lakich Date: Mon, 16 Feb 2026 12:12:11 -0800 Subject: [PATCH 5/9] ZIP-562: removes version check from GH Actions --- .github/workflows/version-check.yml | 296 ---------------------------- 1 file changed, 296 deletions(-) delete mode 100644 .github/workflows/version-check.yml diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml deleted file mode 100644 index a8119bf..0000000 --- a/.github/workflows/version-check.yml +++ /dev/null @@ -1,296 +0,0 @@ -name: Version Check - -on: - pull_request: - branches: [main] - types: [opened, synchronize, reopened, labeled, unlabeled] - -permissions: - contents: read - pull-requests: write - -jobs: - check-version-bump: - name: Check Version Bump - runs-on: ubuntu-latest - steps: - - name: Checkout PR branch - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - - - name: Checkout base branch - run: | - git fetch origin ${{ github.event.pull_request.base.ref }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20.x' - - - name: Install semver - run: npm install -g semver - - - name: Get version from PR branch - id: pr-version - run: | - PR_VERSION=$(node -p "require('./package.json').version") - echo "version=$PR_VERSION" >> $GITHUB_OUTPUT - echo "PR branch version: $PR_VERSION" - - - name: Get version from base branch - id: base-version - run: | - git checkout origin/${{ github.event.pull_request.base.ref }} -- package.json - BASE_VERSION=$(node -p "require('./package.json').version") - echo "version=$BASE_VERSION" >> $GITHUB_OUTPUT - echo "Base branch version: $BASE_VERSION" - git checkout HEAD -- package.json - - - name: Check for skip-version-check label - id: check-skip - run: | - if [[ "${{ contains(github.event.pull_request.labels.*.name, 'skip-version-check') }}" == "true" ]]; then - echo "skip=true" >> $GITHUB_OUTPUT - echo "โš ๏ธ Version check skipped due to 'skip-version-check' label" - else - echo "skip=false" >> $GITHUB_OUTPUT - fi - - - name: Validate semantic version bump - if: steps.check-skip.outputs.skip == 'false' - id: validate - run: | - PR_VERSION="${{ steps.pr-version.outputs.version }}" - BASE_VERSION="${{ steps.base-version.outputs.version }}" - - echo "Comparing versions:" - echo " Base: $BASE_VERSION" - echo " PR: $PR_VERSION" - - # Skip check if versions are identical - if [[ "$PR_VERSION" == "$BASE_VERSION" ]]; then - echo "โŒ Version not bumped!" - echo "status=failed" >> $GITHUB_OUTPUT - echo "message=Version in package.json ($PR_VERSION) has not been bumped from base branch ($BASE_VERSION). Please update the version following semantic versioning." >> $GITHUB_OUTPUT - exit 1 - fi - - # Compare versions using node-semver gt (handles prerelease correctly) - IS_GREATER=$(node -e " - const semver = require('semver'); - const valid_pr = semver.valid('$PR_VERSION'); - const valid_base = semver.valid('$BASE_VERSION'); - if (!valid_pr || !valid_base) { - console.log('invalid'); - } else if (semver.gt('$PR_VERSION', '$BASE_VERSION')) { - console.log('true'); - } else { - console.log('false'); - } - ") - - if [ "$IS_GREATER" == "invalid" ]; then - echo "โŒ Invalid semver format!" - echo "status=failed" >> $GITHUB_OUTPUT - echo "message=One or both versions are not valid semver: base=$BASE_VERSION, pr=$PR_VERSION" >> $GITHUB_OUTPUT - exit 1 - fi - - if [ "$IS_GREATER" == "true" ]; then - echo "โœ… Version successfully bumped from $BASE_VERSION to $PR_VERSION" - echo "status=success" >> $GITHUB_OUTPUT - - # Determine bump type - BUMP_TYPE=$(node -e " - const semver = require('semver'); - const diff = semver.diff('$BASE_VERSION', '$PR_VERSION'); - console.log(diff || 'prerelease'); - ") - - echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT - echo "message=Version bumped from $BASE_VERSION to $PR_VERSION (${BUMP_TYPE} bump)" >> $GITHUB_OUTPUT - else - echo "โŒ Invalid version bump!" - echo "status=failed" >> $GITHUB_OUTPUT - echo "message=Version $PR_VERSION is not greater than base version $BASE_VERSION. Please ensure you're following semantic versioning." >> $GITHUB_OUTPUT - exit 1 - fi - - - name: Check CHANGELOG update - if: steps.check-skip.outputs.skip == 'false' - id: changelog - run: | - git diff origin/${{ github.event.pull_request.base.ref }} HEAD -- CHANGELOG.md > /dev/null 2>&1 - if [ $? -eq 0 ]; then - if git diff origin/${{ github.event.pull_request.base.ref }} HEAD -- CHANGELOG.md | grep -q "^+"; then - echo "โœ… CHANGELOG.md has been updated" - echo "updated=true" >> $GITHUB_OUTPUT - else - echo "โš ๏ธ CHANGELOG.md not updated" - echo "updated=false" >> $GITHUB_OUTPUT - fi - else - echo "โš ๏ธ CHANGELOG.md not updated" - echo "updated=false" >> $GITHUB_OUTPUT - fi - - - name: Post success comment - if: steps.validate.outputs.status == 'success' && steps.check-skip.outputs.skip == 'false' - uses: actions/github-script@v7 - with: - script: | - const bumpType = '${{ steps.validate.outputs.bump_type }}'; - const prVersion = '${{ steps.pr-version.outputs.version }}'; - const baseVersion = '${{ steps.base-version.outputs.version }}'; - const changelogUpdated = '${{ steps.changelog.outputs.updated }}' === 'true'; - - const bumpEmoji = { - major: '๐Ÿš€', - minor: 'โœจ', - patch: '๐Ÿ›', - prerelease: '๐Ÿงช' - }; - - let body = `## ${bumpEmoji[bumpType]} Version Check Passed\n\n`; - body += `โœ… Version successfully bumped from \`${baseVersion}\` to \`${prVersion}\`\n`; - body += `๐Ÿ“ฆ Bump type: **${bumpType}**\n\n`; - - if (changelogUpdated) { - body += `โœ… CHANGELOG.md has been updated\n\n`; - } else { - body += `โš ๏ธ **Warning**: CHANGELOG.md does not appear to be updated. `; - body += `Please ensure you've documented your changes.\n\n`; - } - - body += `### Semantic Versioning Guide\n`; - body += `- **Major** (X.0.0): Breaking changes\n`; - body += `- **Minor** (0.X.0): New features (backward compatible)\n`; - body += `- **Patch** (0.0.X): Bug fixes (backward compatible)\n`; - - // Find existing bot comments - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const botComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('Version Check') - ); - - if (botComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body - }); - } - - - name: Post failure comment - if: failure() && steps.check-skip.outputs.skip == 'false' - uses: actions/github-script@v7 - with: - script: | - const prVersion = '${{ steps.pr-version.outputs.version }}'; - const baseVersion = '${{ steps.base-version.outputs.version }}'; - - let body = `## โŒ Version Check Failed\n\n`; - body += `The version in \`package.json\` has not been properly updated.\n\n`; - body += `- **Base branch version**: \`${baseVersion}\`\n`; - body += `- **PR branch version**: \`${prVersion}\`\n\n`; - body += `### Required Actions\n\n`; - body += `Please update the version in \`package.json\` following [Semantic Versioning](https://semver.org/):\n\n`; - body += `\`\`\`bash\n`; - body += `# For breaking changes (major)\n`; - body += `npm version major\n\n`; - body += `# For new features (minor)\n`; - body += `npm version minor\n\n`; - body += `# For bug fixes (patch)\n`; - body += `npm version patch\n\n`; - body += `# For prerelease versions\n`; - body += `npm version prerelease --preid=beta\n`; - body += `\`\`\`\n\n`; - body += `### Update CHANGELOG.md\n\n`; - body += `Don't forget to document your changes in \`CHANGELOG.md\` under the \`[Unreleased]\` section.\n\n`; - body += `---\n\n`; - body += `๐Ÿ’ก **Tip**: If this is a documentation-only change or you have a valid reason to skip version checking, `; - body += `add the \`skip-version-check\` label to this PR.\n`; - - // Find existing bot comments - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const botComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('Version Check') - ); - - if (botComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body - }); - } - - - name: Post skip notice comment - if: steps.check-skip.outputs.skip == 'true' - uses: actions/github-script@v7 - with: - script: | - const body = `## โš ๏ธ Version Check Skipped\n\n` + - `The version check has been skipped due to the \`skip-version-check\` label.\n\n` + - `This should only be used for:\n` + - `- Documentation-only changes\n` + - `- CI/CD configuration updates\n` + - `- Repository maintenance tasks\n\n` + - `If this PR includes code changes, please remove the label and bump the version appropriately.`; - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const botComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('Version Check') - ); - - if (botComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body - }); - } From e07faa8d5b19137adda91c95124f077d1b50dc1d Mon Sep 17 00:00:00 2001 From: Eric Lakich Date: Mon, 16 Feb 2026 12:31:04 -0800 Subject: [PATCH 6/9] ZIP-562: fixes historical format --- README.md | 4 ++-- docs/spec.yaml | 8 ++++---- src/client.ts | 4 ++-- src/config.ts | 4 ++-- tests/client.test.ts | 20 +++++++++++++++++++- 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4949884..62f23e1 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ const result = await client.getSalesTaxByAddress({ address: '200 Spectrum Center Drive, Irvine, CA 92618', taxabilityCode?: '12345', // Optional: Product/service taxability code countryCode?: 'USA', // Optional: 'USA' or 'CAN' (default: 'USA') - historical?: '2024-01', // Optional: Historical date (YYYY-MM format) + historical?: '202401', // Optional: Historical date (YYYYMM format) format?: 'json', // Optional: 'json' or 'xml' (default: 'json') }); ``` @@ -100,7 +100,7 @@ const result = await client.getSalesTaxByGeoLocation({ lat: '33.65253', lng: '-117.74794', countryCode?: 'USA', // Optional: 'USA' or 'CAN' (default: 'USA') - historical?: '2024-01', // Optional: Historical date (YYYY-MM format) + historical?: '202401', // Optional: Historical date (YYYYMM format) format?: 'json', // Optional: 'json' or 'xml' (default: 'json') }); ``` diff --git a/docs/spec.yaml b/docs/spec.yaml index 4ccea85..4d1b42c 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -152,9 +152,9 @@ resources: format: "date" required: false location: "query" - description: "Historical date for rates (YYYY-MM format)" + description: "Historical date for rates (YYYYMM format, e.g., '202401')" validation: - pattern: "^[0-9]{4}-[0-9]{2}$" + pattern: "^[0-9]{6}$" - name: "format" type: "string" required: false @@ -197,9 +197,9 @@ resources: format: "date" required: false location: "query" - description: "Historical date for rates (YYYY-MM format)" + description: "Historical date for rates (YYYYMM format, e.g., '202401')" validation: - pattern: "^[0-9]{4}-[0-9]{2}$" + pattern: "^[0-9]{6}$" - name: "format" type: "string" required: false diff --git a/src/client.ts b/src/client.ts index aa04980..dbc328e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -91,7 +91,7 @@ export class ZiptaxClient { } if (params.historical) { - validatePattern(params.historical, /^[0-9]{4}-[0-9]{2}$/, 'historical', 'YYYY-MM format'); + validatePattern(params.historical, /^[0-9]{6}$/, 'historical', 'YYYYMM format'); } // Make API request @@ -120,7 +120,7 @@ export class ZiptaxClient { // Validate optional parameters if (params.historical) { - validatePattern(params.historical, /^[0-9]{4}-[0-9]{2}$/, 'historical', 'YYYY-MM format'); + validatePattern(params.historical, /^[0-9]{6}$/, 'historical', 'YYYYMM format'); } // Make API request diff --git a/src/config.ts b/src/config.ts index 31e58f2..2c3881a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -43,7 +43,7 @@ export interface GetSalesTaxByAddressParams { taxabilityCode?: string; /** Country code (default: USA) */ countryCode?: 'USA' | 'CAN'; - /** Historical date for rates (YYYY-MM format) */ + /** Historical date for rates (YYYYMM format, e.g., '202401') */ historical?: string; /** Response format (default: json) */ format?: 'json' | 'xml'; @@ -59,7 +59,7 @@ export interface GetSalesTaxByGeoLocationParams { lng: string; /** Country code (default: USA) */ countryCode?: 'USA' | 'CAN'; - /** Historical date for rates (YYYY-MM format) */ + /** Historical date for rates (YYYYMM format, e.g., '202401') */ historical?: string; /** Response format (default: json) */ format?: 'json' | 'xml'; diff --git a/tests/client.test.ts b/tests/client.test.ts index 8e2a07f..c760c70 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -208,8 +208,16 @@ describe('ZiptaxClient', () => { ).rejects.toThrow(ZiptaxValidationError); }); - it('should validate historical date format', async () => { + it('should validate historical date format (YYYYMM)', async () => { const client = new ZiptaxClient({ apiKey: 'test-api-key' }); + // Reject YYYY-MM (with dash) + await expect( + client.getSalesTaxByAddress({ + address: '200 Spectrum Center Drive', + historical: '2024-01', + }) + ).rejects.toThrow(ZiptaxValidationError); + // Reject arbitrary string await expect( client.getSalesTaxByAddress({ address: '200 Spectrum Center Drive', @@ -217,6 +225,16 @@ describe('ZiptaxClient', () => { }) ).rejects.toThrow(ZiptaxValidationError); }); + + it('should accept valid YYYYMM historical date', async () => { + mockHttpClient.get.mockResolvedValue(mockV60Response); + const client = new ZiptaxClient({ apiKey: 'test-api-key' }); + const result = await client.getSalesTaxByAddress({ + address: '200 Spectrum Center Drive', + historical: '202401', + }); + expect(result).toEqual(mockV60Response); + }); }); describe('getSalesTaxByGeoLocation', () => { From a0bec389dde3e2812935e99b994ec5aadd64bec8 Mon Sep 17 00:00:00 2001 From: Eric Lakich Date: Mon, 16 Feb 2026 12:43:49 -0800 Subject: [PATCH 7/9] ZIP-562: documentation updates --- .github/workflows/README.md | 21 ++++++------ CHANGELOG.md | 64 ++++++++++++++++++++++++------------- CLAUDE.md | 43 +++++++++++++++---------- CONTRIBUTING.md | 18 +++++------ 4 files changed, 88 insertions(+), 58 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index c9b10fa..7ecea93 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -41,16 +41,16 @@ The workflow will: **Before creating a PR**, bump the version appropriately: ```bash -# Breaking changes (1.0.0 โ†’ 2.0.0) +# Breaking changes (0.2.0-beta โ†’ 1.0.0) npm version major -# New features, backward compatible (1.0.0 โ†’ 1.1.0) +# New features, backward compatible (0.2.0-beta โ†’ 0.3.0-beta) npm version minor -# Bug fixes, backward compatible (1.0.0 โ†’ 1.0.1) +# Bug fixes, backward compatible (0.2.0-beta โ†’ 0.2.1-beta) npm version patch -# Prerelease versions (1.0.0 โ†’ 1.0.1-beta.0) +# Prerelease versions (0.2.0 โ†’ 0.2.1-beta.0) npm version prerelease --preid=beta ``` @@ -76,9 +76,10 @@ Publishes package to npm: #### Manual Publishing -1. Create a release on GitHub with a version tag (e.g., `v1.2.3`) +1. Create a release on GitHub with a version tag (e.g., `v0.2.0-beta`) 2. Workflow automatically publishes to npm -3. Or trigger manually via Actions tab with a specific tag +3. Prerelease versions (e.g., `-beta`) are published under the `beta` dist-tag, not `latest` +4. Or trigger manually via Actions tab with a specific tag **Required Secret**: `NPM_TOKEN` must be configured in repository secrets @@ -135,16 +136,16 @@ Add these badges to your README.md: Following [SemVer](https://semver.org/): - **Major (X.0.0)**: Breaking changes, incompatible API changes - - Example: Removing a public method, changing return types + - Example: Removing a public method, changing return types (0.2.0-beta -> 1.0.0) - **Minor (0.X.0)**: New features, backward compatible - - Example: Adding new methods, new optional parameters + - Example: Adding new methods, new optional parameters (0.2.0-beta -> 0.3.0-beta) - **Patch (0.0.X)**: Bug fixes, backward compatible - - Example: Fixing bugs, typos, performance improvements + - Example: Fixing bugs, typos, performance improvements (0.2.0-beta -> 0.2.1-beta) - **Prerelease (0.0.0-beta.X)**: Pre-production versions - - Example: Beta releases, release candidates + - Example: Beta releases, release candidates (0.2.0 -> 0.2.1-beta.0) ## Contributing diff --git a/CHANGELOG.md b/CHANGELOG.md index 38af39f..fe9e149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,43 +5,63 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.2.0-beta] - 2026-02-16 ### Added - TaxCloud API integration for order management (optional) -- Support for CreateOrder endpoint - create orders from marketplace transactions -- Support for GetOrder endpoint - retrieve specific orders by ID -- Support for UpdateOrder endpoint - update order completedDate -- Support for RefundOrder endpoint - create partial or full refunds -- Support for GetRatesByPostalCode endpoint - postal code tax rate lookups -- New configuration options: `taxCloudConnectionId` and `taxCloudAPIKey` -- Comprehensive TypeScript types for all TaxCloud API models -- TaxCloud examples and documentation + - `createOrder()` - Create orders from marketplace transactions, pre-existing systems, or bulk uploads + - `getOrder()` - Retrieve a specific order by ID from TaxCloud + - `updateOrder()` - Update an existing order's completedDate in TaxCloud + - `refundOrder()` - Create partial or full refunds against an order in TaxCloud +- `getRatesByPostalCode()` - Get sales and use tax rates by 5-digit US postal code +- New configuration options: `taxCloudConnectionId` and `taxCloudAPIKey` for TaxCloud credentials +- Comprehensive TypeScript types for all TaxCloud API models (addresses, orders, refunds, currency, exemptions) +- Full type exports for all public types including TaxCloud models and postal code types +- `ZiptaxConfigurationError` thrown when TaxCloud methods are called without credentials +- API response-body error checking for invalid API keys (HTTP 200 with error code 101) +- TaxCloud example script (`examples/taxcloud-orders.ts`) +- GitHub Actions workflow for semantic version enforcement on PRs (`version-check.yml`) +- CONTRIBUTING.md with contribution guidelines and versioning requirements +- CLAUDE.md project context file for AI assistants ### Changed -- Client initialization now supports optional TaxCloud credentials -- Updated README with TaxCloud documentation and examples -- Enhanced HTTP client with PATCH method support for order updates +- Client initialization now supports optional TaxCloud credentials alongside ZipTax API key +- Enhanced HTTP client with PATCH method support for TaxCloud order updates +- Dynamic User-Agent header now uses package version (`ziptax-node/0.2.0-beta`) instead of hardcoded value +- `V60AccountMetrics` type corrected to use `request_count`, `request_limit`, `usage_percent`, `is_active`, `message` fields (matching actual API response) +- Historical date parameter format corrected to `YYYYMM` (e.g., `202401`) across all endpoints and documentation +- `refundOrder()` request parameter is now optional - omitting items creates a full refund per TaxCloud API spec +- `RefundTransactionRequest.items` made optional to support full refunds +- `RefundTransactionResponse.returnedDate` made optional to match API behavior -## [1.0.0] - 2024-01-15 +### Fixed +- Invalid API key now correctly throws `ZiptaxAuthenticationError` instead of returning error in response body +- `verifyTaxCloudCredentials()` now throws `ZiptaxConfigurationError` instead of generic `Error` +- Historical date validation regex corrected from `/^[0-9]{4}-[0-9]{2}$/` to `/^[0-9]{6}$/` +- Removed unused `TaxCloudHTTPClientConfig` interface from HTTP client +- Fixed Prettier formatting across all source files + +## [0.1.4-beta] - 2024-01-15 ### Added -- Initial release of ZipTax Node.js SDK -- Support for GetSalesTaxByAddress API endpoint -- Support for GetSalesTaxByGeoLocation API endpoint -- Support for GetAccountMetrics API endpoint +- Initial beta release of ZipTax Node.js SDK +- Support for `getSalesTaxByAddress()` API endpoint +- Support for `getSalesTaxByGeoLocation()` API endpoint +- Support for `getAccountMetrics()` API endpoint - Full TypeScript support with comprehensive type definitions - Automatic retry logic with exponential backoff - Request/response logging -- Comprehensive error handling with custom error types +- Comprehensive error handling with custom error types: + - `ZiptaxError` (base), `ZiptaxAPIError`, `ZiptaxAuthenticationError` + - `ZiptaxRateLimitError`, `ZiptaxValidationError`, `ZiptaxNetworkError`, `ZiptaxRetryError` - Support for both CommonJS and ES Modules - 80%+ test coverage - Complete documentation and examples ### Features - Promise-based async/await API -- Configurable retry options -- Input validation -- Rate limit handling +- Configurable retry options with exponential backoff +- Input validation for all parameters +- Rate limit handling with retry-after support - Network error handling -- Authentication error handling \ No newline at end of file +- Authentication error handling diff --git a/CLAUDE.md b/CLAUDE.md index cb13d72..3c0ce4d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,16 +115,16 @@ The `version-check` GitHub Action automatically validates: **Before creating a PR:** ```bash -# Breaking changes (1.0.0 โ†’ 2.0.0) +# Breaking changes (0.2.0-beta โ†’ 1.0.0) npm version major -# New features, backward compatible (1.0.0 โ†’ 1.1.0) +# New features, backward compatible (0.2.0-beta โ†’ 0.3.0-beta) npm version minor -# Bug fixes, backward compatible (1.0.0 โ†’ 1.0.1) +# Bug fixes, backward compatible (0.2.0-beta โ†’ 0.2.1-beta) npm version patch -# Prerelease versions (1.0.0 โ†’ 1.0.1-beta.0) +# Prerelease versions (0.2.0 โ†’ 0.2.1-beta.0) npm version prerelease --preid=beta # Then update CHANGELOG.md and commit @@ -177,17 +177,18 @@ npm run test # Verify tests pass 1. Ensure version is bumped and CHANGELOG.md is updated 2. Merge PR to `main` (after passing all checks) -3. Create a GitHub Release with version tag (e.g., `v1.2.3`) +3. Create a GitHub Release with version tag (e.g., `v0.2.0-beta`) 4. Publish workflow automatically runs and publishes to npm +5. Prerelease versions (e.g., `-beta`) are published under the `beta` dist-tag, not `latest` **Manual publishing (if needed):** -1. Update version: `npm version [major|minor|patch]` -2. Move "[Unreleased]" changes to new version in `CHANGELOG.md` +1. Update version: `npm version [major|minor|patch|prerelease --preid=beta]` +2. Move changes to new version section in `CHANGELOG.md` 3. Run `npm run prepublishOnly` (builds, tests, lints) -4. Create git tag: `git tag v1.x.x` +4. Create git tag: `git tag v0.x.x-beta` 5. Push with tags: `git push origin main --tags` -6. Publish: `npm publish --access public` +6. Publish: `npm publish --access public` (or `npm publish --access public --tag beta` for prereleases) ## Testing Strategy @@ -195,10 +196,13 @@ npm run test # Verify tests pass ``` tests/ -โ”œโ”€โ”€ client.test.ts # Client method tests -โ”œโ”€โ”€ http.test.ts # HTTPClient tests -โ”œโ”€โ”€ retry.test.ts # Retry logic tests -โ””โ”€โ”€ setup.ts # Test configuration +โ”œโ”€โ”€ client.test.ts # ZipTax client method tests (address, geo, postal code, metrics) +โ”œโ”€โ”€ taxcloud-orders.test.ts # TaxCloud order management tests (CRUD + refunds) +โ”œโ”€โ”€ http.test.ts # HTTPClient tests (requests, error handling, response body checks) +โ”œโ”€โ”€ validation.test.ts # Input validation utility tests +โ”œโ”€โ”€ exceptions.test.ts # Custom error class tests +โ”œโ”€โ”€ retry.test.ts # Retry logic tests (backoff, max attempts) +โ””โ”€โ”€ setup.ts # Test configuration ``` ### Mocking Strategy @@ -271,7 +275,9 @@ const client = new ZiptaxClient({ 1. **TaxCloud not configured**: Check both `taxCloudConnectionId` and `taxCloudAPIKey` are set 2. **Type errors**: Ensure types match API responses (check docs/spec.yaml) 3. **Rate limiting**: SDK includes automatic retry with backoff -4. **Validation errors**: Check required fields and formats (e.g., postal code is 5-digit) +4. **Validation errors**: Check required fields and formats (e.g., postal code is 5-digit, historical date is YYYYMM) +5. **Historical date format**: Must be `YYYYMM` (e.g., `202401`), not `YYYY-MM` +6. **Account metrics fields**: Use snake_case (`request_count`, `usage_percent`), not the deprecated `core_` prefixed fields ## Best Practices @@ -310,7 +316,7 @@ Example: `feat: add support for TaxCloud order refunds` **Important**: Commit both version bump and changelog update: ```bash -npm version minor # Bumps version and creates commit +npm version minor # e.g., 0.2.0-beta โ†’ 0.3.0-beta (bumps version and creates commit) git add CHANGELOG.md git commit --amend --no-edit # Add CHANGELOG to version commit ``` @@ -319,10 +325,13 @@ git commit --amend --no-edit # Add CHANGELOG to version commit ### HTTP Client -- Two instances: one for ZipTax, one for TaxCloud (if configured) +- Two instances: one for ZipTax API, one for TaxCloud API (if configured) - Automatic retry with exponential backoff - Custom error handling based on HTTP status codes -- Optional request/response logging +- Response-body error checking: API returns HTTP 200 with error codes (e.g., code 101 = invalid key throws `ZiptaxAuthenticationError`) +- Dynamic User-Agent header using `npm_package_version` environment variable +- Optional request/response logging via interceptors +- Supports GET, POST, and PATCH methods ### Validation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a831f71..34f8ca2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -103,16 +103,16 @@ The project enforces semantic versioning via GitHub Actions. Your PR will fail i Use npm's built-in version command: ```bash -# For breaking changes (1.0.0 โ†’ 2.0.0) +# For breaking changes (0.2.0-beta โ†’ 1.0.0) npm version major -# For new features, backward compatible (1.0.0 โ†’ 1.1.0) +# For new features, backward compatible (0.2.0-beta โ†’ 0.3.0-beta) npm version minor -# For bug fixes, backward compatible (1.0.0 โ†’ 1.0.1) +# For bug fixes, backward compatible (0.2.0-beta โ†’ 0.2.1-beta) npm version patch -# For prerelease versions (1.0.0 โ†’ 1.0.1-beta.0) +# For prerelease versions (0.2.0 โ†’ 0.2.1-beta.0) npm version prerelease --preid=beta ``` @@ -131,7 +131,7 @@ After bumping the version, update `CHANGELOG.md`: Example: ```markdown -## [Unreleased] +## [0.3.0-beta] - 2026-03-01 ### Added - New method `getOrder()` for retrieving TaxCloud orders @@ -210,10 +210,10 @@ Use conventional commit format: | Change Type | Version Bump | Example | |-------------|--------------|---------| -| Breaking changes, incompatible API changes | **Major** (x.0.0) | Removing methods, changing signatures | -| New features, backward compatible | **Minor** (0.x.0) | Adding new methods, optional parameters | -| Bug fixes, backward compatible | **Patch** (0.0.x) | Fixing bugs, performance improvements | -| Pre-release versions | **Prerelease** (0.0.0-beta.x) | Beta/RC versions | +| Breaking changes, incompatible API changes | **Major** (x.0.0) | Removing methods, changing signatures (e.g., 0.2.0-beta -> 1.0.0) | +| New features, backward compatible | **Minor** (0.x.0) | Adding new methods, optional parameters (e.g., 0.2.0-beta -> 0.3.0-beta) | +| Bug fixes, backward compatible | **Patch** (0.0.x) | Fixing bugs, performance improvements (e.g., 0.2.0-beta -> 0.2.1-beta) | +| Pre-release versions | **Prerelease** (0.0.0-beta.x) | Beta/RC versions (e.g., 0.2.0 -> 0.2.1-beta.0) | ## Coding Standards From 8369964390a10242f612d81ae5f24dea74a39b77 Mon Sep 17 00:00:00 2001 From: Eric Lakich Date: Tue, 17 Feb 2026 10:23:10 -0800 Subject: [PATCH 8/9] ZIP-562: resolves PR comments --- .gitignore | 3 + docs/spec.yaml | 10 +-- package.json | 8 ++- scripts/generate-version.js | 21 +++++++ src/utils/http.ts | 3 +- tests/http.test.ts | 122 ++++++++++++++++++++++++++++++++++++ 6 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 scripts/generate-version.js diff --git a/.gitignore b/.gitignore index 9a5aced..0c44b69 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,6 @@ dist # Vite logs files vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Generated version file (created by scripts/generate-version.js) +src/version.ts diff --git a/docs/spec.yaml b/docs/spec.yaml index 4d1b42c..66f43e3 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -77,7 +77,7 @@ api: notes: - "TaxCloud features are OPTIONAL and only available when both Connection ID and API Key are provided during client initialization" - "Order management functions will return error if TaxCloud credentials not configured" - - "Uses Header authentication with the X-API-KEY header for both APIs, but TaxCloud also requires the connectionId in the path for order endpoints" + - "Uses Header authentication with the X-API-Key header for both APIs, but TaxCloud also requires the connectionId in the path for order endpoints" # ----------------------------------------------------------------------------- # SDK Configuration @@ -283,7 +283,7 @@ resources: status_code: 201 authentication: type: "header" - header: "X-API-KEY" + header: "X-API-Key" format: "{taxCloudAPIKey}" note: "Uses TaxCloud API Key configured during client initialization" @@ -314,7 +314,7 @@ resources: status_code: 200 authentication: type: "header" - header: "X-API-KEY" + header: "X-API-Key" format: "{taxCloudAPIKey}" note: "Uses TaxCloud API Key configured during client initialization" @@ -348,7 +348,7 @@ resources: status_code: 200 authentication: type: "header" - header: "X-API-KEY" + header: "X-API-Key" format: "{taxCloudAPIKey}" note: "Uses TaxCloud API Key configured during client initialization" @@ -384,7 +384,7 @@ resources: status_code: 201 authentication: type: "header" - header: "X-API-KEY" + header: "X-API-Key" format: "{taxCloudAPIKey}" note: "Uses TaxCloud API Key configured during client initialization" diff --git a/package.json b/package.json index 53adc78..dcf38fc 100644 --- a/package.json +++ b/package.json @@ -18,19 +18,21 @@ "LICENSE" ], "scripts": { - "build": "npm run build:clean && npm run build:cjs && npm run build:esm && npm run build:types", + "build": "npm run build:clean && npm run build:version && npm run build:cjs && npm run build:esm && npm run build:types", "build:clean": "rm -rf dist", + "build:version": "node scripts/generate-version.js", "build:cjs": "tsc -p tsconfig.cjs.json", "build:esm": "tsc -p tsconfig.esm.json", "build:types": "tsc -p tsconfig.types.json", + "pretest": "npm run build:version", "test": "jest", - "test:watch": "jest --watch", + "test:watch": "npm run build:version && jest --watch", "test:coverage": "jest --coverage", "lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'", "lint:fix": "eslint 'src/**/*.ts' 'tests/**/*.ts' --fix", "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts' 'examples/**/*.ts'", "format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts' 'examples/**/*.ts'", - "type-check": "tsc --noEmit", + "type-check": "npm run build:version && tsc --noEmit", "example:basic": "tsx examples/basic-usage.ts", "example:async": "tsx examples/async-usage.ts", "example:errors": "tsx examples/error-handling.ts", diff --git a/scripts/generate-version.js b/scripts/generate-version.js new file mode 100644 index 0000000..c3635ef --- /dev/null +++ b/scripts/generate-version.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +/** + * Generates src/version.ts with the current package version. + * Run as part of the build pipeline to avoid stale hardcoded version strings. + */ + +const fs = require('fs'); +const path = require('path'); + +const packageJson = require(path.resolve(__dirname, '..', 'package.json')); +const version = packageJson.version; + +const content = `// Auto-generated by scripts/generate-version.js โ€” do not edit manually +export const SDK_VERSION = '${version}'; +`; + +const outPath = path.resolve(__dirname, '..', 'src', 'version.ts'); +fs.writeFileSync(outPath, content, 'utf-8'); + +console.log(`Generated src/version.ts with version ${version}`); diff --git a/src/utils/http.ts b/src/utils/http.ts index 2f8554c..e2555ac 100644 --- a/src/utils/http.ts +++ b/src/utils/http.ts @@ -10,6 +10,7 @@ import { ZiptaxRateLimitError, } from '../exceptions'; import { retryWithBackoff, RetryOptions } from './retry'; +import { SDK_VERSION } from '../version'; export interface HTTPClientConfig { /** Base URL for API requests */ @@ -42,7 +43,7 @@ export class HTTPClient { headers: { 'X-API-Key': config.apiKey, 'Content-Type': 'application/json', - 'User-Agent': `ziptax-node/${process.env.npm_package_version || '0.2.0-beta'}`, + 'User-Agent': `ziptax-node/${SDK_VERSION}`, }, }); diff --git a/tests/http.test.ts b/tests/http.test.ts index f638f6b..cf39390 100644 --- a/tests/http.test.ts +++ b/tests/http.test.ts @@ -12,6 +12,7 @@ import { } from '../src/exceptions'; jest.mock('axios'); +jest.mock('../src/version', () => ({ SDK_VERSION: '0.2.0-beta' })); const mockedAxios = axios as jest.Mocked; describe('HTTPClient', () => { @@ -103,6 +104,23 @@ describe('HTTPClient', () => { }); }); + describe('patch', () => { + it('should make PATCH request with data and return response', async () => { + const mockData = { result: 'updated' }; + const patchData = { key: 'new-value' }; + mockAxiosInstance.request.mockResolvedValue({ data: mockData }); + + const result = await httpClient.patch('/test', patchData); + + expect(result).toEqual(mockData); + expect(mockAxiosInstance.request).toHaveBeenCalledWith({ + method: 'PATCH', + url: '/test', + data: patchData, + }); + }); + }); + describe('error handling', () => { it('should throw ZiptaxAuthenticationError for 401', async () => { mockAxiosInstance.request.mockRejectedValue({ @@ -223,4 +241,108 @@ describe('HTTPClient', () => { await expect(httpClient.get('/test')).rejects.toThrow('string error'); }); }); + + describe('response body error checking', () => { + it('should throw ZiptaxAuthenticationError for code 101 (invalid key)', async () => { + mockAxiosInstance.request.mockResolvedValue({ + data: { + metadata: { + response: { + code: 101, + message: 'Invalid API key', + }, + }, + }, + }); + + await expect(httpClient.get('/test')).rejects.toThrow(ZiptaxAuthenticationError); + await expect(httpClient.get('/test')).rejects.toThrow('Invalid API key'); + }); + + it('should throw ZiptaxAPIError for non-100 error codes other than 101', async () => { + mockAxiosInstance.request.mockResolvedValue({ + data: { + metadata: { + response: { + code: 200, + message: 'Some API error', + }, + }, + }, + }); + + await expect(httpClient.get('/test')).rejects.toThrow(ZiptaxAPIError); + await expect(httpClient.get('/test')).rejects.toThrow('Some API error'); + }); + + it('should use default message when response code message is missing', async () => { + mockAxiosInstance.request.mockResolvedValue({ + data: { + metadata: { + response: { + code: 102, + }, + }, + }, + }); + + await expect(httpClient.get('/test')).rejects.toThrow(ZiptaxAPIError); + await expect(httpClient.get('/test')).rejects.toThrow('API request failed'); + }); + + it('should not throw for code 100 (success)', async () => { + const mockData = { + results: [{ taxRate: 0.0825 }], + metadata: { + response: { + code: 100, + message: 'Success', + }, + }, + }; + mockAxiosInstance.request.mockResolvedValue({ data: mockData }); + + const result = await httpClient.get('/test'); + expect(result).toEqual(mockData); + }); + + it('should not throw for responses without metadata', async () => { + const mockData = { results: [{ taxRate: 0.0825 }] }; + mockAxiosInstance.request.mockResolvedValue({ data: mockData }); + + const result = await httpClient.get('/test'); + expect(result).toEqual(mockData); + }); + + it('should not throw for non-object response data', async () => { + mockAxiosInstance.request.mockResolvedValue({ data: 'plain string' }); + + const result = await httpClient.get('/test'); + expect(result).toBe('plain string'); + }); + + it('should not throw for null response data', async () => { + mockAxiosInstance.request.mockResolvedValue({ data: null }); + + const result = await httpClient.get('/test'); + expect(result).toBeNull(); + }); + + it('should include response body in ZiptaxAPIError for non-101 codes', async () => { + const responseData = { + metadata: { + response: { + code: 105, + message: 'Service unavailable', + }, + }, + }; + mockAxiosInstance.request.mockResolvedValue({ data: responseData }); + + const error = (await httpClient.get('/test').catch((e) => e)) as ZiptaxAPIError; + expect(error).toBeInstanceOf(ZiptaxAPIError); + expect(error.message).toBe('Service unavailable'); + expect(error.responseBody).toEqual(responseData); + }); + }); }); From 7a85ddef99a2f9736b12e329bc3f962ad971b3bf Mon Sep 17 00:00:00 2001 From: Eric Lakich Date: Tue, 17 Feb 2026 13:02:12 -0800 Subject: [PATCH 9/9] ZIP-562: fixes test coverage Action --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dcf38fc..adcb63b 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "pretest": "npm run build:version", "test": "jest", "test:watch": "npm run build:version && jest --watch", - "test:coverage": "jest --coverage", + "test:coverage": "npm run build:version && jest --coverage", "lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'", "lint:fix": "eslint 'src/**/*.ts' 'tests/**/*.ts' --fix", "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts' 'examples/**/*.ts'",