From ff7870cee7586c69976994cab5291fcafcabfa7d Mon Sep 17 00:00:00 2001 From: "V. David Zvenyach" Date: Fri, 2 Oct 2026 17:11:04 -0500 Subject: [PATCH] feat(vehicles): request holder_count and order_winner_count in the default vehicle shapes Tango API 5.8.0 adds two company counts to vehicles: holder_count (distinct companies holding one of the vehicle's IDVs) and order_winner_count (distinct companies that have won a task order under it). awardee_count is now a deprecated alias of order_winner_count and stays in the shapes so existing readers keep working. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +++++ crates/tango/src/shapes.rs | 25 +++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1986f51..6c13f49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This project follows [Semantic Versioning](https://semver.org/). - **Budget-account `source_anomalies` and `account_category`** (Tango API 5.7.0). `models::BudgetSourceAnomaly` (with `BudgetSourceAnomalySource` and `BudgetSourceAnomalyRow`) types each element of a row's `source_anomalies` list, and `BudgetSourceAnomaly::from_record` decodes it from a `list_budget_accounts` / `get_budget_account` record, returning an empty list when the field is `[]`, absent or null. Every field is optional and `code` is an open string (`contract_exceeds_obligations`, `assistance_exceeds_obligations`, `contract_without_obligations` today). `reported_value`, `served_value` and `transaction_obligated_amount` decode from a JSON number or a numeric string. There is no filter on anomalies. - **Budget-account `data_through_period`** (Tango API 5.8.0): the File A fiscal period (1–12) an account-year's figures run through, so the latest fiscal year is partial until it reaches 12; null when the row has no File A data. `models::budget_data_through_period` reads it from a record as `Option`, and `ListBudgetAccountsOptions` gains `data_through_period`, `data_through_period_gte`, `data_through_period_lte` (sent as `__gte` / `__lte`) and `data_through_period_isnull` (sent as `data_through_period__isnull`). - **`account_category` and `account_category_in` on `ListBudgetAccountsOptions`** (sent as `account_category` and `account_category__in`). Values are `budgetary` or `credit_financing` today; credit financing accounts have null `enacted_ba` and are left out of organization budget totals. +- **Vehicle company counts** (Tango API 5.8.0): `SHAPE_VEHICLES_MINIMAL` and `SHAPE_VEHICLES_COMPREHENSIVE` now request `holder_count` (distinct companies holding one of the vehicle's IDVs) and `order_winner_count` (distinct companies that have won a task order under it), next to `awardee_count`. #### Changed @@ -29,6 +30,10 @@ This project follows [Semantic Versioning](https://semver.org/). - **`SHAPE_BUDGET_ACCOUNTS_MINIMAL` matches the API's default shape again.** It adds `data_through_period` (right after `fiscal_year`), `account_category` and `source_anomalies`, plus `attribution_status`, `attribution_confidence` and `contract_obligated_estimated`, which the API's default already carried. - **The API's default budget-account ordering now puts null `enacted_ba` last** within each fiscal year, with `id` breaking ties so pages are stable. No SDK change is needed; the `ordering` field documents it. +#### Deprecated + +- **`awardee_count` on vehicles** is deprecated by the Tango API as an alias of `order_winner_count`, and is removed at the API's next major version. The vehicle shapes still request it so existing readers keep working; read `order_winner_count` instead. + ## [0.2.0] — 2026-09-23 Pre-1.0 (SemVer 0.x): the removals under **Breaking** ship without a deprecation cycle. diff --git a/crates/tango/src/shapes.rs b/crates/tango/src/shapes.rs index 5e5db4d..b44f681 100644 --- a/crates/tango/src/shapes.rs +++ b/crates/tango/src/shapes.rs @@ -113,7 +113,7 @@ pub const SHAPE_IDVS_COMPREHENSIVE: &str = concat!( pub const SHAPE_VEHICLES_MINIMAL: &str = concat!( "uuid,solicitation_identifier,is_synthetic_solicitation,program_acronym,", "organization_id,organization,vehicle_type,description,", - "idv_count,awardee_count,order_count,total_obligated,", + "idv_count,awardee_count,holder_count,order_winner_count,order_count,total_obligated,", "vehicle_obligations,vehicle_contracts_value,latest_award_date,", "solicitation_title,solicitation_date", ); @@ -125,7 +125,7 @@ pub const SHAPE_VEHICLES_COMPREHENSIVE: &str = concat!( "solicitation_title,solicitation_description,solicitation_date,opportunity_id,", "naics_code,psc_code,set_aside,", "fiscal_year,award_date,latest_award_date,last_date_to_order,", - "description,idv_count,awardee_count,order_count,total_obligated,", + "description,idv_count,awardee_count,holder_count,order_winner_count,order_count,total_obligated,", "vehicle_obligations,vehicle_contracts_value,", "type_of_idc,contract_type,metrics(*)", ); @@ -269,3 +269,24 @@ pub const SHAPE_EBUY_REQUESTS_MINIMAL: &str = concat!( "buyer_agency_code,reference_number,issue_date,close_date,attachment_count,", "link_count,last_seen", ); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn vehicle_shapes_carry_company_counts() { + for (name, shape) in [ + ("SHAPE_VEHICLES_MINIMAL", SHAPE_VEHICLES_MINIMAL), + ("SHAPE_VEHICLES_COMPREHENSIVE", SHAPE_VEHICLES_COMPREHENSIVE), + ] { + let fields: Vec<&str> = shape.split(',').collect(); + for want in ["awardee_count", "holder_count", "order_winner_count"] { + assert!( + fields.contains(&want), + "{name} should carry {want:?}: {shape:?}" + ); + } + } + } +}