From cf1fdefe92ffd4416c99c7302aa71b27a3a2c548 Mon Sep 17 00:00:00 2001 From: Fayez Bast <96446332+FayezBast@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:45:32 +0200 Subject: [PATCH] feat: effective tariff rate source (#1790) * feat: effective tariff rate source * fix(trade): extract parse helpers, fix tests, add health monitoring - Extract htmlToPlainText/toIsoDate/parseBudgetLabEffectiveTariffHtml to scripts/_trade-parse-utils.mjs so tests can import directly - Fix toIsoDate to use month-name lookup instead of fragile new Date(\`\${text} UTC\`) which is not spec-guaranteed - Replace new Function() test reconstruction with direct ESM import - Add test fixtures for parser patterns 2 and 3 (previously untested) - Add tariffTrendsUs to health.js STANDALONE_KEYS + SEED_META (key trade:tariffs:v1:840:all:10, maxStaleMin 900 = 2.5x the 6h TTL) * fix(test): update sourceVersion assertion for budgetlab addition --------- Co-authored-by: Elie Habib --- api/health.js | 2 + docs/api/TradeService.openapi.json | 2 +- docs/api/TradeService.openapi.yaml | 22 +++ .../trade/v1/get_tariff_trends.proto | 2 + proto/worldmonitor/trade/v1/trade_data.proto | 14 ++ scripts/_trade-parse-utils.mjs | 79 ++++++++ scripts/seed-supply-chain-trade.mjs | 37 +++- .../trade/v1/get-tariff-trends.ts | 4 +- .../trade/v1/get-trade-restrictions.ts | 2 +- src/components/TradePolicyPanel.ts | 125 ++++++++++++- .../worldmonitor/trade/v1/service_client.ts | 9 + .../worldmonitor/trade/v1/service_server.ts | 9 + src/locales/en.json | 15 +- src/services/trade/index.ts | 7 +- src/styles/main.css | 81 +++++++++ tests/freight-indices.test.mjs | 2 +- tests/trade-policy-tariffs.test.mjs | 168 ++++++++++++++++++ 17 files changed, 560 insertions(+), 20 deletions(-) create mode 100644 scripts/_trade-parse-utils.mjs create mode 100644 tests/trade-policy-tariffs.test.mjs diff --git a/api/health.js b/api/health.js index 289d0f11f..898b81398 100644 --- a/api/health.js +++ b/api/health.js @@ -73,6 +73,7 @@ const STANDALONE_KEYS = { chokepointTransits: 'supply_chain:chokepoint_transits:v1', transitSummaries: 'supply_chain:transit-summaries:v1', thermalEscalation: 'thermal:escalation:v1', + tariffTrendsUs: 'trade:tariffs:v1:840:all:10', }; const SEED_META = { @@ -135,6 +136,7 @@ const SEED_META = { sanctionsPressure: { key: 'seed-meta:sanctions:pressure', maxStaleMin: 720 }, radiationWatch: { key: 'seed-meta:radiation:observations', maxStaleMin: 30 }, thermalEscalation: { key: 'seed-meta:thermal:escalation', maxStaleMin: 240 }, + tariffTrendsUs: { key: 'seed-meta:trade:tariffs:v1:840:all:10', maxStaleMin: 900 }, }; // Standalone keys that are populated on-demand by RPC handlers (not seeds). diff --git a/docs/api/TradeService.openapi.json b/docs/api/TradeService.openapi.json index 888b875ff..e3a2fa508 100644 --- a/docs/api/TradeService.openapi.json +++ b/docs/api/TradeService.openapi.json @@ -1 +1 @@ -{"components":{"schemas":{"CustomsRevenueMonth":{"description":"Monthly US customs duties revenue from Treasury MTS data.","properties":{"calendarMonth":{"format":"int32","type":"integer"},"calendarYear":{"format":"int32","type":"integer"},"fiscalYear":{"format":"int32","type":"integer"},"fytdAmountBillions":{"format":"double","type":"number"},"monthlyAmountBillions":{"format":"double","type":"number"},"recordDate":{"type":"string"}},"type":"object"},"Error":{"description":"Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.","properties":{"message":{"description":"Error message (e.g., 'user not found', 'database connection failed')","type":"string"}},"type":"object"},"FieldViolation":{"description":"FieldViolation describes a single validation error for a specific field.","properties":{"description":{"description":"Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')","type":"string"},"field":{"description":"The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')","type":"string"}},"required":["field","description"],"type":"object"},"GetCustomsRevenueRequest":{"type":"object"},"GetCustomsRevenueResponse":{"properties":{"fetchedAt":{"type":"string"},"months":{"items":{"$ref":"#/components/schemas/CustomsRevenueMonth"},"type":"array"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"GetTariffTrendsRequest":{"description":"Request for tariff timeseries data.","properties":{"partnerCountry":{"description":"WTO member code of partner country (e.g. \"156\" = China).","type":"string"},"productSector":{"description":"Product sector filter (HS chapter). Empty = aggregate.","type":"string"},"reportingCountry":{"description":"WTO member code of reporting country (e.g. \"840\" = US).","type":"string"},"years":{"description":"Number of years to look back (default 10, max 30).","format":"int32","type":"integer"}},"type":"object"},"GetTariffTrendsResponse":{"description":"Response containing tariff trend datapoints.","properties":{"datapoints":{"items":{"$ref":"#/components/schemas/TariffDataPoint"},"type":"array"},"fetchedAt":{"description":"ISO 8601 timestamp when data was fetched from WTO.","type":"string"},"upstreamUnavailable":{"description":"True if upstream fetch failed and results may be stale/empty.","type":"boolean"}},"type":"object"},"GetTradeBarriersRequest":{"description":"Request for SPS/TBT trade barrier notifications.","properties":{"countries":{"items":{"description":"WTO member codes to filter by. Empty = all.","type":"string"},"type":"array"},"limit":{"description":"Max results to return (server caps at 100).","format":"int32","type":"integer"},"measureType":{"description":"Filter by measure type: \"SPS\", \"TBT\", or empty for both.","type":"string"}},"type":"object"},"GetTradeBarriersResponse":{"description":"Response containing trade barrier notifications.","properties":{"barriers":{"items":{"$ref":"#/components/schemas/TradeBarrier"},"type":"array"},"fetchedAt":{"description":"ISO 8601 timestamp when data was fetched from WTO.","type":"string"},"upstreamUnavailable":{"description":"True if upstream fetch failed and results may be stale/empty.","type":"boolean"}},"type":"object"},"GetTradeFlowsRequest":{"description":"Request for bilateral trade flow data.","properties":{"partnerCountry":{"description":"WTO member code of partner country.","type":"string"},"reportingCountry":{"description":"WTO member code of reporting country.","type":"string"},"years":{"description":"Number of years to look back (default 10, max 30).","format":"int32","type":"integer"}},"type":"object"},"GetTradeFlowsResponse":{"description":"Response containing trade flow records.","properties":{"fetchedAt":{"description":"ISO 8601 timestamp when data was fetched from WTO.","type":"string"},"flows":{"items":{"$ref":"#/components/schemas/TradeFlowRecord"},"type":"array"},"upstreamUnavailable":{"description":"True if upstream fetch failed and results may be stale/empty.","type":"boolean"}},"type":"object"},"GetTradeRestrictionsRequest":{"description":"Request for quantitative restriction data.","properties":{"countries":{"items":{"description":"WTO member codes to filter by. Empty = all.","type":"string"},"type":"array"},"limit":{"description":"Max results to return (server caps at 100).","format":"int32","type":"integer"}},"type":"object"},"GetTradeRestrictionsResponse":{"description":"Response containing trade restrictions and fetch metadata.","properties":{"fetchedAt":{"description":"ISO 8601 timestamp when data was fetched from WTO.","type":"string"},"restrictions":{"items":{"$ref":"#/components/schemas/TradeRestriction"},"type":"array"},"upstreamUnavailable":{"description":"True if upstream fetch failed and results may be stale/empty.","type":"boolean"}},"type":"object"},"TariffDataPoint":{"description":"Single tariff data point for a reporter-partner-product combination.","properties":{"boundRate":{"description":"WTO bound tariff rate (percentage).","format":"double","type":"number"},"indicatorCode":{"description":"WTO indicator code used for this datapoint.","type":"string"},"partnerCountry":{"description":"WTO member code of partner country.","type":"string"},"productSector":{"description":"Product sector or HS chapter.","type":"string"},"reportingCountry":{"description":"WTO member code of reporting country.","type":"string"},"tariffRate":{"description":"Applied MFN tariff rate (percentage).","format":"double","type":"number"},"year":{"description":"Year of observation.","format":"int32","type":"integer"}},"type":"object"},"TradeBarrier":{"description":"SPS or TBT trade barrier notification.","properties":{"dateDistributed":{"description":"ISO 8601 date when notification was distributed.","type":"string"},"id":{"description":"Unique barrier notification identifier.","type":"string"},"measureType":{"description":"Measure classification: \"SPS\" or \"TBT\".","type":"string"},"notifyingCountry":{"description":"Country that notified the measure.","type":"string"},"objective":{"description":"Stated objective of the measure.","type":"string"},"productDescription":{"description":"Product description or affected goods.","type":"string"},"sourceUrl":{"description":"WTO source document URL (must be http/https protocol).","type":"string"},"status":{"description":"Status of the notification.","type":"string"},"title":{"description":"Title of the notification.","type":"string"}},"type":"object"},"TradeFlowRecord":{"description":"Bilateral trade flow record for a reporting-partner pair.","properties":{"exportValueUsd":{"description":"Merchandise export value in millions USD.","format":"double","type":"number"},"importValueUsd":{"description":"Merchandise import value in millions USD.","format":"double","type":"number"},"partnerCountry":{"description":"WTO member code of partner country.","type":"string"},"productSector":{"description":"Product sector or HS chapter.","type":"string"},"reportingCountry":{"description":"WTO member code of reporting country.","type":"string"},"year":{"description":"Year of observation.","format":"int32","type":"integer"},"yoyExportChange":{"description":"Year-over-year export change (percentage).","format":"double","type":"number"},"yoyImportChange":{"description":"Year-over-year import change (percentage).","format":"double","type":"number"}},"type":"object"},"TradeRestriction":{"description":"Quantitative restriction or export control measure notified to WTO.","properties":{"affectedCountry":{"description":"Country affected by the restriction.","type":"string"},"description":{"description":"Human-readable description of the measure.","type":"string"},"id":{"description":"Unique restriction identifier from WTO.","type":"string"},"measureType":{"description":"Measure classification: \"QR\", \"EXPORT_BAN\", \"IMPORT_BAN\", \"LICENSING\".","type":"string"},"notifiedAt":{"description":"ISO 8601 date when measure was notified.","type":"string"},"productSector":{"description":"Product sector or HS chapter description.","type":"string"},"reportingCountry":{"description":"ISO 3166-1 alpha-3 or WTO member code of reporting country.","type":"string"},"sourceUrl":{"description":"WTO source document URL (must be http/https protocol).","type":"string"},"status":{"description":"Current status: \"IN_FORCE\", \"TERMINATED\", \"NOTIFIED\".","type":"string"}},"type":"object"},"ValidationError":{"description":"ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.","properties":{"violations":{"description":"List of validation violations","items":{"$ref":"#/components/schemas/FieldViolation"},"type":"array"}},"required":["violations"],"type":"object"}}},"info":{"title":"TradeService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/trade/v1/get-customs-revenue":{"get":{"description":"Get US customs duties revenue (Treasury MTS data, seeded by Railway).","operationId":"GetCustomsRevenue","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCustomsRevenueResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCustomsRevenue","tags":["TradeService"]}},"/api/trade/v1/get-tariff-trends":{"get":{"description":"Get tariff rate timeseries for a country pair.","operationId":"GetTariffTrends","parameters":[{"description":"WTO member code of reporting country (e.g. \"840\" = US).","in":"query","name":"reporting_country","required":false,"schema":{"type":"string"}},{"description":"WTO member code of partner country (e.g. \"156\" = China).","in":"query","name":"partner_country","required":false,"schema":{"type":"string"}},{"description":"Product sector filter (HS chapter). Empty = aggregate.","in":"query","name":"product_sector","required":false,"schema":{"type":"string"}},{"description":"Number of years to look back (default 10, max 30).","in":"query","name":"years","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetTariffTrendsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetTariffTrends","tags":["TradeService"]}},"/api/trade/v1/get-trade-barriers":{"get":{"description":"Get SPS/TBT barrier notifications.","operationId":"GetTradeBarriers","parameters":[{"description":"WTO member codes to filter by. Empty = all.","in":"query","name":"countries","required":false,"schema":{"type":"string"}},{"description":"Filter by measure type: \"SPS\", \"TBT\", or empty for both.","in":"query","name":"measure_type","required":false,"schema":{"type":"string"}},{"description":"Max results to return (server caps at 100).","in":"query","name":"limit","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetTradeBarriersResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetTradeBarriers","tags":["TradeService"]}},"/api/trade/v1/get-trade-flows":{"get":{"description":"Get bilateral merchandise trade flows.","operationId":"GetTradeFlows","parameters":[{"description":"WTO member code of reporting country.","in":"query","name":"reporting_country","required":false,"schema":{"type":"string"}},{"description":"WTO member code of partner country.","in":"query","name":"partner_country","required":false,"schema":{"type":"string"}},{"description":"Number of years to look back (default 10, max 30).","in":"query","name":"years","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetTradeFlowsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetTradeFlows","tags":["TradeService"]}},"/api/trade/v1/get-trade-restrictions":{"get":{"description":"Get quantitative restrictions and export controls.","operationId":"GetTradeRestrictions","parameters":[{"description":"WTO member codes to filter by. Empty = all.","in":"query","name":"countries","required":false,"schema":{"type":"string"}},{"description":"Max results to return (server caps at 100).","in":"query","name":"limit","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetTradeRestrictionsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetTradeRestrictions","tags":["TradeService"]}}}} \ No newline at end of file +{"components":{"schemas":{"CustomsRevenueMonth":{"description":"Monthly US customs duties revenue from Treasury MTS data.","properties":{"calendarMonth":{"format":"int32","type":"integer"},"calendarYear":{"format":"int32","type":"integer"},"fiscalYear":{"format":"int32","type":"integer"},"fytdAmountBillions":{"format":"double","type":"number"},"monthlyAmountBillions":{"format":"double","type":"number"},"recordDate":{"type":"string"}},"type":"object"},"EffectiveTariffRate":{"description":"Current effective tariff estimate for countries with coverage beyond WTO MFN baselines.","properties":{"observationPeriod":{"description":"Human-readable observation period (for example \"December 2025\").","type":"string"},"sourceName":{"description":"Source name for the effective-rate estimate.","type":"string"},"sourceUrl":{"description":"Canonical source URL for the estimate/methodology.","type":"string"},"tariffRate":{"description":"Effective tariff rate (percentage).","format":"double","type":"number"},"updatedAt":{"description":"ISO 8601 date when the source page was last updated, if known.","type":"string"}},"type":"object"},"Error":{"description":"Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.","properties":{"message":{"description":"Error message (e.g., 'user not found', 'database connection failed')","type":"string"}},"type":"object"},"FieldViolation":{"description":"FieldViolation describes a single validation error for a specific field.","properties":{"description":{"description":"Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')","type":"string"},"field":{"description":"The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')","type":"string"}},"required":["field","description"],"type":"object"},"GetCustomsRevenueRequest":{"type":"object"},"GetCustomsRevenueResponse":{"properties":{"fetchedAt":{"type":"string"},"months":{"items":{"$ref":"#/components/schemas/CustomsRevenueMonth"},"type":"array"},"upstreamUnavailable":{"type":"boolean"}},"type":"object"},"GetTariffTrendsRequest":{"description":"Request for tariff timeseries data.","properties":{"partnerCountry":{"description":"WTO member code of partner country (e.g. \"156\" = China).","type":"string"},"productSector":{"description":"Product sector filter (HS chapter). Empty = aggregate.","type":"string"},"reportingCountry":{"description":"WTO member code of reporting country (e.g. \"840\" = US).","type":"string"},"years":{"description":"Number of years to look back (default 10, max 30).","format":"int32","type":"integer"}},"type":"object"},"GetTariffTrendsResponse":{"description":"Response containing tariff trend datapoints.","properties":{"datapoints":{"items":{"$ref":"#/components/schemas/TariffDataPoint"},"type":"array"},"effectiveTariffRate":{"$ref":"#/components/schemas/EffectiveTariffRate"},"fetchedAt":{"description":"ISO 8601 timestamp when data was fetched from WTO.","type":"string"},"upstreamUnavailable":{"description":"True if upstream fetch failed and results may be stale/empty.","type":"boolean"}},"type":"object"},"GetTradeBarriersRequest":{"description":"Request for SPS/TBT trade barrier notifications.","properties":{"countries":{"items":{"description":"WTO member codes to filter by. Empty = all.","type":"string"},"type":"array"},"limit":{"description":"Max results to return (server caps at 100).","format":"int32","type":"integer"},"measureType":{"description":"Filter by measure type: \"SPS\", \"TBT\", or empty for both.","type":"string"}},"type":"object"},"GetTradeBarriersResponse":{"description":"Response containing trade barrier notifications.","properties":{"barriers":{"items":{"$ref":"#/components/schemas/TradeBarrier"},"type":"array"},"fetchedAt":{"description":"ISO 8601 timestamp when data was fetched from WTO.","type":"string"},"upstreamUnavailable":{"description":"True if upstream fetch failed and results may be stale/empty.","type":"boolean"}},"type":"object"},"GetTradeFlowsRequest":{"description":"Request for bilateral trade flow data.","properties":{"partnerCountry":{"description":"WTO member code of partner country.","type":"string"},"reportingCountry":{"description":"WTO member code of reporting country.","type":"string"},"years":{"description":"Number of years to look back (default 10, max 30).","format":"int32","type":"integer"}},"type":"object"},"GetTradeFlowsResponse":{"description":"Response containing trade flow records.","properties":{"fetchedAt":{"description":"ISO 8601 timestamp when data was fetched from WTO.","type":"string"},"flows":{"items":{"$ref":"#/components/schemas/TradeFlowRecord"},"type":"array"},"upstreamUnavailable":{"description":"True if upstream fetch failed and results may be stale/empty.","type":"boolean"}},"type":"object"},"GetTradeRestrictionsRequest":{"description":"Request for quantitative restriction data.","properties":{"countries":{"items":{"description":"WTO member codes to filter by. Empty = all.","type":"string"},"type":"array"},"limit":{"description":"Max results to return (server caps at 100).","format":"int32","type":"integer"}},"type":"object"},"GetTradeRestrictionsResponse":{"description":"Response containing trade restrictions and fetch metadata.","properties":{"fetchedAt":{"description":"ISO 8601 timestamp when data was fetched from WTO.","type":"string"},"restrictions":{"items":{"$ref":"#/components/schemas/TradeRestriction"},"type":"array"},"upstreamUnavailable":{"description":"True if upstream fetch failed and results may be stale/empty.","type":"boolean"}},"type":"object"},"TariffDataPoint":{"description":"Single tariff data point for a reporter-partner-product combination.","properties":{"boundRate":{"description":"WTO bound tariff rate (percentage).","format":"double","type":"number"},"indicatorCode":{"description":"WTO indicator code used for this datapoint.","type":"string"},"partnerCountry":{"description":"WTO member code of partner country.","type":"string"},"productSector":{"description":"Product sector or HS chapter.","type":"string"},"reportingCountry":{"description":"WTO member code of reporting country.","type":"string"},"tariffRate":{"description":"Applied MFN tariff rate (percentage).","format":"double","type":"number"},"year":{"description":"Year of observation.","format":"int32","type":"integer"}},"type":"object"},"TradeBarrier":{"description":"SPS or TBT trade barrier notification.","properties":{"dateDistributed":{"description":"ISO 8601 date when notification was distributed.","type":"string"},"id":{"description":"Unique barrier notification identifier.","type":"string"},"measureType":{"description":"Measure classification: \"SPS\" or \"TBT\".","type":"string"},"notifyingCountry":{"description":"Country that notified the measure.","type":"string"},"objective":{"description":"Stated objective of the measure.","type":"string"},"productDescription":{"description":"Product description or affected goods.","type":"string"},"sourceUrl":{"description":"WTO source document URL (must be http/https protocol).","type":"string"},"status":{"description":"Status of the notification.","type":"string"},"title":{"description":"Title of the notification.","type":"string"}},"type":"object"},"TradeFlowRecord":{"description":"Bilateral trade flow record for a reporting-partner pair.","properties":{"exportValueUsd":{"description":"Merchandise export value in millions USD.","format":"double","type":"number"},"importValueUsd":{"description":"Merchandise import value in millions USD.","format":"double","type":"number"},"partnerCountry":{"description":"WTO member code of partner country.","type":"string"},"productSector":{"description":"Product sector or HS chapter.","type":"string"},"reportingCountry":{"description":"WTO member code of reporting country.","type":"string"},"year":{"description":"Year of observation.","format":"int32","type":"integer"},"yoyExportChange":{"description":"Year-over-year export change (percentage).","format":"double","type":"number"},"yoyImportChange":{"description":"Year-over-year import change (percentage).","format":"double","type":"number"}},"type":"object"},"TradeRestriction":{"description":"Quantitative restriction or export control measure notified to WTO.","properties":{"affectedCountry":{"description":"Country affected by the restriction.","type":"string"},"description":{"description":"Human-readable description of the measure.","type":"string"},"id":{"description":"Unique restriction identifier from WTO.","type":"string"},"measureType":{"description":"Measure classification: \"QR\", \"EXPORT_BAN\", \"IMPORT_BAN\", \"LICENSING\".","type":"string"},"notifiedAt":{"description":"ISO 8601 date when measure was notified.","type":"string"},"productSector":{"description":"Product sector or HS chapter description.","type":"string"},"reportingCountry":{"description":"ISO 3166-1 alpha-3 or WTO member code of reporting country.","type":"string"},"sourceUrl":{"description":"WTO source document URL (must be http/https protocol).","type":"string"},"status":{"description":"Current status: \"IN_FORCE\", \"TERMINATED\", \"NOTIFIED\".","type":"string"}},"type":"object"},"ValidationError":{"description":"ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.","properties":{"violations":{"description":"List of validation violations","items":{"$ref":"#/components/schemas/FieldViolation"},"type":"array"}},"required":["violations"],"type":"object"}}},"info":{"title":"TradeService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/trade/v1/get-customs-revenue":{"get":{"description":"Get US customs duties revenue (Treasury MTS data, seeded by Railway).","operationId":"GetCustomsRevenue","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetCustomsRevenueResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetCustomsRevenue","tags":["TradeService"]}},"/api/trade/v1/get-tariff-trends":{"get":{"description":"Get tariff rate timeseries for a country pair.","operationId":"GetTariffTrends","parameters":[{"description":"WTO member code of reporting country (e.g. \"840\" = US).","in":"query","name":"reporting_country","required":false,"schema":{"type":"string"}},{"description":"WTO member code of partner country (e.g. \"156\" = China).","in":"query","name":"partner_country","required":false,"schema":{"type":"string"}},{"description":"Product sector filter (HS chapter). Empty = aggregate.","in":"query","name":"product_sector","required":false,"schema":{"type":"string"}},{"description":"Number of years to look back (default 10, max 30).","in":"query","name":"years","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetTariffTrendsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetTariffTrends","tags":["TradeService"]}},"/api/trade/v1/get-trade-barriers":{"get":{"description":"Get SPS/TBT barrier notifications.","operationId":"GetTradeBarriers","parameters":[{"description":"WTO member codes to filter by. Empty = all.","in":"query","name":"countries","required":false,"schema":{"type":"string"}},{"description":"Filter by measure type: \"SPS\", \"TBT\", or empty for both.","in":"query","name":"measure_type","required":false,"schema":{"type":"string"}},{"description":"Max results to return (server caps at 100).","in":"query","name":"limit","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetTradeBarriersResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetTradeBarriers","tags":["TradeService"]}},"/api/trade/v1/get-trade-flows":{"get":{"description":"Get bilateral merchandise trade flows.","operationId":"GetTradeFlows","parameters":[{"description":"WTO member code of reporting country.","in":"query","name":"reporting_country","required":false,"schema":{"type":"string"}},{"description":"WTO member code of partner country.","in":"query","name":"partner_country","required":false,"schema":{"type":"string"}},{"description":"Number of years to look back (default 10, max 30).","in":"query","name":"years","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetTradeFlowsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetTradeFlows","tags":["TradeService"]}},"/api/trade/v1/get-trade-restrictions":{"get":{"description":"Get quantitative restrictions and export controls.","operationId":"GetTradeRestrictions","parameters":[{"description":"WTO member codes to filter by. Empty = all.","in":"query","name":"countries","required":false,"schema":{"type":"string"}},{"description":"Max results to return (server caps at 100).","in":"query","name":"limit","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetTradeRestrictionsResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetTradeRestrictions","tags":["TradeService"]}}}} \ No newline at end of file diff --git a/docs/api/TradeService.openapi.yaml b/docs/api/TradeService.openapi.yaml index 30b2a870b..ab71986ae 100644 --- a/docs/api/TradeService.openapi.yaml +++ b/docs/api/TradeService.openapi.yaml @@ -334,6 +334,8 @@ components: upstreamUnavailable: type: boolean description: True if upstream fetch failed and results may be stale/empty. + effectiveTariffRate: + $ref: '#/components/schemas/EffectiveTariffRate' description: Response containing tariff trend datapoints. TariffDataPoint: type: object @@ -363,6 +365,26 @@ components: type: string description: WTO indicator code used for this datapoint. description: Single tariff data point for a reporter-partner-product combination. + EffectiveTariffRate: + type: object + properties: + sourceName: + type: string + description: Source name for the effective-rate estimate. + sourceUrl: + type: string + description: Canonical source URL for the estimate/methodology. + observationPeriod: + type: string + description: Human-readable observation period (for example "December 2025"). + updatedAt: + type: string + description: ISO 8601 date when the source page was last updated, if known. + tariffRate: + type: number + format: double + description: Effective tariff rate (percentage). + description: Current effective tariff estimate for countries with coverage beyond WTO MFN baselines. GetTradeFlowsRequest: type: object properties: diff --git a/proto/worldmonitor/trade/v1/get_tariff_trends.proto b/proto/worldmonitor/trade/v1/get_tariff_trends.proto index 8dd085596..e1d5a8203 100644 --- a/proto/worldmonitor/trade/v1/get_tariff_trends.proto +++ b/proto/worldmonitor/trade/v1/get_tariff_trends.proto @@ -25,4 +25,6 @@ message GetTariffTrendsResponse { string fetched_at = 2; // True if upstream fetch failed and results may be stale/empty. bool upstream_unavailable = 3; + // Optional effective tariff snapshot for countries with additional coverage (currently US only). + EffectiveTariffRate effective_tariff_rate = 4; } diff --git a/proto/worldmonitor/trade/v1/trade_data.proto b/proto/worldmonitor/trade/v1/trade_data.proto index fe3f77cc9..8f0146ef1 100644 --- a/proto/worldmonitor/trade/v1/trade_data.proto +++ b/proto/worldmonitor/trade/v1/trade_data.proto @@ -42,6 +42,20 @@ message TariffDataPoint { string indicator_code = 7; } +// Current effective tariff estimate for countries with coverage beyond WTO MFN baselines. +message EffectiveTariffRate { + // Source name for the effective-rate estimate. + string source_name = 1; + // Canonical source URL for the estimate/methodology. + string source_url = 2; + // Human-readable observation period (for example "December 2025"). + string observation_period = 3; + // ISO 8601 date when the source page was last updated, if known. + string updated_at = 4; + // Effective tariff rate (percentage). + double tariff_rate = 5; +} + // Bilateral trade flow record for a reporting-partner pair. message TradeFlowRecord { // WTO member code of reporting country. diff --git a/scripts/_trade-parse-utils.mjs b/scripts/_trade-parse-utils.mjs new file mode 100644 index 000000000..d4d887798 --- /dev/null +++ b/scripts/_trade-parse-utils.mjs @@ -0,0 +1,79 @@ +/** + * Pure parse helpers for trade-data seed scripts. + * Extracted so test files can import directly without new Function() hacks. + */ + +export const BUDGET_LAB_TARIFFS_URL = 'https://budgetlab.yale.edu/research/tracking-economic-effects-tariffs'; + +const MONTH_MAP = { + january: '01', february: '02', march: '03', april: '04', + may: '05', june: '06', july: '07', august: '08', + september: '09', october: '10', november: '11', december: '12', +}; + +export function htmlToPlainText(html) { + return String(html ?? '') + .replace(//gi, ' ') + .replace(//gi, ' ') + .replace(/<[^>]+>/g, ' ') + .replace(/ /gi, ' ') + .replace(/&/gi, '&') + .replace(/"/gi, '"') + .replace(/'/gi, '\'') + .replace(/\s+/g, ' ') + .trim(); +} + +/** + * Convert a human-readable date string like "March 2, 2026" to ISO "2026-03-02". + * Falls back to '' on failure. + */ +export function toIsoDate(value) { + const text = String(value ?? '').trim(); + if (!text) return ''; + if (/^\d{4}-\d{2}-\d{2}/.test(text)) return text.slice(0, 10); + const m = text.match(/^([A-Za-z]+)\s+(\d{1,2}),?\s+(\d{4})/); + if (m) { + const mm = MONTH_MAP[m[1].toLowerCase()]; + if (mm) return `${m[3]}-${mm}-${m[2].padStart(2, '0')}`; + } + return ''; +} + +/** + * Parse the Yale Budget Lab tariff-tracking page and extract effective tariff rate. + * + * Tries three patterns in priority order: + * 1. "effective tariff rate reaching X% in [month year]" + * 2. "average effective [U.S.] tariff rate ... to X% ... in/by [month year]" + * 3. Same as 2 but no period capture + * + * Returns null when no recognisable rate is found. + */ +export function parseBudgetLabEffectiveTariffHtml(html) { + const text = htmlToPlainText(html); + if (!text) return null; + + const updatedAt = toIsoDate(text.match(/\bUpdated:\s*([A-Za-z]+\s+\d{1,2},\s+\d{4})/i)?.[1] ?? ''); + const patterns = [ + /effective tariff rate reaching\s+(\d+(?:\.\d+)?)%\s+in\s+([A-Za-z]+\s+\d{4})/i, + /average effective (?:u\.s\.\s*)?tariff rate[^.]{0,180}?\bto\s+(\d+(?:\.\d+)?)%[^.]{0,180}?\b(?:in|by)\s+([A-Za-z]+\s+\d{4})/i, + /average effective (?:u\.s\.\s*)?tariff rate[^.]{0,180}?\bto\s+(\d+(?:\.\d+)?)%/i, + ]; + + for (const pattern of patterns) { + const match = text.match(pattern); + if (!match) continue; + const tariffRate = parseFloat(match[1]); + if (!Number.isFinite(tariffRate)) continue; + return { + sourceName: 'Yale Budget Lab', + sourceUrl: BUDGET_LAB_TARIFFS_URL, + observationPeriod: match[2] ?? '', + updatedAt, + tariffRate: Math.round(tariffRate * 100) / 100, + }; + } + + return null; +} diff --git a/scripts/seed-supply-chain-trade.mjs b/scripts/seed-supply-chain-trade.mjs index 9bddc752f..9d457ac2d 100755 --- a/scripts/seed-supply-chain-trade.mjs +++ b/scripts/seed-supply-chain-trade.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node import { loadEnvFile, CHROME_UA, runSeed, writeExtraKeyWithMeta, sleep, verifySeedKey } from './_seed-utils.mjs'; +import { BUDGET_LAB_TARIFFS_URL, htmlToPlainText, toIsoDate, parseBudgetLabEffectiveTariffHtml } from './_trade-parse-utils.mjs'; loadEnvFile(import.meta.url); @@ -234,6 +235,30 @@ async function wtoFetch(path, params) { return resp.json(); } +async function fetchBudgetLabEffectiveTariffRate() { + try { + const resp = await fetch(BUDGET_LAB_TARIFFS_URL, { + headers: { Accept: 'text/html', 'User-Agent': CHROME_UA }, + signal: AbortSignal.timeout(15_000), + }); + if (!resp.ok) { + console.warn(` Budget Lab tariffs: HTTP ${resp.status}`); + return null; + } + const html = await resp.text(); + const parsed = parseBudgetLabEffectiveTariffHtml(html); + if (!parsed) { + console.warn(' Budget Lab tariffs: effective tariff rate not found in page content'); + return null; + } + console.log(` Budget Lab effective tariff: ${parsed.tariffRate.toFixed(1)}%${parsed.observationPeriod ? ` (${parsed.observationPeriod})` : ''}`); + return parsed; + } catch (e) { + console.warn(` Budget Lab tariffs: ${e.message}`); + return null; + } +} + // ─── Trade Flows (WTO) — pre-seed major reporters vs World + key bilateral pairs ─── const BILATERAL_PAIRS = [ @@ -407,7 +432,7 @@ async function fetchTradeRestrictions() { id: `${cc}-${year}-${row.IndicatorCode ?? ''}`, reportingCountry: WTO_MEMBER_CODES[cc] ?? String(row.ReportingEconomy ?? ''), affectedCountry: 'All trading partners', productSector: 'All products', - measureType: 'MFN Applied Tariff', description: `Average tariff rate: ${value.toFixed(1)}%`, + measureType: 'WTO MFN Baseline', description: `WTO MFN baseline: ${value.toFixed(1)}%`, status: value > 10 ? 'high' : value > 5 ? 'moderate' : 'low', notifiedAt: year, sourceUrl: 'https://stats.wto.org', }; @@ -426,6 +451,7 @@ async function fetchTradeRestrictions() { async function fetchTariffTrends() { const currentYear = new Date().getFullYear(); const trends = {}; + const usEffectiveTariffRate = await fetchBudgetLabEffectiveTariffRate(); for (const reporter of MAJOR_REPORTERS) { const years = 10; @@ -449,7 +475,12 @@ async function fetchTariffTrends() { if (datapoints.length > 0) { const cacheKey = `trade:tariffs:v1:${reporter}:all:${years}`; - trends[cacheKey] = { datapoints, fetchedAt: new Date().toISOString(), upstreamUnavailable: false }; + trends[cacheKey] = { + datapoints, + ...(reporter === '840' && usEffectiveTariffRate ? { effectiveTariffRate: usEffectiveTariffRate } : {}), + fetchedAt: new Date().toISOString(), + upstreamUnavailable: false, + }; } await sleep(500); } @@ -566,7 +597,7 @@ function validate(data) { runSeed('supply_chain', 'shipping', KEYS.shipping, fetchAll, { validateFn: validate, ttlSeconds: SHIPPING_TTL, - sourceVersion: 'fred-wto-sse-bdi', + sourceVersion: 'fred-wto-sse-bdi-budgetlab', }).catch((err) => { const _cause = err.cause ? ` (cause: ${err.cause.message || err.cause.code || err.cause})` : ''; console.error('FATAL:', (err.message || err) + _cause); process.exit(1); diff --git a/server/worldmonitor/trade/v1/get-tariff-trends.ts b/server/worldmonitor/trade/v1/get-tariff-trends.ts index 58f900a2e..a5fd0771b 100644 --- a/server/worldmonitor/trade/v1/get-tariff-trends.ts +++ b/server/worldmonitor/trade/v1/get-tariff-trends.ts @@ -1,6 +1,6 @@ /** - * RPC: getTariffTrends -- reads seeded WTO tariff trend data from Railway seed cache. - * All external WTO API calls happen in seed-supply-chain-trade.mjs on Railway. + * RPC: getTariffTrends -- reads seeded WTO MFN tariff trends from Railway seed cache. + * The seed payload may also include an optional US effective tariff snapshot. */ import type { ServerContext, diff --git a/server/worldmonitor/trade/v1/get-trade-restrictions.ts b/server/worldmonitor/trade/v1/get-trade-restrictions.ts index 4f0c642bf..9c2957c50 100644 --- a/server/worldmonitor/trade/v1/get-trade-restrictions.ts +++ b/server/worldmonitor/trade/v1/get-trade-restrictions.ts @@ -1,5 +1,5 @@ /** - * RPC: getTradeRestrictions -- reads seeded WTO tariff restriction data from Railway seed cache. + * RPC: getTradeRestrictions -- reads seeded WTO MFN baseline overview data from Railway seed cache. * All external WTO API calls happen in seed-supply-chain-trade.mjs on Railway. */ import type { diff --git a/src/components/TradePolicyPanel.ts b/src/components/TradePolicyPanel.ts index 5d53414a5..05adaeb6c 100644 --- a/src/components/TradePolicyPanel.ts +++ b/src/components/TradePolicyPanel.ts @@ -5,6 +5,8 @@ import type { GetTradeFlowsResponse, GetTradeBarriersResponse, GetCustomsRevenueResponse, + TariffDataPoint, + EffectiveTariffRate, } from '@/services/trade'; import { t } from '@/services/i18n'; import { escapeHtml } from '@/utils/sanitize'; @@ -81,7 +83,7 @@ export class TradePolicyPanel extends Panel { const tabsHtml = `
${wtoAvailable ? `` : ''} ${hasTariffs ? `