Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@ 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<u8>`, 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

- The retry loop is shared between body-returning requests and the redirect-reading call, so `get_ebuy_attachment_url` retries 429, 5xx and transport failures exactly as every other method does.
- **`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.
Expand Down
25 changes: 23 additions & 2 deletions crates/tango/src/shapes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
Expand All @@ -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(*)",
);
Expand Down Expand Up @@ -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:?}"
);
}
}
}
}
Loading