Skip to Content
InternalDocsSpikesP7 C Zonos Spike

P7 C Zonos Spike

Source: docs/spikes/p7-c-zonos-spike.md

# P7-C: Zonos Adapter Spike — Go/No-Go Decision Memo **Status:** CLOSED — CONDITIONAL GO **Date:** 2026-02-13 (spike), 2026-02-14 (closeout) **Author:** Engineering **Plan ID:** P7-C --- ## Executive Summary This spike proves end-to-end external landed cost calculator integration via a provider adapter pattern. The architecture is validated: typed adapter contract, lazy factory, comparison response, error handling, and 27 unit/integration tests all pass. Live local validation confirms the full flow works against the running API with `provider=mock`. **Decision: CONDITIONAL GO.** The adapter architecture is production-ready (~1 sprint to harden). Criteria 1-4 (accuracy, latency, stability) require Zonos sandbox credentials, which is a business gate (Zonos account setup), not an engineering gate. The spike exit criterion is met: one external adapter spike completes end-to-end with a go/no-go decision memo. --- ## What Was Built | Component | Description | |-----------|-------------| | `lib/calc/provider.interface.ts` | Typed adapter contract (ProviderRequest/Response/Adapter) | | `lib/calc/zonos.adapter.ts` | Zonos REST adapter (native fetch, AbortController timeout) | | `lib/calc/mock.adapter.ts` | Mock adapter for testing with HS-code-based duty rates | | `lib/calc/provider.factory.ts` | Lazy factory — creates adapter on demand, no startup failures | | `lib/calc/config.ts` | Env var parsing (ZONOS_API_KEY, ZONOS_BASE_URL, CALC_TIMEOUT_MS) | | Route enhancement | `POST /api/trade/landed-cost` accepts optional `provider` body field | | Error codes | PROVIDER_NOT_CONFIGURED (503), PROVIDER_TIMEOUT (504), PROVIDER_ERROR (502) | | Tests | 27 unit/integration tests covering adapter, factory, transforms, error paths | **Reuse from archived code:** ~75% structural reuse from `_archived/services/external-calculator-adapters/`. --- ## Go/No-Go Criteria | # | Criterion | GO | CONDITIONAL | NO-GO | Result | |---|-----------|-----|-------------|-------|--------| | 1 | **Duty rate accuracy** | Delta ≤ 2% absolute for 80%+ of test HS codes | Delta ≤ 5% for 80%+ | Delta > 5% for majority | **TBD — requires sandbox** | | 2 | **Tax rate accuracy** | Delta ≤ 2% absolute for 80%+ of test HS codes | Delta ≤ 5% for 80%+ | Delta > 5% for majority | **TBD — requires sandbox** | | 3 | **p95 latency** | ≤ 2s | ≤ 5s | > 5s | **TBD — requires sandbox** | | 4 | **API stability** | No errors in 20 consecutive requests | ≤ 5% error rate | > 10% error rate | **TBD — requires sandbox** | | 5 | **Integration effort to production** | ≤ 2 sprints estimated | ≤ 4 sprints | > 4 sprints | **GO — ~1 sprint** | ### Criterion 5 Detail: Integration Effort Estimate Production readiness from this spike requires: | Work item | Estimate | |-----------|----------| | `ExternalCalculation` Prisma model + migration | 2 days | | Redis/Prisma result caching | 2 days | | Retry with exponential backoff + circuit breaker | 2 days | | Rate limiting per provider | 1 day | | Multi-item batch support | 1 day | | Currency conversion integration | 1 day | | Production hardening (logging, monitoring, alerts) | 1 day | | **Total** | **~10 days (~1 sprint)** | --- ## Test HS Codes for Sandbox Validation When Zonos sandbox credentials are available, test with these codes to fill criteria 1-4: | HS Code | Product | Expected US Duty | Origin | |---------|---------|-----------------|--------| | 6402.99 | Athletic footwear | ~12.5% | CN | | 6205.30 | Cotton shirts | ~16.5% | BD | | 8471.30 | Laptop computers | 0% | TW | | 2204.21 | Wine | 6.3¢/liter | FR | | 0901.21 | Roasted coffee | 0% | CO | | 7306.30 | Steel tubing | 0% (check AD/CVD) | KR | | 8517.12 | Smartphones | 0% | CN | | 6110.20 | Cotton sweaters | ~17% | VN | | 4202.12 | Leather handbags | ~8% | IT | | 9403.60 | Wooden furniture | ~3.5% | CN | **Test matrix:** 10 HS codes × {US, CA, GB} destinations = 30 calculations. --- ## Comparison Architecture The spike runs both internal and external calculations in parallel and returns a structured comparison: ```json { "breakdown": { "...internal result..." }, "source": "zonos", "comparison": { "internal": { "dutyRate": 0.125, "dutyAmount": 7.24, "taxRate": 0, "taxAmount": 0, "otherFees": 25, "totalLandedCost": 90.19 }, "external": { "dutyRate": 0.125, "dutyAmount": 5.63, "taxRate": 0.085, "taxAmount": 3.82, "totalFees": 1.50, "totalLandedCost": 68.45 }, "delta": { "dutyRatePct": 0, "dutyAmount": -1.61, "taxRatePct": 0.085, "taxAmount": 3.82, "totalLandedCost": -21.74 } }, "providerMeta": { "provider": "zonos", "calculationId": "calc_abc123", "latencyMs": 450, "status": "success" } } ``` **Primary accuracy signals:** `delta.dutyRatePct` and `delta.taxRatePct` (rate-level comparison avoids noise from internal hardcoded `otherFees`). **Rate fallback:** When Zonos returns duty/tax amounts but not rates, effective rates are computed using the same CIF customs value base as the internal calculator. --- ## Known Limitations | Limitation | Impact | Resolution | |-----------|--------|------------| | Single-item requests only | Can't batch-test multi-item shipments | Production PR adds batch support | | No caching | Every request hits Zonos API | Production PR adds Redis/Prisma cache | | No retry/circuit breaker | Single failure = error response | Deferred to INFRA-B | | Hardcoded USD currency | Can't test cross-currency scenarios | Production PR adds currency param | | No province/state in request | May affect tax rate accuracy for CA/AU | Add to production `LandedCostInput` | | Spike defaults (quantity=1, description=hsCode) | May slightly affect Zonos classification | Production should pass real values | --- ## Cost Model | Factor | Detail | |--------|--------| | Pricing model | Per-request (REST API call) | | Sandbox | Free (rate-limited) | | Production estimate | Contact Zonos sales for volume pricing | | Lock-in risk | Low — adapter pattern allows swapping providers | --- ## Risks | Risk | Severity | Mitigation | |------|----------|------------| | Zonos sandbox ≠ production accuracy | Medium | Validate with production API key before go-live | | Zonos API breaking changes | Low | Pin to `/v1`, monitor changelog | | Rate limit exceeded in production | Medium | Cache results, implement circuit breaker | | Zonos downtime affects our SLA | Medium | Fallback to internal calc when provider errors | --- ## Recommendation **CONDITIONAL GO** — architecture validated, accuracy criteria deferred to business gate. The integration effort is clearly within GO threshold (~1 sprint). The adapter architecture is clean, testable, and provider-agnostic. Criteria 1-4 (accuracy, latency, stability) require a Zonos business relationship — this is a sales/partnership step, not an engineering blocker. ### Live Local Validation (2026-02-14) Verified against local API (`localhost:3001`) with `provider=mock`: | Test | Result | |------|--------| | `POST /api/trade/landed-cost` (no provider) | Internal calc returns `{ breakdown }` — backward compatible | | `provider=mock` (footwear CN→US) | Full comparison: internal dutyRate=3.5%, mock dutyRate=12.5%, delta=+9% | | `provider=mock` (apparel BD→CA) | Full comparison: internal dutyRate=17%, mock dutyRate=17.5%, taxRate delta=+8% | | `provider=zonos` (no creds) | 503 `PROVIDER_NOT_CONFIGURED` — correct | | `provider=nonexistent` | 400 `INVALID_REQUEST` with available providers list — correct | **Next steps (when Zonos relationship is pursued):** 1. Obtain Zonos sandbox API key (business/sales step) 2. Run 30-calculation test matrix: `npx tsx scripts/run-zonos-sandbox-matrix.ts` 3. Fill in criteria 1-4 results in this memo 4. If GO: file production PR (P7-C-PROD) with caching, retry, dedicated table 5. If CONDITIONAL: identify specific HS code gaps, evaluate alternative providers 6. If NO-GO: evaluate Avalara or Descartes as next spike candidate